如何通过合约定义 IEnumerable 行为?

How to define IEnumerable behavior by contract?

提问人:AntonioR 提问时间:10/29/2010 最后编辑:AntonioR 更新时间:10/30/2010 访问量:777

问:

请考虑返回 IEnumerable 的 2 个方法:

    private IEnumerable<MyClass> GetYieldResult(int qtResult)
    {
        for (int i = 0; i < qtResult; i++)
        {
            count++;
            yield return new MyClass() { Id = i+1 };
        }
    }

    private IEnumerable<MyClass> GetNonYieldResult(int qtResult)
    {
        var result = new List<MyClass>();

        for (int i = 0; i < qtResult; i++)
        {
            count++;
            result.Add(new MyClass() { Id = i + 1 });
        }

        return result;
    }

此代码在调用 IEnumerable 的某些方法时显示 2 种不同的行为:

    [TestMethod]
    public void Test1()
    {
        count = 0;

        IEnumerable<MyClass> yieldResult = GetYieldResult(1);

        var firstGet = yieldResult.First();
        var secondGet = yieldResult.First();

        Assert.AreEqual(1, firstGet.Id);
        Assert.AreEqual(1, secondGet.Id);

        Assert.AreEqual(2, count);//calling "First()" 2 times, yieldResult is created 2 times
        Assert.AreNotSame(firstGet, secondGet);//and created different instances of each list item
    }

    [TestMethod]
    public void Test2()
    {
        count = 0;

        IEnumerable<MyClass> yieldResult = GetNonYieldResult(1);

        var firstGet = yieldResult.First();
        var secondGet = yieldResult.First();

        Assert.AreEqual(1, firstGet.Id);
        Assert.AreEqual(1, secondGet.Id);

        Assert.AreEqual(1, count);//as expected, it creates only 1 result set
        Assert.AreSame(firstGet, secondGet);//and calling "First()" several times will always return same instance of MyClass
    }

当我的代码返回 IEnumerables 时,选择我想要的行为很简单,但是我如何显式定义某个方法获取 IEnumerable 作为参数,该参数创建单个结果集,而不管它调用“First()”方法的次数。

当然,我不想强制不必要地创建所有 iten,我想将参数定义为 IEnumerable,以表示不会包含或从集合中删除任何项。

编辑:需要明确的是,问题不在于yield如何工作,也不在于为什么IEnumerable可以为每个调用返回不同的实例。问题是,当我多次调用“First()”或“Take(1)”等方法时,如何指定参数应该是“仅搜索”集合,该集合返回相同的MyClass实例。

有什么想法吗?

提前致谢!

C# .NET LINQ LINQ-to-Objects 按协定设计

评论


答:

2赞 Richard 10/29/2010 #1

当然,我不想强迫所有 itens 不必要地创建

在这种情况下,您需要允许方法按需创建它们,如果对象是按需创建的(并且没有某种形式的缓存),它们将是不同的对象(至少在不同引用的意义上 - 非值对象的默认相等定义)。

如果你的对象本质上是唯一的(即它们没有定义一些基于值的相等性),那么每次调用都会创建一个不同的对象(无论构造函数参数如何)。new

所以答案是

但是我如何明确定义某个方法获取一个 IEnumerable 作为参数,该参数创建单个结果集,而不管它调用“First()”方法的次数。

是“你不能”,除非通过创建一组对象并重复返回同一组对象,或者通过将相等定义为不同的东西。


其他(基于评论)。如果你真的希望能够重放(因为想要一个更好的术语),同一组对象,而不构建你可以缓存的整个集合,你想要的已经生成了,并首先重放。像这样:

private static List<MyData> cache = new List<MyData>();
public IEnumerable<MyData> GetData() {
  foreach (var d in cache) {
    yield return d;
  }

  var position = cache.Count;

  while (maxItens < position) {
    MyData next = MakeNextItem(position);
    cache.Add(next);
    yield return next;
  }
}

我希望也可以围绕迭代器构建这样的缓存包装器( 会变成底层迭代器,但如果调用者迭代超过 cahing,则需要缓存该迭代器或所需位置)。whileforeachSkipList

注意:任何缓存方法都很难使线程安全。

评论

0赞 Rangoric 10/29/2010
哦,是的,这是第三种选择,重新定义他们如何测试他们是否相等。
0赞 Richard 10/29/2010
@AntonioR 这种缓存只能通过您调用的方法完成。调用方可以在不合作的情况下将这种策略强加给它调用的方法。
0赞 AntonioR 10/29/2010
比较这些对象并不是真正的问题。问题是重新实例化 MyClass 项时,我丢失了对这些实例的引用。因此,如果某个方法迭代一些 itens 并更改一些 os 它们的属性,然后我将此 IEnumerable 发送到另一个方法,它会获得具有原始属性值的 MyClass 的新实例。因此,我希望有一种方法可以享受按需填充集合的好处,而无需冒险创建同一集合项的多个实例。
0赞 Richard 10/30/2010
@AntonioR 我扩展了一个想法,我认为这个想法可能是你正在寻找的——一种混合方法。
0赞 AntonioR 11/10/2010
@Richard:谢谢。我已经更改了我的代码以避免这种情况。我仍然担心没有语言/框架支持这个合同。但幸运的是,我可以更改我的代码,因为不希望再向团队传播一个“记住”的注释。
0赞 Rangoric 10/29/2010 #2

然后,您需要缓存结果,当您调用迭代它的东西时,IEnumerable 总是会重新执行。我倾向于使用:

private List<MyClass> mEnumerable;
public IEnumerable<MyClass> GenerateEnumerable()
{
    mEnumerable = mEnumerable ?? CreateEnumerable()
    return mEnumerable;
}
private List<MyClass> CreateEnumerable()
{
    //Code to generate List Here
}

在另一端授予(例如,对于您的示例),您可以在此处末尾进行 ToList 调用,将迭代并创建一个存储的列表,并且 yieldResult 仍将是 IEnumerable,没有问题。

[TestMethod]
public void Test1()
{
    count = 0;


    IEnumerable<MyClass> yieldResult = GetYieldResult(1).ToList();

    var firstGet = yieldResult.First();
    var secondGet = yieldResult.First();

    Assert.AreEqual(1, firstGet.Id);
    Assert.AreEqual(1, secondGet.Id);

    Assert.AreEqual(2, count);//calling "First()" 2 times, yieldResult is created 1 time
    Assert.AreSame(firstGet, secondGet);
}

评论

0赞 AntonioR 10/29/2010
这两个建议都避免了两次创建 MyClass,但它们还涉及在调用“First()”方法之前创建所有项目。
1赞 Charles Bretana 10/29/2010 #3

除非我误读了你,否则你的问题可能是由误解引起的。没有任何内容返回 IEnumerable。第一种情况返回一个枚举器,该枚举器实现 foreach,允许您一次获取一个 MyClass 实例。它(函数返回值)的类型为 IEnumerable,以指示它支持 foreach 行为(以及其他一些行为)

第二个函数实际上返回一个 List,当然它也支持 IEnumerable(foreach 行为)。但它是 MyClass 对象的实际具体集合,由您调用的方法(第二个)创建

第一种方法根本不返回任何 MyClass 对象,它返回该枚举器对象,该对象由 dotNet 框架创建并在后台编码,以便在每次迭代时实例化新的 MyClass 对象。

编辑:更多细节 一个更重要的区别是,你是否希望在迭代时在类中以状态方式为你保留这些项,或者是否希望在迭代时为你创建这些项。

另一个考虑因素是..您希望归还给您的物品是否已经在其他地方存在?也就是说,此方法是否要遍历某些现有集合的集合(或过滤子集)?还是在动态中创建项目?如果是后者,那么每次“获得”该物品时是否完全相同的实例重要吗? 对于定义 t 表示可以称为实体的事物的对象 - 具有已定义标识的某物,您可能希望连续的 fetchs 返回相同的实例。

但也许具有相同状态的另一个实例是完全等价的?(这称为值类型对象,如电话号码、地址或屏幕上的点。这些对象除了它们的状态所暗示的标识外,没有其他标识。在后一种情况下,枚举器是每次“获取”它时返回相同的实例还是新创建的相同副本都无关紧要......这些对象通常是不可变的,它们是相同的,它们保持不变,并且它们的功能相同。

评论

1赞 Richard 10/29/2010
迂腐地:迭代器方法返回,因此它们肯定会返回一个可枚举对象,从中可以获取枚举器(实现或更好,)。IEnumerable<T>IEnumeratorIEnumerator<T>
0赞 Charles Bretana 10/29/2010
从迂腐的角度来看,没有 OR 这样的对象。这些术语是定义合约的接口。他们所做的只是指定无论方法实际返回什么对象,它都必须满足该约定。这正是我所提出的观点的要点。IEnumerableIEnumerable<T>
0赞 AntonioR 10/29/2010
谢谢你的回答。我知道,当我迭代枚举器时,使用第一种方法 MyClass 会实例化,第二个函数返回一个 List,这已经是集合实例。但我想使用 IEnumerable 参数说“嘿,我只会阅读这个集合,所以不用担心它被添加或删除”。但我仍然想确保我不会获得新的集合项目,而不是多次调用按需填充的相同集合。
0赞 Charles Bretana 10/29/2010
更重要的区别是,你是否希望在迭代时在类中以状态方式为你保留这些项,或者是否希望在迭代时为你创建这些项。请参阅扩展答案。
0赞 AntonioR 10/30/2010
@Charles:实际上,我想确保我的测试方法可以部分迭代IEnumerable几次(调用“First()”,“Take(n)”或其他什么),并且始终获得相同的MyClass实例,而无需更改IEnumerable源选择的策略。使用 IEnumerable 作为参数类型,我明确表示我不会在它之间添加或删除项目,但我没有将其指定为具有不可变搜索结果的“仅搜索”集合,这就是我需要的。
1赞 Nicole Calinoiu 10/29/2010 #4

一段时间以来,我一直在试图找到一个优雅的解决方案来解决这个问题。我希望框架设计者在 IEnumerable 中添加了一些“IsImmutable”或类似的属性 getter,以便可以轻松添加一个 Evaluate (或类似的) 扩展方法,该方法对已经处于“完全评估”状态的 IEnumerable 不做任何事情。

但是,由于不存在,这是我能想到的最好的:

  1. 我创建了自己的接口来公开 immutability 属性,并在所有自定义集合类型中实现它。
  2. 我的 Evaluate 的实现 扩展方法知道这一点 新界面以及 子集的不变性 我使用的相关 BCL 类型 最常见的。
  3. 我避免返回 来自我的“原始”BCL 集合类型 API,以提高我的 Evaluate 方法的效率(至少在针对我自己的代码运行时)。

这相当笨拙,但这是我迄今为止能够找到的侵入性最小的方法,以解决允许 IEnumerable 使用者仅在实际需要时创建本地副本的问题。我非常希望您的问题能从木制品中引出一些更有趣的解决方案......

评论

0赞 Monoman 10/29/2010
实际上,正如我在回答中建议的那样,您可以创建一个包装类,并使用扩展方法轻松地将任何 IEnumerables 转换为标记不可变性并由包装类实现的新接口,然后您可以简单地在函数参数级别指定需要新的标记接口。
0赞 Nicole Calinoiu 10/29/2010
它们的方法风格大致相同,但缺点也差不多。首先,除了极少数情况外,扩展方法必须对源的不变性进行可能不必要的昂贵猜测。其次,为了选择进入“不要复制我”的路径,IEnumerable<T> 发布者必须跳过一些不必要的障碍(其中一些最终可能会导致性能下降)。
1赞 Monoman 10/29/2010 #5

可以混合建议,可以实现基于泛型的包装类,该类采用 IEnumerable 并返回一个新类,该新类在每个后续构造缓存,并根据需要在进一步的枚举中重用部分缓存。这并不容易,但只会根据需要创建对象(实际上仅适用于动态构造对象的迭代器)。最难的部分是确定何时从部分缓存切换回原始枚举器,以及如何使其具有事务性(一致性)。

使用经过测试的代码进行更新:

public interface ICachedEnumerable<T> : IEnumerable<T>
{
}

internal class CachedEnumerable<T> : ICachedEnumerable<T>
{
    private readonly List<T> cache = new List<T>();
    private readonly IEnumerator<T> source;
    private bool sourceIsExhausted = false;

    public CachedEnumerable(IEnumerable<T> source)
    {
        this.source = source.GetEnumerator();
    }

    public T Get(int where)
    {
        if (where < 0)
            throw new InvalidOperationException();
        SyncUntil(where);
        return cache[where];
    }

    private void SyncUntil(int where)
    {
        lock (cache)
        {
            while (where >= cache.Count && !sourceIsExhausted)
            {
                sourceIsExhausted = source.MoveNext();
                cache.Add(source.Current);
            }
            if (where >= cache.Count)
                throw new InvalidOperationException();
        }
    }

    public bool GoesBeyond(int where)
    {
        try
        {
            SyncUntil(where);
            return true;
        }
        catch (InvalidOperationException)
        {
            return false;
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        return new CachedEnumerator<T>(this);
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return new CachedEnumerator<T>(this);
    }

    private class CachedEnumerator<T> : IEnumerator<T>, System.Collections.IEnumerator
    {
        private readonly CachedEnumerable<T> parent;
        private int where;

        public CachedEnumerator(CachedEnumerable<T> parent)
        {
            this.parent = parent;
            Reset();
        }

        public object Current
        {
            get { return Get(); }
        }

        public bool MoveNext()
        {
            if (parent.GoesBeyond(where))
            {
                where++;
                return true;
            }
            return false;
        }

        public void Reset()
        {
            where = -1;
        }

        T IEnumerator<T>.Current
        {
            get { return Get(); }
        }

        private T Get()
        {
            return parent.Get(where);
        }

        public void Dispose()
        {
        }
    }
}

public static class CachedEnumerableExtensions
{
    public static ICachedEnumerable<T> AsCachedEnumerable<T>(this IEnumerable<T> source)
    {
        return new CachedEnumerable<T>(source);
    }
}

有了这个,你现在可以添加一个新的测试来证明它的工作原理:

    [Test]
    public void Test3()
    {
        count = 0;

        ICachedEnumerable<MyClass> yieldResult = GetYieldResult(1).AsCachedEnumerable();

        var firstGet = yieldResult.First();
        var secondGet = yieldResult.First();

        Assert.AreEqual(1, firstGet.Id);
        Assert.AreEqual(1, secondGet.Id);

        Assert.AreEqual(1, count);//calling "First()" 2 times, yieldResult is created 2 times
        Assert.AreSame(firstGet, secondGet);//and created different instances of each list item
    }

代码将合并到我的项目 http://github.com/monoman/MSBuild.NUnit,以后也可能出现在 Managed.Commons 项目中