はじめに:「キャッシュ付き検索」は“同じものを何度も探さない”ための技
LINQ で検索を書くとき、最初はだいたいこうなります。
var user = users.FirstOrDefault(x => x.Id == id);
C#これ自体は悪くないのですが、「同じ id を何度も検索する」ような処理になってくると、
毎回コレクションをなめるのはもったいないです。
そこで出てくるのが「キャッシュ付き検索」という発想です。
一度探した結果を覚えておいて、
次に同じ条件で検索されたときは、
もう一度 LINQ で探さずに“キャッシュから返す”
という仕組みを、小さなユーティリティとして持っておくイメージです。
ここでは、初心者向けに
キャッシュ付き検索の基本アイデア
Dictionary を使ったシンプルなキャッシュ
LINQ と組み合わせた「遅延キャッシュ」パターン
業務での具体例(マスタ参照・外部 API 結果のキャッシュ)
を、例題付きでかみ砕いて説明していきます。
基本アイデア:「キー → 結果」を Dictionary に覚えさせる
まずは素朴な「毎回検索」から
例えば、商品コードから商品情報を引く処理を考えます。
public class Item
{
public string Code { get; set; } = "";
public string Name { get; set; } = "";
}
var items = new[]
{
new Item { Code = "A", Name = "Apple" },
new Item { Code = "B", Name = "Banana" },
new Item { Code = "C", Name = "Cherry" },
};
Item? FindItem(string code)
{
return items.FirstOrDefault(x => x.Code == code);
}
C#FindItem("A") を 1 回だけ呼ぶならこれで十分ですが、
同じコードを何度も引くような処理だと、毎回 FirstOrDefault が走ります。
ここでの重要ポイントは、「“同じ条件で何度も検索する”なら、結果を覚えておいたほうが得」という感覚を持つことです。
Dictionary を使って「一度調べたら覚えておく」
キャッシュ付き検索の一番シンプルな形は、こうです。
public class ItemFinder
{
private readonly Item[] _items;
private readonly Dictionary<string, Item?> _cache = new();
public ItemFinder(Item[] items)
{
_items = items;
}
public Item? Find(string code)
{
if (_cache.TryGetValue(code, out var cached))
{
// すでに調べたことがある → キャッシュから返す
return cached;
}
// 初めてのコード → LINQ で検索
var item = _items.FirstOrDefault(x => x.Code == code);
// 結果をキャッシュに保存(見つからなかった null も含めて)
_cache[code] = item;
return item;
}
}
C#使い方はこうなります。
var finder = new ItemFinder(items);
var a1 = finder.Find("A"); // LINQ で検索してキャッシュ
var a2 = finder.Find("A"); // キャッシュから即返る
C#ここでの重要ポイントは、「FirstOrDefault 自体をやめるのではなく、“一度だけやって、あとは結果を再利用する”」ということです。
LINQ は「初回の検索」にだけ使い、2 回目以降は Dictionary の TryGetValue で高速に返します。
LINQ と組み合わせた「遅延キャッシュ」パターン
「必要になったものだけキャッシュする」という考え方
さきほどの ItemFinder は、呼ばれたコードだけをキャッシュしていました。
これをもう少し汎用的にして、「任意のキーでキャッシュ付き検索できる」ユーティリティにしてみます。
public class CachedLookup<TSource, TKey, TValue>
where TKey : notnull
{
private readonly IEnumerable<TSource> _source;
private readonly Func<TSource, TKey> _keySelector;
private readonly Func<TSource, TValue> _valueSelector;
private readonly Dictionary<TKey, TValue?> _cache = new();
public CachedLookup(
IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
Func<TSource, TValue> valueSelector)
{
_source = source;
_keySelector = keySelector;
_valueSelector = valueSelector;
}
public TValue? Find(TKey key)
{
if (_cache.TryGetValue(key, out var cached))
{
return cached;
}
var found = _source.FirstOrDefault(x => _keySelector(x)!.Equals(key));
if (found is null)
{
_cache[key] = default;
return default;
}
var value = _valueSelector(found);
_cache[key] = value;
return value;
}
}
C#使う側はこう書けます。
var itemNameLookup = new CachedLookup<Item, string, string>(
items,
x => x.Code,
x => x.Name);
var name1 = itemNameLookup.Find("A"); // LINQ で検索してキャッシュ
var name2 = itemNameLookup.Find("A"); // キャッシュから返る
C#ここでの重要ポイントは、「キャッシュ付き検索の“型”を 1 回作ってしまえば、LINQ の FirstOrDefault を中に閉じ込められる」ということです。
呼び出し側は「Find を呼ぶだけ」で、キャッシュか LINQ かを意識しなくて済みます。
「最初から全部 Dictionary にしてしまう」との違い
ToDictionary との比較
高速検索の話で出てきたように、最初からこうしてしまう手もあります。
var itemByCode = items.ToDictionary(x => x.Code);
C#これは「全件を一度に Dictionary に変換する」やり方です。
一方、キャッシュ付き検索は「必要になったキーだけ、順次キャッシュしていく」やり方です。
どちらを選ぶかは、だいたい次のような感覚で決めます。
全件をほぼ確実に使う → 最初から ToDictionary でいい
使うキーは一部だけかもしれない → キャッシュ付き検索が向いている
ここでの重要ポイントは、「キャッシュ付き検索は“部分的にしか使わないかもしれないデータ”に対して有効」ということです。
全部を Dictionary にするコストを払うほどではないけれど、同じ検索を何度もやるのはもったいない、という中間地帯で効きます。
業務での具体例 1:マスタ参照のキャッシュ付き検索
商品マスタを何度も引く処理
例えば、注文明細を処理しながら、商品名を表示するケース。
public class OrderLine
{
public string ItemCode { get; set; } = "";
public int Quantity { get; set; }
}
var lines = new[]
{
new OrderLine { ItemCode = "A", Quantity = 1 },
new OrderLine { ItemCode = "B", Quantity = 2 },
new OrderLine { ItemCode = "A", Quantity = 3 },
};
C#素朴に書くとこうなります。
foreach (var line in lines)
{
var item = items.FirstOrDefault(x => x.Code == line.ItemCode);
Console.WriteLine($"{line.ItemCode} {item?.Name} x {line.Quantity}");
}
C#ここで「A」が何度も出てくると、そのたびに FirstOrDefault が走ります。
キャッシュ付き検索を使うと、こう書き換えられます。
var finder = new ItemFinder(items);
foreach (var line in lines)
{
var item = finder.Find(line.ItemCode);
Console.WriteLine($"{line.ItemCode} {item?.Name} x {line.Quantity}");
}
C#ここでの重要ポイントは、「同じ商品コードが何度出てきても、実際に LINQ で検索するのは最初の 1 回だけ」ということです。
以降は Dictionary のキャッシュから即座に返ります。
業務での具体例 2:外部 API の結果をキャッシュする
「同じキーで何度も API を叩きたくない」
例えば、「郵便番号から住所を引く外部 API」があるとします。
string CallAddressApi(string zip)
{
// 実際には HTTP で外部サービスを呼ぶ
Console.WriteLine($"API 呼び出し: {zip}");
return "ダミー住所";
}
C#素朴に書くと、こうなります。
var zips = new[] { "1000001", "1000002", "1000001", "1000003" };
foreach (var zip in zips)
{
var address = CallAddressApi(zip);
Console.WriteLine($"{zip}: {address}");
}
C#"1000001" が 2 回出てくるので、API も 2 回呼ばれます。
キャッシュ付き検索の発想を使うと、こうできます。
public class AddressClient
{
private readonly Dictionary<string, string> _cache = new();
public string GetAddress(string zip)
{
if (_cache.TryGetValue(zip, out var cached))
{
return cached;
}
var address = CallAddressApi(zip);
_cache[zip] = address;
return address;
}
private string CallAddressApi(string zip)
{
Console.WriteLine($"API 呼び出し: {zip}");
return "ダミー住所";
}
}
C#使い方はこうです。
var client = new AddressClient();
foreach (var zip in zips)
{
var address = client.GetAddress(zip);
Console.WriteLine($"{zip}: {address}");
}
C#"1000001" に対して API が呼ばれるのは最初の 1 回だけになります。
ここでの重要ポイントは、「キャッシュ付き検索は“重い処理(外部 API・DB)”と相性が抜群」ということです。
LINQ というよりは「検索の考え方」ですが、同じパターンで組み込めます。
どこまでキャッシュするか、どう無効化するか
キャッシュは“永遠に正しい”とは限らない
キャッシュ付き検索は便利ですが、「データが変わる」世界では注意が必要です。
マスタが更新される
外部 API の結果が変わる
一定時間経ったら取り直したい
といった要件がある場合は、
一定時間でキャッシュをクリアする
明示的に Clear() するメソッドを用意する
キーごとに再取得する手段を用意する
など、「キャッシュの寿命」を設計する必要があります。
初心者のうちは、まずは「バッチ処理の中だけで使い捨てるキャッシュ」から始めるのがおすすめです。
プロセス内で完結し、処理が終わったら捨ててしまうので、整合性の問題をあまり気にせずに済みます。
ここでの重要ポイントは、「キャッシュ付き検索は“速くなる代わりに、古くなる可能性がある”」というトレードオフを意識することです。
まとめ:「キャッシュ付き検索ユーティリティ」は“同じ検索を二度しないための道具”
キャッシュ付き検索の本質は、
同じ条件で何度も検索するくらいなら、
一度だけ LINQ(や API)で探して、
結果を Dictionary に覚えさせておく
という発想です。
押さえておきたいポイントを整理すると、
FirstOrDefault を何度も呼ぶくらいなら、「一度だけ呼んで結果をキャッシュする」ほうが得
Dictionary を使えば、「キー → 結果」を高速に引ける
汎用的な CachedLookup のような型を作ると、LINQ 検索を中に閉じ込められる
マスタ参照や外部 API の結果など、「重い or 繰り返し使う検索」と相性が良い
キャッシュは“速さと引き換えに古くなる可能性がある”ので、寿命やクリア方法も設計の一部
ここまで腹落ちしていれば、
「なんとなく毎回 LINQ で検索する」段階から一歩進んで、
“検索の仕方そのものを設計できる C# エンジニア”に近づけます。
