今回は、.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
#継承関係概略
本当に概略です。
この図では、上の段に行くほど継承関係は上になります。
各段について見てみましょう。
-
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>
になっています。
以下のように使えます。
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 をしています。
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 の使用者はリストへの追加・更新・削除が可能になってしまいます。
こういった場合、次のようにすると内容の参照だけができるリストを提供できます。
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>
の変更を監視して、元のコレクションを加工したコレクションを作成できます。
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>
を監視することもできます。
この場合、できるのはアイテムの追加とリセットだけです。
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>>
を監視すれば、コレクションの内容を細かく操作することも可能です。
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よりインストールする必要があります。
不変なコレクションは、状態の管理が容易であるというメリットがあります。
また、コスト面でも有利になるようです(未確認)。
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 を使用して生成する方法もあります。
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();
}
}
}
#おわりに
本当に掻い摘んだだけでしたが、ざっと紹介することができました。
もし知らないものがありましたら、是非お試しください。