インターフェイスの静的抽象メンバー
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
/virtual
と static
を併用できるようになりました。
interface IA { static abstract void StaticAbstract(); static virtual void StaticVirtual() { } }
このまま「abstract
/virtual
と static
を同時に指定できるようになっただけです」と簡単に済ませられればいいんですが、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()
と呼び出したとき、
メソッド M
を M<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; }
int
や float
などの組み込みの数値型は一通りこのインターフェイスを実装しています。
(さらにいうと、この手のインターフェイスをまとめた 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
多態性の問題 1の Shape
クラスをインターフェース化せよ。
Triangle
や Shape
関係の例題は一応、これで完成形。
余力があれば、楕円、長方形、平行四辺形、(任意の頂点の)多角形等、さまざまな図形クラスを作成せよ。 また、これらの図形の面積と周の比を計算するプログラムを作成せよ。