今日は「拡張」(拡張メソッド的なものの改良)の話。
(今日のこれは、C# 12 で全て実装されるかどうか怪しく、
一部 13 以降になる可能性も結構高いです。)
結構昔から、
みたいな案があったんですが、結局、この Roles をベースに、Extensions とか Extension types という名称で実装が進みそうです。
原案で「Roles/Extensions」と呼ばれていたものは、「Explicit /Implicit extensions」となります。
extension キーワード
提案されている現状の文法では、新たに extension
キーワードを使った「型定義」できるようにするみたいです。
例えば、int
に対する「拡張」を書くのなら、以下のような書き方をします。
implicit extension Ex for int
{
}
なんでも拡張
現状の拡張メソッドの仕様では、名前通り、メソッドしか定義できません。
プロパティなどを「拡張」したいという要望は長らくあるんですが、
今の拡張メソッドの文法がプロパティなどに向いていなさ過ぎて、導入できずにいます。
また、静的メンバーにも対応していません。
static class Extensions
{
public static void Method(this int x) { }
public static int Property { }
public static int this[int index] { }
public static int operator +() { }
}
extension
を使った定義では、インスタンス フィールドと自動プロパティ・自動イベント(暗黙的にフィールドが必要)を除いて、どのメンバーでも使えます。
implicit extension Ex for int
{
public void Method() { }
public int Property => this;
public int this[int index] => index;
public static void StaticMethod() { }
public static Ex operator+ (Ex x) => x;
}
ちなみに、インターフェイスも実装できる予定です。
既存の(第3者が作っていて自分では手を入れられない)型にインターフェイスを後挿しできます。
implicit extension Ex for bool : IFormattable
{
public void ToString(string? format, IFormatProvider? formatProvider) => this ? "true" : "false";
}
これで、以下のような呼び出しができるようになる予定です。
int x = 0;
x.Method();
_ = x.Property;
_ = x[1];
int.StaticMethod();
IFormattable f = true;
拡張「型」
既存の拡張メソッドでも起こるんですが、
複数の拡張があるとき、同名のメソッドが被ってどちらを呼ぶべきか解決できない時があります。
int x = 0;
x.Method();
Ex1.Method(x);
Ex2.Method(x);
static class Ex1
{
public static void Method(this int x) { }
}
static class Ex2
{
public static void Method(this int x) { }
}
また、拡張メソッドは元々あるインスタンス メソッドよりも優先度が低いので、
同名のメソッドで「上書き」することもできません。
int x = 0;
x.ToString();
Ex1.ToString(x);
static class Ex1
{
public static void ToString(this int x) => x.ToString("X2");
}
これらの例の通り、
名前被り時の解決方法は「普通の静的メソッドとして呼ぶ」という手段です。
一方、extension
では、以下のように、キャスト的な文法で解決します。
int x = 0;
x.Method();
x.ToString();
((Ex1)x).Method();
((Ex2)x).Method();
((Ex2)x).ToString();
Ex1 ex = x;
ex.Method();
ex.ToString();
implicit extension Ex1 for int
{
public void Method(this int x) { }
public void ToString(this int x) => x.ToString("X2");
}
implicit extension Ex2 for int
{
public void Method(this int x) { }
}
実際に型として使える
Ex1 ex
みたいな変数を定義できることからもわかる通り、
extension
は普通に「型」という扱いです。
なので、拡張型 (extension types)と呼びます。
変数だけではなく、引数、型引数などにも使えます。
using System.Collections;
int x = 0;
M1(x);
M2(new[] { 1, 2, 3 });
static void M1(Ex1 x) => Console.WriteLine(x);
static void M2(IEnumerable<Ex1> x)
{
foreach (var item in x) Console.WriteLine(item);
}
implicit extension Ex1 for int
{
}
explicit extension
これまで説明なしで implicit extension
という書き方をしてきましたが、
そこから察していただける通り、explicit extension
もあります。
名前通り型の明示が必須になって、
int
などの元の型のままでメンバーを呼ぶことができなくなります。
1.Method();
int.StaticMethod();
Ex ex = 1;
ex.Method();
Ex.StaticMethod();
explicit extension Ex for int
{
public void Method() { }
public static void StaticMethod() { }
}
「1.Method()
みたな呼び方ができないものが『extension』なのか?」みたいな話はあります。
なので、元々は role, view, shape (同じデータの別の役割・見え方・輪郭)みたいな言葉を使おうかという話も出ていました。
ただ、変に用語を増やすよりは、「暗黙的拡張」、「明示的拡張」と呼び分ける方がいいのではないかということになって、こちらにも extension
を使おうという流れになっています。
ちなみに、同じ型に対する別の extension はお互い型変換させるつもりはないそうです。
Ex1 ex1 = 1;
Ex2 ex2 = 2;
Ex2 ex3 = ex1;
explicit extension Ex1 for int { }
explicit extension Ex2 for int { }
要は、strong-typedef 的なものに使えます。
(この辺りが「それは extension なのか?」と言われるゆえんです。
拡張するメンバーが一切なくても使い道があります。)
細かい文法話
extension は別の extension からの派生もOKで、
多重継承も認めるそうです。
インターフェイス実装もできるわけで、
:
の後ろには他の extension とインターフェイスが並びます。
例えば以下のような感じ。
(T
は通常の型、I
始まりのものがインターフェイス、X
始まりのものが extension。)
implicit extension X for T : XA, XB, IA, IB
{
}
ちなみに、ここでいう T
(for
の後ろの型)のことを「基になる型」(underlying type: 根底にある型、基礎となる型)と言います。
(C# 的には、enum
なんかの enum E : int { }
とかの int
の部分も underlying type と言います。Microsoft の和訳では undelying type = 基になる型。)
クラスの場合は基底クラスとインターフェイスをあまり区別せず、class Derived : Base, IA, IB
と書ける(ただし、基底クラスは先頭である必要あり)わけですが、
extension の場合は for
を使って :
とは分ける方向で考えているみたいです。
基底型をいくつも持てるし、ただでさえ基底型とインターフェイスの混在があるのに、さらに基になる型 T
も並べた時に、「同じ :
を使って、一番先頭という縛りを設ける」というのはいささか不安だったそうです。
特に、partial
を認めるつもりなので、その場合に「一番先頭」があやふやになるのを懸念したみたいです。
implicit partial extension X for T : XA, IA
{
}
implicit partial extension X : XB, IB
{
}
また、既存の拡張メソッドがトップレベルの型での定義以外を認めていないのに対して、
新しい extension は入れ子を認めるそうです。
using static Ex;
using static C;
1.M1();
2.M2();
implicit extension Ex for T
{
implicit extension NextedEx for int
{
void M1() { }
}
}
class C
{
implicit extension NextedEx for int
{
void M2() { }
}
}
さらに、ジェネリックにもできるそうです。
implicit extension X<T> for T : XA, IA
where T : IT
{
}
派生 extension を作る際には、
基となる型の条件を強める方向でなら、基となる型の変更もできるみたいです。
implicit extension XBase for IEnumerable<object>
{
}
implicit extension XDerived1 for IEnumerable<string> : XBase
{
}
implicit extension XDerived2 : XBase
{
}
実装方法
現状、文法面をどうするかが議論の中心で、
あんまり実装方法に関する決定はないみたいなんですが、
案として挙がっているのは以下のような方向性です。
例えば、前述の(以下に再掲) extension に対して、
implicit extension Ex for int
{
public void Method() { }
public int Property => int;
public int this[int index] => index;
public static void StaticMethod() { }
public static Ex operator+ (Ex x) => x;
}
以下のようなラッパー構造体を作るのはどうかという案になっています。
ref struct Ex
{
private ref int @this;
public Ex(ref int @this) => this.@this = ref @this;
public void Method() { }
public int Property => @this;
public int this[int index] => index;
public static void StaticMethod() { }
public static Ex operator +(Ex x) => x;
}
ref 構造体、ref フィールドを使う想定なので、
別途以下のような機能(C# 11 時点で認められていない)が必要になります。
- ref 構造体の ref フィールドを持てるようにする
- ref 構造体をジェネリック型引数にする
- ref 構造体でインターフェイスを実装する
ref struct S : IEnumerable<int>
{
ref S _refS;
IEnumerable<S> GetItems()
{
yield return default;
}
}
実装フェーズ
冒頭に「C# 12 で全て実装されるかどうか怪しい」という話をしましたが、
具体的には以下のような3つのフェーズに分かれています。
- 静的メンバーの拡張だけ認める
- インスタンス メンバーも認める
- インターフェイス実装を認める
前節で説明したように、ref フィールドを使った実装にする可能性が濃厚なわけで、
これら3フェーズは要するに、
- 静的メンバー: 現状でもできる
- インスタンス メンバー: ref 構造体の ref フィールドを認めた上でやりたい
- インターフェイス実装: ref 構造体のインターフェイス実装を認めた上でやりたい
という区分だったりします。
1と2を分けるのは少々気持ち悪いので実際にはこの2つは同時に提供されるかもしれませんが、
実装都合でいうと結構な難易度の隔たりがあるそうです。
ちなみに、「静的メソッドの拡張をしたい、既存の型に静的メソッドを追加したい」という要望もそれなりに昔からあるので、
1だけ先行実装というのもそこまで不自然でもないかもしれません。