C# required キーワードの使い方【必須プロパティ】

C#のrequiredキーワード徹底解説 – 必須プロパティで堅牢なオブジェクト設計を実現

はじめに

ソフトウェア開発において、オブジェクトの状態管理は非常に重要です。特に、オブジェクトが有効な状態で機能するためには、特定のプロパティが必ず初期化されている必要がある、という要件は頻繁に発生します。例えば、ユーザー情報を表すクラスであれば、ユーザー名やメールアドレスは必須かもしれません。商品を扱うクラスであれば、商品名や価格は必須でしょう。

しかし、従来のC#では、これらの「必須」なプロパティの初期化をコンパイラの力で強制する方法が限られていました。開発者は、以下のような方法で対応してきました。

  1. コンストラクタを使用する: 必須プロパティをすべてコンストラクタの引数として定義します。これにより、インスタンス生成時に必ず必要な値を渡すことを強制できます。
    “`csharp
    public class Product
    {
    public string Name { get; }
    public decimal Price { get; }
    public int Stock { get; set; } // Optional

    public Product(string name, decimal price)
    {
        Name = name;
        Price = price;
        // Stock は後で設定可能
    }
    

    }

    // 使用例
    var product = new Product(“Laptop”, 1200.00m); // OK
    // var product = new Product(“Tablet”); // コンパイルエラー (price が足りない)
    “`
    この方法はシンプルですが、プロパティが増えるとコンストラクタの引数が増大し、可読性が低下したり、複数の必須プロパティの組み合わせによってコンストラクタのオーバーロードが増えすぎたりする「コンストラクタ overload hell」に陥りがちです。また、すべての必須プロパティを引数として渡す必要があるため、オブジェクト初期化子のような柔軟な初期化(プロパティ名を指定して任意の順番で設定)ができません。

  2. ファクトリメソッドを使用する: 複雑な初期化ロジックや、複数の必須プロパティを持つオブジェクトの生成をカプセル化するために、静的ファクトリメソッドを使用する方法です。
    “`csharp
    public class Order
    {
    public int OrderId { get; private set; }
    public DateTime OrderDate { get; private set; }
    public Customer Customer { get; private set; }
    public List Items { get; private set; } = new List();

    private Order() { } // Hidden constructor
    
    public static Order CreateNewOrder(int orderId, Customer customer)
    {
        if (customer == null) throw new ArgumentNullException(nameof(customer));
    
        return new Order
        {
            OrderId = orderId,
            OrderDate = DateTime.UtcNow,
            Customer = customer
            // Items は後で追加される
        };
    }
    

    }

    // 使用例
    var customer = new Customer { CustomerId = 1, Name = “Alice” };
    var order = Order.CreateNewOrder(101, customer); // OK
    // var order = Order.CreateNewOrder(102, null); // ランタイムエラー
    “`
    この方法は、オブジェクト生成のロジックを完全に制御できる強力なパターンですが、メソッドを定義するボイラープレートコードが増え、また、コンストラクタと同様に、柔軟なプロパティ設定には向きません。ランタイムエラーになる可能性も残ります。

  3. ドキュメントや慣習に頼る: プロパティが必須であることをドキュメントに記載したり、コーディング規約で定めたりする方法です。
    “`csharp
    public class Configuration
    {
    // Required: Database connection string
    public string ConnectionString { get; set; }

    // Required: Log file path
    public string LogFilePath { get; set; }
    
    // Optional: Timeout in seconds
    public int Timeout { get; set; } = 30;
    

    }

    // 使用例
    var config = new Configuration();
    config.ConnectionString = “Data Source=…”; // 忘れがち
    // LogFilePath を設定し忘れてもコンパイルエラーにならない
    config.Timeout = 60;
    “`
    この方法は最も柔軟ですが、開発者が規約を守るかどうかに依存するため、必須プロパティの設定漏れによるバグが発生しやすいという深刻な欠点があります。設定漏れはランタイムで初めて発覚することが多く、デバッグが困難になることがあります。Null許容参照型 (NRT) を使用しても、プロパティ 自体 がnullではないことを保証できても、オブジェクト初期化時にプロパティが設定 されたか どうかをコンパイル時に強制することはできませんでした。

これらの課題を解決し、オブジェクト初期化子のような柔軟な初期化方法を維持しつつ、必須プロパティの設定漏れをコンパイル時に検出できるようにするために、C# 11ではrequiredキーワードが導入されました。

この記事では、C# 11で導入されたrequiredキーワード、特に必須プロパティの使い方について、その目的、使い方、詳細な挙動、メリット・デメリット、そして従来の初期化方法との比較などを約5000語にわたって徹底的に解説します。この記事を読むことで、requiredキーワードを理解し、より堅牢で意図が明確なオブジェクト設計を実現できるようになるでしょう。

requiredキーワードとは?

requiredキーワードは、プロパティまたはフィールド(ただし、現状はプロパティでの使用が主であり、フィールドでの使用は限定的か非推奨な場合があります。公式ドキュメントではプロパティに適用される例が多いです。)に適用できる修飾子です。この修飾子をプロパティに付けると、そのプロパティはオブジェクト初期化子(Object Initializer)を使用する際に必ず値を設定しなければならない必須メンバーとなります。

最も一般的な使い方は、クラスや構造体のプロパティにrequired修飾子を付けることです。

“`csharp
public class User
{
public required int Id { get; init; } // C# 9 の init と組み合わせ
public required string Username { get; set; }
public string? Email { get; set; } // Optional, nullable

// デフォルトコンストラクタは自動生成(必須メンバーを含むクラスでは注意が必要)

}
“`

このUserクラスをインスタンス化する際、オブジェクト初期化子を使用する場合、IdUsernameプロパティには必ず値を設定する必要があります。

“`csharp
// OK: required なプロパティをすべて初期化している
var user1 = new User { Id = 101, Username = “alice” };

// OK: optional なプロパティも初期化
var user2 = new User { Id = 102, Username = “bob”, Email = “[email protected]” };

// コンパイルエラー: Username が初期化されていない
// var user3 = new User { Id = 103 };

// コンパイルエラー: Id が初期化されていない
// var user4 = new User { Username = “charlie” };
“`

このように、required修飾子を付けるだけで、コンパイル時に必須プロパティの初期化漏れを検出できるようになります。これにより、ランタイムエラーや意図しないオブジェクト状態によるバグを防ぐことができます。

requiredプロパティの初期化は、主にオブジェクト初期化子を使用することを想定しています。しかし、コンストラクタを使用してすべてのrequiredプロパティを初期化することも可能です。この場合、コンストラクタに特別な属性[SetsRequiredMembers]を付けることで、コンパイラに「このコンストラクタはすべてのrequiredプロパティを初期化している」と通知する必要があります。これについては後述します。

重要な点は、required初期化に関する制約であり、そのプロパティがnull許容かどうか (string vs string?) とは独立した概念であるということです。required stringは「初期化時に必ずnull以外の文字列を設定する必要がある」という意味になり、required string?は「初期化時に文字列またはnullを設定する必要がある」という意味になります。

なぜrequiredが必要なのか? – 従来の課題とrequiredによる解決

前述のように、従来のC#における必須プロパティの初期化にはいくつかの課題がありました。requiredキーワードがこれらの課題をどのように解決するのか、より深く掘り下げてみましょう。

従来の初期化方法の限界

  1. コンストラクタの限界:

    • 柔軟性の欠如: コンストラクタは引数の順序が決まっており、名前付き引数を使わない限り、可読性が低下しやすいです。また、すべての必須プロパティを引数として渡す必要があります。オブジェクト初期化子のように、必要なプロパティだけを、好きな順番で設定するという柔軟性がありません。
    • コンストラクタオーバーロードの増大: オプションのプロパティや、異なる組み合わせの必須プロパティが存在する場合、それらをサポートするためにコンストラクタのオーバーロードが指数関数的に増える可能性があります。これはコードの保守性を著しく低下させます。
    • コンストラクタ引数とプロパティ名の不一致: コンストラクタ引数の名前とプロパティの名前が異なると、どの引数がどのプロパティに対応するのか分かりにくくなることがあります。これは特に、データ構造が大きい場合に顕著です。
  2. ファクトリメソッドの限界:

    • ボイラープレート: オブジェクト生成のためだけに静的メソッドを定義する必要があり、コード量が増えます。
    • ランタイムチェック: ファクトリメソッド内で引数のnullチェックなどを行うのが一般的ですが、これはランタイムでのチェックであり、コンパイル時に問題を検出することはできません。
  3. ドキュメント/規約の限界:

    • 強制力がない: 人間の注意や規約順守に依存するため、設定漏れが発生しやすいです。
    • デバッグの困難さ: 設定漏れによるバグは、プログラムの実行中の予期しないタイミングで、原因不明のエラーとして現れることが多く、デバッグが非常に困難になります。
  4. Null許容参照型 (NRT) の限界:

    • NRTは、参照型の変数やプロパティがnullを保持する可能性があるかどうかをコンパイラに伝える機能です。string?のように宣言することで、nullかもしれないことを明示し、nullチェックの警告などを有効にできます。
    • しかし、NRTは「そのプロパティがnullになりうるか」を示すものであり、「そのプロパティが初期化時に必ず値を設定されなければならないか」を示すものではありません。例えば、public string? Username { get; set; }というプロパティは、オブジェクト初期化子でUsername = nullと初期化しても、Usernameプロパティを完全にスキップしても、コンパイラエラーにはなりませんでした。つまり、NRTだけでは「必須だけどnullかもしれないプロパティ」や「必須でnullであってはならないプロパティ」の「必須」の部分をコンパイル時に強制できなかったのです。

requiredが解決する問題点

requiredキーワードは、これらの課題に対して、特に「オブジェクト初期化子を使用する際の必須プロパティの設定漏れ」という問題に焦点を当てて解決を提供します。

  • 初期化忘れの防止 (コンパイル時保証): これが最も大きなメリットです。requiredプロパティを持つオブジェクトをオブジェクト初期化子で生成する場合、すべてのrequiredプロパティに値を設定しないとコンパイルエラーになります。これにより、開発の初期段階で必須プロパティの設定漏れという種類のバグを検出・修正できます。
  • 意図しない状態のオブジェクト生成防止: 必須プロパティが設定されていないオブジェクトは、多くの場合、不完全または無効な状態です。requiredを使用することで、このような無効な状態のオブジェクトが誤って生成されることを防ぎます。
  • コードの可読性・意図の明確化: required修飾子がプロパティに付いていることで、そのプロパティがオブジェクトを有効な状態にするために必須であることがコードを読んだだけで明確に分かります。これはドキュメントやコメントに頼るよりも強力で信頼性があります。
  • 柔軟な初期化の維持: requiredプロパティはオブジェクト初期化子と組み合わせて使用することを主な想定としているため、コンストラクタのように引数の順序に縛られず、プロパティ名を指定して任意の順番で設定できます。これは、特に多数のプロパティを持つクラスで便利です。
  • Immutable (init-only) プロパティとの親和性: required initと組み合わせることで、「初期化時しか設定できず、かつ初期化時に必ず設定しなければならない」という、Immutableかつ必須なプロパティを簡単に定義できます。これは、より安全で予測可能なオブジェクトを作成するのに役立ちます。
  • バージョニングの考慮: requiredは、既存のクラスに後から必須プロパティを追加したい場合など、ライブラリのバージョニングにも影響を与えます。この点については注意が必要ですが、明確なルールがあるため、計画的に変更を行うことができます。

requiredプロパティの基本的な使い方

requiredプロパティの使い方は非常にシンプルです。プロパティ宣言の際に、型名の前にrequired修飾子を追加するだけです。

“`csharp
public class Product
{
// ProductName は必須
public required string ProductName { get; set; }

// Price は必須
public required decimal Price { get; init; } // init-only プロパティも可

// Stock はオプション
public int Stock { get; set; } = 0; // デフォルト値があってもオプション

// Description はオプションかつ null を許容
public string? Description { get; set; }

}
“`

このProductクラスを使用するコードでは、オブジェクト初期化子を使用する場合、ProductNamePriceプロパティを必ず設定する必要があります。

“`csharp
// 成功例: すべての required プロパティを初期化
var laptop = new Product
{
ProductName = “Laptop”,
Price = 1200.00m,
Stock = 50, // Optional は設定しなくてもOK
Description = “A powerful laptop.”
};

// 成功例: Optional なプロパティは省略可能
var keyboard = new Product
{
ProductName = “Mechanical Keyboard”,
Price = 75.00m
// Stock も Description も省略
};

// コンパイルエラー例 1: ProductName が初期化されていない
// var mouse = new Product
// {
// Price = 25.00m
// };
// エラーメッセージ例: ‘Product’ には、初期化する必要がある required メンバー ‘ProductName’ があります。

// コンパイルエラー例 2: Price が初期化されていない
// var monitor = new Product
// {
// ProductName = “Monitor”
// };
// エラーメッセージ例: ‘Product’ には、初期化する必要がある required メンバー ‘Price’ があります。

// コンパイルエラー例 3: 両方初期化されていない
// var desk = new Product { };
// エラーメッセージ例: ‘Product’ には、初期化する必要がある required メンバー ‘ProductName’、’Price’ があります。
“`

このように、requiredキーワードを使用すると、必須プロパティの初期化漏れが明確なコンパイルエラーとして報告されます。これは、開発プロセスのできるだけ早い段階で問題を特定し修正するのに非常に効果的です。

構造体 (Struct) への適用

requiredキーワードは、クラスだけでなく構造体にも適用できます。

“`csharp
public struct Point
{
public required int X { get; init; }
public required int Y { get; init; }
public int Z { get; init; } // Optional
}

// 成功例
var p1 = new Point { X = 10, Y = 20 };
var p2 = new Point { X = 5, Y = 15, Z = 30 };

// コンパイルエラー例
// var p3 = new Point { X = 10 }; // Y が不足
“`

構造体の場合も、クラスと同様にオブジェクト初期化子を使用する際にrequiredプロパティの初期化が強制されます。

requiredプロパティの詳細

適用可能なメンバー

required修飾子は以下のメンバーに適用できます。

  • プロパティ: get; set; または get; init; のアクセサーを持つプロパティ。これが最も一般的で推奨される使い方です。
  • フィールド: requiredフィールドは、requiredプロパティと同様にオブジェクト初期化子または[SetsRequiredMembers]付きコンストラクタで初期化される必要があります。ただし、フィールドにrequiredを付けることは推奨されない場合があります。理由としては、通常、オブジェクトの状態はプロパティを通じて公開・制御されるべきであり、フィールドは実装の詳細として隠蔽されることが多いためです。また、プロパティの方がアクセサーを通じて追加のロジック(検証など)を組み込みやすいという利点があります。そのため、requiredはプロパティに適用するのが一般的です。

初期化の方法

requiredプロパティを持つオブジェクトのインスタンス化および初期化方法は主に2つあります。

  1. オブジェクト初期化子:
    これがrequiredキーワードの主要な使用シナリオです。new ClassName { RequiredProp1 = value1, RequiredProp2 = value2, ... }のように、オブジェクト初期化子構文を使用してすべてのrequiredプロパティに値を設定する必要があります。これが満たされない場合、コンパイルエラーが発生します。

  2. コンストラクタ内での初期化 ([SetsRequiredMembers]属性):
    クラスがコンストラクタを提供し、そのコンストラクタがすべてのrequiredプロパティを完全に初期化する場合、そのコンストラクタに[System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]属性を付けることができます。この属性はコンパイラに対するヒントであり、「このコンストラクタを呼び出して生成されたオブジェクトは、すべてのrequiredプロパティが初期化済みとみなして良い」ということを伝えます。
    “`csharp
    using System.Diagnostics.CodeAnalysis;

    public class Book
    {
    public required string Title { get; init; }
    public required string Author { get; init; }
    public int PageCount { get; set; }

    // このコンストラクタは全ての required メンバーを初期化している
    [SetsRequiredMembers]
    public Book(string title, string author)
    {
        Title = title;
        Author = author;
        // PageCount は optional なので初期化は必須ではない
    }
    
    // このコンストラクタは required メンバーを初期化していない(PageCount のみ)
    // [SetsRequiredMembers] を付けると警告またはエラーになるべき(C# 12現在、これを付けてもエラーにはなりませんが、コンパイラは warning を出す場合があります。意図としては、すべての required メンバーを初期化しないコンストラクタには付けないでください、です。)
    // したがって、このコンストラクタでは Book のインスタンスを正常に初期化できないため、通常は提供しません。
    // public Book(int pageCount)
    // {
    //     PageCount = pageCount;
    // }
    
    // デフォルトコンストラクタは required メンバーを初期化しないため、[SetsRequiredMembers] を付けることはできません。
    // required メンバーを持つクラスは、オブジェクト初期化子を使用するか、[SetsRequiredMembers] 付きコンストラクタを使用するかのどちらかで初期化される必要があります。
    // デフォルトコンストラクタを生成させない(またはプライベートにする)などの考慮が必要になる場合があります。
    

    }

    // 使用例
    // [SetsRequiredMembers] 付きコンストラクタを使用
    var book1 = new Book(“The Hitchhiker’s Guide to the Galaxy”, “Douglas Adams”); // OK

    // オブジェクト初期化子を使用 (コンストラクタがなくてもOK)
    // ただし、上記の Book クラスのように [SetsRequiredMembers] 付きコンストラクタ のみ 提供している場合、
    // new Book() { … } というオブジェクト初期化子はデフォルトコンストラクタを呼び出そうとするため、エラーになることがあります。
    // デフォルトコンストラクタを隠蔽しない限り、以下の書き方も可能です。
    // var book2 = new Book { Title = “Foundation”, Author = “Isaac Asimov” }; // OK (デフォルトコンストラクタ + オブジェクト初期化子)

    // [SetsRequiredMembers] 付きコンストラクタを使用しつつ、オブジェクト初期化子も併用することも可能。
    // オブジェクト初期化子で設定された値が優先される。
    var book3 = new Book(“Placeholder Title”, “Placeholder Author”)
    {
    Title = “Dune”, // ここで上書き
    Author = “Frank Herbert”, // ここで上書き
    PageCount = 896 // Optional なプロパティを設定
    };
    Console.WriteLine(book3.Title); // Output: Dune

    // [SetsRequiredMembers] が付いていないコンストラクタを呼び出した場合 (上記 PageCount のみのコンストラクタがあったとして)
    // var book4 = new Book(500); // コンパイルは通るが、Title, Author は初期化されていない状態になる
    // この状態で book4.Title などにアクセスすると問題が発生する可能性があります。
    // したがって、[SetsRequiredMembers] はコンパイラに正確な情報を提供することが重要です。
    “`

[SetsRequiredMembers]属性は、主に以下のようなシナリオで有用です。

  • オブジェクト初期化子を使わず、コンストラクタですべての必須値を渡してインスタンスを生成したい場合。
  • ベースクラスがrequiredプロパティを持ち、派生クラスのコンストラクタからベースクラスのコンストラクタを呼び出す場合。

コンストラクタと[SetsRequiredMembers]属性

requiredプロパティを持つクラスでコンストラクタを定義する場合、そのコンストラクタが全てのrequiredプロパティを初期化しているか、または[SetsRequiredMembers]属性が付いているかのいずれかである必要があります。

  • コンストラクタ内で全てのrequiredプロパティを直接初期化する: この場合、[SetsRequiredMembers]属性は必須ではありませんが、付けることで意図が明確になります。コンパイラは、コンストラクタの本体や、コンストラクタが呼び出す他のコンストラクタ/メソッド(this(...)base(...))がrequiredプロパティを初期化しているかを静的に分析しようとします。この分析は限定的であり、複雑な初期化ロジックの場合は[SetsRequiredMembers]を明示的に付けることが推奨されます。
    “`csharp
    public class Example
    {
    public required string Name { get; init; }

    // コンストラクタ内で Name を直接初期化
    // [SetsRequiredMembers] は必須ではないが、付けても良い
    public Example(string name)
    {
        Name = name;
    }
    

    }
    “`

  • コンストラクタ内でrequiredプロパティを初期化せず、[SetsRequiredMembers]属性を付ける: これは、コンストラクタ自体はrequiredプロパティに直接値を代入しないが、間接的に(例えば別のメソッドを呼び出すなどで)初期化を保証する場合や、コンパイラが静的に初期化を検証できない複雑なケースで使用されます。ただし、この属性は「コンパイラに信じてもらう」ためのものなので、実際には初期化していないコンストラクタに誤って付けてしまうと、ランタイムエラーの原因となります。したがって、この属性は本当に全てのrequiredプロパティが初期化されることを保証できるコンストラクタにのみ付けるべきです。

  • デフォルトコンストラクタ: requiredプロパティを持つクラスにユーザー定義のコンストラクタが一つも無い場合、コンパイラは引数なしのデフォルトコンストラクタを自動生成します。しかし、この自動生成されたデフォルトコンストラクタはrequiredプロパティを初期化しないため、通常はこのデフォルトコンストラクタを使ってオブジェクトをインスタンス化しようとするとコンパイルエラーになります。requiredプロパティを持つクラスは、オブジェクト初期化子を使用するか、[SetsRequiredMembers]属性付きのコンストラクタを使用するかのどちらかで初期化される必要があります。

継承とrequired

継承において、requiredプロパティの扱いは以下のようになります。

  • ベースクラスのrequiredプロパティ: ベースクラスでrequiredとして宣言されたプロパティは、その派生クラスでもrequiredです。派生クラスのインスタンスを初期化する際、ベースクラスで定義されたrequiredプロパティも初期化される必要があります。
    “`csharp
    public class Base
    {
    public required int BaseId { get; init; }
    public string BaseName { get; set; }
    }

    public class Derived : Base
    {
    public required string DerivedCode { get; init; }
    public double DerivedValue { get; set; }

    // オブジェクト初期化子を使用する場合、BaseId と DerivedCode の両方が必要
    

    }

    // 使用例
    var d1 = new Derived
    {
    BaseId = 1, // Base の required
    DerivedCode = “A”, // Derived の required
    BaseName = “Base”, // Optional
    DerivedValue = 1.2 // Optional
    }; // OK

    // コンパイルエラー: BaseId が不足
    // var d2 = new Derived { DerivedCode = “B” };

    // コンパイルエラー: DerivedCode が不足
    // var d3 = new Derived { BaseId = 2 };
    “`

  • 派生クラスのコンストラクタと[SetsRequiredMembers]: 派生クラスがコンストラクタを提供する場合、そのコンストラクタまたはそれが呼び出すベースクラスのコンストラクタが、ベースクラスと派生クラスで定義されているすべてのrequiredプロパティを初期化する必要があります。派生クラスのコンストラクタに[SetsRequiredMembers]属性を付ける場合、それはベースクラスのrequiredプロパティと派生クラスのrequiredプロパティ すべて をこのコンストラクタが初期化することを意味します。

    もし派生クラスのコンストラクタがbase(...)でベースクラスの[SetsRequiredMembers]付きコンストラクタを呼び出す場合、派生クラス側では、ベースクラスのrequiredプロパティについては初期化済みとみなされます。派生クラスのコンストラクタ自身は、派生クラスで追加されたrequiredプロパティのみを初期化すればよく、そのコンストラクタに[SetsRequiredMembers]を付ける場合、それは派生クラスで追加されたrequiredプロパティのみを初期化することをコンパイラに伝えることになります。
    “`csharp
    public class BaseWithRequired
    {
    public required int Id { get; init; }

    [SetsRequiredMembers] // このコンストラクタは Id を初期化する
    public BaseWithRequired(int id)
    {
        Id = id;
    }
    

    }

    public class DerivedWithRequired : BaseWithRequired
    {
    public required string Name { get; init; }

    // このコンストラクタは base(id) で Id を初期化し、自身で Name を初期化する
    // したがって、DerivedWithRequired の全ての required メンバー(Id と Name)を初期化している
    [SetsRequiredMembers]
    public DerivedWithRequired(int id, string name) : base(id)
    {
        Name = name;
    }
    
    // オブジェクト初期化子を使用する場合、デフォルトコンストラクタ (hidden) + オブジェクト初期化子の形になる
    // BaseId と DerivedCode の両方が必要
    

    }

    // 使用例
    var derived1 = new DerivedWithRequired(10, “Test”); // OK ([SetsRequiredMembers] 付きコンストラクタ使用)

    // オブジェクト初期化子を使用する場合 (デフォルトコンストラクタ + 初期化子)
    var derived2 = new DerivedWithRequired
    {
    Id = 20, // Base の required
    Name = “Another Test” // Derived の required
    }; // OK
    “`

継承とrequiredを組み合わせる場合、どのコンストラクタに[SetsRequiredMembers]を付けるか、そしてそのコンストラクタが実際にどのrequiredプロパティを初期化しているかについて、慎重に検討する必要があります。

requiredinit

C# 9で導入されたinitアクセサーは、プロパティをオブジェクト初期化時のみ設定可能にする機能です。requiredinitは非常に相性が良い組み合わせです。

“`csharp
public class Settings
{
// アプリケーション名: 必須かつ不変
public required string AppName { get; init; }

// バージョン: 必須かつ不変
public required Version AppVersion { get; init; }

// ログレベル: オプションかつ初期化時のみ設定可能
public LogLevel LogLevel { get; init; } = LogLevel.Information;

// データベース接続文字列: 必須だが実行中に変更される可能性 (稀だが)
public required string DbConnectionString { get; set; }

}
“`

required initプロパティは、「インスタンス生成時に必ず値を設定しなければならず、一度設定したら変更できない」という強力な制約を提供します。これは、アプリケーションの設定や、一度生成したら状態が変わらないようなドメインオブジェクトなど、Immutableな必須データを表現するのに非常に役立ちます。

requiredrecord

C# 9で導入されたrecord型は、主にデータを表現するための参照型です。recordはデフォルトで値による等価性を持ち、immutableなプロパティを簡単に定義できます。recordrequiredも組み合わせて使用できます。

recordには、Positional parameters を使用するPrimary constructor構文と、標準的なプロパティ定義構文があります。

  • Positional parameters を使用する場合: Primary constructor の引数は、自動的に対応するinit-onlyプロパティになります。これらのプロパティはコンストラクタの引数として渡されるため、デフォルトで必須のような振る舞いをしますが、requiredキーワードを Positional parameter に付けることはできません。
    csharp
    // Positional parameter を使う場合
    public record Person(required int Id, required string Name); // Error: 'required' はここで使えません
    public record Person(int Id, string Name); // これで Id と Name は必須 (コンストラクタ引数として)

  • 標準的なプロパティ定義構文を使用する場合: record型でも、クラスと同様にプロパティを明示的に定義し、それにrequired修飾子を付けることができます。この場合、そのプロパティはオブジェクト初期化子を使用する際に必須となります。
    “`csharp
    public record Customer
    {
    public required int CustomerId { get; init; }
    public required string Name { get; init; }
    public string? Address { get; init; }
    }

    // 使用例 (オブジェクト初期化子が必要)
    var customer = new Customer { CustomerId = 1, Name = “Alice” }; // OK

    // コンパイルエラー: Name が不足
    // var customer = new Customer { CustomerId = 2 };
    “`

recordrequiredプロパティを使用する主なシナリオは、Primary constructorでは表現しきれない複雑な初期化ロジックが必要な場合や、コンストラクタ引数ではなくオブジェクト初期化子での初期化を強制したい場合などです。また、recordwith式はrequiredプロパティを考慮して動作します。with式でrequiredプロパティを省略すると、コンパイルエラーになります。

“`csharp
var original = new Customer { CustomerId = 1, Name = “Alice” };

// with 式で required プロパティを省略するとコンパイルエラー
// var modified = original with { }; // Error: CustomerId, Name が不足

// with 式で required プロパティを設定する必要がある (元の値を使う場合も明示が必要)
var modified = original with { CustomerId = original.CustomerId, Name = “Bob” }; // OK
// もしくは、全ての required プロパティを with 式で指定する必要がある
var modified2 = original with { CustomerId = original.CustomerId, Name = original.Name, Address = “Tokyo” }; // OK

// with 式で optional プロパティのみ変更する場合は required は指定不要
var modified3 = original with { Address = “Osaka” }; // OK
``
**補足:** C# 12では、
with式におけるrequiredメンバーの扱いは少し緩和され、with式のターゲットオブジェクトが全てのrequiredメンバーが初期化済みであることがコンパイラによって保証される場合、with式内でそれらを再指定する必要がなくなりました。上記の例で言えば、originalが適切に初期化されているなら、var modified3 = original with { Address = “Osaka” };だけでなく、var modified4 = original with { Name = “Bob” };のように、with式で変更しないrequiredメンバー (CustomerId) を省略してもコンパイルエラーにならない場合があります。しかし、一般的なケースでは、required`メンバーを明示的に指定する方が安全で意図が明確になりやすいでしょう。

requiredとNullable

requiredは初期化に関する制約であり、null許容性 (string?) とは独立した概念です。これらは組み合わせて使用できます。

  • required string: 初期化時に必ずnullではない文字列を設定する必要がある。
  • required string?: 初期化時に必ず文字列またはnullを設定する必要がある。

“`csharp
public class Item
{
// 名前: 必須かつ null 不可
public required string Name { get; set; }

// シリアル番号: 必須だが null も許容 (存在しない場合など)
public required string? SerialNumber { get; set; }

// 色: オプションかつ null 可
public string? Color { get; set; }

}

// 使用例
var item1 = new Item { Name = “Widget”, SerialNumber = “SN123” }; // OK
var item2 = new Item { Name = “Gadget”, SerialNumber = null }; // OK (required string? なので null 初期化も可)
// var item3 = new Item { Name = “Thing” }; // Compile Error (SerialNumber が不足)
// var item4 = new Item { SerialNumber = “SN456” }; // Compile Error (Name が不足)
// var item5 = new Item { Name = null, SerialNumber = “SN789” }; // Compile Error (Name は required string なので null 不可)
“`

required string?のように宣言することで、「このプロパティは初期化時に必ず設定される必要があるが、その値はnullであっても構わない」というセマンティクスを表現できます。これは、例えばデータベースの列でNULLを許可しつつ、アプリケーション側ではその列に対応するプロパティが常に初期化されていることを保証したい、といった場合に便利です。

requiredとデフォルト値

requiredプロパティにフィールド初期化子(プロパティ宣言時に = value の形式で指定するデフォルト値)を設定することは可能ですが、これはrequiredの振る舞いに影響しません。requiredはあくまでオブジェクト初期化子または[SetsRequiredMembers]付きコンストラクタによる初期化を強制するものであり、フィールド初期化子による初期化を初期化済みとはみなしません。

“`csharp
public class SettingsWithDefault
{
public required int TimeoutSeconds { get; set; } = 30; // デフォルト値がある

public required string ConnectionString { get; set; } // デフォルト値なし

}

// 使用例
// コンパイルエラー: ConnectionString が不足。TimeoutSeconds にデフォルト値があっても必須。
// var settings1 = new SettingsWithDefault { };

// OK: 両方の required プロパティを初期化
var settings2 = new SettingsWithDefault
{
TimeoutSeconds = 60, // デフォルト値を上書き
ConnectionString = “Server=…”
};

// OK: ConnectionString のみ初期化。TimeoutSeconds はデフォルト値で初期化された後、オブジェクト初期化子で明示的に設定されないためコンパイルエラーになる… はずが、
// 実は C# の仕様では、オブジェクト初期化子で required プロパティを省略するとコンパイルエラーになります。
// required は「初期化子か [SetsRequiredMembers] 付きコンストラクタで設定すること」を強制するものであり、
// フィールド初期化子は初期化方法としてはカウントされません。
// したがって、以下のコードはコンパイルエラーです。
// var settings3 = new SettingsWithDefault { ConnectionString = “Server=…” }; // Compile Error: TimeoutSeconds が不足

// オブジェクト初期化子を使う場合は、デフォルト値の有無に関わらず、全ての required プロパティを初期化子内で明示的に指定する必要があります。
// これにより、デフォルト値がある required プロパティも初期化子で意図的に設定していることが明確になります。
var settings4 = new SettingsWithDefault
{
TimeoutSeconds = 30, // デフォルト値と同じだが明示的に指定
ConnectionString = “Server=…”
}; // OK
``
この挙動は一見直感に反するかもしれませんが、
requiredの目的が「オブジェクト初期化子を使うなら、必須プロパティは**全て**初期化子に列挙されているべき」という意図表明にあると考えれば理にかなっています。デフォルト値は、初期化子やコンストラクタで値が指定されなかった場合のフォールバックではなく、フィールドレベルでの初期値設定に過ぎません。required`はそのプロパティが「初期化シーケンスの中で明示的に設定されること」を要求するのです。

配列、コレクション、辞書

配列やコレクション型のプロパティにrequiredを付けることも可能です。

“`csharp
public class Report
{
public required string Title { get; set; }
public required List Sections { get; set; } // List は必須
public Dictionary? Metadata { get; set; } // オプション
}

// 使用例
var report1 = new Report
{
Title = “Annual Report”,
Sections = new List { “Introduction”, “Body”, “Conclusion” } // OK
};

var report2 = new Report
{
Title = “Monthly Summary”,
Sections = new List() // OK (必須は List 自体が null でないことで、中身が空でも良い)
};

// コンパイルエラー: Sections が不足
// var report3 = new Report { Title = “Weekly Update” };

// コンパイルエラー: Sections に null を設定しようとしている (List は参照型で、required なので null 不可)
// var report4 = new Report { Title = “Daily Note”, Sections = null };
“`

required List<string>のような宣言は、「このプロパティは初期化時に必ずList<string>型の非nullな値を設定されなければならない」ことを意味します。これは、リストや配列のインスタンスそのものが必須であることを保証しますが、そのコレクションが空でないことや、含まれる要素が特定の条件を満たすことまでは保証しません。コレクションの中身に関する検証が必要な場合は、別途ロジック(例: コンストラクタやセッター内での検証、または専用の検証メソッド)を実装する必要があります。

requiredプロパティの内部動作

requiredキーワードは、C#コンパイラと.NETランタイムライブラリ(BCL)の連携によって実現されています。

  1. コンパイル時:

    • required修飾子が付いたプロパティやフィールドは、コンパイラによってIL (Intermediate Language) コードに特別な属性が付けられます。具体的には、System.Runtime.CompilerServices.RequiredMemberAttributeという属性が付与されます。
    • クラスや構造体でrequiredプロパティが一つ以上宣言されている場合、コンパイラはデフォルトの引数なしコンストラクタの挙動を変更します。通常のクラスではデフォルトコンストラクタが自動生成され、自由に呼び出せますが、requiredプロパティを持つクラスのデフォルトコンストラクタは、オブジェクト初期化子と組み合わせて呼び出されるか、または[SetsRequiredMembers]属性を持つコンストラクタから呼ばれる特定の文脈でのみ有効とみなされます。
    • コンパイラは、オブジェクト初期化子を使用するコードを検出すると、その初期化子で使用されているプロパティのリストを確認します。もし、対象の型のrequiredプロパティがすべてそのリストに含まれていない場合、コンパイルエラー(CS9035など)を生成します。
    • コンパイラは、[SetsRequiredMembers]属性が付いたコンストラクタを検出すると、「このコンストラクタはrequiredプロパティを初期化している」というヒントとして利用します。この属性を持つコンストラクタを呼び出したコードは、たとえオブジェクト初期化子を伴っていなくても、requiredプロパティが初期化済みであるとみなされます。
  2. ランタイム時:

    • requiredキーワード自体は、実行時の動作(例えば、実行時に初期化漏れを検出して例外を投げるなど)に直接影響を与えるものではありません。requiredによる初期化の強制は完全にコンパイル時に行われます。
    • RequiredMemberAttribute属性はILに含まれていますが、通常のアプリケーションコードが実行時にこの属性を直接利用して特別な処理を行うことは稀です。しかし、リフレクションや特定のフレームワーク(シリアライザなど)は、この属性を読み取ってrequiredメンバーの存在を認識し、それに応じた動作(例えば、逆シリアル化時に必須メンバーの値が入力に含まれているかを確認するなど)を実装する可能性があります。
    • [SetsRequiredMembers]属性も同様にILに含まれる属性であり、主にコンパイラが静的解析を行うためのヒントとして使用されます。ランタイムにおける特別な効果はありません。

要するに、requiredはコンパイラが開発者に対して「このプロパティは初期化必須だよ」と教え、それを守らないコードをエラーにするための機能であり、その挙動は主にコンパイル時に決定されます。ランタイムは、そのコンパイラによって生成されたコードを実行するだけです。この「コンパイル時保証」こそが、requiredの最大の価値です。

requiredプロパティのメリット・デメリット

requiredキーワードを導入することには、いくつかのメリットとデメリットがあります。

メリット

  • 初期化漏れをコンパイル時保証: 最大のメリットです。これにより、ランタイムエラーやデバッグ困難なバグの発生を劇的に減らすことができます。コードの信頼性と堅牢性が向上します。
  • コードの意図が明確になる: プロパティ宣言にrequiredが付いていることで、そのプロパティがオブジェクトの有効な状態にとって不可欠であることがコードを読むだけで明確に分かります。これは、APIの意図を開発者に伝える上で非常に効果的です。
  • 開発効率の向上: 必須プロパティの設定漏れによるデバッグ時間を削減できます。コンパイラがエラーを教えてくれるため、より迅速に問題を特定し修正できます。
  • 柔軟な初期化を維持: オブジェクト初期化子の柔軟性(任意の順番でプロパティを設定できる)を維持したまま、必須プロパティの強制が可能です。コンストラクタの引数リストが長くなる問題や、多数のオーバーロードを作成する手間を避けることができます。
  • Immutable (init) との組み合わせ: required initにより、変更不可かつ必須という強力な制約を持つプロパティを簡単に定義できます。

デメリット

  • C# 11以降の機能: requiredキーワードを使用するには、C# 11以降のコンパイラが必要です。また、RequiredMemberAttributeSetsRequiredMembersAttributeは.NET 7で導入された属性であるため、ターゲットフレームワークも.NET 7以降である必要があります。(古いフレームワークをターゲットにC# 11でコンパイルすることも技術的には可能ですが、属性が見つからない警告やエラーが発生する可能性があります。現実的には、.NET 7以降とC# 11以降を組み合わせて使用するのが一般的です。)
  • 初期化方法に制約: requiredプロパティを持つオブジェクトは、オブジェクト初期化子を使用するか、[SetsRequiredMembers]属性付きコンストラクタを使用するかのどちらかで初期化される必要があります。これにより、特にデフォルトコンストラクタのみでインスタンス化して後からプロパティを設定するというようなパターンは、requiredプロパティに関しては直接は行えなくなります(ただし、これは多くの場合、望ましくない初期化パターンです)。
  • バージョニングに関する注意: ライブラリ開発などにおいて、既存のpublicなクラスにrequiredプロパティを後から追加することは、そのクラスを使用している既存のコードにとって破壊的変更となります。既存のコードがオブジェクト初期化子を使っている場合、追加されたrequiredプロパティを初期化しないとコンパイルエラーになるからです。このため、互換性を維持するためには、[SetsRequiredMembers]属性付きの新しいコンストラクタを追加するなどの対応が必要になる場合があります。
  • 多くのrequiredプロパティを持つ場合の初期化子の長さ: requiredプロパティが多いクラスでは、オブジェクト初期化子が非常に長くなる可能性があります。これは可読性に影響を与える可能性があります。

requiredプロパティを使うべきケース・使うべきでないケース

requiredキーワードは強力ですが、すべてのプロパティに無差別に適用すべきではありません。その特性を理解し、適切に適用することが重要です。

requiredプロパティを使うべきケース

  • オブジェクトの有効な状態に不可欠なプロパティ: ビジネスロジック上、そのプロパティに値が設定されていないとオブジェクトが機能しない、または無効な状態になる場合に最適です。
  • データ転送オブジェクト (DTO): 異なるレイヤー間やプロセス間でデータをやり取りする際に使用されるDTOでは、必須フィールドが明確であることが重要です。requiredを使用することで、DTOの必須項目を明確にし、設定漏れを防ぐことができます。
  • 設定オブジェクト: アプリケーションやモジュールの設定値を保持するオブジェクトにおいて、必須の設定項目にrequiredを付けることで、設定漏れを防ぎ、アプリケーションの起動失敗といった問題を早期に検出できます。
  • Immutableな必須データ: required initと組み合わせて、オブジェクトが生成されたら二度と変更されないが、かつ必ず設定される必要があるデータを表現する場合に有効です。
  • 明確なコントラクト: APIやライブラリにおいて、ユーザーに必ず設定してほしいプロパティがある場合、requiredを使用することでその意図を強力に伝えることができます。

requiredプロパティを使うべきでないケース

  • オプションなプロパティ: 値が設定されていなくてもオブジェクトが有効な状態を保てるプロパティには、requiredを付けるべきではありません。
  • デフォルト値で十分なプロパティ: 常に同じデフォルト値が設定されることが期待され、ユーザーが明示的に設定する必要がないプロパティ。
  • 依存性注入 (DI) によってフレームワークが設定するプロパティ: ASP.NET Core のControllerのプロパティや、DIコンテナが自動的に解決して設定するプロパティなどは、開発者がオブジェクト初期化子で設定するものではないため、通常はrequiredにはしません。(ただし、一部のDIコンテナはrequiredプロパティへの注入に対応している場合があります。)
  • シリアライザによって逆シリアル化されるプロパティで、入力ファイルにそのフィールドが含まれない可能性がある場合: 例えばJSONやXMLから逆シリアル化する際に、特定のフィールドが入力に含まれていないことを許容する場合、そのプロパティをrequiredにすると、入力ファイルにフィールドがない場合に逆シリアル化が失敗します。シリアライザによってはrequiredプロパティの扱いを設定できますが、デフォルトでは厳密な挙動になることが多いです。
  • プロパティの初期化順序が厳密に重要な場合: オブジェクト初期化子はプロパティを設定する順序を保証しません(リフレクションを使って設定されるため)。もしプロパティAを設定してからプロパティBを設定する必要がある、といった依存関係がある場合は、オブジェクト初期化子ではなく、コンストラクタやファクトリメソッドで順序を制御する方が適切です。requiredは初期化の「存在」を保証しますが、「順序」は保証しません。

従来の初期化方法との比較

requiredプロパティは、従来の初期化方法(コンストラクタ、ファクトリメソッド)の代替というよりは、オブジェクト初期化子の利用シーンにおける必須プロパティの保証という、特定の課題に特化したソリューションと捉えるべきです。それぞれの方法には利点があり、状況に応じて使い分けることが重要です。

  • コンストラクタ:

    • 利点: 初期化に必要なすべての値を引数として強制できる。初期化の順序や依存関係を厳密に制御できる。Immutableなプロパティ(get; init;get;)の初期化に適している。
    • 欠点: 引数が増えると扱いにくい。柔軟な初期化(一部プロパティのみ設定)が難しい。オーバーロードが増えやすい。
    • requiredとの関係: requiredプロパティを持つクラスでもコンストラクタは定義できます。[SetsRequiredMembers]属性を使用することで、コンストラクタによる初期化もrequiredの要件を満たす初期化方法としてコンパイラに認識させることができます。多くの必須引数を持つコンストラクタの代替として、requiredプロパティ+オブジェクト初期化子が有効な選択肢となります。
  • ファクトリメソッド:

    • 利点: オブジェクト生成に関する複雑なロジック(条件分岐、他のオブジェクトの生成など)をカプセル化できる。コンストラクタだけでは表現できない多様な生成シナリオに対応できる。生成されるオブジェクトの型のサブタイプを返すなど、柔軟な設計が可能。
    • 欠点: 追加のメソッド定義が必要(ボイラープレート)。生成処理が静的メソッドに集中しがち。初期化の強制はランタイムチェックに依存しやすい。
    • requiredとの関係: ファクトリメソッド内でrequiredプロパティを持つオブジェクトを生成する際に、オブジェクト初期化子を使用することで、メソッド内の必須プロパティ設定漏れをコンパイル時に検出できます。ファクトリメソッドは複雑な生成ロジックを担当し、その中で生成されるオブジェクトの必須プロパティ設定はrequiredに任せる、という組み合わせが可能です。
  • requiredプロパティ + オブジェクト初期化子:

    • 利点: オブジェクト初期化子の柔軟性(任意のプロパティを任意の順序で設定)を維持しつつ、必須プロパティの設定漏れをコンパイル時に保証できる。コードの意図が明確。コンストラクタの引数過多問題を緩和。
    • 欠点: 初期化の順序は保証されない。オブジェクト初期化子を使用する必要がある。コンストラクタベースの初期化や複雑な生成ロジックには不向き。
    • 他の方法との関係: シンプルなデータオブジェクトや設定オブジェクトなど、初期化の順序が重要でなく、主に値を設定するだけでオブジェクトが有効になる場合に最も適しています。コンストラクタやファクトリメソッドは、より複雑な初期化や生成ロジックが必要な場合に補完的に使用されます。

適切な初期化方法の選択は、クラスの目的、プロパティ間の依存関係、初期化ロジックの複雑さ、そして開発チームのコーディング規約など、様々な要因に依存します。requiredはこれらの選択肢に新たな、強力な選択肢を加えるものと言えます。

requiredプロパティに関するよくある質問 (FAQ)

Q: requiredフィールドは作れますか?
A: はい、技術的には可能です。public required string _name;のように宣言できます。しかし、一般的にはプロパティを使用することが推奨されます。プロパティはアクセサーを通じてカプセル化や追加ロジックの組み込みが容易だからです。特別な理由がない限り、requiredはプロパティに適用するのがベストプラクティスです。

Q: requiredプロパティにデフォルト値を設定できますか?
A: はい、フィールド初期化子を使ってデフォルト値を設定することは可能です (public required int Count { get; set; } = 0;)。しかし、前述のように、オブジェクト初期化子を使用する場合、このデフォルト値はrequiredプロパティの初期化としてはカウントされません。オブジェクト初期化子内でそのプロパティを明示的に設定する必要があります。

Q: requiredプロパティを持つオブジェクトを、デフォルトコンストラクタだけで生成できますか?
A: 通常はできません。requiredプロパティを持つクラスのデフォルトコンストラクタ(明示的に定義されているか、コンパイラが自動生成したものかに関わらず)は、requiredプロパティを初期化しないため、そのまま呼び出すとコンパイルエラーになります。インスタンス化するには、オブジェクト初期化子を使用するか、[SetsRequiredMembers]属性付きコンストラクタを使用する必要があります。もしデフォルトコンストラクタで全てのrequiredプロパティを初期化する(稀なケースですが)のであれば、そのデフォルトコンストラクタに[SetsRequiredMembers]属性を付ける必要があります。

Q: シリアライザはrequiredプロパティに対応していますか?
A: 主要なシリアライザ(System.Text.Json, Newtonsoft.Jsonなど)はrequiredプロパティに対応しています。.NET 7以降のSystem.Text.Jsonはデフォルトでrequiredプロパティを認識し、逆シリアル化時に対応するJSONフィールドが存在しない場合に例外(JsonException)をスローします。Newtonsoft.Jsonも、設定(Required.Alwaysなど)を組み合わせることで同様の挙動を実現できます。requiredはシリアライザ/逆シリアライザにとって有用なメタデータとして機能します。

Q: requiredプロパティを持つクラスを古いバージョンのC#コンパイラでコンパイルできますか?
A: いいえ、できません。requiredキーワードはC# 11で導入された言語機能です。C# 11以降のコンパイラが必要です。

Q: requiredプロパティを持つオブジェクトを古い.NETランタイム(.NET 6以下)で実行できますか?
A: はい、可能です。requiredキーワードの挙動は主にコンパイル時(コンパイラによる初期化チェックと属性の付与)によって実現されるため、実行時のランタイムバージョンに依存しません。ただし、RequiredMemberAttributeSetsRequiredMembersAttribute自体は.NET 7でBCLに追加された属性です。もし古いランタイムでこれらの属性が見つからないという問題が発生する場合(これはあまり一般的ではないですが)、.NETPortability.Analyzerなどのツールで互換性を確認する必要があります。実際には、C# 11を使用する場合はターゲットフレームワークも.NET 7以降にするのが最も安全で一般的です。

実践例

requiredプロパティの具体的な使用例をいくつか示します。

例1: DTO (Data Transfer Object) での使用

“`csharp
public class UserDto
{
public required int Id { get; init; }
public required string FirstName { get; init; }
public required string LastName { get; init; }
public required string Email { get; init; }
public DateTime CreatedDate { get; init; } // Optional but init-only
}

// 使用例 (API レスポンスのマッピングなど)
var userDto = new UserDto
{
Id = user.Id,
FirstName = user.FirstName,
LastName = user.LastName,
Email = user.Email,
CreatedDate = user.CreatedDate
};

// 後から別の UserDto を作成する際に必須項目を忘れない
var newUserDto = new UserDto
{
Id = 101,
FirstName = “Alice”,
LastName = “Smith”,
Email = “[email protected]
// CreatedDate は省略してもコンパイルエラーにならない (required ではない)
};
``
DTOは通常、シンプルにデータを保持する目的で使用されるため、オブジェクト初期化子による初期化が適しています。
required`を付けることで、DTOを作成する際に必須フィールドを設定し忘れることを防ぎ、不完全なデータがシステムを流れるリスクを減らせます。

例2: 設定クラスでの使用

“`csharp
public class AppSettings
{
public required string DatabaseConnectionString { get; init; }
public required string BlobStorageAccountName { get; init; }
public int CacheExpirationMinutes { get; init; } = 60; // Optional
public LogLevel MinLogLevel { get; init; } = LogLevel.Information; // Optional
}

// アプリケーション起動時の設定読み込みなど
// 実際には設定プロバイダーなどから値を取得してマッピングすることが多い
var settings = new AppSettings
{
DatabaseConnectionString = configuration[“ConnectionStrings:DefaultConnection”],
BlobStorageAccountName = configuration[“Storage:AccountName”],
// CacheExpirationMinutes と MinLogLevel は設定ファイルになければデフォルト値が使用されることを期待
// (ただし、required init なので、デフォルト値があってもオブジェクト初期化子での指定は必須)
// 実際のアプリケーションでは、設定ライブラリがこれらの required プロパティを正しくマッピングする必要があります。
// System.Extensions.Configuration や .NET 8 の Options Pattern では required メンバーのマッピングが改善されています。
};
``
設定クラスでは、一部の設定項目がアプリケーションの動作に必須であることがよくあります。
required initとすることで、設定項目が必須かつアプリケーション起動後に変更されないことを表現できます。設定プロバイダーからのマッピングにおいても、required`メタデータはマッピングの検証に利用される可能性があります。

例3: ドメインモデルでの使用 (Immutableな必須属性)

“`csharp
public class Product
{
// ID: 必須かつ不変
public required int ProductId { get; init; }

// 商品名: 必須かつ不変
public required string Name { get; init; }

// 価格: 必須かつ不変
public required decimal Price { get; init; }

// 在庫数: 必須だが変更可能
public required int Stock { get; set; }

// Description はオプションで変更可能
public string? Description { get; set; }

// ドメインロジック(例: 在庫を減らす)
public void DecreaseStock(int quantity)
{
    if (Stock < quantity)
    {
        throw new InvalidOperationException("Insufficient stock.");
    }
    Stock -= quantity;
}

}

// 使用例
var product = new Product
{
ProductId = 1,
Name = “Widget”,
Price = 9.99m,
Stock = 100
// Description は省略
}; // OK

// product.ProductId = 2; // Compile Error (init-only)
// product.Name = “Gadget”; // Compile Error (init-only)
// product.Price = 19.99m; // Compile Error (init-only)

product.Stock = 50; // OK (set アクセサーがある)
product.DecreaseStock(10); // OK (メソッド呼び出しで状態変更)

// コンパイルエラー: required な Stock が不足
// var incompleteProduct = new Product { ProductId = 2, Name = “Gadget”, Price = 19.99m };
``
ドメインモデルにおいて、オブジェクトの同一性や核となる属性が必須かつ不変である場合、
required initは非常に強力な表現力を持ます。一方で、在庫数のように、オブジェクトのライフサイクルの中で変更される可能性のある必須属性にはrequired set`を使用できます。これにより、ドメインモデルの契約をコンパイル時に強制し、無効な状態のオブジェクトが生成されることを防ぎます。

これらの例からわかるように、requiredキーワードは、特にオブジェクト初期化子と組み合わせて使用することで、必須プロパティの初期化をコンパイル時に保証し、コードの堅牢性と意図の明確さを向上させるのに非常に効果的です。

まとめ

この記事では、C# 11で導入されたrequiredキーワード、特に必須プロパティの使い方について詳細に解説しました。

requiredキーワードは、プロパティに適用することで、そのプロパティがオブジェクト初期化子を使用する際に必ず値を設定しなければならない必須メンバーであることをコンパイラに伝える機能です。これにより、従来のC#における必須プロパティの初期化漏れという課題に対し、コンパイル時エラーによる強力な保証を提供します。

主なポイントは以下の通りです。

  • requiredプロパティは、オブジェクト初期化子を使用する際にコンパイル時に初期化が強制されます。
  • コンストラクタで全てのrequiredプロパティを初期化する場合、コンストラクタに[SetsRequiredMembers]属性を付けることで、そのコンストラクタによる初期化も有効な初期化方法としてコンパイラに認識させることができます。
  • requiredは初期化の必須性を保証し、プロパティのnull許容性 (string vs string?) とは独立した概念です。
  • initアクセサーと組み合わせることで、必須かつImmutableなプロパティを定義できます (required init)。
  • 構造体やrecord型でも使用可能です。
  • デフォルト値があるrequiredプロパティでも、オブジェクト初期化子を使用する際は明示的に値を指定する必要があります。
  • 継承の場合、ベースクラスのrequiredプロパティは派生クラスでもrequiredとなります。
  • requiredはコンパイル時の機能であり、実行時の動作に直接的な変更はありません。

requiredキーワードは、コードの堅牢性、可読性、そして開発効率を向上させるための重要な機能です。特にDTOや設定オブジェクト、Immutableなドメインモデルなど、オブジェクト初期化子による初期化が頻繁に行われるシーンでその真価を発揮します。

ただし、C# 11以降の機能であること、そして既存のpublicなクラスに後からrequiredプロパティを追加すると破壊的変更になりうるという点には注意が必要です。

オブジェクト設計において、どのプロパティが必須であるかを明確にし、それをコードレベルで強制したい場面では、ぜひrequiredキーワードの活用を検討してみてください。これにより、意図しないオブジェクトの状態を防ぎ、より信頼性の高いソフトウェアを開発することができるでしょう。

Happy Coding!


参考資料

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール