次の方法で共有


クラスと構造体のプライマリ コンストラクターを宣言する

C# 12 には プライマリ コンストラクターが導入されています。これにより、型の本体の任意の場所でパラメーターを使用できるコンストラクターを宣言するための簡潔な構文が提供されます。

この記事では、型でプライマリ コンストラクターを宣言し、プライマリ コンストラクター パラメーターを格納する場所を認識する方法について説明します。 他のコンストラクターからプライマリ コンストラクターを呼び出し、型のメンバーでプライマリ コンストラクター パラメーターを使用できます。

前提条件

  • 最新の .NET SDK
  • Visual Studio Codeエディター
  • C# DevKit

プライマリ コンストラクターの規則を理解する

パラメーターを struct または class 宣言に追加して、 プライマリ コンストラクターを作成できます。 プライマリ コンストラクター パラメーターは、クラス定義全体のスコープ内にあります。 プライマリ コンストラクター パラメーターは、クラス定義全体のスコープ内にある場合でも 、パラメーター として表示することが重要です。

いくつかの規則では、これらのコンストラクターがパラメーターであることを明確にしています。

  • プライマリ コンストラクター パラメーターは、必要ない場合は格納されない可能性があります。
  • プライマリ コンストラクター パラメーターは、クラスのメンバーではありません。 たとえば、 param という名前のプライマリ コンストラクター パラメーターには、 this.paramとしてアクセスできません。
  • プライマリ コンストラクター パラメーターを割り当てることができます。
  • プライマリ コンストラクター パラメーターは、 レコード 型を除き、プロパティになりません。

これらの規則は、他のコンストラクター宣言を含め、任意のメソッドのパラメーターに対して既に定義されているのと同じ規則です。

プライマリ コンストラクター パラメーターの最も一般的な用途を次に示します。

  • base() コンストラクターの呼び出しに引数として渡す
  • メンバー フィールドまたはプロパティを初期化する
  • インスタンス メンバーのコンストラクター パラメーターを参照する

クラスの他のすべてのコンストラクターは、コンストラクターの呼び出しを通じて、直接または間接的にプライマリ コンストラクターを呼び出すthis()。 この規則により、プライマリ コンストラクター パラメーターが型の本体内のあらゆる場所に確実に割り当てられます。

変更できないプロパティまたはフィールドを初期化する

次のコードは、プライマリ コンストラクター パラメーターから計算される 2 つの読み取り (変更できない) プロパティを初期化します。

public readonly struct Distance(double dx, double dy)
{
    public readonly double Magnitude { get; } = Math.Sqrt(dx * dx + dy * dy);
    public readonly double Direction { get; } = Math.Atan2(dy, dx);
}

この例では、プライマリ コンストラクターを使用して、計算された読み取り専用プロパティを初期化します。 MagnitudeプロパティとDirection プロパティのフィールド初期化子は、プライマリ コンストラクター パラメーターを使用します。 プライマリ コンストラクター パラメーターは、構造体の他の場所では使用されません。 このコードでは、次の方法で記述されたかのように構造体が作成されます。

public readonly struct Distance
{
    public readonly double Magnitude { get; }

    public readonly double Direction { get; }

    public Distance(double dx, double dy)
    {
        Magnitude = Math.Sqrt(dx * dx + dy * dy);
        Direction = Math.Atan2(dy, dx);
    }
}

この機能を使用すると、フィールドまたはプロパティを初期化するために引数が必要な場合に、フィールド初期化子を簡単に使用できます。

変更可能な状態を作成する

前の例では、プライマリ コンストラクター パラメーターを使用して、読み取り専用プロパティを初期化します。 読み取り専用ではないプロパティにプライマリ コンストラクターを使用することもできます。

次のコードについて考えてみましょう。

public struct Distance(double dx, double dy)
{
    public readonly double Magnitude => Math.Sqrt(dx * dx + dy * dy);
    public readonly double Direction => Math.Atan2(dy, dx);

    public void Translate(double deltaX, double deltaY)
    {
        dx += deltaX;
        dy += deltaY;
    }

    public Distance() : this(0,0) { }
}

この例では、 Translate メソッドによって dx コンポーネントと dy コンポーネントが変更されます。これには、アクセス時に Magnitude プロパティと Direction プロパティを計算する必要があります。 ラムダ演算子 (=>) は式形式の get アクセサーを指定しますが、等しい演算子 (=) は初期化子を指定します。

このバージョンのコードでは、パラメーターなしのコンストラクターが構造体に追加されます。 パラメーターなしのコンストラクターは、プライマリ コンストラクターを呼び出す必要があります。これにより、すべてのプライマリ コンストラクター パラメーターが確実に初期化されます。 プライマリ コンストラクターのプロパティはメソッドでアクセスされ、コンパイラは各パラメーターを表す非表示フィールドを作成します。

次のコードは、コンパイラによって生成される内容の近似を示しています。 実際のフィールド名は、有効な共通中間言語 (CIL) 識別子ですが、有効な C# 識別子ではありません。

public struct Distance
{
    private double __unspeakable_dx;
    private double __unspeakable_dy;

    public readonly double Magnitude => Math.Sqrt(__unspeakable_dx * __unspeakable_dx + __unspeakable_dy * __unspeakable_dy);
    public readonly double Direction => Math.Atan2(__unspeakable_dy, __unspeakable_dx);

    public void Translate(double deltaX, double deltaY)
    {
        __unspeakable_dx += deltaX;
        __unspeakable_dy += deltaY;
    }

    public Distance(double dx, double dy)
    {
        __unspeakable_dx = dx;
        __unspeakable_dy = dy;
    }
    public Distance() : this(0, 0) { }
}

コンパイラによって作成されたストレージ

このセクションの最初の例では、コンパイラはプライマリ コンストラクター パラメーターの値を格納するフィールドを作成する必要はありませんでした。 ただし、2 番目の例では、プライマリ コンストラクター パラメーターがメソッド内で使用されるため、コンパイラはパラメーターのストレージを作成する必要があります。

コンパイラは、型のメンバーの本体でパラメーターにアクセスする場合にのみ、すべてのプライマリ コンストラクターのストレージを作成します。 それ以外の場合、プライマリ コンストラクターパラメーターはオブジェクトに格納されません。

依存関係の挿入を使用する

プライマリ コンストラクターのもう 1 つの一般的な用途は、依存関係挿入のパラメーターを指定することです。 次のコードでは、その使用にサービス インターフェイスを必要とする単純なコントローラーを作成します。

public interface IService
{
    Distance GetDistance();
}

public class ExampleController(IService service) : ControllerBase
{
    [HttpGet]
    public ActionResult<Distance> Get()
    {
        return service.GetDistance();
    }
}

プライマリ コンストラクターは、クラスで必要なパラメーターを明確に示します。 クラス内の他の変数と同様に、プライマリ コンストラクター パラメーターを使用します。

基本クラスを初期化する

基底クラスのプライマリ コンストラクターは、派生クラスのプライマリ コンストラクターから呼び出すことができます。 この方法は、基底クラスでプライマリ コンストラクターを呼び出す必要がある派生クラスを記述する最も簡単な方法です。 異なる口座タイプを表すクラスの階層を銀行として考えてみましょう。 次のコードは、基底クラスの外観を示しています。

public class BankAccount(string accountID, string owner)
{
    public string AccountID { get; } = accountID;
    public string Owner { get; } = owner;

    public override string ToString() => $"Account ID: {AccountID}, Owner: {Owner}";
}

種類に関係なく、すべての銀行口座には、口座番号と所有者のプロパティがあります。 完成したアプリケーションでは、基本クラスに他の一般的な機能を追加できます。

多くの型では、コンストラクター パラメーターに対してより具体的な検証が必要です。 たとえば、 BankAccount クラスには、 owner パラメーターと accountID パラメーターに固有の要件があります。 owner パラメーターはnullまたは空白にすることはできません。また、accountID パラメーターは 10 桁の文字列である必要があります。 この検証は、対応するプロパティを割り当てるときに追加できます。

public class BankAccount(string accountID, string owner)
{
    public string AccountID { get; } = ValidAccountNumber(accountID) 
        ? accountID 
        : throw new ArgumentException("Invalid account number", nameof(accountID));

    public string Owner { get; } = string.IsNullOrWhiteSpace(owner) 
        ? throw new ArgumentException("Owner name cannot be empty", nameof(owner)) 
        : owner;

    public override string ToString() => $"Account ID: {AccountID}, Owner: {Owner}";

    public static bool ValidAccountNumber(string accountID) => 
    accountID?.Length == 10 && accountID.All(c => char.IsDigit(c));
}

この例では、コンストラクター パラメーターをプロパティに割り当てる前に検証する方法を示します。 String.IsNullOrWhiteSpace(String)などの組み込みメソッドや、独自の検証メソッド (ValidAccountNumber など) を使用できます。 この例では、初期化子を呼び出すと、コンストラクターから例外がスローされます。 コンストラクター パラメーターを使用してフィールドを割り当てない場合、コンストラクター パラメーターに最初にアクセスしたときに例外がスローされます。

1 つの派生クラスが当座預金アカウントを表す場合があります。

public class CheckingAccount(string accountID, string owner, decimal overdraftLimit = 0) : BankAccount(accountID, owner)
{
    public decimal CurrentBalance { get; private set; } = 0;

    public void Deposit(decimal amount)
    {
        if (amount < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount), "Deposit amount must be positive");
        }
        CurrentBalance += amount;
    }

    public void Withdrawal(decimal amount)
    {
        if (amount < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount), "Withdrawal amount must be positive");
        }
        if (CurrentBalance - amount < -overdraftLimit)
        {
            throw new InvalidOperationException("Insufficient funds for withdrawal");
        }
        CurrentBalance -= amount;
    }
    
    public override string ToString() => $"Account ID: {AccountID}, Owner: {Owner}, Balance: {CurrentBalance}";
}

派生 CheckingAccount クラスには、基底クラスで必要なすべてのパラメーターを受け取るプライマリ コンストラクターと、既定値を持つ別のパラメーターがあります。 プライマリ コンストラクターは、 : BankAccount(accountID, owner) 構文を使用して基本コンストラクターを呼び出します。 この式は、基底クラスの型と、プライマリ コンストラクターの引数の両方を指定します。

派生クラスは、プライマリ コンストラクターを使用する必要はありません。 次の例に示すように、基底クラスのプライマリ コンストラクターを呼び出すコンストラクターを派生クラスに作成できます。

public class LineOfCreditAccount : BankAccount
{
    private readonly decimal _creditLimit;
    public LineOfCreditAccount(string accountID, string owner, decimal creditLimit) : base(accountID, owner)
    {
        _creditLimit = creditLimit;
    }
    public decimal CurrentBalance { get; private set; } = 0;

    public void Deposit(decimal amount)
    {
        if (amount < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount), "Deposit amount must be positive");
        }
        CurrentBalance += amount;
    }

    public void Withdrawal(decimal amount)
    {
        if (amount < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount), "Withdrawal amount must be positive");
        }
        if (CurrentBalance - amount < -_creditLimit)
        {
            throw new InvalidOperationException("Insufficient funds for withdrawal");
        }
        CurrentBalance -= amount;
    }

    public override string ToString() => $"{base.ToString()}, Balance: {CurrentBalance}";
}

クラス階層とプライマリ コンストラクターには、潜在的な懸念事項が 1 つあります。 パラメーターは派生クラスと基底クラスの両方で使用されるため、プライマリ コンストラクター パラメーターの複数のコピーを作成できます。 次のコードでは、各 owner パラメーターと accountID パラメーターの 2 つのコピーが作成されます。

public class SavingsAccount(string accountID, string owner, decimal interestRate) : BankAccount(accountID, owner)
{
    public SavingsAccount() : this("default", "default", 0.01m) { }
    public decimal CurrentBalance { get; private set; } = 0;

    public void Deposit(decimal amount)
    {
        if (amount < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount), "Deposit amount must be positive");
        }
        CurrentBalance += amount;
    }

    public void Withdrawal(decimal amount)
    {
        if (amount < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount), "Withdrawal amount must be positive");
        }
        if (CurrentBalance - amount < 0)
        {
            throw new InvalidOperationException("Insufficient funds for withdrawal");
        }
        CurrentBalance -= amount;
    }

    public void ApplyInterest()
    {
        CurrentBalance *= 1 + interestRate;
    }

    public override string ToString() => $"Account ID: {accountID}, Owner: {owner}, Balance: {CurrentBalance}";
}

この例で強調表示されている行は、ToString メソッドが基底クラスのプロパティ (OwnerAccountID) ではなく、プライマリ コンストラクター パラメーター (owneraccountID) を使用していることを示しています。 その結果、派生クラス SavingsAccountパラメーター コピーのストレージが作成されます。 派生クラスのコピーは、基底クラスのプロパティとは異なります。 基底クラスのプロパティを変更できる場合、派生クラスのインスタンスに変更は表示されません。 コンパイラは、派生クラスで使用され、基底クラスのコンストラクターに渡されるプライマリ コンストラクター パラメーターに対して警告を発行します。 この場合、修正は基底クラスのプロパティを使用することです。