C# プログラミングにおいて、データの集まりを一時的に保持したい場面は頻繁に発生します。

例えば、データベースから取得した特定の列だけを組み合わせて処理したい場合や、LINQクエリの結果を一時的なオブジェクトとしてまとめたい場合などです。

このようなシーンで、わざわざクラスや構造体を定義することなく、その場で名前のない型を作成できる機能が匿名型 (Anonymous Types)です。

匿名型は、開発効率を向上させるだけでなく、コードの可読性を保つための強力な武器となります。

しかし、近年では ValueTuplerecord (レコード型)といった新しい機能が登場しており、これらとの使い分けに迷う方も少なくありません。

本記事では、C# の匿名型の基本的な使い方から内部動作、さらには最新の C# 仕様を踏まえた他の型との比較・使い分けまでを徹底的に解説します。

匿名型とは何か

匿名型は、一連の読み取り専用プロパティを単一のオブジェクトにカプセル化するための便利な手法です。

最大の特徴は、明示的に型を定義(クラス定義など)することなく、コンパイラが自動的に型を生成してくれる点にあります。

通常、データを保持するためには classstruct を宣言する必要がありますが、匿名型を使えばその手間を省くことができます。

匿名型の基本構文

匿名型を作成するには、new キーワードとオブジェクト初期化子を使用します。

型名が指定されていないため、変数の宣言には必ず var を使用する必要があります。

C#
using System;

public class Program
{
    public static void Main()
    {
        // 匿名型の作成
        var userInfo = new { Name = "田中太郎", Age = 30, IsActive = true };

        // プロパティへのアクセス
        Console.WriteLine($"名前: {userInfo.Name}");
        Console.WriteLine($"年齢: {userInfo.Age}");
        Console.WriteLine($"有効: {userInfo.IsActive}");
        
        // 型情報の表示
        Console.WriteLine($"型の名前: {userInfo.GetType().Name}");
    }
}
実行結果
名前: 田中太郎
年齢: 30
有効: True
型の名前: <>f__AnonymousType0`3

この例では、NameAgeIsActive という3つのプロパティを持つ型がその場で生成されています。

実行結果を見るとわかる通り、コンパイラは内部的に <>f__AnonymousType0 のような特殊な名前を持つクラスを生成しています。

匿名型の主な特徴

匿名型には、一般的なクラスとは異なるいくつかの重要な制約と特徴があります。

  1. 読み取り専用 (Immutable): 匿名型のプロパティは、インスタンス化された後に値を変更することはできません。すべて get 専用のプロパティとして定義されます。
  2. 強い型付け: 名前こそありませんが、コンパイラによって型が決定されるため、IntelliSense(入力補完)の恩恵を受けることができます。
  3. スコープの限定: 原則として、匿名型はそのメソッド内でのみ利用することを想定しています。メソッドの戻り値として直接返すことは困難です(object 型や dynamic 型を使えば可能ですが、型安全性が失われます)。

匿名型のプロパティ名の推論

匿名型を定義する際、すでに存在する変数やオブジェクトのプロパティを利用する場合、プロパティ名を省略することができます。

これを「プロパティ名の推論」と呼びます。

C#
string city = "Tokyo";
int population = 14000000;

// 変数名をそのままプロパティ名として使用
var areaInfo = new { city, population };

Console.WriteLine($"City: {areaInfo.city}, Pop: {areaInfo.population}");

この機能は、特に既存のオブジェクトから一部のデータを抽出する際に、コードを非常に簡潔にします。

匿名型の比較と等価性

匿名型の大きな利点の一つが、値による比較が自動的に実装される点です。

コンパイラは、すべてのプロパティの値が等しい場合にのみ「等しい」と判定するように Equals メソッドと GetHashCode メソッドをオーバーライドします。

C#
var p1 = new { ID = 1, Category = "Book" };
var p2 = new { ID = 1, Category = "Book" };
var p3 = new { ID = 2, Category = "Book" };

Console.WriteLine($"p1 == p2: {p1.Equals(p2)}"); // True
Console.WriteLine($"p1 == p3: {p1.Equals(p3)}"); // False
実行結果
p1 == p2: True
p1 == p3: False

ただし、比較に使用される2つの匿名型は、プロパティの順序、名前、型がすべて一致している必要があります。

順序が入れ替わっているだけで、コンパイラは別の型として認識するため注意が必要です。

匿名型の主な活用シーン:LINQでの射影

匿名型が最も威力を発揮するのは、LINQ (Language Integrated Query) を使用したデータの加工処理です。

大量のデータから特定の項目だけを抽出して新しい形に整形する「射影 (Projection)」において、使い捨てのデータ構造として最適です。

C#
using System;
using System.Collections.Generic;
using System.Linq;

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Score { get; set; }
}

public class Program
{
    public static void Main()
    {
        var students = new List<Student>
        {
            new Student { Id = 1, Name = "Alice", Score = 85 },
            new Student { Id = 2, Name = "Bob", Score = 70 },
            new Student { Id = 3, Name = "Charlie", Score = 95 }
        };

        // LINQで特定の条件を満たす生徒の名前とスコアだけを匿名型で抽出
        var highScorers = students
            .Where(s => s.Score >= 80)
            .Select(s => new { s.Name, IsExcellent = true });

        foreach (var item in highScorers)
        {
            Console.WriteLine($"{item.Name} は優秀な成績です。");
        }
    }
}

このコードでは、Student クラスの全データを持ち回るのではなく、Name と新しく作成した IsExcellent フラグのみを持つ軽量なオブジェクトに変換しています。

このように、「その場限りのデータ構造」を定義なしで使えるのが匿名型の真骨頂です。

Tuple、レコード型との違いと使い分け

C# には匿名型と似た役割を持つ機能がいくつかあります。

特に、C# 7.0 で強化された ValueTuple (タプル) と、C# 9.0 で導入された record (レコード) との使い分けを理解することは非常に重要です。

以下に、それぞれの特徴を比較した表を示します。

機能匿名型Tuple (ValueTuple)レコード型 (record)
定義の必要性不要不要必要
主な用途LINQ等のメソッド内一時利用メソッドの多値返却永続的なデータモデル
プロパティ変更不可 (読み取り専用)可能 (基本は可変)デフォルト不可 (init)
メソッド間の受け渡し困難容易非常に容易
等価性の判定値ベース値ベース値ベース
継承不可不可可能

匿名型 vs Tuple

Tuple(特に ValueTuple)は、匿名型と同様に型定義なしで利用できます。

大きな違いは、Tuple はメソッドの戻り値として利用できる点です。

  • 匿名型を選ぶべき時: 主に LINQ などのクエリ操作で、プロパティ名に意味を持たせてコードの可読性を高めたい場合。
  • Tuple を選ぶべき時: プライベートなメソッドから複数の値を返したい場合や、順序に意味がある一時的なペアを作りたい場合。

匿名型 vs レコード型

record は、C# 9.0 以降で推奨される「不変データ」のための型定義です。

  • 匿名型を選ぶべき時: クラスを定義するまでもない、1つのメソッド内だけで完結する一時的な処理。
  • レコード型を選ぶべき時: そのデータ構造を複数のメソッドやクラス間で共有する場合、あるいは将来的に継承やパターンマッチングを利用したい場合。

C# 10.0 以降の匿名型の進化:with 式のサポート

長らく匿名型は「一度作ったらそれまで」の不変オブジェクトでしたが、C# 10.0 において with 式 が匿名型にも対応しました。

これにより、既存の匿名型の一部を変更した新しいインスタンスを簡単に作成できるようになりました。

C#
var original = new { Name = "C#", Version = 9 };

// Versionだけを変更した新しい匿名型を作成
var updated = original with { Version = 10 };

Console.WriteLine($"Original: {original.Name} v{original.Version}");
Console.WriteLine($"Updated: {updated.Name} v{updated.Version}");
実行結果
Original: C# v9
Updated: C# v10

このアップデートにより、匿名型を多段階の処理で少しずつ加工していくようなロジックが、非常にシンプルに記述できるようになりました。

不変性を保ちつつ、非破壊的な変更が可能になった点は大きなメリットです。

匿名型を使用する際の注意点

非常に便利な匿名型ですが、いくつか注意すべき制限事項があります。

1. メソッドの境界を越えられない

匿名型の型名はコンパイラが自動生成するため、プログラマがその型名をコード上で書くことができません。

そのため、パブリックなメソッドの引数や戻り値に匿名型を指定することはできません。

どうしても渡したい場合は、C# 9.0 以降であれば record を定義するか、それ以前であれば ValueTuple を検討してください。

2. リフレクションへの影響

匿名型は内部的にクラスとして生成されますが、その名前は予測不可能です。

リフレクションを使用して動的にプロパティを操作しようとする場合、通常のクラスよりも実装が複雑になったり、メンテナンス性が低下したりする恐れがあります。

3. IntelliSense の制限

同一アセンブリ内であれば型推論が効きますが、アセンブリ(プロジェクト)を跨いで匿名型をやり取りすることはできません。

そもそも、匿名型を外部に公開する設計自体がアンチパターンとされています。

まとめ

C# の匿名型は、「型定義のオーバーヘッドを減らし、局所的なデータ操作を簡潔にする」ための優れた機能です。

特に LINQ と組み合わせた際の効果は絶大で、現代の C# 開発において欠かせない要素となっています。

最後に、匿名型を使いこなすためのポイントをまとめます。

  • 一時的な利用に留める: メソッド内の LINQ 射影など、スコープが狭い範囲で使用するのがベストです。
  • 不変性を活用する: プロパティは読み取り専用であるため、意図しない副作用を防ぐことができます。
  • 最新機能を活用する: C# 10.0 以降であれば with 式を使い、非破壊的なコピーを積極的に利用しましょう。
  • 適切な型選択を行う: メソッドの外にデータを出すなら ValueTuple、再利用するデータ構造なら record と、状況に応じて使い分けることがプロフェッショナルなコードへの第一歩です。

匿名型の特性を正しく理解し、他の型と適切に使い分けることで、よりクリーンで保守性の高い C# プログラムを記述できるようになります。