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

考虑这两个返回IEnumerable的方法:

private IEnumerable GetYieldResult(int qtResult) { for (int i = 0; i < qtResult; i++) { count++; yield return new MyClass() { Id = i+1 }; } } private IEnumerable GetNonYieldResult(int qtResult) { var result = new List(); 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 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 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()”方法的次数。

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

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

有任何想法吗?

提前致谢!

当然,我不想强​​制所有的itens不必要地创建

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

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

所以答案

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

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


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

 private static List cache = new List(); public IEnumerable 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; } } 

我希望有可能在迭代器周围构建这样的缓存包装器( while会成为底层迭代器的foreach ,但如果调用者迭代到cahing List之外,你需要缓存迭代器或Skip到require位置)。

NB任何缓存方法都难以使线程安全。

我一直试图找到一个优雅的问题解决方案一段时间了。 我希望框架设计者在IEnumerable中添加了一点“IsImmutable”或类似的属性getter,以便人们可以轻松添加一个对于已经在其“完全评估”中的IEnumerable不执行任何操作的Evaluate(或类似)扩展方法“国家。

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

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

它是相当kludgy,但它是迄今为止我能够找到的最不具侵入性的方法来解决允许IEnumerable消费者仅在实际需要时才创建本地副本的问题。 我非常希望你的问题能从木工中吸引一些更有趣的解决方案……

除非我误读你,否则你的问题可能是由误解造成的。没有任何东西可以返回IEnumerable。 第一种情况返回一个实现foreach的Enumerator,允许您一次获取一个MyClass实例。 它,(函数返回值)被输入为IEnumerable以表示它支持foreach行为(以及其他一些行为)

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

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

编辑:更多细节一个更重要的区别是,您是否希望在您进行迭代时,在您进行迭代时,是否希望在项目中为您保留状态,或者是否希望在迭代时为您创建它们。

另一个考虑因素是……您希望在其他地方存在的物品已经存在吗? 也就是说,这种方法是否会迭代一些现有集合的集合(或过滤子集)? 或者它是在动态创建项目? 如果是后者,那么每次你“获得”该项目是否完全相同的实例是否重要? 对于定义的对象,可以称为实体的东西 – 具有已定义标识的 ssomething,您可能希望连续的提取返回相同的实例。

但也许另一个具有相同状态的实例完全不相同? (这将被称为值类型对象,如电话号码,地址或屏幕上的点。这些对象除了其状态所暗示的之外没有任何身份。在后一种情况下,无论是否每次“获取”时,枚举器都返回相同的实例或新创建的相同副本……这些对象通常是不可变的,它们是相同的,它们保持不变,并且它们的function相同。

你可以混合这些建议,你可以实现一个包装类,基于generics,它接受IEnumerable并返回一个新的,在每个下一个构造一个缓存,并根据需要在进一步的枚举上重用部分缓存。 这并不容易,但只会根据需要创建一次对象(实际上只适用于即时构建对象的迭代器)。 最难的部分是确保何时从部分缓存切换回原始枚举器以及如何使其成为事务性(一致)。

更新测试代码:

 public interface ICachedEnumerable : IEnumerable { } internal class CachedEnumerable : ICachedEnumerable { private readonly List cache = new List(); private readonly IEnumerator source; private bool sourceIsExhausted = false; public CachedEnumerable(IEnumerable 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 GetEnumerator() { return new CachedEnumerator(this); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return new CachedEnumerator(this); } private class CachedEnumerator : IEnumerator, System.Collections.IEnumerator { private readonly CachedEnumerable parent; private int where; public CachedEnumerator(CachedEnumerable 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.Current { get { return Get(); } } private T Get() { return parent.Get(where); } public void Dispose() { } } } public static class CachedEnumerableExtensions { public static ICachedEnumerable AsCachedEnumerable(this IEnumerable source) { return new CachedEnumerable(source); } } 

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

  [Test] public void Test3() { count = 0; ICachedEnumerable 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项目中

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

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

另一方面(例如你的例子)你可以在这里结束ToList调用迭代并创建一个存储的列表,而yieldResult仍然是一个没有问题的IEnumerable。

 [TestMethod] public void Test1() { count = 0; IEnumerable 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); }