Help us understand the problem. What is going on with this article?

意外と知らない!? C#の便利なコレクション!

今回は、.NET Framework のコレクションについて、少し見ていきたいと思います。
Reactive なコレクションについても取り上げてみます。
ご興味がおありの方は是非お付き合いください。

目次

  • 継承関係概略
  • IList<T>ICollection<T>
  • System.Linq.ILookup<TKey, TElement>
  • System.Collections.Concurrent
  • System.Collections.ObjectModel
  • ReadOnlyCollection<T>
  • ObservableCollection<T>
  • ReadOnlyReactiveCollection<T>
  • System.Collections.Immutable

継承関係概略

本当に概略です。

Collections.jpg

この図では、上の段に行くほど継承関係は上になります。
各段について見てみましょう。

  • 4段目(一番下)
    この段は具象クラスですね。
    実際に働くのはこれらのクラスです。

  • 3段目
    具体的な機能を持ったコレクションを規定するインターフェイス群です。

  • 2段目
    コレクション全体を統括する ICollection です。

  • 1段目(一番上)
    列挙可能型全体を統括する IEnumerable です。

上に行くほどインターフェイスの持つ機能は少なく、抽象化されてゆくのが分かるかと思います。

IList<T>ICollection<T>

両者とも似たような名前なので、どう違うのかわからないという方も多いのではないでしょうか。
前掲の図の通り、ICollection<T> の方がより抽象的なインターフェイスなのですが、では IList<T> にできて ICollection<T> にできないこととは何でしょうか。

答えは、IList<T> は添字を使用したアクセスができる、ということです。
以下のメソッドは、IList<T> 独自のものです。
- [ ]
- IndexOf
- Insert
- RemoveAt
どれも添字関係のメソッドですね。
これらは ICollection<T> では使えませんので、ご注意ください。

System.Linq.ILookup<TKey, TElement>

Dictionary<TKey, TValue>TValue に複数の値を格納したい場合って結構ありますよね。
そんなとき、

Dictionary<int, List<string>>

というコードを書きがちです。
要件によってはそれでいいのかもしれませんが、LINQ の ILookup<TKey, TElement> を使うとすっきり書けます。

ILookup<TKey, TElement>IDictionary<TKey, IEnumerable<TElement>>

といった感じで、Value が IEnumerable<TElement> になっています。
以下のように使えます。

Sample.cs
using System;
using System.Linq;

namespace ConsoleSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var モブリスト = new[]
            {
                new { 戦闘力 = 0, 名前 = "羊" },  // 戦闘力をキーにすると重複する
                new { 戦闘力 = 0, 名前 = "牛" },
                new { 戦闘力 = 0, 名前 = "豚" },
                new { 戦闘力 = 0, 名前 = "鶏" },
                new { 戦闘力 = 1, 名前 = "ゾンビ" },
                new { 戦闘力 = 1, 名前 = "蜘蛛" },
                new { 戦闘力 = 2, 名前 = "スケルトン" },
                new { 戦闘力 = 2, 名前 = "匠" },
                new { 戦闘力 = 3, 名前 = "エンダーマン" },
            };

            var lookup = モブリスト.ToLookup(x => x.戦闘力, x => x.名前);  // Lookupを作成

            foreach (var name in lookup[2])  // 添字にキーを指定すると対応する IEnumerable<TElement> が得られる
            {
                Console.WriteLine(name);
            }

            foreach (IGrouping<int, string> group in lookup)  // lookupを列挙すると IGrouping<TKey, TElement> になる
            {
                foreach (string name in group)  // IGrouping<TKey, TElement> から IEnumerable<TElement> に変換可能
                {
                    Console.WriteLine(name);
                }
            }

            Console.ReadLine();
        }
    }
}

System.Collections.Concurrent

スレッドセーフなコレクションが用意されています。
以下の例では ConcurrentQueue<T> に対し複数のスレッドから Enqueue とDequeue をしています。

Sample.cs
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var queue = new ConcurrentQueue<int>();

            foreach (var i in Enumerable.Range(0, 10))
            {
                Task.Run(() =>
                {
                    foreach (var j in Enumerable.Range(0, 10))
                    {
                        queue.Enqueue(i);
                        Thread.Sleep(0);
                    }
                });
            }

            int count = 0;
            while (count < 100)
            {
                int result;
                if (queue.TryDequeue(out result))
                {
                    Console.WriteLine(result);
                    ++count;
                }
                Thread.Sleep(1);
            }

            Console.ReadLine();
        }
    }
}

System.Collections.ObjectModel

ReadOnly なコレクションと、Observable なコレクションが用意されています。
以下で順に見ていくことにします。

ReadOnlyCollection<T>

System.Collections.ObjectModel.ReadOnlyCollection<T> は、任意の IList<T> をラッピングし、そのリストへの読み取り専用なアクセスを提供します。
コンストラクタで IList<T> を渡すことでラッピングができます。
読み取り専用にすると何がいいのか。
それは、リストを外部へ公開したいけれど書き換えはしてほしくない、という場合に、

IList<string> StringList { get; }

と getter のみを提供したとします。
getter のみなので、使用者がリスト本体を丸ごと置き換えることはできません。
しかし、リストの中身は書き換え可能なので、getter の使用者はリストへの追加・更新・削除が可能になってしまいます。
こういった場合、次のようにすると内容の参照だけができるリストを提供できます。

Sample.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace ConsoleSample
{
    class SampleClass
    {
        private IList<string> _stringList;
        public IReadOnlyList<string> StringList { get; }

        public SampleClass()
        {
            _stringList = new List<string>();
            StringList = new ReadOnlyCollection<string>(_stringList);
        }
    }
}

クラス内部からは _stringList を使用し、クラス外部には StringList を公開しています。

ちなみに、ReadOnlyCollection<T> という名前ですが、IReadOnlyList<T> を実装したリストであることにご注意ください。
このクラスと同様に、ReadOnlyDictionary<T> もあります。

ObservableCollection<T>

コレクションのアイテムに対する、追加、削除、変更、移動操作があった場合、またはリスト全体が更新されたとき、CollectionChanged イベントを発生させることができるコレクションです。
「Observable」という名前がついていますが、IObservable<T>IObserver<T> とは直接の関連はありません。
むしろ、INotifyPropertyChanged に近いイメージです。
ObservableCollection<T>INotifyPropertyChanged も実装していますが、そのイベントを直接購読することはできないようになっています。)

CollectionChanged イベントについて見てみましょう。
このイベントは、System.Collections.Specialized.INotifyCollectionChanged インターフェイスのものです。
ハンドラは以下の形式です。

public delegate void NotifyCollectionChangedEventHandler(
    object sender,
    NotifyCollectionChangedEventArgs e
);

NotifyCollectionChangedEventArgs には、

  • Action
  • NewItems
  • NewStartingIndex
  • OldItems
  • OldStartingIndex

というプロパティが定義されていて、コレクションの変更内容を必要十分に取得できるようになっています。

WPF でコレクションをデータバインドする際に使用できます。

ReadOnlyReactiveCollection<T>

neueccさん、xin9leさん、okazukiさんが開発しているライブラリ「ReactiveProperty」の中に含まれているコレクションです。
NuGetよりプロジェクトにインストール可能です。
このライブラリは「System.Reactive」(通称:Rx)を参照する形で作られています。
ReactiveProperty<T> は、IObservable<T> を監視して WPF や UWP のデータバインディングに使用できるようにするものです。
ReadOnlyReactiveCollection<T> はそのコレクション版ですね。

下記のように、ObservableCollection<T> の変更を監視して、元のコレクションを加工したコレクションを作成できます。

Sample.cs
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using Reactive.Bindings;

namespace ConsoleSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var oc = new ObservableCollection<int>();
            var rc = oc.ToReadOnlyReactiveCollection(x => x * 2);

            oc.Add(1);
            oc.Add(2);
            oc.Add(3);

            // System.Interactive の ForEach
            rc.ForEach(Console.WriteLine);  // 2 4 6

            Console.ReadLine();
        }
    }
}

IObservable<T> を監視することもできます。
この場合、できるのはアイテムの追加とリセットだけです。

Sample.cs
using System;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Reactive.Bindings;

namespace ConsoleSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var sbj = new Subject<int>();
            var rc = sbj.ToReadOnlyReactiveCollection();

            sbj.OnNext(1);
            sbj.OnNext(2);
            sbj.OnNext(3);
            sbj.OnCompleted();

            // System.Interactive の ForEach
            rc.ForEach(Console.WriteLine);  // 1 2 3

            Console.ReadLine();
        }
    }
}

IObservable<CollectionChanged<T>> を監視すれば、コレクションの内容を細かく操作することも可能です。

Sample.cs
using System;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Reactive.Bindings;

namespace ConsoleSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var sbj = new Subject<CollectionChanged<int>>();
            var rc = sbj.ToReadOnlyReactiveCollection<int>();

            sbj.OnNext(CollectionChanged<int>.Add(0, 0));
            sbj.OnNext(CollectionChanged<int>.Add(1, 1));
            sbj.OnNext(CollectionChanged<int>.Add(2, 2));
            sbj.OnNext(CollectionChanged<int>.Add(3, 3));
            sbj.OnNext(CollectionChanged<int>.Remove(0, rc[0]));
            sbj.OnNext(CollectionChanged<int>.Replace(1, 5));
            sbj.OnCompleted();

            // System.Interactive の ForEach
            rc.ForEach(Console.WriteLine);  // 1 5 3

            Console.ReadLine();
        }
    }
}

ReadOnlyReactiveCollection<T> は MVVM パターンに適用すると威力を発揮します。

System.Collections.Immutable

最後に紹介するのは、イミュータブル(不変)なコレクションです。
標準ライブラリではないので、NuGetよりインストールする必要があります。

不変なコレクションは、状態の管理が容易であるというメリットがあります。
また、コスト面でも有利になるようです(未確認)。

Sample.cs
using System;
using System.Collections.Immutable;

namespace ConsoleSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var list = ImmutableList.Create<int>(1, 2, 3);
            var anotherList = list.Add(4);  // listとは別のインスタンスが返る

            list.ForEach(Console.WriteLine);  // 1 2 3
            anotherList.ForEach(Console.WriteLine);  // 1 2 3 4

            Console.ReadLine();
        }
    }
}

下記のように、Builder を使用して生成する方法もあります。

Sample.cs
using System;
using System.Collections.Immutable;

namespace ConsoleSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var builder = ImmutableList.CreateBuilder<int>();

            builder.Add(1);
            builder.Add(2);
            builder.Add(3);
            builder.RemoveAt(1);
            builder.Add(5);

            var list = builder.ToImmutable();

            list.ForEach(Console.WriteLine);  // 1 3 5

            Console.ReadLine();
        }
    }
}

おわりに

本当に掻い摘んだだけでしたが、ざっと紹介することができました。
もし知らないものがありましたら、是非お試しください。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away