次の方法で共有


インターフェイス - 複数の型の動作を定義する

ヒント

ソフトウェアの開発は初めてですか? 最初に、 作業の開始 に関するチュートリアルから始めます。 関連のない型間で共有する動作を定義する必要がある場合、インターフェイスに直面することになります。

別の言語で経験がありますか? C# インターフェイスは、Javaのインターフェイスや Swift のプロトコルに似ています。 C# 固有のパターンに関する 明示的な実装 セクションをざっと読んでください。

インターフェイスはコントラクトを定義 します 。コントラクトは、 class または struct が実装する必要がある関連するメソッド、プロパティ、イベント、インデクサーのグループです。 インターフェイスを使用すると、1 つの型で複数のコントラクトを実装できます。これは、C# がクラスの複数の継承をサポートしていないために重要です。 構造体は他の構造体またはクラスから継承できないため、インターフェイスは構造体型間で共有動作を追加する唯一の方法です。

次の例では、インターフェイスとそれを実装するクラスを宣言します。

interface IEquatable<T>
{
    bool Equals(T obj);
}
public class Car : IEquatable<Car>
{
    public string? Make { get; set; }
    public string? Model { get; set; }
    public string? Year { get; set; }

    public bool Equals(Car? car) =>
        car is not null &&
        (Make, Model, Year) == (car.Make, car.Model, car.Year);
}

IEquatable<T>を実装するクラスまたは構造体は、インターフェイスシグネチャに一致するEqualsメソッドを提供する必要があります。 具体的な型に関係なく、等価比較をサポートする任意の IEquatable<T> 実装を利用できます。 その予測可能性は、インターフェイスのコア値です。

インターフェイスを宣言する

interface キーワードを使用してインターフェイスを定義します。 慣例により、インターフェイス名は大文字の Iで始まります。

interface ILogger
{
    void Log(string message);
    string Name { get; }
}

インターフェイスには、メソッド、プロパティ、イベント、およびインデクサーを含めることができます。 インターフェイスには、インスタンス フィールド、インスタンス コンストラクター、またはファイナライザーを含めることができません。 メンバーは既定で public されます。 必要に応じて、他のアクセシビリティ修飾子を指定できます。 たとえば、アセンブリの外部に表示すべきではないメンバーには、 internal を使用します。

インターフェイスを実装する

クラスまたは構造体は、宣言のコロンの後に実装するインターフェイスを一覧表示します。 このクラスは、インターフェイスで宣言されているすべてのメンバーに対して実装を提供する必要があります。

public class ConsoleLogger : ILogger
{
    public string Name => "Console";

    public void Log(string message) =>
        Console.WriteLine($"[{Name}] {message}");
}

public class FileLogger : ILogger
{
    public string Name => "File";

    public void Log(string message)
    {
        // In a real app, write to a file
        Console.WriteLine($"[{Name}] Writing to file: {message}");
    }
}

クラスは、コンマで区切って複数のインターフェイスを実装できます。 一覧表示されるすべてのインターフェイスのすべてのメンバーに対して実装を提供する必要があります。

明示的な実装

場合によっては、インターフェイス メンバーをクラスのパブリック API の一部にせずに実装する必要があります。 明示的な実装は、メンバーをインターフェース名で修飾します。 メンバーには、インターフェイス型の変数を介してのみアクセスできます。

interface IMetric
{
    double GetDistance(); // Returns meters
}

interface IImperial
{
    double GetDistance(); // Returns feet
}

public class Runway(double meters) : IMetric, IImperial
{
    // Explicit implementation for IMetric
    double IMetric.GetDistance() => meters;

    // Explicit implementation for IImperial
    double IImperial.GetDistance() => meters * 3.28084;
}

明示的な実装は、2 つのインターフェイスで同じ名前のメンバーを宣言する場合や、クラスのパブリック サーフェスをクリーンに保つ場合に便利です。 詳細については、「 明示的なインターフェイスの実装」を参照してください。

インターフェイスの継承

インターフェイスは、1 つ以上の他のインターフェイスから継承できます。 派生インターフェイスを実装するクラスは、派生インターフェイスのすべてのメンバーとそのすべての基本インターフェイスを実装する必要があります。

interface IDrawable
{
    void Draw();
}

interface IShape : IDrawable
{
    double Area { get; }
}

public class Circle(double radius) : IShape
{
    public double Area => Math.PI * radius * radius;

    public void Draw() =>
        Console.WriteLine($"Drawing circle with area {Area:F2}");
}

IShapeを実装するクラスは、IShapeからの継承関係により、暗黙的に IDrawable に変換できます。

インターフェイスと抽象クラス

インターフェイスと抽象クラスの両方で、派生型が満たす必要があるコントラクトを定義します。

  • 関連する型が状態 (フィールド)、コンストラクター、または非パブリック メンバーを共有する場合は、抽象クラスを使用します。 抽象クラスを使用すると、既存の派生型を中断することなく、既定の動作で新しいメンバーを追加することで階層を進化させることができます。
  • 型が関係のない階層間で切り取るコントラクトを満たす必要がある場合、または複数のコントラクトを実装する必要がある場合は、インターフェイスを使用します。 インターフェイスはインスタンス フィールドまたはコンストラクターを宣言できないため、基底クラスが既に存在する型に機能を追加するのに最適です。 高度なシナリオでは、インターフェイスは 既定のメンバー実装もサポートします。

1 つのクラスは 1 つの基底クラスからしか継承できませんが、複数のインターフェイスを実装できます。 多くの場合、この違いにより、型階層間で切り取られる機能を定義するためのより良い選択肢がインターフェイスになります。

内部インターフェイスの操作

インターフェイスシグネチャ内のすべての型にパブリックにアクセスできる限り、通常はパブリック メンバーを含む内部インターフェイスを実装できます。 インターフェイスがメンバーシグネチャで内部型を使用する場合は、内部型を公開しているときに実装メンバーをパブリックにできないため、明示的な実装を使用する必要があります。

internal class InternalConfiguration
{
    public string Setting { get; set; } = "";
}

internal interface ILoggable
{
    void Log(string message);
}

internal interface IConfigurable
{
    void Configure(InternalConfiguration config);
}

public class ServiceImplementation : ILoggable, IConfigurable
{
    // Implicit implementation: ILoggable uses only public types in its signature
    public void Log(string message) =>
        Console.WriteLine($"Log: {message}");

    // Explicit implementation: IConfigurable uses internal types
    void IConfigurable.Configure(InternalConfiguration config) =>
        Console.WriteLine($"Configured with: {config.Setting}");
}

前の例では、 IConfigurable はメソッド シグネチャに InternalConfiguration 内部型を使用します。 ServiceImplementation は、そのメンバーの明示的な実装を使用します。 これに対し、 ILoggable はシグネチャでパブリック型 (string) のみを使用し、暗黙的に実装できます。

既定のインターフェイス メンバーと静的抽象メンバー

インターフェイスは、基本コントラクトを超える 2 つの高度な機能をサポートします。

  • 既定のインターフェイス メンバー を使用すると、インターフェイスはメソッド本体を提供できます。 型の実装は既定の実装を継承し、必要に応じてオーバーライドできます。 詳細については、既定のインターフェイス メソッドに関する記事をご覧ください。
  • 静的抽象メンバー では、静的メンバーを提供するために型を実装する必要があります。これは、演算子コントラクトまたはファクトリ パターンを定義するのに役立ちます。 詳細については、 インターフェイスの静的抽象メンバーを参照してください。

どちらの機能も、言語リファレンスの インターフェイス に関する記事で説明されています。 ほとんどの日常的なインターフェイスの使用には、この記事で前述したパターンの宣言と実装が含まれます。

インターフェイスの概要

  • インターフェイスは、メソッド、プロパティ、イベント、インデクサーのコントラクトを定義します。
  • インターフェイスを実装するクラスまたは構造体は、宣言されているすべてのメンバーの実装を提供する必要があります (インターフェイスが既定の実装を提供しない限り)。
  • インターフェイスを直接インスタンス化することはできません。
  • クラスまたは構造体は、複数のインターフェイスを実装できます。 クラスは、基本クラスを継承する一方で、1 つまたは複数のインターフェイスを実装できます。
  • インターフェイス名は、通常、 Iで始まります。