次の方法で共有


チュートリアル: プライマリ コンストラクターを調べる

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

このチュートリアルでは、次の内容を学習します。

  • 型でプライマリ コンストラクターを宣言するタイミング
  • 他のコンストラクターからプライマリ コンストラクターを呼び出す方法
  • 型のメンバーでプライマリ コンストラクター パラメーターを使用する方法
  • プライマリ コンストラクター パラメーターが格納される場所

前提条件

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

プライマリ コンストラクター

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

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

これらの規則は、他のコンストラクター宣言を含め、任意のメソッドのパラメーターと同じです。

プライマリ コンストラクター パラメーターの最も一般的な用途は次のとおりです。

  1. base() コンストラクター呼び出しの引数として。
  2. メンバー フィールドまたはプロパティを初期化します。
  3. インスタンス メンバー内のコンストラクター パラメーターを参照しています。

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

プロパティを初期化する

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

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 パラメーターに固有の要件があります。 ownernull または空白でなく、 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などの独自の検証メソッドを使用できます。 前の例では、初期化子が呼び出されると、コンストラクターから例外がスローされます。 コンストラクター パラメーターを使用してフィールドを割り当てない場合、コンストラクター パラメーターに最初にアクセスしたときに例外がスローされます。

ある派生クラスでは、当座預金口座を提示します。

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}";
}

クラス階層とプライマリ コンストラクターには、派生クラスと基底クラスの両方で使用されるため、プライマリ コンストラクター パラメーターの複数のコピーを作成できます。 次のコード例では、各 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 それらのコピーのストレージが作成されます。 派生クラスのコピーは、基底クラスのプロパティとは異なります。 基底クラスのプロパティを変更できる場合、派生クラスのインスタンスにその変更は表示されません。 コンパイラは、派生クラスで使用され、基底クラスのコンストラクターに渡されるプライマリ コンストラクター パラメーターに対して警告を発行します。 この場合、修正は基底クラスのプロパティを使用することです。

概要

設計に最適なプライマリ コンストラクターを使用できます。 クラスと構造体の場合、プライマリ コンストラクター パラメーターは、呼び出す必要があるコンストラクターのパラメーターです。 これらを使用してプロパティを初期化できます。 フィールドを初期化できます。 これらのプロパティまたはフィールドは、変更不可または変更可能です。 メソッドでそれらを使用できます。 これらはパラメーターであり、設計に最も適した方法で使用します。 プライマリ コンストラクターの詳細については、インスタンス コンストラクターと提案されたプライマリ コンストラクターの仕様に関する C# プログラミング ガイドの記事を参照してください。