メンバーコレクションを公開するためのReadOnlyCollectionまたはIEnumerable?


124

呼び出しコードがコレクションを反復するだけの場合、内部コレクションをIEnumerableではなくReadOnlyCollectionとして公開する理由はありますか?

class Bar
{
    private ICollection<Foo> foos;

    // Which one is to be preferred?
    public IEnumerable<Foo> Foos { ... }
    public ReadOnlyCollection<Foo> Foos { ... }
}


// Calling code:

foreach (var f in bar.Foos)
    DoSomething(f);

ご覧のとおり、IEnumerableはReadOnlyCollectionのインターフェイスのサブセットであり、ユーザーがコレクションを変更することはできません。したがって、IEnumberableインターフェースで十分な場合は、それを使用します。それはそれについて推論する適切な方法ですか、それとも何か不足していますか?

ありがとう/ Erik


6
.NET 4.5を使用している場合は、新しい読み取り専用のコレクションインターフェイスを試してみてください。ReadOnlyCollection偏執狂の場合は、返されたコレクションを引き続きにラップする必要がありますが、今は特定の実装に縛られていません。
StriplingWarrior

回答:


96

より近代的なソリューション

内部コレクションを変更可能にする必要がない限り、System.Collections.Immutableパッケージを使用してフィールドタイプを不変コレクションに変更し、それを直接公開することができます- Fooもちろん、それ自体は不変であると仮定します。

質問に直接対応するために回答を更新

呼び出しコードがコレクションを反復するだけの場合、内部コレクションをIEnumerableではなくReadOnlyCollectionとして公開する理由はありますか?

それは、呼び出し元のコードをどれだけ信頼するかによって異なります。このメンバーを呼び出すすべてのものを完全に制御していて、コードが使用しないことを保証する場合:

ICollection<Foo> evil = (ICollection<Foo>) bar.Foos;
evil.Add(...);

そうすれば、コレクションを直接返すだけでも害はありません。私は一般的にそれよりも少し偏執的になるようにしています。

同様に、あなたが言うように:必要な だけの場合IEnumerable<T>は、なぜより強いものに縛られるのですか?

元の答え

.NET 3.5を使用している場合は、Skipへの単純な呼び出しを使用して、コピーの作成避け、単純なキャスト回避できます。

public IEnumerable<Foo> Foos {
    get { return foos.Skip(0); }
}

(簡単にラップするためのオプションは他にもたくさんありますSkip。Select/ Whereの優れた点は、反復ごとに無意味に実行するデリゲートがないことです。)

.NET 3.5を使用していない場合は、非常に単純なラッパーを記述して同じことを行うことができます。

public static IEnumerable<T> Wrapper<T>(IEnumerable<T> source)
{
    foreach (T element in source)
    {
        yield return element;
    }
}

15
これにはパフォーマンスの低下があることに注意してください。AFAIKのEnumerable.CountメソッドはIEnumerablesにキャストされたコレクションに対して最適化されていますが、Skip(0)によって生成された範囲に対しては最適化されていません
shojtsy

6
-1私だけですか、それとも別の質問への回答ですか?この「解決策」を知ることは有用ですが、オペレーションはReadOnlyCollectionとIEnumerableを返すことの間の比較を求めました。この回答は、その決定をサポートする理由なしにIEnumerableを返すことをすでに想定しています。
Zaid Masud

1
答えは、ReadOnlyCollectionをにキャストしてから正常にICollection実行されていることを示していますAdd。私の知る限り、それは不可能であるべきです。evil.Add(...)エラーをスローする必要があります。dotnetfiddle.net/GII2jW
Shaun Luttin

1
@ShaunLuttin:はい、まさにその通りです。しかし、私の答えでは、aはキャストしませんReadOnlyCollection- IEnumerable<T>実際には読み取り専用ではないanをキャストします...それは公開する代わりにですReadOnlyCollection
Jon Skeet

1
あは。あなたのパラノイアは、邪悪なキャスティングから保護するために、のReadOnlyCollection代わりにを使用するように導きますIEnumerable
Shaun Luttin、2015年

43

コレクションを反復するだけでよい場合:

foreach (Foo f in bar.Foos)

その後、IEnumerableを返すだけで十分です。

アイテムへのランダムアクセスが必要な場合:

Foo f = bar.Foos[17];

次に、それをReadOnlyCollectionでラップします。


@Vojislav Stojkovic、+ 1。このアドバイスは今日でも当てはまりますか?
w0051977

1
@ w0051977それはあなたの意図に依存します。使用してSystem.Collections.Immutable、あなたはそれへの参照を取得した後、変更に行くことはありません何かを露出しているときにジョンスキートが推奨するようすることは完全に有効です。一方、変更可能なコレクションの読み取り専用ビューを公開している場合、私はおそらくそれを公開しますがIReadOnlyCollection(おそらくこれを書いたときには存在しませんでした)、このアドバイスは依然として有効です。
Vojislav Stojkovic

@VojislavStojkovic bar.Foos[17]と一緒に使用できませんIReadOnlyCollection。唯一の違いは追加のCountプロパティです。 public interface IReadOnlyCollection<out T> : IEnumerable<T>, IEnumerable
Konrad

@Konradそうですね、必要ならbar.Foos[17]、として公開する必要がありますReadOnlyCollection<Foo>。サイズを知り、それを反復処理する場合は、として公開しますIReadOnlyCollection<Foo>。サイズを知る必要がない場合は、を使用してくださいIEnumerable<Foo>。他の解決策やその他の詳細がありますが、それはこの特定の回答の議論の範囲を超えています。
Vojislav Stojkovic

31

これを行う場合、呼び出し元がIEnumerableをICollectionにキャストして変更するのを止めるものは何もありません。ReadOnlyCollectionはこの可能性を排除しますが、リフレクションを介して基になる書き込み可能なコレクションにアクセスすることは依然として可能です。コレクションが小さい場合、この問題を回避する安全で簡単な方法は、代わりにコピーを返すことです。


14
そういう「パラノイアなデザイン」には心から賛成しません。ポリシーを実施するために不適切なセマンティクスを選択することは悪い習慣だと思います。
Vojislav Stojkovic

24
あなたが反対してもそれは間違いではありません。外部配布用のライブラリを設計している場合は、APIを誤用しようとするユーザーによって発生したバグを処理するよりも、偏執的なインターフェースを使用するほうがはるかに便利です。msdn.microsoft.com/en-us/library/k2604h5s(VS.71).aspxの
Stu Mackellar

5
はい、外部配布用のライブラリを設計する特定のケースでは、公開するものすべてに偏執的なインターフェイスを持つことは理にかなっています。それでも、Foosプロパティのセマンティクスが順次アクセスの場合は、ReadOnlyCollectionを使用してからIEnumerableを返します。間違ったセマンティクスを使用する必要はありません;)
Vojislav Stojkovic

9
どちらも行う必要はありません。コピーせずに読み取り専用のイテレータを返すことができます...
Jon Skeet

1
@shojtsyコード行が機能していないようです。 asnull 生成します。キャストは例外をスローします。
Nick Alexeev、

3

私はReadOnlyCollectionをできるだけ使用しないようにします。実際には、通常のListを使用するよりもかなり遅くなります。この例を見てください:

List<int> intList = new List<int>();
        //Use a ReadOnlyCollection around the List
        System.Collections.ObjectModel.ReadOnlyCollection<int> mValue = new System.Collections.ObjectModel.ReadOnlyCollection<int>(intList);

        for (int i = 0; i < 100000000; i++)
        {
            intList.Add(i);
        }
        long result = 0;

        //Use normal foreach on the ReadOnlyCollection
        TimeSpan lStart = new TimeSpan(System.DateTime.Now.Ticks);
        foreach (int i in mValue)
            result += i;
        TimeSpan lEnd = new TimeSpan(System.DateTime.Now.Ticks);
        MessageBox.Show("Speed(ms): " + (lEnd.TotalMilliseconds - lStart.TotalMilliseconds).ToString());
        MessageBox.Show("Result: " + result.ToString());

        //use <list>.ForEach
        lStart = new TimeSpan(System.DateTime.Now.Ticks);
        result = 0;
        intList.ForEach(delegate(int i) { result += i; });
        lEnd = new TimeSpan(System.DateTime.Now.Ticks);
        MessageBox.Show("Speed(ms): " + (lEnd.TotalMilliseconds - lStart.TotalMilliseconds).ToString());
        MessageBox.Show("Result: " + result.ToString());

15
時期尚早の最適化。私の測定では、forループ内で何も行わないと仮定すると、ReadOnlyCollectionの反復処理は通常のリストよりも約36%長くかかります。おそらく、この違いに気付くことはないでしょうが、これがコードのホットスポットであり、パフォーマンスの最後のすべてのビットを絞り込める必要がある場合は、代わりに配列を使用しませんか?それでもまだ速いでしょう。
StriplingWarrior

ベンチマークに欠陥があります。ブランチ予測を完全に無視しています。
Trojaner

0

ユニットテスト中にコレクションを模擬したい場合などのために、インターフェイスを使用したい場合があります。アダプターを使用して独自のインターフェイスをReadonlyCollectionに追加する方法については、私のブログエントリを参照してください。

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