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

发布于 2024-09-29 23:03:23 字数 2031 浏览 9 评论 0原文

考虑这 2 个返回 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()”方法多少次,都会创建单个结果集。

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

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

有什么想法吗?

提前致谢!

Consider this 2 methods that returns 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;
    }

This code shows 2 different behaviors when calling some method of 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
    }

It's simple to choose which behavior I want when my code returns IEnumerables, but how can I explicitly define that some method gets an IEnumerable as parameter that creates a single result set dispite of how many times it calls "First()" method.

Of course, I don't want to force all itens to be created unnecessarily and I want to define the parameter as IEnumerable to say that no item will be included or removed from the collection.

EDIT: Just to be clear, the question is not about how yield works or why IEnumerable can return different instances for each call. The question is how can I specify that a parameter should be a "search only" collection that returns same instances of MyClass when I call methods like "First()" or "Take(1)" several times.

Any ideas?

Thanks in advance!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

听你说爱我 2024-10-06 23:03:23

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

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

如果您的对象本质上是唯一的(即它们没有定义某些基于值的相等),那么每次调用 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)。

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

Of course, I don't want to force all itens to be created unnecessarily

In which case you need to allow the method to create them on demand, and if objects are created on demand (and without some form of cache) they will be different objects (at least in the sense of being different references—the default definition of equality for non-value objects).

If your objects are inherently unique (i.e. they don't define some value based equality) then each call to new will create a different object (whatever the constructor parameters).

So the answer to

but how can I explicitly define that some method gets an IEnumerable as parameter that creates a single result set dispite of how many times it calls "First()" method.

is "you can't" except by creating one set of objects and repeatedly returning the same set, or by defining equality to be something different.


Additional (based on comments). If you really want to be able to replay (for want of a better term) the same set of objects without building the whole collection you could cache want has already been generated and replay that first. Something like:

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

I expect it would be possible to build such a caching wrapper around an iterator as well (while would become foreach over underlying iterator, but you would need to cache that iterator or Skip to the require position if the caller iterated beyond the cahing List).

NB any caching approach would be hard to make thread safe.

谎言月老 2024-10-06 23:03:23

一段时间以来,我一直在努力寻找解决该问题的优雅方法。我希望框架设计者向 IEnumerable 添加一点“IsImmutable”或类似的属性 getter,以便可以轻松添加一个 Evaluate(或类似)扩展方法,该方法不会对已经处于“完全评估状态”的 IEnumerable 执行任何操作“ 状态。

然而,由于它不存在,所以这是我能想到的最好的:

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

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

I've been trying to find an elegant solution to the problem for a while now. I wish that the framework designers had added a little "IsImmutable" or similar property getter to IEnumerable so that one could easily add an Evaluate (or similar) extension method that doesn't do anything for an IEnumerable that is already in its "fully evaluated" state.

However, since that doesn't exist, here's the best I've been able to come up with:

  1. I've created my own interface that exposes the immutability property, and I implement it in all of my custom collection types.
  2. My implementation of the Evaluate
    extension method is aware of this
    new interface as well as the
    immutability of the subset of
    relevant BCL types that I consume
    most frequently.
  3. I avoid returning
    "raw" BCL collection types from my
    APIs in order to increase the efficiency of my Evaluate method (at least when running against my own code).

It's rather kludgy, but it's the least intrusive approach I've been able to find so far to address the problem of allowing an IEnumerable consumer to create a local copy only when this is actually necessary. I very much hope that your question lures some more interesting solutions out of the woodwork...

随梦而飞# 2024-10-06 23:03:23

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

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

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

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

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

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

Unless I'm misreading you, your question may be caused by a misunderstanding.. Nothing ever returns an IEnumerable. The first case returns an Enumerator, which implements foreach, allowing you to get instances of MyClass, one at a time. It, (the function return value) is typed as IEnumerable to indicate that it supports the foreach behavior (and a few others)

The second function actually returns a List, which of course also supports IEnumerable (foreach behavior). But it is an actual concrete collection of MyClass Objects, created by the method you called (the second one)

The first method doesn't return any MyClass Objects at all, it returns that enumerator object, which is created by the dotNet framework and coded behind the scenes to instantiate a new MyClass object each time you iterate against it.

EDIT: More detail
A more important distinction is whether or not you want the items to be statefully held in place for you within the class, while you iterate, or whether you want them created for you when you iterate.

Another consideration is.. are the items you wish returned to you already in existence somewhere else? i.e., is this method going to iterate through a set (or filtered subset) of some existing collection? or is it creating the items on the fly? if the latter, does it matter if the item is the exact same instance each time you "get" it?
For objects defined t orepresent things that could be called an entity - ssomething with a defined identity, you probably want successive fetches to return the same instance.

But maybe another instance with the same state is totally equivilent? (This would be called a value type object, like a telephone Number, or an address, or a point on the screen. Such objects have no identity except that implied by their state. In this latter case, it doesn't matter if the enumerator returns the same instance or a newly created identical copy each time you "get" it... Such objects are generally immutable, they are the same, they stay the same, and they function identically.

明月松间行 2024-10-06 23:03:23

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

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

爱她像谁 2024-10-06 23:03:23

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

Then you need to cache the result, an IEnumerable is always re-executed when you call something that iterates over it. I tend to use:

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

Granted on the other side (say for your example) you can have the ToList Call at the end here will iterate and create a list that is stored, and yieldResult will still be an IEnumerable without an issue.

[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);
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文