イテレーターには非破壊的な暗黙の契約がありますか?


11

スタックやキューなどのカスタムデータ構造を設計しているとしましょう(たとえば、論理的に同等なメソッドpushpop破壊的なアクセサーメソッドを持つ他の任意の順序付きコレクションなど)。

IEnumerable<T>各反復でポップされたこのコレクションに対して(具体的には.NETで)イテレータを実装している場合、それはブレークIEnumerable<T>の暗黙のコントラクトですか?

IEnumerable<T>この暗黙の契約を持っていますか?

例えば:

public IEnumerator<T> GetEnumerator()
{
    if (this.list.Count > 0)
        yield return this.Pop();
    else
        yield break;
}

回答:


12

破壊的列挙子は最小驚きの原理に違反していると思います。人為的な例として、汎用の便利な機能を提供するビジネスオブジェクトライブラリを想像してください。ビジネスオブジェクトのコレクションを更新する関数を純粋に作成します。

static void UpdateStatus<T>(IEnumerable<T> collection) where T : IBusinessObject
{
    foreach (var item in collection)
    {
        item.Status = BusinessObjectStatus.Foo;
    }
}

私の実装またはあなたの実装について何も知らない別の開発者が、両方を使用することにしました。彼らは結果に驚かれるでしょう:

//developer uses your type without thinking about the details
var collection = GetYourEnumerableType<SomeBusinessObject>();

//developer uses my function without thinking about the details
UpdateStatus<SomeBusinessObject>(collection);

開発者が破壊的な列挙を認識していても、コレクションをブラックボックス関数に渡すときの影響については考えないかもしれません。の著者として、UpdateStatusおそらく私の設計では破壊的な列挙を考慮しないでしょう。

ただし、これは暗黙の契約にすぎません。などの.NETコレクションは、「列挙子がインスタンス化された後にコレクションが変更されました」Stack<T>と明示的なコントラクトを強制しInvalidOperationExceptionます。真のプロは、自分のコードではないコードに対して空虚な態度を持っていると主張することができます。破壊的な列挙子の驚きは、最小限のテストで発見されるでしょう。


1
+1-「最小驚tonの原理」については、人々にそれを引用します。

+1これも基本的に私が考えていたものです。また、破壊的であることは、LINQ IEnumerable拡張機能の期待される動作を破壊する可能性があります。通常の/破壊的な操作を実行せずに、このタイプの順序付きコレクションを反復処理するのは奇妙に感じます。
スティーブンエバーズ

1

インタビューのために私に与えられた最も興味深いコーディングの課題の1つは、機能的なキューを作成することでした。要件は、エンキューを呼び出すたびに、古いキューと末尾の新しいアイテムを含む新しいキューが作成されることでした。また、デキューは、新しいキューとデキューされたアイテムを出力パラメータとして返します。

この実装からIEnumeratorを作成することは非破壊的です。また、パフォーマンスの高い機能スタックを実装するよりも、パフォーマンスの高い機能キューを実装する方がはるかに難しいことを教えてください(スタックプッシュ/ポップの両方がテールで機能し、キューエンキューがテールで機能し、デキューがヘッドで機能します)。

私のポイントは...独自のPointerメカニズム(StackNode <T>)を実装し、Enumeratorで機能セマンティクスを使用して非破壊的なStack Enumeratorを作成するのは簡単です。

public class Stack<T> implements IEnumerator<T>
{
  private class StackNode<T>
  {
    private readonly T _data;
    private readonly StackNode<T> _next;
    public StackNode(T data, StackNode<T> next)
    {
       _data=data;
       _next=next;
    }
    public <T> Data{get {return _data;}}
    public StackNode<T> Next{get {return _Next;}}
  }

  private StackNode<T> _head;

  public void Push(T item)
  {
    _head =new StackNode<T>(item,_head);
  }

  public T Pop()
  {
    //Add in handling for a null head (i.e. fresh stack)
    var temp=_head.Data;
    _head=_head.Next;
    return temp;
  }

  ///Here's the fun part
  public IEnumerator<T> GetEnumerator()
  {
    //make a copy.
    var current=_head;
    while(current!=null)
    {
       yield return current.Data;
       current=_head.Next;
    }
  }    
}

注意すべき点がいくつかあります。ステートメントcurrent = _headの前にプッシュまたはポップする呼び出し。completesは、マルチスレッドがない場合とは異なる列挙型のスタックを提供します(ReaderWriterLockを使用してこれを防ぐことができます)。StackNodeのフィールドを読み取り専用にしましたが、もちろんTが可変オブジェクトである場合、その値を変更できます。StackNodeをパラメーターとして使用するStackコンストラクターを作成する場合(および、ノードに渡されたものにheadを設定する場合)。この方法で構築された2つのスタックは、互いに影響しません(前述の可変Tを除く)。1つのスタックで必要なすべてをプッシュおよびポップできますが、他のスタックは変更されません。

そして、私の友人は、スタックを非破壊的に列挙する方法です。


+1:非破壊的なStackおよびQueue列挙子は同様に実装されています。カスタム比較器を使用して、PQ /ヒープのラインに沿ってさらに考えていました。
スティーブンエバーズ

1
私は同じアプローチを使用すると言います...しかし、以前にFunctional PQを実装したとは言えません... この記事では、順序付けされたシーケンスを非線形近似として使用することを提案しているようです
マイケル・ブラウン

0

「単純な」ものを維持しようとする試みには、.NETだけで呼び出すことによって列挙されているもののためのインタフェースの1つの汎用/非ジェネリック組ありGetEnumerator()、その後使用MoveNextしてCurrent、オブジェクトの少なくとも4種類があるにもかかわらず、オブジェクト受信そこにはこれらのメソッドのみをサポートする必要があります:

  1. 少なくとも1回列挙できるが、必ずしもそれ以上ではないもの。
  2. フリースレッドコンテキストで任意の回数列挙できるが、毎回異なるコンテンツを任意に生成できるもの
  3. フリースレッドコンテキストで任意の回数列挙できるものですが、それらを繰り返し列挙するコードが変更メソッドを呼び出さない場合、すべての列挙が同じコンテンツを返すことを保証できます。
  4. 任意の回数列挙することができ、存在する限り毎回同じコンテンツを返すことが保証されているもの。

上位の定義の1つを満たすインスタンスは下位の定義もすべて満たすことになりますが、上位の定義の1つを満たすオブジェクトを必要とするコードは、下位の定義の1つが与えられると壊れる可能性があります。

マイクロソフトはIEnumerable<T>、実装するクラスが2番目の定義を満たす必要があるが、それ以上のものを満たす義務はないと判断したようです。おそらく、最初の定義を満たすことができる何かが実装する必要があることを多くの理由がないIEnumerable<T>というよりはIEnumerator<T>foreachループが受け入れられる場合IEnumerator<T>は、後者のインターフェイスを単に実装するために一度だけ列挙できるものには意味があります。残念ながら、IEnumerator<T>C#およびVB.NETで実装するタイプは、GetEnumeratorメソッドを持つタイプよりも使いにくいです。

いずれにせよ、異なる保証を行うことができるものに異なる列挙型が存在する場合、および/またはIEnumerable<T>どのような保証を実装するかをインスタンスに尋ねる標準的な方法があれば有用ですが、そのような型はまだ存在しません。


0

これは、リスコフの代替原則の違反であり、

プログラム内のオブジェクトは、そのプログラムの正確さを変更することなく、サブタイプのインスタンスで置き換え可能でなければなりません。

私のプログラムで複数回呼び出される次のようなメソッドがあった場合、

PrintCollection<T>(IEnumerable<T> collection)
{
    foreach (T item in collection)
    {
        Console.WriteLine(item);
    }
}

プログラムの正確さを変更せずにIEnumerableを交換できるはずですが、破壊的なキューを使用すると、プログラムの動作が大幅に変更されます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.