C#の匿名型と匿名メソッド:.NET 10で確かめた等価性とwith
C#匿名型のEqualsと==の違い、読み取り専用、C# 10のwith式、タプルやレコードに切り替える基準と割り当て量の実測、delegate { }だけにできることを.NET 10で検証。
2026年11月改訂: この記事は2021年に韓国語で書いた講座を、日本語向けに書き直したものです。2つの例を .NET 10.0.14(C# 14)でコンパイルし直したところ、匿名型の例はコンパイルできませんでした。float リテラルを
0.1 fと空白入りで書いていたため error CS1003、console.Writeが小文字で error CS0103 です。匿名メソッドの例はエントリポイントをpublic static void main()と小文字で書いていたため、実行ファイルそのものが作られませんでした(error CS5001)。コメントにあった出力1,2,3,4,5も、実際は1 2 3 4 5でした。修正したコードに加え、元記事になかった等価性・with・型が同一になる条件、そして匿名型をやめるべき場面を実測つきでまとめています。
LINQ のコードで new { … } を、古いイベント処理で delegate { } を見かけて、結局何が保証されているのかを確かめたい人向けの記事です。先に結論を書くと、匿名型はコンパイラーが生成する読み取り専用のクラスで、Equals は値で比較するのに == は参照比較のまま。そして作ったメソッドの中だけで使うのが前提です。匿名メソッドはラムダの前身(C# 2)で、2026年の今もラムダにない機能は「引数リストの省略」ひとつだけです。デリゲート自体は C#のデリゲートとイベント、ラムダの書き方は C#のラムダ式 で扱ったので、ここでは繰り返しません。
匿名型:2021年の例を直して動かす
int や string、自作クラスには型名があります。匿名型は名前を持たず、new とオブジェクト初期化子だけで作る型なので、受け取る変数は必ず var になります。書ける型名が存在しないからです。誤字を2か所直した2021年の例です(値は差し替えています)。
var temp = new { Age = 40, Name = "Mina" };
Console.WriteLine("{0},{1}", temp.Age, temp.Name);
var tempArr = new {
Int = new int[] { 1, 2, 3, 4, 5 },
Float = new float[] { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f }
};
foreach (var element in tempArr.Int) Console.Write("{0} ", element);
Console.WriteLine();
foreach (var element in tempArr.Float) Console.Write("{0} ", element);
Console.WriteLine();
40,Mina
1 2 3 4 5
0.1 0.2 0.3 0.4 0.5
2021年の原文のままだと、ビルドはこう止まりました(先頭のみ)。
Program.cs(17,21): error CS1003: Syntax error, ',' expected
Program.cs(25,13): error CS0103: The name 'console' does not exist in the current context
0.1 f は「数値 0.1 の後ろに正体不明の識別子 f」と解釈されるので、カンマが要るというエラーになります。サフィックスは数字にくっつけて 0.1f と書きます。原文の冒頭にあった宣言の例 class D; も C# の構文ではありません。class は型を宣言するキーワードで、変数の前に置く型名ではないからです。
「生成したインスタンスは読み取り専用」という原文の説明は正しく、実際のエラーは次のとおりです。
Program.cs(2,1): error CS0200: Property or indexer '<anonymous type: int Id, string Name>.Id' cannot be assigned to -- it is read only
原文になかったコンパイル時のルールも2つ確認しました。new { Tag = null } のように null だけを入れると型を推論できず error CS0828。匿名オブジェクトの配列 new[] { new { Id = 1 }, new { Id = 2L } } は要素の形(int と long)が違うので error CS0826 です。
等価性・ハッシュ・型が同じになる条件
原文では匿名型を「一時変数に便利」と紹介しただけでしたが、実務で効いてくるのは比較の挙動です。.NET 10 での実行結果です。
var a = new { Id = 7, Name = "Mina" };
var b = new { Id = 7, Name = "Mina" };
var c = new { Name = "Mina", Id = 7 }; // メンバーは同じで順番だけ違う
Console.WriteLine(a);
Console.WriteLine($"Equals: {a.Equals(b)} ==: {a == b}");
Console.WriteLine($"same type a/b: {a.GetType() == b.GetType()}, a/c: {a.GetType() == c.GetType()}");
Console.WriteLine($"hash equal: {a.GetHashCode() == b.GetHashCode()}");
Console.WriteLine(a.GetType().Name);
{ Id = 7, Name = Mina }
Equals: True ==: False
same type a/b: True, a/c: False
hash equal: True
<>f__AnonymousType0`2
EqualsとGetHashCodeは値で比較します。 だからGroupBy、Distinct、HashSetの複合キーに使えます。HashSet<object>にa、b、名前だけ違うオブジェクトを入れるとCountは 2 でした。==は参照比較です。==演算子は生成されないため、a == bはFalse。いちばん勘違いされやすいところです。タプルの(Id: 7, Name: "Mina") == (7, "Mina")と、値が同じレコード2つの==はどちらもTrueでした。- 名前・型・順番がそろえば同じ型になります(同じアセンブリ内)。順番を入れ替えた
cは別の型でした。実際の型名<>f__AnonymousType02` は C# のコードには書けない名前です。 ToString()が全プロパティを出してくれるので、ログでの確認に便利です。
new { user.Id, user.Name } のように式からプロパティ名を取る射影初期化子も使えます。LINQ の Select はほとんどこの形です。
C# 10:匿名型でも with が使える
C# 10 から、with 式で匿名オブジェクトをコピーしながら一部のプロパティだけを変えられます。元のオブジェクトはそのままです。
var d = a with { Name = "Jun" };
Console.WriteLine($"{d} original: {a}");
{ Id = 7, Name = Jun } original: { Id = 7, Name = Mina }
「C# 10 から」が本当か確かめるため、同じファイルを LangVersion=9 でビルドしました。
Program.cs(3,9): error CS8773: Feature 'with on anonymous types' is not available in C# 9.0. Please use language version 10.0 or greater.
with は新しいインスタンスを作るだけなので、読み取り専用という性質は変わりません。
タプルやレコードに切り替えるべきとき
匿名型はメソッドのシグネチャに書けません。object や dynamic で返すことはできますが、その瞬間にコンパイル時のチェックをすべて失います。境目ははっきりしていて、データが作ったメソッドの外に出るなら、タプルかレコードに切り替えます。
| 要件 | 匿名型 | 名前付きタプル | record / record struct |
|---|---|---|---|
| メソッドの外へ渡す | 不可 | 可 | 可 |
== で値比較 |
しない(参照) | する | する |
Equals/GetHashCode が値ベース |
はい | はい | はい |
| プロパティ名が JSON に残る | はい | いいえ | はい |
| インスタンスごとのヒープ割り当て | あり | なし(構造体) | class はあり / struct はなし |
JSON の行は実際に事故が起きるところです。同じデータを System.Text.Json でシリアル化しました。
new { Id = 7, Name = "Mina" } → {"Id":7,"Name":"Mina"}
(Id: 7, Name: "Mina") → {}
(Id: 7, Name: "Mina"), IncludeFields = true → {"Item1":7,"Item2":"Mina"}
タプルの要素名はコンパイル時にしか存在せず、要素はフィールドなので既定ではシリアル化されません。タプルをそのまま返す API は、黙って {} を返します。
割り当て量も測りました。整数100万個に対して Select で匿名型・タプル・readonly record struct を作り、Where と Sum をつなげています。条件:.NET 10.0.14、Release ビルド、Stopwatch と GC.GetAllocatedBytesForCurrentThread、ウォームアップ1回のあと3回計測、Apple Silicon Mac(M1 Pro)。
| 射影 | 1回目 | 2回目 | 3回目 | 割り当て |
|---|---|---|---|---|
new { Id, Sq } |
25.1 ms | 26.1 ms | 24.8 ms | 24.0 MB |
(Id, Sq) タプル |
5.2 ms | 5.3 ms | 5.0 ms | 1 KB 未満 |
readonly record struct |
5.9 ms | 6.1 ms | 6.0 ms | 1 KB 未満 |
匿名オブジェクト1個がヒープに24バイトを使い(24MB ÷ 100万)、全体は構造体の約5倍遅くなりました。数百件を1回処理するクエリなら読みやすい匿名型で構いませんが、リクエストごと・フレームごとに大きな配列を回すコードでは、タプルかレコード構造体を既定にするのが妥当です。LINQ 自体は C# LINQの基本 を参照してください。
匿名メソッド:2021年の例と今も残る独自機能
匿名メソッドは delegate キーワードで書く名前のないメソッド本体で、デリゲート変数に入れて呼び出します。main を Main に直すと、2021年の例は 33 を出力しました。
delegate int MyDelegate(int a, int b);
// Main の中で:
MyDelegate add = delegate (int a, int b) { return a + b; };
Console.WriteLine(add(11, 22)); // 33
小文字の main のままだと error CS5001: Program does not contain a static 'Main' method suitable for an entry point でした。C# は大文字と小文字を区別するので、main はただのメソッドです。
C# 3 のラムダが、匿名メソッドの役割をほぼすべてもっと短く引き継いでいます。残った独自機能は引数リストをまるごと省略できることで、省略すると引数が何であれ、どのデリゲート型にも変換できます。
EventHandler h = delegate { Console.WriteLine("clicked"); };
Func<int, int, int> zero = delegate { return 0; };
Action<string, int, DateTime> ignore = delegate { };
h(null, EventArgs.Empty); // clicked
Console.WriteLine(zero(3, 4)); // 0
ラムダは引数を必ず書く必要があり、C# 9 の破棄パラメーター (_, _) => 0 がいちばん近い書き方です。省略形で出会った制限は2つです。
error CS8917: The delegate type could not be inferred.
error CS1688: Cannot convert anonymous method block without a parameter list to delegate type 'TryParser' because it has one or more out parameters
1つ目は var g = delegate { return 1; }; で出ました。引数リストがないので型を推論する手がかりがありません。リストを書いた var f = delegate (int x) { return x * 2; }; はコンパイルでき、自然型は System.Func<int, int> になりました。2つ目は out 引数を持つデリゲートで、本体が値を代入しなければならないためです。
C# 9 の static delegate (int x) { … }(キャプチャできない静的匿名関数)と delegate (int _, int _) { … } は匿名メソッドでも使え、実行まで確認しました。逆に、ラムダにだけ追加された機能もあります。式本体(delegate (int x) => x は構文エラー)、属性([Obsolete] delegate (…) は error CS1002、ラムダならコンパイルできる)、明示的な戻り値の型です。
省略形の古典的な使い道は、public event EventHandler Changed = delegate { }; のようにイベントを空のハンドラーで初期化して null にしない書き方です。動きはしますが、発行のたびに空のメソッドを1回余分に呼びます。null 許容参照型が有効な今は、event EventHandler? Changed; と Changed?.Invoke(this, e) の組み合わせのほうがすっきりします。イベントの応用は C#イベントの応用 にまとめました。
使い分けの早見表
| 状況 | 選ぶもの |
|---|---|
| 1つの LINQ クエリの中でデータの形を変える | 匿名型 |
1つのメソッド内の GroupBy/Distinct の複合キー |
匿名型(値ベースの Equals) |
== で2つの値を比べる |
タプルかレコード |
| private な補助メソッドから複数の値を返す | 名前付きタプル |
| 公開 API、DTO、名前でシリアル化するデータ | record / record struct |
| 大きな配列を回すホットループ | タプルか record struct |
| 引数をすべて無視するハンドラー | delegate { … } か (_, _) => … |
| それ以外で関数を渡すところ | ラムダ |
検証環境:.NET SDK 10.0.403 / ランタイム 10.0.14、C# 14、macOS(Apple Silicon、M1 Pro)。
よくある質問
値が同じ匿名型のオブジェクト同士は等しいと判定される?
Equals は等しい、== は等しくない、と答えます。new { Id = 7, Name = "Mina" } を2つ作って比べると、a.Equals(b) は True、a == b は False でした。匿名型は Equals と GetHashCode を値ベースでオーバーライドしますが、== 演算子は生成されず参照比較のままだからです。レコードとタプルは == でも値を比較します。
匿名型のプロパティを書き換えられる?
できません。すべて読み取り専用で、a.Id = 8; は error CS0200 になります。C# 10 からは with 式で一部だけ変えたコピーを作れます(var d = a with { Name = "Jun" };)。LangVersion 9 では同じ行が error CS8773 になります。
匿名メソッドにしかできないことはまだある?
引数リストをまるごと省略できる点だけです。delegate { ... } は引数を使わない限り、EventHandler にも Func<int, int, int> にも変換できます。ラムダは引数を必ず書く必要があり、(_, _) => のような破棄パラメーターが一番近い書き方です。