4

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 を返すときに必要な動作を選択するのは簡単ですが、「First()」メソッドを呼び出す回数に関係なく、単一の結果セットを作成するパラメーターとして一部のメソッドが IEnumerable を取得することを明示的に定義するにはどうすればよいでしょうか。

もちろん、すべての itens を不必要に強制的に作成したくはありません。また、パラメーターを IEnumerable として定義して、コレクションに含まれるアイテムやコレクションから削除されるアイテムがないことを示したいと考えています。

EDIT:明確にするために、問題はyieldがどのように機能するか、またはIEnumerableが呼び出しごとに異なるインスタンスを返す理由についてではありません。問題は、「First()」や「Take(1)」などのメソッドを数回呼び出したときに、パラメーターが MyClass の同じインスタンスを返す「検索のみ」のコレクションになるように指定するにはどうすればよいかということです。

何か案は?

前もって感謝します!

4

5 に答える 5

2

もちろん、すべてのitenを不必要に作成するように強制したくはありません

この場合、メソッドがオンデマンドでオブジェクトを作成できるようにする必要があります。オブジェクトがオンデマンドで作成される場合(および何らかの形式のキャッシュなしで)、オブジェクトは異なるオブジェクトになります(少なくとも、異なる参照であるという意味で、デフォルトの定義は非値オブジェクトの同等性)。

オブジェクトが本質的に一意である場合(つまり、値ベースの同等性を定義していない場合)、を呼び出すたびnewに異なるオブジェクトが作成されます(コンストラクターパラメーターが何であれ)。

だから答えは

しかし、「First()」メソッドを何度呼び出しても、単一の結果セットを作成するパラメーターとしてIEnumerableを取得するメソッドを明示的に定義するにはどうすればよいですか。

1つのオブジェクトのセットを作成して同じセットを繰り返し返すか、等式を別のものに定義することを除いて、「できません」です。


追加(コメントに基づく)。本当に(より良い用語が必要なために)再生できるようにしたい場合は、キャッシュしたいコレクション全体を構築せずに同じオブジェクトのセットがすでに生成されており、最初にそれを再生します。何かのようなもの:

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なるイテレータを超えますが、呼び出し元がcahingを超えて反復した場合は、foreachそのイテレータをキャッシュするか、require位置にキャッシュする必要があります)。SkipList

注意:キャッシングアプローチはスレッドセーフにするのが難しいでしょう。

于 2010-10-29T13:26:15.737 に答える
1

私があなたを誤解していない限り、あなたの質問は誤解が原因である可能性があります..IEnumerableを返すものはありません。最初のケースは、foreach を実装する Enumerator を返し、MyClass のインスタンスを一度に 1 つずつ取得できるようにします。これ (関数の戻り値) は IEnumerable として型指定され、foreach の動作 (およびその他の動作) をサポートしていることを示します。

2 番目の関数は、実際には List を返します。もちろん、これは IEnumerable (foreach 動作) もサポートしています。しかし、これは MyClass オブジェクトの実際の具体的なコレクションであり、呼び出したメソッド (2 番目のもの) によって作成されます。

最初のメソッドは MyClass オブジェクトをまったく返さず、その列挙子オブジェクトを返します。これは、dotNet フレームワークによって作成され、舞台裏でコード化されて、反復処理を行うたびに新しい MyClass オブジェクトをインスタンス化します。

EDIT:詳細より重要な違いは、反復中にアイテムをクラス内でステートフルに保持するかどうか、または反復時にアイテムを作成するかどうかです。

もう1つの考慮事項は..返品を希望するアイテムは、すでにどこか別の場所に存在していますか? つまり、このメソッドは、既存のコレクションのセット (またはフィルタリングされたサブセット) を反復処理しますか? それともその場でアイテムを作成していますか?後者の場合、「取得」するたびにアイテムがまったく同じインスタンスであるかどうかは問題ですか? エンティティーと呼ばれるものを表すために定義されたオブジェクト(定義されたIDを持つもの) の場合、おそらく、連続するフェッチで同じインスタンスを返す必要があります。

しかし、同じ状態の別のインスタンスが完全に同等である可能性はありますか? (これは、電話番号、住所、または画面上の点などの値型オブジェクトと呼ばれます。このようなオブジェクトには、状態によって暗示される以外のアイデンティティはありません。後者の場合、列挙子は、「取得」するたびに同じインスタンスまたは新しく作成された同一のコピーを返します...そのようなオブジェクトは一般に不変であり、同じであり、同じままであり、同じように機能します。

于 2010-10-29T13:28:49.513 に答える
1

提案を組み合わせることができます。ジェネリックベースのラッパー クラスを実装できます。これは IEnumerable を受け取り、次のたびにキャッシュを構築する新しいものを返し、さらなる列挙で必要に応じて部分キャッシュを再利用します。簡単ではありませんが、必要に応じて一度だけオブジェクトを作成します (実際には、オンザフライでオブジェクトを構築する Iterator の場合のみ)。最も困難な部分は、部分キャッシュから元の列挙子に切り替えるタイミングと、それをトランザクション (一貫性) にする方法を確認することです。

テスト済みのコードで更新します。

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 プロジェクトにも表示される可能性があります

于 2010-10-29T14:15:56.900 に答える
1

私はしばらくの間、問題に対するエレガントな解決策を見つけようとしています。フレームワークの設計者が「IsImmutable」または同様のプロパティ ゲッターを IEnumerable に少し追加して、すでに「完全に評価された」IEnumerable に対して何もしない Evaluate (または同様の) 拡張メソッドを簡単に追加できるようにしてほしい" 州。

ただし、それは存在しないため、これが私が思いついた最高のものです。

  1. 不変プロパティを公開する独自のインターフェイスを作成し、それをすべてのカスタム コレクション型に実装しました。
  2. 私の Evaluate 拡張メソッドの実装は、この新しいインターフェイスと、最も頻繁に使用する関連する BCL 型のサブセットの不変性を認識しています。
  3. Evaluate メソッドの効率を高めるために (少なくとも自分のコードに対して実行する場合)、API から "生の" BCL コレクション型を返すことは避けています。

これはかなり厄介ですが、実際に必要な場合にのみ 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 に答える