C# 9.0は、.NET 5のリリースとともに導入されたモダンなプログラミング言語の進化における重要なマイルストーンです。

このバージョンでは、データ中心のプログラミングをより簡潔かつ安全に行うための機能が数多く追加されました。

特に「レコード型」や「トップレベルステートメント」といった機能は、開発者の生産性を劇的に向上させるだけでなく、コードの可読性を根本から変える力を持っています。

本記事では、C# 9.0で導入された主要な新機能を、具体的なサンプルコードを交えながら詳細に解説します。

レコード型(Records)による不変性の実現

C# 9.0における最大の目玉機能と言えるのが、このレコード型(Records)です。

従来のクラス(class)は、状態を保持し、その状態を変化させる「オブジェクト指向」の考え方が中心でした。

しかし、近年のソフトウェア開発では、一度作成したデータの内容を変更しない「不変性(Immutability)」の重要性が高まっています。

レコード型は、値に基づいた等価比較をサポートする参照型です。

クラスはデフォルトで「参照の等価性(同じメモリ空間を指しているか)」で比較されますが、レコード型は「プロパティの値がすべて等しいか」で比較されます。

ポジショナルレコードの基本

もっとも簡潔な記述方法は「ポジショナルレコード」と呼ばれる形式です。

以下のコードは、名前と年齢を持つ Person レコードを定義しています。

C#
using System;

// 1行で定義可能。プロパティはデフォルトで読み取り専用(init)
public record Person(string FirstName, string LastName);

class Program
{
    static void Main()
    {
        // インスタンスの生成
        var person1 = new Person("Taro", "Yamada");
        var person2 = new Person("Taro", "Yamada");

        // 値の等価性の確認
        Console.WriteLine($"person1 == person2: {person1 == person2}");
        
        // ToString() も自動でオーバーライドされる
        Console.WriteLine($"ToString: {person1}");
    }
}
実行結果
person1 == person2: True
ToString: Person { FirstName = Taro, LastName = Yamada }

このように、わずか1行の定義で、コンパイラがバッキングフィールド、プロパティ、コンストラクタ、デコンストラクタ、そして等価比較のロジックを自動生成します。

非破壊的改変(with式)

不変のデータを扱う際、一部の値だけを変更した新しいコピーを作成したい場面が多くあります。

これを実現するのが with式 です。

C#
var person1 = new Person("Taro", "Yamada");

// person1 をベースに LastName だけを変更した新しいインスタンスを作成
var person2 = person1 with { LastName = "Sato" };

Console.WriteLine(person1);
Console.WriteLine(person2);
実行結果
Person { FirstName = Taro, LastName = Yamada }
Person { FirstName = Taro, LastName = Sato }

元のインスタンスは変更されず、新しいインスタンスが生成されるため、サイドエフェクトの少ない安全なコードを記述できます。

Init専用セッター(Init-only setters)

レコード型を支える基盤技術の一つが、Init専用セッターです。

従来のC#では、プロパティを読み取り専用にするには get のみ、あるいはプライベートな set を使用していました。

しかし、それでは「オブジェクト初期化子」を使って値を設定することができませんでした。

init キーワードを使用すると、オブジェクトの構築中(インスタンス化のタイミング)にのみ値を設定でき、その後の変更を禁止することができます。

C#
public class Product
{
    // 初期化時のみ書き込み可能
    public string Name { get; init; }
    public decimal Price { get; init; }
}

class Program
{
    static void Main()
    {
        // オブジェクト初期化子が使用可能
        var p = new Product { Name = "Laptop", Price = 150000 };

        // 以下のコードはコンパイルエラーになる
        // p.Price = 140000; 
    }
}

これにより、不変性を保ちつつ、柔軟なインスタンス生成が可能になりました。

これはクラス定義においても非常に有用な機能です。

トップレベルステートメント(Top-level statements)

C# 9.0からは、プログラムのエントリポイントにおける「おまじない」のようなボイラープレートコードを省略できるようになりました。

これがトップレベルステートメントです。

従来の記述方式

これまでのC#では、単純な「Hello World」を表示するだけでも、名前空間、クラス定義、static void Main メソッドを書く必要がありました。

C#
using System;

namespace MyProject
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

C# 9.0以降の記述方式

トップレベルステートメントを使用すると、ファイル全体が直接 Main メソッドの中身であるかのように記述できます。

C#
using System;

// これだけで実行可能
Console.WriteLine("Hello C# 9.0!");

if (args.Length > 0)
{
    Console.WriteLine($"引数: {args[0]}");
}

この機能により、スクリプト言語のような手軽さでC#を書くことができるようになりました。

ただし、1つのプロジェクト内でトップレベルステートメントを持てるファイルは1つだけという制約があります。

主に小規模なコンソールアプリや、学習用のコード、Azure Functionsなどのサーバーレス関数での利用が推奨されます。

パターンマッチングの強化

C# 7.0以降、段階的に強化されてきたパターンマッチングが、C# 9.0でさらに進化しました。

新しく導入された「論理パターン」と「関係パターン」により、複雑な条件分岐を非常に読みやすく記述できます。

関係パターンと論理パターンの組み合わせ

数値を判定する際、比較演算子をパターンの一部として使用できるようになりました。

また、andornot というキーワードを使って条件を組み合わせることが可能です。

C#
public static string GetCategory(int age) => age switch
{
    < 13 => "Children",
    >= 13 and < 20 => "Teenager",
    >= 20 and < 65 => "Adult",
    _ => "Senior"
};

class Program
{
    static void Main()
    {
        Console.WriteLine(GetCategory(15)); // Teenager
        
        object data = "Hello";
        // not パターンの使用例
        if (data is not int)
        {
            Console.WriteLine("これは整数ではありません。");
        }
    }
}
実行結果
Teenager
これは整数ではありません。

is not の導入は、これまで !(obj is string) と書いていた読みにくいコードを、自然言語に近い形で表現できるようにしました。

ターゲット型の new 式(Target-typed new expressions)

変数の型が自明である場合、インスタンス生成時のクラス名を省略できるようになりました。

これをターゲット型の new 式と呼びます。

C#
using System.Collections.Generic;

// 従来
List<string> list1 = new List<string>();

// C# 9.0
List<string> list2 = new();

// フィールド定義でも有効
private readonly Dictionary<int, string> _cache = new();

特に、ジェネリクスの型引数が長い場合や、フィールド宣言において、同じ型名を二度書く冗長さを排除できるため、コードが非常にスッキリします。

ただし、var と組み合わせて使うことはできません(型が推論できなくなるため)。

その他の重要な新機能

C# 9.0には、これら以外にも細かな、しかし強力な改善が多数含まれています。

共変戻り値(Covariant return types)

オーバーライドしたメソッドの戻り値の型を、基底クラスの戻り値型の「派生型」に変更できるようになりました。

C#
public class Food { }
public class Meat : Food { }

public abstract class Animal
{
    public abstract Food GetFood();
}

public class Lion : Animal
{
    // 基底クラスでは Food だったが、Meat に変更可能
    public override Meat GetFood() => new Meat();
}

これにより、呼び出し側で不必要なキャストを行う手間が省けます。

ネイティブサイズ整数(nint, nuint)

実行環境のポインタサイズ(32ビットまたは64ビット)に応じた整数型が導入されました。

主に低レベルなプログラミングや、相互運用性(Interop)が必要な場面で活用されます。

  • nint: 符号付きネイティブ整数
  • nuint: 符号なしネイティブ整数

スタティックな匿名関数(Static anonymous functions)

ラムダ式や匿名メソッドに static 修飾子を付与できるようになりました。

これにより、意図せずローカル変数やインスタンスの状態をキャプチャしてしまい、メモリ割り当て(アロケーション)が発生することを防げます。

C#
int y = 10;

// static を付けることで y のキャプチャを禁止する
// コンパイルエラー:外部変数を参照できない
// Func<int, int> func = static x => x + y; 

Func<int, int> safeFunc = static x => x * 2;

パフォーマンスが極めて重要なシナリオにおいて、不要なメモリ消費を避けるための強力なツールとなります。

C# 9.0 を活用するための設計指針

これら多くの新機能が導入された背景には、C#をより「関数型プログラミング」の良さを取り入れた、堅牢な言語に進化させる意図があります。

機能主な用途期待される効果
レコード型DTO、イベントメッセージ、状態管理不変性の確保、比較ロジックの簡略化
Init専用セッター読み取り専用プロパティの設定安全な初期化プロセスの実現
パターンマッチング複雑なビジネスルール、型の判定条件分岐の可読性向上、バグの削減
ターゲット型の newフィールド宣言、引数の受け渡しタイピング量の削減、冗長性の排除

開発者は、単に新しい機能を使うだけでなく、「なぜこの機能が必要なのか」を理解することが重要です。

例えば、副作用を避けるためにレコード型を選択し、ロジックを簡潔に保つためにパターンマッチングを活用するという一貫した方針を持つことで、C# 9.0の真価を引き出すことができます。

まとめ

C# 9.0は、言語の表現力を飛躍的に向上させたバージョンです。

特にレコード型による不変データの扱いやすさと、パターンマッチングによる直感的な条件記述は、日常的なコーディングのスタイルを大きく進化させました。

また、トップレベルステートメントやターゲット型の new 式は、C#特有の冗長さを削ぎ落とし、本質的なロジックに集中できる環境を提供しています。

これらの機能を適切に組み合わせることで、保守性が高く、かつ意図が明確な美しいソースコードを記述することが可能になります。

C# 9.0以降も、C#はさらに進化を続けていますが、本バージョンで導入された概念は現在のモダンC#開発における「基礎」となっています。

ぜひ実際のプロジェクトに積極的に取り入れ、その利便性を体感してみてください。