コレクション
最終更新:
atachi
コレクションの種類
- WPFでの使用が可能
- データバインディング対応
LinkedList
- 双方向リスト
- 途中に要素を追加したり、削除するなら通常のListより効率がよい。
Queue
- 先入れ先出し(FIFO)リスト
Stack
- 後入れ先出し(LIFO)リスト
IEnumerator<T>
コレクションの運用方法
クラスがpublicとして外部にコレクションを公開するフィールドやプロパティにはICollection / ICollection<T>を使います。
Listなどのコレクションの実態を返してしまうと、クラスが別の実体のコレクションを実装する場合に公開しているプロパティの型と参照している参照先をすべて変更しなければならなくなります。
線形リスト
List<T> | 非推奨。複雑であり、拡張しずらい |
Collection<T> | 推奨。シンプル。 |
ObservableCollection<T> | カスタムコントロールの設計でコレクションのプロパティを公開する場合に使用する |
Collection<T>型 を使うことが推奨されます。
その理由にはCollectionは拡張性を考慮した設計を行っていることと、コレクションとして最低限必要なインターフェースだけを実装している点です。また、Collection<T>→List<T>への変換は簡単に行えます。
Collection<T>型 を使用する場合でも、「Collection<MyClass> arr = new Collection<MyClass>()」とするよりもMyClassクラスを要素に持つCollection<T>型を継承したクラスを作成すべき。(小さな手間以上の利点がある)
class MyClassCollection : Collection<MyClass> {
// 利点として、SortやEmptyなどのメソッドを独自に実装できる
}
下記は、List<T>型 によるコレクション。
List<string> messages = new List<string>();
messages.Add("リストでは");
messages.Add("任意の場所に要素を追加できるよ");
List<string> messages = new List<string>();
messages.Add("リストでは");
messages.Add("任意の場所に要素を追加できるよ");
List<string> messages2 = new List<string>();
messages2.Add("末尾に付け足すこともできる");
messages.AddRange(messages2);
読み取り専用のコレクション
- List<T>.AsReadOnly()
- ReadOnlyCollection
foreachによるコレクションの操作
foreach内での要素追加・削除
foreach内ではコレクションへの要素の追加・削除はできません。
これらを行おうとすると、実行時に「コレクションが変更されました。列挙操作は実行されない可能性があります。」というエラーメッセージとともに例外が投げられます。
理由は説明するまでもなく、コレクションの走査中に内部に格納した要素の位置や順序が変わるということは正しい順序で走査できないためです。
解決策には、走査したいコレクションと同じ順番のコレクション(配列)を作成し、そのコレクションでforeachを実行する事になります。
// MyDataは何らかのクラス(参照クラスまたは値クラス)
ICollection<MyData> mydatas = new Collection<MyData>();
mydatas.Add(new MyData{name="test"});
... // 適当にmydatasコレクションへ要素を追加
// foreachのための配列を作成
MyData[] dmy = new MyData[mydatas.Count];
foreach(var obj in dmy){
if(obj.name.StartWith("t")){
mydatas.Remove(obj);
}
}
foreachを直接mydatasコレクションを使うと上記のコードでは例外が発生する(*1)
CopyToメソッドは任意の配列に要素をコピー(シャローコピー)するメソッド。
これにより新たな配列が確保される。シャローコピーなので、要素のインスタンスは新しくヒープに作成されません。
ただし、調子によって次のようなコードでも...と思うと痛い目に遭う。
// foreachのためのコレクションを作成
ICollection<MyData> secondCollection = new Collection(mydatas);
foreach(var obj in secondCollection){
if(obj.name.StartWith("t")){
mydatas.Remove(obj);
}
}
Collectionクラスのコンストラクタには別のコレクションを引数にする定義があります。
しかし、このコンストラクタは新たなコレクションを作り出す意味ではなく、引数のコレクションをラップするコレクションを作成するためのコンストラクタです。
要素を格納しているリストそのものは、mydatasとsecondCollectionは同じものを示しているため、foreach文が使うsecondCollectionのリストへの変更はそのままmydatasコレクションのリスト操作へ繋がるため、上記のコードではやはり例外が発生します。
foreach文での要素への値設定
foreach文の要素の走査でのループ決定のタイミングについてですが、
foreach文では最初の呼び出し時にコレクションのIEnumerable.GetEnumerator()が呼び出されイテレータが取得されます。
あとはイテレータのMoveNext()を呼び出し次の要素を取得していきます。最後はMoveNext()がnullを返したときにループを終了します。
foreachのループ順序はforeachが決めているのではなく、コレクションが定義したIEnumerableによって決定されます。
そのため、コレクションが持つIEnumerableの実装方法によってforeach内での要素への値設定は不正となる可能性が出てきます。
ICollection<MyData> items = new MyDataCollection(mydatas); // MyDataCollectionは独自に定義したコレクションクラス
int lastOrder = 0;
foreach(var item in items){
lastOrder = item.Order;
item.Order = items[MyDataCollection.Count];
}
潜在的にバグがあるであろうと誰でもわかるように上記のコードとしました。
このバグはコンパイラは報告してくれません(*2)
「誰でもわかる」というのは、MyData.OrderというプロパティがおそらくMyDataCollectionでの要素の位置を示しているであろうことを、開発者は直感で感じ取るからです。
foreach内で要素の順番に関わるプロパティの値を変更することは、プログラムが復帰不可能となるか無限ループに陥ります。
このサンプルのように、明らかに要素の順番に関わっているであろうことがわかる場合ならよいのですが、現実にはそうでない場合がほとんどです。
この問題を引き起こさないためには、開発時にプロジェクト全体で1つのシナリオを定義しておくべきです。
ただし、「foreach内で要素のプロパティ値を設定しない」だけでは不十分です。要素のメソッドの呼び出しも注意が必要です。
foreach内で呼び出した要素のメソッドがOrder値を変更するような実装をしていたら上記と同じことになります。
このようなバグを回避するには、foreachを使わない、もしくはOrderをreadonlyとして宣言しておくなどのプロジェクト全体でこの問題についてのシナリオを議論する必要があります。
要素への逐次処理
C#ではラムダ式が使えるので、配列やコレクション内の要素に対して値を取得したり設定する場合に次のような記述ができます。
int[] array = { 1, 2, 3 };
int sum = 0;
Array.ForEach<int>(array, (i) => // Array.ForEach<>
{
sum += i
});
ただし、この書き方は推奨できません。
デバッグがしづらいです。そのままforeach文として記述できるので、foreach文で書くべきです。
System.Array
C#ではラムダ式が使えるので、System.Arrayが定義する配列に対する操作がかなり簡潔に書けるようになりました(ほとんど、ワンライナーで記述が可能)
string[] names = new[] { "北海道", "本州", "四国", "九州", "沖縄" };
// ■ Findメソッド
string name = Array.Find(list, s => s.Length == 3);
Console.WriteLine(name);
// ■ Exists
// 任意の条件に一致する要素がある場合true
bool b = Array.Exists(list, s => s.StartWith("四") );
Console.WriteLine(b);
// ■FindIndex
// 任意の要素の位置
int index = Array.FindIndex(list, s => s == "本州");
Console.WriteLine(index);
// ■ForEachメソッド
// foreach構文と同じ。
Array.ForEach(list, s => Console.WriteLine(s));
// ■Sortメソッド
Array.Sort(list, (x, y) => x.Length - y.Length);
Array.ForEach(list, s => Console.WriteLine(s));
// ■ConvertAll
string[] list2 = Array.ConvertAll(list, s => s.ToLower());
Array.ForEach(list2, s => Console.WriteLine(s));
文字列配列の文字列一致
文字列型の配列から、特定の文字と完全に一致する要素があるかを検証する処理は簡単にできます。
string[] PrefectureNames = new string[] {
"北海道",
"青森",
"岩手",
"宮城",
"秋田"
};
if(PrefectureNames.Contains("岩手"))
System.Console.WriteLine("「岩手」は配列に存在します");
IList.Containsメソッドを使うと、引数のオブジェクトと一致する要素がある場合にtrueを返します。
- .NET Frameworks3.0以降で有効