C# では、クラスや構造体をあらかじめ定義しなくても、匿名型(anonymous type) を使うことで、その場限りのオブジェクトを手軽に作れます。
var person = new { Name = "Alice", Age = 30 };
Console.WriteLine(person.Name); // Alice
LINQ の select 句で結果を整形するときによく使われますが、単独で使っても便利です。この記事では次の 5 つを順に説明します。
- 匿名型の定義
- 匿名型の内部表現
- ToString と GetHashCode の実装
- 匿名型の等価性
- 匿名型を含む匿名型(ネスト)
匿名型の定義
匿名型は new { プロパティ名 = 値, ... } の形で定義します。型名を書かないため、変数には var を使います。
var car = new
{
Make = "Toyota",
Model = "Prius",
Year = 2024
};
Console.WriteLine(car.Make); // Toyota
Console.WriteLine(car.Model); // Prius
Console.WriteLine(car.Year); // 2024
プロパティは定義した時点で型が推論されます。car.Make は string、car.Year は int です。
プロジェクション初期化子
既存の変数名からプロパティを作るとき、プロパティ名を省略できます。これをプロジェクション初期化子(projection initializer) といいます。
string name = "Bob";
int age = 25;
// プロパティ名は変数名と同じ Name, Age になる
var person = new { name, age };
Console.WriteLine(person.name); // Bob
Console.WriteLine(person.age); // 25
読み取り専用
匿名型のプロパティは読み取り専用です。定義後に値を変更しようとするとコンパイルエラーになります。
var point = new { X = 10, Y = 20 };
point.X = 99; // コンパイルエラー: プロパティまたはインデクサーに値を割り当てられません
匿名型の内部表現
匿名型は、コンパイル時にコンパイラが自動でクラスを生成します。生成されたクラスは次のような特徴を持ちます。
sealed classとして生成される- プロパティは
getのみ(読み取り専用) objectを継承する
IL(中間言語)レベルでは、おおむね次のようなクラスが自動生成されます。
// コンパイラが自動生成するクラスのイメージ
// (実際にはこの名前ではなく '<>f__AnonymousType0`2' のような名前になる)
internal sealed class AnonymousType<TName, TAge>
{
private readonly TName _name;
private readonly TAge _age;
public TName Name => _name;
public TAge Age => _age;
public AnonymousType(TName name, TAge age)
{
_name = name;
_age = age;
}
// ToString, Equals, GetHashCode も自動生成される
}
同じアセンブリ内で同じプロパティ名・同じ型・同じ順番の匿名型は、コンパイラが同一の生成クラスを再利用します。
var a = new { Name = "Alice", Age = 30 };
var b = new { Name = "Bob", Age = 25 };
// a と b は同じ型(プロパティ名・型・順番が一致するため)
Console.WriteLine(a.GetType() == b.GetType()); // True
プロパティの順番が違うと別の型になります。
var c = new { Age = 30, Name = "Carol" }; // 順番が違う
Console.WriteLine(a.GetType() == c.GetType()); // False
ToString と GetHashCode の実装
匿名型のコンパイラ生成クラスは ToString、GetHashCode、Equals を自動でオーバーライドします。
ToString
ToString はすべてのプロパティ名と値を含む文字列を返します。
var product = new { Name = "Widget", Price = 9.99m, InStock = true };
Console.WriteLine(product.ToString());
// { Name = Widget, Price = 9.99, InStock = True }
デバッグ時にオブジェクトの中身をすぐ確認できるため、便利です。
GetHashCode
GetHashCode はすべてのプロパティの値を組み合わせたハッシュを返します。同じプロパティ値を持つ匿名型のインスタンスは同じハッシュコードを返します。
var p1 = new { Name = "Alice", Age = 30 };
var p2 = new { Name = "Alice", Age = 30 };
Console.WriteLine(p1.GetHashCode() == p2.GetHashCode()); // True(値が同じ)
匿名型の等価性
匿名型の Equals は値ベースの比較を行います。すべてのプロパティが等しければ true を返します。これは通常のクラスの参照比較とは異なります。
var p1 = new { Name = "Alice", Age = 30 };
var p2 = new { Name = "Alice", Age = 30 };
var p3 = new { Name = "Bob", Age = 25 };
// Equals は値で比較
Console.WriteLine(p1.Equals(p2)); // True
Console.WriteLine(p1.Equals(p3)); // False
// == 演算子は参照比較のまま(object から継承)
Console.WriteLine(p1 == p2); // False(異なるインスタンス)
== は匿名型に対してオーバーロードされないため、参照比較のままです。値を比較したい場合は必ず Equals を使います。
// LINQ での活用例: Distinct で重複を除去する
var items = new[]
{
new { Category = "Food", Tag = "A" },
new { Category = "Food", Tag = "A" }, // 重複
new { Category = "Drink", Tag = "B" },
};
foreach (var item in items.Distinct())
Console.WriteLine(item);
// { Category = Food, Tag = A }
// { Category = Drink, Tag = B }
Distinct は内部で Equals を使うため、匿名型の重複除去が自然に機能します。
匿名型を含む匿名型
匿名型のプロパティに別の匿名型を入れることで、ネストした構造を作れます。
var order = new
{
OrderId = 1001,
Customer = new
{
Name = "Alice",
Address = new
{
City = "Tokyo",
Country = "Japan"
}
},
TotalAmount = 5400m
};
Console.WriteLine(order.OrderId); // 1001
Console.WriteLine(order.Customer.Name); // Alice
Console.WriteLine(order.Customer.Address.City); // Tokyo
Console.WriteLine(order.Customer.Address.Country); // Japan
Console.WriteLine(order.TotalAmount); // 5400
ToString も自動的にネスト構造を展開してくれます。
Console.WriteLine(order);
// { OrderId = 1001, Customer = { Name = Alice, Address = { City = Tokyo, Country = Japan } }, TotalAmount = 5400 }
LINQ でのネスト活用例
匿名型のネストは LINQ の select 句で結果を整形するときによく使われます。
var products = new[]
{
new { Name = "Apple", Price = 100, Category = "Fruit" },
new { Name = "Milk", Price = 200, Category = "Drink" },
new { Name = "Banana", Price = 80, Category = "Fruit" },
new { Name = "Water", Price = 50, Category = "Drink" },
};
var result = products
.Where(p => p.Price >= 100)
.Select(p => new
{
p.Name,
p.Price,
Info = new
{
p.Category,
IsPremium = p.Price >= 150
}
});
foreach (var item in result)
Console.WriteLine(item);
// { Name = Apple, Price = 100, Info = { Category = Fruit, IsPremium = False } }
// { Name = Milk, Price = 200, Info = { Category = Drink, IsPremium = True } }
まとめ
| トピック | ポイント |
|---|---|
| 定義 | new { Name = 値, ... } で作成。変数は var で受け取る |
| 内部表現 | コンパイラが sealed class を自動生成。同じ構造は同じ型を再利用 |
| ToString / GetHashCode | すべてのプロパティを使って自動生成される |
| 等価性 | Equals は値ベース比較。== は参照比較のまま |
| ネスト | 匿名型のプロパティに別の匿名型を使える |
匿名型は LINQ の select 句での結果整形や、メソッド内の一時的なデータ構造として便利です。ただし、メソッドの戻り値としては使えない(型名がないため)ので、複数のメソッドで共有したいデータは名前付きのクラスや record として定義するのが適切です。