👻

【C#14新機能】extensionブロック

に公開

extensionブロックは、拡張メソッドのグルーピングが行えると同時に、拡張できるのがメソッドだけではなくなり、プロパティ、演算子、静的メソッド、静的プロパティの拡張も書けるようになりました。

これまでの拡張メソッド

拡張メソッドは、静的クラスに、静的メソッドで、第1引数にthisキーワードをつけることで、その第1引数の型のメソッドとして使えるようになり、変更のできない型に、別途メソッドを追加する機能として、C#3より追加された機能です。

例えば、intという型の中身を我々プログラマーが変更することはできませんが、拡張メソッドを使えば、intにメソッドを追加できます。値がゼロの時にtureを返却するIsZero()というメソッドを追加する場合は、次のように記述します。

  public static class IntEx
  {
    public static bool IsZero(this int value)
    {
      return value == 0;
    }
  }

IntExという静的(static)なクラスに、publicの静的なメソッドIsZeroを作ります。その際に、拡張したい型を第1引数に書きます。今回はintなのでint valueとしています。さらにこれだけでは普通の静的メソッドになるので、第1引数にthisキーワードをつけることで、int型の拡張メソッドとして、IsZero()メソッドが作成されたことになります。

これを使う場合は、intのインスタンスのメソッドを呼び出すことで、使用できます。

      int a = 1;
      bool result = a.IsZero();
      int b = 0;
      bool result2 = b.IsZero();

このようにintのインスタンスであるaに対してIsZero()が使えるようになっていることがわかります。これでintという変更のできない型に対して、メソッドを追加できたことになり、これを拡張メソッドと呼んでいます。

extensionブロック

extensionブロックを使うと、拡張メソッドをグルーピングすることができ、コードを整理しやすくなります。extension ブロックを使わずに、intに対する拡張メソッドを追加する場合、次のようにIsNegativeを追加することになります。

  public static class IntEx
  {
    public static bool IsZero(this int value)
    {
      return value == 0;
    }

    public static bool IsNegative(this int value)
    {
      return value < 0;
    }
  }

extensionブロックを使うと次のように書くことができます。

  public static class IntEx
  {
    extension(int value)
    {
      public bool IsZero()
      {
        return value == 0;
      }
      public bool IsNegative()
      {
        return value < 0;
      }
    }
  }

extensionというキーワードのあとに、拡張したい型と変数名を書きます。拡張メソッドの第1引数のthisキーワードがない状態です。このextensionのブロックの中に、通常のメソッドと同じ書き方で、メソッドを書きます。するとそのすべては、intの拡張メソッドになります。これまでのように、メソッドごとにthisキーワードとintの引数を書いていましたが、その必要がなくなり、int型に拡張するメソッドをまとめて書くことができるようになりました。この書き方に変更しても、使う側のコードは同じです。

      int a = 1;
      bool ret1 = a.IsZero();
      int b = 0;
      bool ret2 = b.IsZero();

これまで通り、同じ結果となります。

型パラメータがある拡張メソッド

型パラメータを使用する場合は、次のようにメソッド名に続けて<T>などと記述します。

  public static class Ex
  {
    public static bool IsEmpty<T>(this IEnumerable<T> list)
    {
      if (list == null) return true;
      return !list.Any();
    }
  }

これで任意の型のコレクションに対応できます。
例えば、string型のListを使用する場合は次のように記述します。

      var la = new List<string>();
      la.Add("aaa");
      var retla = la.IsEmpty();

      var lb = new List<string>();
      var retlb = lb.IsEmpty();

      List<string> lc = null;
      var retlc = lc.IsEmpty();

結果

名前
retla false
retlb true
retlc true

一見nullのListに対してIsEmpty()というメソッドを呼び出しているように見えるので、例外になるように感じますが、実際には、Ex.IsEmpty(lc)という感じで、lcを引数で渡しているので、ちゃんとnullチェックが行われ、trueが返却されます。

型パラメータありのextensionブロック

先ほどの型パラメータありの拡張メソッドIsEmpty を、extensionブロックで書く場合は、次のようになります。

public static class Ex
{
  extension<T>(IEnumerable<T> list)
  {
    public  bool IsEmpty()
    {
      if (list == null) return true;
      return !list.Any();
    }
  }
}

型パラメータなしと同様にextensionキーワードでブロックを作り、第1引数にしていたものをthisキーワードを除いて記述します。拡張メソッドだったIsEmptyはstaticを削除し、引数及び、IsEmptyの後ろの型パラメータ<T>を削除します。これによりIEnumerable<T>の拡張を記載するグループとしてextensionブロックが作成されたことになります。クライアントコードはそのままで、今まで通り使用することができます。

拡張プロパティ

extensionブロックにすることで、今まではメソッドしか書くことのできなかった拡張メソッドですが、プロパティ、演算子、静的メソッド/プロパティも書けるようになりました。次のようなIsZero()メソッドは、通常のクラスに記述するようにプロパティとしても書くことができます。

public static class Ex
{
  extension(int value)
  {
    public bool IsZero()
    {
      return value == 0;
    }
    public bool IsNegative()
    {
      return value < 0;
    }
  }
}

次のようにIsZeroを、プロパティにすることができます。

    extension(int value)
    {
      public bool IsZero
      {
        get
        {
          return value == 0;
        }
      }

      public bool IsNegative()
      {
        return value < 0;
      }
    }

当然もっとシンプルにこう書くこともできます。

    extension(int value)
    {
      public bool IsZero => value == 0;

      public bool IsNegative()
      {
        return value < 0;
      }
    }

これにより、クライアントコードは、次のように()が不要となり、今まではプロパティ的な処理もメソッドとしてしか書けませんでしたが、プロパティが適しているものは、プロパティとして書くことが可能となりました。

      int a = 1;
      bool result = a.IsZero;

余談ですが、次のようなIEnumerableのAny()がメソッドである()が必要な理由は、Any()メソッドが拡張メソッドであり、プロパティとして記述できなかったことがその理由です。よって、Any()以外にもCount()などもすべてメソッドになっているのはそのためです。

    extension<T>(IEnumerable<T> list)
    {
      public  bool IsEmpty()
      {
        if (list == null) return true;
        return !list.Any();
      }
    }

extensionの登場により、メソッドでは違和感のあったものも、プロパティとして記述することができます。

拡張静的メソッド/プロパティ

extensionブロックには静的なメソッドやプロパティを記述することができます。

例えばアプリケーションで「色」を定義する場合、Color構造体の拡張として定義しておくことができます。

  public static class Ex
  {
    extension(Color color)
    {
      public static Color SelectedColor => Color.Blue;
    }
  }

クライアントコードからは、次のように使用します。
this.BackColor = Color.SelectedColor;
このようにすれば、まるでColor構造体に、アプリケーションレベルでの定義も入れることができ、アプリケーション全体でSelectedColorをGreenに変更したければ、簡単に変更することができます。従来はconstなどを別クラスで管理していたと思いますが、このようにColor構造体に入れることができます。

これを静的ではないインスタンスのプロパティにすると、次のようになり、クライアントコードからは使いづらくなります。

    extension(Color color)
    {
      public  Color SelectedColor => Color.Blue;
    }

クライアントコードは無駄な生成が必要になります。

      var c = new Color();
      this.BackColor = c.SelectedColor;

こういったインスタンスの生成が必要ないケースは、静的なプロパティや静的なメソッドを作成すると効果的です。

拡張演算子

extensionブロックを使えば、演算子のオーバーロードも書くことができます。演算子のオーバーロードとは、クラスに演算子を追加することができるということです。例えば、次のようにMoneyクラスがあり、Valueという値があったとします。

  public class Money
  {
    public int Value { get; set; }
  }

クライアントコードで次のように書いたとしても、当然コンパイルエラーとなります。

      var m1 = new Money();
      m1.Value = 111;
      var m2 = new Money();
      m2.Value = 222;
      var mm = m1 + m2;

Moneyのインスタンスの足し算といわれても、コンパイラは理解できません。ただ、実際には、Moneyクラスのインスタンスの足し算は、Valueの足し算をしてほしいということであれば、次のように書けば、足し算が書けるようになります。

  public class Money
  {
    public int Value { get; set; }

    public static int operator +(Money a, Money b)
    {
      return a.Value + b.Value;
    }
  }

これで+演算子が使われた場合は、次のコードのコンパイルエラーではなくなり、MoneyインスタンスのValue同士を足した結果を得ることができます。もちろん、MoneyクラスをValueObjectなどの不変クラスにしている場合は、intを返すのではなく新しい値でMoneyクラスを生成して返却してもよいでしょう。ここでの詳細は割愛します。

      var m1 = new Money();
      m1.Value = 111;
      var m2 = new Money();
      m2.Value = 222;
      var mm = m1 + m2;

このコードを実行すると、結果は333となります。

このように演算子オーバーロードを書くことで、クラスの演算子を定義することができますが、変更できないクラスの場合、例えばMicrosoftの標準ライブラリ、サードパーティのライブラリ、参照しているDLLのクラスなど、クラスを直接変更できない場合は、extensionブロックに演算子オーバーロードを書くことで、演算子を拡張できます。
Moneyクラスを直接変更できない場合は、次のように書けばよいことになります。

    extension(Money money)
    {
      public static int operator +(Money a, Money b)
      {
        return a.Value + b.Value;
      }
    }

演算子の種類はこちらを参照してください。
https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/operator-overloading

Discussion