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 で返すことはできますが、その瞬間にコンパイル時のチェックをすべて失います。境目ははっきりしていて、データが作ったメソッドの外に出るなら、タプルかレコードに切り替えます。

判断フロー:値を少しまとめたい。1つのメソッドの中(LINQの射影など)で完結するなら匿名型。メソッドの外に出すなら、2〜3個の値を返す private な補助メソッドかを確認し、そうなら名前付きタプル。公開API・DTO・== で値比較したいものはレコードかレコード構造体。
匿名型が向かなくなる境目。匿名型は作ったメソッドの外に出さない。メソッドの境界を越えるなら、タプルかレコードに切り替える。
要件 匿名型 名前付きタプル 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> にも変換できます。ラムダは引数を必ず書く必要があり、(_, _) => のような破棄パラメーターが一番近い書き方です。

  • #C#
  • #匿名型
  • #匿名メソッド
  • #タプル
  • #レコード
  • #.NET

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑