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

时间:2010-10-29 13:13:33

标签: c# .net linq linq-to-objects design-by-contract

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

    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的某个方法时显示了两种不同的行为:

    [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()”方法的次数

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

编辑:为了清楚起见,问题不在于收益率如何工作或IEnumerable为每次调用返回不同实例的原因。问题是如何指定参数应该是一个“仅搜索”集合,当我多次调用“First()”或“Take(1)”等方法时,它返回MyClass的相同实例。

有什么想法吗?

提前致谢!

5 个答案:

答案 0 :(得分:2)

  

当然,我不想强​​迫所有的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;
  }
}

我希望围绕迭代器构建这样的缓存包装器是可能的(while将通过底层迭代器变为foreach,但是您需要缓存该迭代器或{{1}如果调用者迭代超出cahing Skip),则转到require位置。

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

答案 1 :(得分:1)

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

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

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

编辑:更多细节    一个更重要的区别是,您是否希望在迭代时迭代,或者是否希望在迭代时为您创建项目,从而在课堂上为您保留状态。

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

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

答案 2 :(得分:1)

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

然而,由于这不存在,这是我能够提出的最好的:

  1. 我创建了自己的界面来公开immutability属性,并在所有自定义集合类型中实现它。
  2. 我对Evaluate的实施 扩展方法意识到这一点 新的界面以及 不可变性的子集 我消耗的相关BCL类型 最常见的。
  3. 我避免回来 来自我的“原始”BCL收藏类型 API,以提高我的Evaluate方法的效率(至少在我自己的代码运行时)。
  4. 它相当kludgy,但它是迄今为止我能够找到的最不具侵入性的方法来解决允许IEnumerable使用者仅在实际需要时才创建本地副本的问题。我非常希望你的问题能从木工中找出一些更有趣的解决方案......

答案 3 :(得分:1)

你可以混合这些建议,你可以实现一个包装类,基于泛型,它接受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项目中

答案 4 :(得分:0)

然后你需要缓存结果,当你调用迭代它的东西时,总会重新执行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);
}