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

Ver. 11.0

C# 11 (.NET 7) で、インターフェイスの静的メンバーを abstract/virtual にできるようになりました。

using System.Buffers.Text;
using System.Text;

interface IUtf8Parsable<T>
    where T : IUtf8Parsable<T>
{
    // 静的メンバーにしたいもの筆頭が、ファクトリメソッドの類。
    // この例では Parse (文字列から T のインスタンスを作る)にしているものの、
    // 例えば static T Create(); みたいなものの需要も結構高いはず。
    public static abstract T Parse(ReadOnlySpan<byte> utf8);

    // virtual にもできる。
    // デフォルト実装を持ちつつ、必要であればクラス側で別実装を書ける。
    public static virtual T Parse(string s)
    {
        var buffer = (stackalloc byte[s.Length]);
        var read = Encoding.ASCII.GetBytes(s, buffer);
        return T.Parse(buffer[..read]);
    }
}

// 実装例:
record struct Point(int X, int Y) : IUtf8Parsable<Point>
{
    public static Point Parse(ReadOnlySpan<byte> utf8)
    {
        var i = utf8.IndexOf((byte)',');
        var xs = utf8[..i];
        var ys = utf8[(i + 1)..];

        Utf8Parser.TryParse(xs, out int x, out _);
        Utf8Parser.TryParse(ys, out int y, out _);

        return new(x, y);
    }
}

C# 8 のときのデフォルト実装と同じく、ランタイム側の修正が必要な機能で、C# バージョンだけを 11 に上げても、古い .NET をターゲットにしていると利用できません。

静的抽象メンバーの宣言

文法的には割かし素直で、 abstract/virtualstatic を併用できるようになりました。

interface IA
{
    static abstract void StaticAbstract();
    static virtual void StaticVirtual() { }
}

このまま「abstract/virtualstatic を同時に指定できるようになっただけです」と簡単に済ませられればいいんですが、C# 11 にもなって後付けしている経緯から、 ちょっと他の文法との整合性が悪かったりします。

以下のように、インスタンス メンバーと静的メンバーで、何も修飾子を付けないときの挙動が異なります。

interface IA
{
    // インスタンス メンバーの場合、abstract 修飾を付けなくても元から abstract。
    void Instance();

    // C# 8
    abstract void InstanceAbstract();
    virtual void InstanceVirtual() { }

    // C# 8
    // 静的メンバーの場合、何も修飾しないときは non-virtual。
    static void Static() { }

    // C# 11
    static abstract void StaticAbstract();
    static virtual void StaticVirtual() { }
}

ちなみに、この C# 8 の頃からの「何も付けないと non-virtual」の仕様があるのでわざわざ付ける意味はないんですが、一応、sealed 修飾子を付けれるようになっています。

interface IA
{
    // 何もつけない = non-virtual。
    void Static() { }

    // わざわざつける意味はない(元から sealed)けども、一応、明示的に sealed を付けることは認められてる。
    sealed void StaticSealed() { }
}

静的抽象メンバーの実装

インターフェイスの静的メンバーの実装方法はインスタンス メンバーの場合とそれほど変わりません。 以下のように、public で同名のメソッドを定義する(暗黙的実装)か、 インターフェイス名. で実装する(明示的実装)かです。

interface IA
{
    abstract void Instance();
    static abstract void Static();
}

class Implicit : IA
{
    // 暗黙的実装。
    // public にする必要あり。
    public void Instance() { }
    public static void Static() { }
}

class Explicit : IA
{
    // 明示的実装。
    // アクセシビリティは書けない(public と付けちゃダメ)。
    void IA.Instance() { }
    static void IA.Static() { }
}

ただ、静的メンバーを virtual / abstract にできるのはインターフェイスだけなので、 この点はインスタンス メンバーと同じというわけにはいきません。 以下のようなコードはエラーになります。

interface IA
{
    abstract void Instance();
    static abstract void Static();
}

class Virtual : IA
{
    // これは書ける(元々)。
    public virtual void Instance() { }

    // こうは書けない。
    public static virtual void Static() { }
}

静的抽象メンバーの呼び出し

インターフェイスの静的抽象メンバーは、ジェネリック型引数越しにしか呼べません。

例えば前節で例に挙げた IA インターフェイスの場合、以下のような呼び出し方になります。

static void M<T>()
    where T : IA
{
    // non-virtual の場合、インターフェイス名. 開始。
    // T.Static(); とは書けない。
    IA.Static();

    // virtual/abstract の場合、型引数. 開始。
    // IA.StaticAbstract(); IA.StaticVirtual(); とは書けない。
    T.StaticAbstract();
    T.StaticVirtual();
}

interface IA
{
    // non-virtual。
    static void Static() { }

    // virtual/abstract
    static abstract void StaticAbstract();
    static virtual void StaticVirtual() { }
}

注意: 静的抽象メンバー呼び出しは静的な型に紐づく

インスタンス メンバーと違って、 静的抽象メンバーの呼び出しは静的な型に紐づきます。

以下のように、M<T>() 内で T.Static() と呼び出したとき、 メソッド MM<A>() で呼び出した場合に常に A.Static が呼ばれます。

// 静的な型(変数/引数の型)とインスタンスの型(変数に格納した値の型)が一致してるときはそんなに変な挙動はしない。

M(new ABase()); // Base Instance / Base Static
M(new ADerived()); // Derived Instance / Derived Static

// 問題は、それが違うとき。

ABase a = new ADerived();
M(a); // Derived Instance / Base Static

M<ABase>(new ADerived()); // Derived Instance / Base Static

static void M<T>(T x)
    where T : IA
{
    x.Instance();
    T.Static();
}
 
// static abstract (実装を持っていない)メンバーがあるとと M<IA>() と書けなくなる。
interface IA
{
    abstract void Instance();
    static abstract void Static();
}

class ABase : IA
{
    void IA.Instance() => Console.WriteLine("Base Instance");
    static void IA.Static() => Console.WriteLine("Base Static");
}

class ADerived : ABase, IA
{
    void IA.Instance() => Console.WriteLine("Derived Instance");
    static void IA.Static() => Console.WriteLine("Derived Static");
}

これまでのインターフェイスの「インスタンスの型に紐づいて動的な呼び出しが行われる」という感覚とずれるので注意が必要です。

このことを指して、他のプログラミングの機能名と照らし合わせて、 「インターフェイスの静的抽象メンバーは、インターフェイスというよりも型クラス(type class)だ」と説明する人もいるくらいです。

注意: 静的抽象メンバーを持っていると型実引数に渡せない

前節で説明したように、静的な型に紐づく以上、 abstract な(実装を持っていない)型を型引数にすることはできません。

以下のように、virtual (実装を持っている)であれば問題ありません。

M<IA>();

static void M<T>()
    where T : IA
    => T.M();

// static abstract (実装を持っていない)メンバーがいないときは、M<IA>() と書ける。
interface IA
{
    static virtual void M() => Console.WriteLine("IA.M");
}

一方で、以下のように abstract (実装を持っていない)だとコンパイル エラーになります。

M<IA>(); // ここでエラーに。

M<A>(); // これ(実装クラス)ならOK。

static void M<T>()
    where T : IA
    => T.M();

// static abstract (実装を持っていない)メンバーがあると M<IA>() と書けなくなる。
interface IA
{
    static abstract void M();
}

class A : IA
{
    public static void M() { }
}

演算子

静的メンバーを virtual / abstract にできて一番うれしいのは、 演算子を定義できることでしょう。

例えばこれまで、以下のようなメソッドすらジェネリックな実装を持てませんでした。

Console.WriteLine(Sum(new[] { 1, 2, 3, 4 }));

Console.WriteLine(Sum(new float[] { 1, 2, 3, 4 })); // こう書きたいのにエラーに…

static int Sum(int[] items) // Sum<T>(T[]) にしてしまうと += が書けない。
{
    var sum = 0;
    foreach (var x in items) sum += x;
    return sum;
}

C# 11 でインターフェイスに virtual / abstract な演算子を持てるようになったことに伴って、 .NET 7 で標準ライブラリに以下のようなインターフェイスが用意されました。

namespace System.Numerics;

public interface IAdditionOperators<TSelf, TOther, TResult>
    where TSelf : IAdditionOperators<TSelf, TOther, TResult>?
{
    static abstract TResult operator +(TSelf left, TOther right);
    static virtual TResult operator checked +(TSelf left, TOther right) => left + right;
}

intfloat などの組み込みの数値型は一通りこのインターフェイスを実装しています。 (さらにいうと、この手のインターフェイスをまとめた INumeber<T> というインターフェイスを実装しています。) その結果、本節冒頭で挙げたような Sum メソッドをジェネリックに書けるようになりました。

using System.Numerics;

// よくある「和を取るコード」なものですら、これまでだとジェネリックに書く手段がなかった。
// C# 11 で可能に。
static T Sum<T>(IEnumerable<T> items)
    where T : INumber<T>
{
    var sum = T.Zero;
    foreach (var x in items) sum += x;
    return sum;
}

// いろんな型に対して sum<T> を呼ぶ。
Console.WriteLine(Sum(new byte[] { 1, 2, 3, 4, 5 }));
Console.WriteLine(Sum(new int[] { 1, 2, 3, 4, 5 }));
Console.WriteLine(Sum(new float[] { 1, 2, 3, 4, 5 }));
Console.WriteLine(Sum(new double[] { 1, 2, 3, 4, 5 }));
Console.WriteLine(Sum(new decimal[] { 1, 2, 3, 4, 5 }));

Generic Math

加減乗除や論理演算はもちろん、float, double などの一部の型は Math.Sin などの数学関数も使えます。 コンセプト的に、この新機能を使ったジェネリックな数値処理の事を通称 Generic Math と呼んでいたりします。

また、 .NET 5 以降、数値関連の型がいくつか追加されています。

  • Half: 16ビット浮動小数点数
  • Int128, UInt128: 128ビットの整数
  • CLong, CULong: C/C++ との相互運用のために使う、環境によってビット幅が違う整数
  • nint, nuint: CPU 依存幅の整数

これらの新しい数値型も、Generic Math の対象で、INumber<T> などのインターフェイスを実装しています。

演習問題

問題1

多態性問題 1Shape クラスをインターフェース化せよ。

TriangleShape 関係の例題は一応、これで完成形。

余力があれば、楕円、長方形、平行四辺形、(任意の頂点の)多角形等、さまざまな図形クラスを作成せよ。 また、これらの図形の面積と周の比を計算するプログラムを作成せよ。

解答例1

三角形、円に加え、多角形を実装した物を示します。

using System;

/// <summary>
/// 2次元の点をあらわす構造体
/// </summary>
struct Point
{
  double x; // x 座標
  double y; // y 座標

  #region 初期化

  /// <summary>
  /// 座標値 (x, y) を与えて初期化。
  /// </summary>
  /// <param name="x">x 座標値</param>
  /// <param name="y">y 座標値</param>
  public Point(double x, double y)
  {
    this.x = x;
    this.y = y;
  }

  #endregion
  #region プロパティ

  /// <summary>
  /// x 座標。
  /// </summary>
  public double X
  {
    get { return this.x; }
    set { this.x = value; }
  }

  /// <summary>
  /// y 座標。
  /// </summary>
  public double Y
  {
    get { return this.y; }
    set { this.y = value; }
  }

  #endregion
  #region 演算子

  /// <summary>
  /// ベクトル和
  /// </summary>
  /// <param name="a">点A</param>
  /// <param name="b">点B</param>
  /// <returns>和</returns>
  public static Point operator +(Point a, Point b)
  {
    return new Point(a.x + b.x, a.y + b.y);
  }

  /// <summary>
  /// ベクトル差
  /// </summary>
  /// <param name="a">点A</param>
  /// <param name="b">点B</param>
  /// <returns>和</returns>
  public static Point operator -(Point a, Point b)
  {
    return new Point(a.x - b.x, a.y - b.y);
  }

  #endregion

  /// <summary>
  /// A-B 間の距離を求める。
  /// </summary>
  /// <param name="a">点A</param>
  /// <param name="b">点B</param>
  /// <returns>距離AB</returns>
  public static double GetDistance(Point a, Point b)
  {
    double x = a.x - b.x;
    double y = a.y - b.y;
    return Math.Sqrt(x * x + y * y);
  }

  public override string ToString()
  {
    return "(" + x + ", " + y + ")";
  }
}

/// <summary>
/// 2次元空間上の図形を表すクラス。
/// 三角形や円等の共通の抽象基底クラス。
/// </summary>
interface Shape
{
  double GetArea();
  double GetPerimeter();
}

/// <summary>
/// 2次元空間上の円をあらわすクラス
/// </summary>
class Circle : Shape
{
  Point center;
  double radius;

  #region 初期化

  /// <summary>
  /// 半径を指定して初期化。
  /// </summary>
  /// <param name="r">半径。</param>
  public Circle(Point center, double r)
  {
    this.center = center;
    this.radius = r;
  }

  #endregion
  #region プロパティ

  /// <summary>
  /// 円の中心。
  /// </summary>
  public Point Center
  {
    get { return this.center; }
    set { this.center = value; }
  }

  /// <summary>
  /// 円の半径。
  /// </summary>
  public double Radius
  {
    get { return this.radius; }
    set { this.radius = value; }
  }

  #endregion
  #region 面積・周

  /// <summary>
  /// 円の面積を求める。
  /// </summary>
  /// <returns>面積</returns>
  public double GetArea()
  {
    return Math.PI * this.radius * this.radius;
  }

  /// <summary>
  /// 円の周の長さを求める。
  /// </summary>
  /// <returns>周</returns>
  public double GetPerimeter()
  {
    return 2 * Math.PI * this.radius;
  }

  #endregion

  public override string ToString()
  {
    return string.Format(
      "Circle (c = {0}, r = {1})",
      this.center, this.radius);
  }
}

/// <summary>
/// 2次元空間上の三角形をあらわすクラス
/// </summary>
class Triangle : Shape
{
  Point a;
  Point b;
  Point c;

  #region 初期化

  /// <summary>
  /// 3つの頂点の座標を与えて初期化。
  /// </summary>
  /// <param name="a">頂点A</param>
  /// <param name="b">頂点B</param>
  /// <param name="c">頂点C</param>
  public Triangle(Point a, Point b, Point c)
  {
    this.a = a;
    this.b = b;
    this.c = c;
  }

  #endregion
  #region プロパティ

  /// <summary>
  /// 頂点A。
  /// </summary>
  public Point A
  {
    get { return a; }
    set { a = value; }
  }

  /// <summary>
  /// 頂点B。
  /// </summary>
  public Point B
  {
    get { return b; }
    set { b = value; }
  }

  /// <summary>
  /// 頂点C。
  /// </summary>
  public Point C
  {
    get { return c; }
    set { c = value; }
  }

  #endregion
  #region 面積・周

  /// <summary>
  /// 三角形の面積を求める。
  /// </summary>
  /// <returns>面積</returns>
  public double GetArea()
  {
    Point ab = b - a;
    Point ac = c - a;
    return 0.5 * Math.Abs(ab.X * ac.Y - ac.X * ab.Y);
  }

  /// <summary>
  /// 三角形の周の長さを求める。
  /// </summary>
  /// <returns>周</returns>
  public double GetPerimeter()
  {
    double l = Point.GetDistance(this.a, this.b);
    l += Point.GetDistance(this.a, this.c);
    l += Point.GetDistance(this.b, this.c);
    return l;
  }

  #endregion

  public override string ToString()
  {
    return string.Format(
      "Circle (a = {0}, b = {1}, c = {2})",
      this.a, this.b, this.c);
  }
}

/// <summary>
/// 自由多角形をあらわすクラス
/// </summary>
class Polygon : Shape
{
  Point[] verteces; // 頂点

  #region 初期化

  /// <summary>
  /// 座標を与えて初期化。
  /// </summary>
  /// <param name="verteces">頂点の座標の入った配列</param>
  public Polygon(params Point[] verteces)
  {
    this.verteces = verteces;
  }

  #endregion
  #region プロパティ

  /// <summary>
  /// 頂点の集合。
  /// </summary>
  public Point[] Verteces
  {
    get { return this.verteces; }
    set { this.verteces = value; }
  }

  #endregion
  #region 面積・周

  /// <summary>
  /// 三角形の面積を求める。
  /// </summary>
  /// <returns>面積</returns>
  public double GetArea()
  {
    double area = 0;
    Point p = this.verteces[this.verteces.Length - 1];
    for (int i = 0; i < this.verteces.Length; ++i)
    {
      Point q = this.verteces[i];
      area += p.X * q.Y - q.X * p.Y;
      p = q;
    }
    return 0.5 * Math.Abs(area);
  }

  /// <summary>
  /// 三角形の周の長さを求める。
  /// </summary>
  /// <returns>周</returns>
  public double GetPerimeter()
  {
    double perimeter = 0;
    Point p = this.verteces[this.verteces.Length - 1];
    for (int i = 0; i < this.verteces.Length; ++i)
    {
      Point q = this.verteces[i];
      perimeter += Point.GetDistance(p, q);
      p = q;
    }
    return perimeter;
  }

  #endregion

  public override string ToString()
  {
    System.Text.StringBuilder sb = new System.Text.StringBuilder();
    sb.AppendFormat("Polygon ({0}", this.verteces[0]);
    for (int i = 1; i < this.verteces.Length; ++i)
    {
      sb.AppendFormat(", {0}", this.verteces[i]);
    }
    sb.Append(")");

    return sb.ToString();
  }
}

/// <summary>
/// Class1 の概要の説明です。
/// </summary>
class Class1
{
  static void Main()
  {
    Triangle t = new Triangle(
      new Point(0, 0),
      new Point(3, 4),
      new Point(4, 3));

    Circle c = new Circle(
      new Point(0, 0), 3);

    Polygon p1 = new Polygon(
      new Point(0, 0),
      new Point(3, 4),
      new Point(4, 3));

    Polygon p2 = new Polygon(
      new Point(0, 0),
      new Point(0, 2),
      new Point(2, 2),
      new Point(2, 0));

    Show(t);
    Show(c);
    Show(p1);
    Show(p2);
  }

  static void Show(Shape f)
  {
    Console.Write("図形 {0}\n", f);
    Console.Write("面積/周 = {0}\n", f.GetArea() / f.GetPerimeter());
  }
}

更新履歴

ブログ