⛳
【C#】Listの使い方と落とし穴
使い方と落とし穴シリーズ一覧
一言
-
HashSet
,Dictionary
とかコレクション色々あってわからんわ~って方は
このList
から慣れた方がイイと思います(経験談)
List
を使用するケース
- 要素数が可変で順序を保ちたい
-
読み取って使用する頻度が高い
- ランダムアクセスがO(1)
-
値型を大量保持する
- 要素が連続したメモリ領域に配置され、キャッシュヒット率が高い
List
の使い方
基本(生成, 追加, 削除, 要素数と容量)
var enemies = new List<Enemy>(100); //初期要素数を指定できる
//追加(リスト最後尾に追加される)
var slime = new Enemy("Slime");
var ogre = new Enemy("Ogre");
var dragon = new Enemy("Dragon");
enemies.Add(slime); //単発で追加
enemies.AddRange(new[] { ogre, dragon }); //まとめて追加
//削除
enemies.Remove(ogre); //値が一致するものを削除
enemies.RemoveAt(0); //インデックス指定で削除
enemies.RemoveAll(e => e.IsDead); //特定条件のものを全て削除
enemies.Clear(); //要素のみ消える(Capacity は残る)
enemies.TrimExcess(); //容量の余りを縮小し、ヒープ開放
//要素数と容量
enemies.Count; //要素数
enemies.Capacity; //容量
任意位置へ挿入
var stages = new List<string>{"1-1", "1-3"};
stages.Insert(1, "1-2");
検索
if (inventory.Contains(keyItem)) {...}
//Rank は列挙体
var boss = enemies.Find(e => e.Rank == Rank.Boss);
int bossIndex = enemies.FindIndex(e => e.Rank == Rank.Boss);
BinarySearch
- ソート済みの
List
で使用することができる - 戻り値について、
- 正の数:見つかった要素のインデックス
- 負の数:ビット反転の形式で挿入すべき位置を返す
- 例:リスト[100,200,300] で-3が返る。これは挿入位置が2(300の前)のため、~2 == -3
var sortedScores = new List<int> { 100, 200, 300, 400, 500 };
int index = sortedScores.BinarySearch(300);
// index == 2 (要素 300 が見つかった位置)
int notFound = sortedScores.BinarySearch(250);
// 要素が見つからない場合、負の値を返す。 ~(挿入位置) の形式。
並べ替え
-
Sort()
は安定ソートでない
var rankings = new List<int>{1200, 800, 3000};
rankings.Sort(); //昇順に整列
rankings.Reverse(); //降順に整列
その他
便利なものを抜粋
カスタムソート
-
Sort()
は既定で要素のIComparable<T>
実装を使う - しかし、以下のように独自の並べ替え基準を指定することもできる
//クラスのメンバ変数を用いてソート(スコア昇順)
players.Sort((a, b) => a.Score.CompareTo(b.Score));
//文字列を長さ → アルファベット順でソート
class LengthThenAlpha : IComparer<string>
{
public int Compare(string? x, string? y)
{
int len = x!.Length.CompareTo(y!.Length);
return len == 0 ? string.CompareOrdinal(x, y) : len;
}
}
words.Sort(new LengthThenAlpha());
ConvertAll
- 型変換しながらコピーができる
var names = new List<string> { "100", "200", "300" };
List<int> numbers = names.ConvertAll(s => int.Parse(s));
AsReadOnly
- 読み取り専用Listを提供
- 元のリストが変更されると、それも反映される
var readOnly = items.AsReadOnly();
Exists
- 条件に合う要素があるか判定
if (players.Exists(p => p.Score >= 1000)) { … }
TrueForAll
- 全ての要素が条件を満たすか判定
bool allDead = enemies.TrueForAll(e => e.IsDead);
オイラはこんな落とし穴に出会った
Add
Capacity 無視で大量 ❌ 再確保が頻発し GC が起きやすい
- さすがにこれだけでソフトが強制終了するような、ド派手な悪影響はそうないが…
var data = new List<int>();
for (int i = 0; i < 10000; i++)
{
data.Add(i); //Addのたび、内部配列が再確保される
}
✅ 回避策
- おおよそ必要な数がわかっていれば最初に確保
var data = new List<int>(10000); //最初に容量を確保
for (int i = 0; i < 10000; i++)
{
data.Add(i);
}
foreach 中にコレクション変更
- foreach は列挙子を用いて List を読み取る
❌ foreach 中に要素が変更されると、列挙子の整合性が崩れる
❌ そのため、実行時に InvalidOperationException が発生する
var list = new List<string> { "a", "b", "c" };
foreach (var item in list)
{
if (item == "b")
{
list.Remove(item); //InvalidOperationException
}
}
✅ 回避策
- for ループで末尾から走査すれば、削除によるズレを防げる
for (int i = list.Count - 1; i >= 0; i--)
{
if (list[i] == "b")
{
list.RemoveAt(i);
}
}
Clear()
でメモリ解放と勘違い
❌ Clear()
で要素は消えるが、容量(内部配列)は残る
var list = new List<byte>(1000);
list.AddRange(new byte[1000]);
//何らかの処理で要素数が500になる
list.Clear();
Console.WriteLine(list.Capacity); //1000 のまま
✅ 回避策
- メモリを解放したい場合、
TrimExcess()
を用いる -
TrimExcess()
は要素数が Capacity の90%未満の場合 Capacity を Count に縮小する
var list = new List<byte>(1000);
list.AddRange(new byte[1000]);
//何らかの処理で要素数が500になる
list.TrimExcess();
Console.WriteLine(list.Capacity); //500
Discussion