C# List<T> の使い方 - 要素の追加・検索・Sort・LINQ

2018/03/27 加筆修正。

はじめに

C#には配列と似た機能で、Listと呼ばれる概念があります。
Listは配列とは違い、動的に要素の追加や削除ができます。
この記事では、Listの基本的な使い方から、Listを用いた検索やソート、LINQまでを紹介します。

記事のコードは、最初のListの説明ではあえてDictionaryを採用しています。
理由としては他のサイトでDictionaryを採用したSampleが少なかった為、Dictionaryを用いたListを取り扱う時の参考にご利用ください。

また、後半のLINQのコードでは、実際にUnityで取り扱う際は、自前のDataオブジェクト等を作成して取り扱う事が圧倒的に多いであろうと思われるので、より実践に近いコードを載せてます。

使い方

まず、usingステートメントに以下を宣言します。

using System.Collections.Generic;

このGeneric(ジェネリック)ですが、List型は、「型パラメータ」を利用します。
List

<T>

がGenericを指しています。
なので、usingステートメントで型パラメータを扱えるようにしておかないといけません。

Listの宣言

Listの生成

初期化する際は、以下の例の様に宣言します。

// int型のListを生成
List<int> listValue = new List<int>();

上記はint型のListですが、\<T>の部分はGenericなので他の型も指定出来ます。
例えば、以下はDictionaryを指定した際のListです。

// Dictionary<string, object>型のListを生成
List<Dictionary<string, object>> result = new List<Dictionary<string, object>>();

Listの初期化

Listは配列と同様、初期化のタイミングで値を入れる事が出来ます。

List<Dictionary<string, string>> result = new List<Dictionary<string, string>> () {
    new Dictionary<string, string>() {{"test-key1", "test-value1"}},
    new Dictionary<string, string>() {{"test-key2", "test-value2"}},
};

要素の追加

末尾に追加

末尾に追加する場合は、「add」を用います。

List<Dictionary<string, string>> result = new List<Dictionary<string, string>> () {
    new Dictionary<string, string>() {{"test-key1", "test-value1"}},
    new Dictionary<string, string>() {{"test-key2", "test-value2"}},
};
// 末尾に追加
result.Add(new Dictionary<string, string>() {{"test-key3", "test-value3"}});
途中に追加

途中に追加する場合は、「Insert」を用います。

List<Dictionary<string, string>> result = new List<Dictionary<string, string>> () {
    new Dictionary<string, string>() {{"test-key1", "test-value1"}},
    new Dictionary<string, string>() {{"test-key2", "test-value2"}},
};
// 2番目の要素に追加
result.Insert(2, new Dictionary<string, string>() {{"test-key3", "test-value3"}});

要素の削除

全て削除
List<Dictionary<string, string>> result = new List<Dictionary<string, string>> () {
    new Dictionary<string, string>() {{"test-key1", "test-value1"}},
    new Dictionary<string, string>() {{"test-key2", "test-value2"}},
};
// 全て削除
result.Clear();
指定したインデックスで削除

配列と同様に、指定したインデックスでの削除も出来ます

List<Dictionary<string, string>> result = new List<Dictionary<string, string>> () {
    new Dictionary<string, string>() {{"test-key1", "test-value1"}},
    new Dictionary<string, string>() {{"test-key2", "test-value2"}},
};
// 1番目の要素を削除
result.RemoveAt(1);

要素の取り出し方

配列と同様な取り出し方
List<Dictionary<string, string>> result = new List<Dictionary<string, string>> () {
    new Dictionary<string, string>() {{"test-key1", "test-value1"}},
    new Dictionary<string, string>() {{"test-key2", "test-value2"}},
};
// 1番目の要素を取り出す
Dictionary<string, string> element = result[1];
foreachと組み合わせる

ListはIEnumerableインタフェースを採用しているので、foreachで取り出すことが出来ます。

List<Dictionary<string, string>> result = new List<Dictionary<string, string>> () {
    new Dictionary<string, string>() {{"test-key1", "test-value1"}},
    new Dictionary<string, string>() {{"test-key2", "test-value2"}},
};

foreach (Dictionary<string, string>element in result) {
   // elementに上から順にresultの要素が入る
}

Linqと組み合わせる

LinqはCollectionを柔軟に扱える機能だけあって、Listとの相性が良いです。
以下はLinqの使い方と活用事例です。
冒頭でも記載しましたが、ここからのコードは、以下のDataクラスを利用します。
※これまでのDictionary型ではありません

public class Data {
    public int ID;
    public string Name;

    public Data(int ID, string Name)  
    {  
        this.ID   = ID;
        this.Name = Name;
    }
}

  

使い方

usingステートメントにLinqを追加します。

using System.Linq;
using System.Collections.Generic;

Listの中を検索して取り出し

Find

FindはCollectionの中から1件だけデータを取得します。
以下の例では、キャラクター毎に一意なIDを振り分けているとして、IDに該当する情報を抜き出すやり方です。

List<Data> result = new List<Data> () {
    new Data(1, "hoge"),
    new Data(2, "fuga")
};
Data chara = result.Find(obj => obj.ID == 2);

Where

Whereは条件に合致するものを取得します。
Findと違うところは、合致するもの全て取得するので、IEnumerableで返却されます。
以下の例では、IDが1となっているキャラクター情報を全て取得します。

List<Data> result = new List<Data> () {
    new Data(1, "hoge1"),
    new Data(1, "hoge2"),
    new Data(2, "fuga")
};
IEnumerable<Data> charas = result.Where(obj => obj.ID == 1);

また、IEnumerableで返却されているので、例えばforeachで結果を取り出します。

foreach(var charaData in charas) {
    Debug.Log(charaData.Name);
}

条件に一致する要素を全て取得する

条件に一致する要素を全て取得したい場合は、TakeWhileを使用します。

List<int> numbers = new List<int> {11, 13, 32, 100, 203};
// 20以下の数を全て取得する
IEnumerable<int> numberResults = numbers.TakeWhile(x => x < 20);

上記の場合、32,100,203が取得できます。

条件に一致する要素を複数個取得する

一方、全てではなく、指定した個数を抜き出したい場合は、LINQのWhereとTakeを使って実現します。

List<int> numbers = new List<int> {11, 13, 32, 100, 203};
// 20以上の数を2個取得する
IEnumerable<int> numberResults = numbers.Where(x => x > 20).Take(2);

上記の場合、32と100が取得されます。

条件に一致する要素を調べる「Any」

条件に一致する要素があるかどうかはAnyを使います。
Anyは条件に一致した要素が見つかった時点で要素の走査をやめるので、高速に動作します。

List<int> numbers = new List<int> {11, 13, 32, 100, 203};
if (numbers.Any(x => x > 100)) {
    // 100以上が存在する
} else {
    // 100以上が存在しない
}

Listの中の存在確認する「Exists」

これまでは、条件に合致した要素の取り出しでしたが、単純にListの中でに要素が含まれているか?を知りたい時は、「Exists」を利用します。

// Listの中でID2のものの存在を確認するサンプル

List<Data> result = new List<Data> () {
    new Data(1, "hoge1"),
    new Data(1, "hoge2"),
    new Data(2, "fuga")
};
if (result.Exists(obj => obj.ID == 2)) {
    Debug.Log("true");
}

Listの中を削除

条件に合致したものを全て削除する「RemoveAll」

条件に合致したものを削除する場合はRemoveAllを使います。
以下の例は、キャラクターIDが1のものを全て削除する例です。

// IDが1のものを探して削除
List<Data> result = new List<Data> () {
    new Data(1, "hoge1"),
    new Data(1, "hoge2"),
    new Data(2, "fuga")
};
result.RemoveAll(obj => obj.ID == 1);

インデックスを指定して削除する「RemoveAt」

削除したい要素がどのインデックスにあるのか分かっている場合は、RemoveAtでインデックスを指定して削除できます。

List<Data> result = new List<Data> () {
    new Data(1, "hoge1"),
    new Data(1, "hoge2"),
    new Data(2, "fuga")
};
result.RemoveAt(1); // hoge2が削除される

範囲を指定して削除する「RemoveRange」

範囲指定して削除したい場合はRemoveRangeを利用します。
RemoveRangeは以下のように2つの引数を要求します。

RemoveRange(インデックス番号, インデックス番号から削除する数);

以下はRemoveRangeのSampleです。

List<Data> result = new List<Data> () {
    new Data(1, "hoge1"),
    new Data(1, "hoge2"),
    new Data(2, "fuga")
};
result.RemoveRange(0, 2); // hoge1とhoge2が削除される

Listの数値系の操作

Listの中の最小値・最大値

最小値・最大値を取得する「Min」「Max」

最小値はMin, 最大値はMaxを使います。

List<int> numbers = new List<int> {11, 13, 32, 100, 203};

// 最小値
int min = numbers.Min();
// 最大値
int max = numbers.Max();
Listの中から、条件に一致した最小値を取得

MinもMaxもLINQで提供されているので、他のLINQと繋いで条件に一致した最小値も以下のように一気に記載できます。
以下はMinのSampleですが、Maxも同様の書き方です。

// 50以上の数の中から最小値を取得するSample
List<int> numbers = new List<int> {11, 13, 32, 100, 203};

// 最小値
int min = numbers.Where(x => x > 50).Min();

上記の場合は、50以上の数の中での最小値を取得するので、100が取得できます。

Listの中から平均値を取得する「Average」

平均値を求めるにはAverageを利用します。
Averageはdouble型を返却します。

List<int> numbers = new List<int> {11, 13, 32, 100, 203};
double average = numbers.Average();

合計値を取得

合計値を求めるにはSumを利用します

List<int> numbers = new List<int> {11, 13, 32, 100, 203};
int average = numbers.Sum();

// 以下はコレクションの例. プロパティにnumberを持ったオブジェクトリストのnumberの合計値を取得
int average = sameObject.Sum(x => x.number);

Listの中で条件に一致した合計数を求める「Count」

条件に一致した要素をカウントするにはCountを使います。

List<int> numbers = new List<int> {11, 13, 32, 100, 203};
// 10以上の数をカウント
int count = numbers.Count(x => x > 20);

List<T> を同じ値で埋める「Enumerable.Repeat」

同じ値で埋めるには、Repeatを使います。

// 10の要素100個分のList
List<int> numbers = Enumerable.Repeat(10, 100).ToList();
// "hoge"の要素100個分のList
List<string> hogeList = Enumerable.Repeat("hoge", 100).ToList();

上記のように、Repeatを用いる事で、同じ値を複数個用意し、その後ToListでListに変換しています。

List<T> を連続した値で埋める「Enumerable.Range」

連続した数値で埋めるには、Rangeを使います

// 1〜100の数値をセット
List<int> numbers = Enumerable.Range(1,100).ToList();

重複を排除した要素を抜き出す「Distinct」

重複を排除したい場合は、Distinctを使います。

// 11の重複が排除される
List<int> numbers = new List<int> {11, 11, 13, 32, 100, 203};
List<int> resultNumbers = numbers.Distinct().ToList();

並べ替え・sort

並べ替え・sortを行うにはOrderBy, OrderByDescendingを使用します。

List<int> numbers = new List<int> {11, 13, 203, 32, 100};
// 昇順
var resultNumbers = numbers.OrderBy(value => value);
// 降順
var resultNumbersDesc = numbers.OrderByDescending(value => value);

応用

応用といっても、これまでの延長線ですが、例えばゲームでHPが60未満のキャラクターに対して回復魔法を実行する機能を作ってみます。
ListとLINQを使ったSampleを強調したい為、すでにHpが減っている状態で、回復が発動したものを想像してください。

public class Charactor {
    public int ID;
    public string Name;
    public int Hp;
    public int Mp;
    public Charactor(int ID, string Name, int hp, int mp) {  
        this.ID   = ID;
        this.Name = Name;
        this.Hp = hp;
        this.Mp = mp;
    }
}

public class Sample : MonoBehaviour {

    List<Charactor> _partyList;

    void Awake() {
        PartySettings();
    }

    void Start () {
        _partyList = PartySettings();
        AutoHeal(_partyList);
    }

    List<Charactor> PartySettings() {
        List<Charactor> partyList = new List<Charactor> () {
            new Charactor(ID: 1, Name: "主人公", hp: 100, mp: 20),
            new Charactor(ID: 2, Name: "戦士", hp: 30, mp: 10),
            new Charactor(ID: 3, Name: "僧侶", hp: 20, mp: 40),
            new Charactor(ID: 4, Name: "魔法使い", hp: 50, mp: 50),
        };

        return partyList;
    }

    void AutoHeal(List<Charactor> partyList) {
        // パーティの中でHpが60未満のキャラを検索して回復
        var healTargets = partyList.Where(chara => chara.Hp <= 60);
        foreach (var chara in healTargets) {
            Debug.Log(chara.Name + " : " + chara.Hp);
            // 10回復
            chara.Hp += 10;
            Debug.Log(chara.Name + "のHPを10回復 -> " + chara.Hp);
        }
    }
}

さいごに

Listはそれ単体でも拡張性の高いデータ構造で大いに役に立ちますが、Linqと組み合わさる事で、簡単に要素へのアクセス・検索ができ、コードの可読性・生産性が大いに向上しますね。
私自身、Linqなどは使い始めたばかりですが、色々使って見て便利なやり方などが見つかったら引き続き記事に記載していこうと思います。