4

考虑这两个返回 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 的某些方法时显示了 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()”方法,但它创建了一个结果集。

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

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

有任何想法吗?

提前致谢!

4

5 回答 5

2

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

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

如果您的对象本质上是唯一的(即它们没有定义一些基于值的相等性),那么每次调用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底层迭代器,但Skip如果调用者迭代超出缓存,则需要缓存该迭代器或缓存到所需位置List)。

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

于 2010-10-29T13:26:15.737 回答
1

You can mix the suggestions, you can implement an wrapper class, generics-based, that takes the IEnumerable and returns a new one that constructs a cache on each next, and reuses the partial cache as needed on further enumerations. It is not easy, but will create objects (in truth only for Iterators that construct objects on-the-fly) only once and as needed. The hardest part is to be sure when to switch from the partial cache back to the original enumerator and how to make it transactional (consistent).

Update with tested code:

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);
    }
}

With this you can now add a new Test that shows it works:

    [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
    }

Code will be incorporated at my project http://github.com/monoman/MSBuild.NUnit , may later appear in the Managed.Commons project too

于 2010-10-29T14:15:56.900 回答
1

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

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

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

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

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

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

于 2010-10-29T13:28:49.513 回答
1

一段时间以来,我一直试图找到一个优雅的解决方案。我希望框架设计者在 IEnumerable 中添加了一点“IsImmutable”或类似的属性获取器,以便人们可以轻松地添加一个 Evaluate(或类似)扩展方法,该方法对已经“完全评估”的 IEnumerable 没有任何作用“ 状态。

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

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

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

于 2010-10-29T14:08:24.640 回答
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);
}
于 2010-10-29T13:26:22.600 回答