提问人:AntonioR 提问时间:10/29/2010 最后编辑:AntonioR 更新时间:10/30/2010 访问量:777
如何通过合约定义 IEnumerable 行为?
How to define IEnumerable behavior by contract?
问:
请考虑返回 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实例。
有什么想法吗?
提前致谢!
答:
当然,我不想强迫所有 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,则需要缓存该迭代器或所需位置)。while
foreach
Skip
List
注意:任何缓存方法都很难使线程安全。
评论
然后,您需要缓存结果,当您调用迭代它的东西时,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);
}
评论
除非我误读了你,否则你的问题可能是由误解引起的。没有任何内容返回 IEnumerable。第一种情况返回一个枚举器,该枚举器实现 foreach,允许您一次获取一个 MyClass 实例。它(函数返回值)的类型为 IEnumerable,以指示它支持 foreach 行为(以及其他一些行为)
第二个函数实际上返回一个 List,当然它也支持 IEnumerable(foreach 行为)。但它是 MyClass 对象的实际具体集合,由您调用的方法(第二个)创建
第一种方法根本不返回任何 MyClass 对象,它返回该枚举器对象,该对象由 dotNet 框架创建并在后台编码,以便在每次迭代时实例化新的 MyClass 对象。
编辑:更多细节 一个更重要的区别是,你是否希望在迭代时在类中以状态方式为你保留这些项,或者是否希望在迭代时为你创建这些项。
另一个考虑因素是..您希望归还给您的物品是否已经在其他地方存在?也就是说,此方法是否要遍历某些现有集合的集合(或过滤子集)?还是在动态中创建项目?如果是后者,那么每次“获得”该物品时是否完全相同的实例重要吗? 对于定义 t 表示可以称为实体的事物的对象 - 具有已定义标识的某物,您可能希望连续的 fetchs 返回相同的实例。
但也许具有相同状态的另一个实例是完全等价的?(这称为值类型对象,如电话号码、地址或屏幕上的点。这些对象除了它们的状态所暗示的标识外,没有其他标识。在后一种情况下,枚举器是每次“获取”它时返回相同的实例还是新创建的相同副本都无关紧要......这些对象通常是不可变的,它们是相同的,它们保持不变,并且它们的功能相同。
评论
IEnumerable<T>
IEnumerator
IEnumerator<T>
IEnumerable
IEnumerable<T>
一段时间以来,我一直在试图找到一个优雅的解决方案来解决这个问题。我希望框架设计者在 IEnumerable 中添加了一些“IsImmutable”或类似的属性 getter,以便可以轻松添加一个 Evaluate (或类似的) 扩展方法,该方法对已经处于“完全评估”状态的 IEnumerable 不做任何事情。
但是,由于不存在,这是我能想到的最好的:
- 我创建了自己的接口来公开 immutability 属性,并在所有自定义集合类型中实现它。
- 我的 Evaluate 的实现 扩展方法知道这一点 新界面以及 子集的不变性 我使用的相关 BCL 类型 最常见的。
- 我避免返回 来自我的“原始”BCL 集合类型 API,以提高我的 Evaluate 方法的效率(至少在针对我自己的代码运行时)。
这相当笨拙,但这是我迄今为止能够找到的侵入性最小的方法,以解决允许 IEnumerable 使用者仅在实际需要时创建本地副本的问题。我非常希望您的问题能从木制品中引出一些更有趣的解决方案......
评论
可以混合建议,可以实现基于泛型的包装类,该类采用 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 项目中
评论