C# 3.0 から拡張メソッドが使えるわけですが、 もうちょっといろんな「拡張」をしたいという話が前々からあります。 例えば以下のような要求。

  • 既存の型に静的メンバーも足したい
  • プロパティや演算子も足したい
  • インターフェイスの後付けもしたい

今では Extensions とか呼ばれていまして、以下の issue でトラッキング中。

ここからさかのぼって、かつては Extension everything とか呼ばれていたり、 個別に「インターフェイスを実装したい」「演算子を拡張したい」など個別の issue がありました。

2015年(Roslyn が GitHub での公開に切り替わった年)にはすでにそんな話が出ています。

結構大きな機能なのでしり込みしていたみたいですが、 去年くらいから Working Group (この機能の追加を推進するメンバーを割り当てて、定期的にミーティング)を設けて作業を始めました。

うちのブログでも去年、1度取り上げています。

もう9年も経ってしまい、C# 12 でも入らなかったわけですが、 ついに今年、C# 13 には一部入りそう(インターフェイスの後付けだけは無理そう)な雰囲気になっています。

最近の話題のうちいくつかを取り上げると、以下のような話が出ています。

extension 構文

ということで、改めて Extensions の話を。 今、以下のような構文を足そうとしています。

// 拡張の構文例。
implicit extension SomeExtension for SomeClass : IEquatable<SomeExtension>
{
    // 追加したいメンバーを書く。

    // 1. 静的メンバーも書ける。
    public static int Y => X * X;

    // 2. メソッド以外も書ける。
    public int Property
    {
        get => GetValue();
        set => SetValue(value);
    }

    public int this[int index] => GetValue(index);

    // 3. インターフェイスの実装を持てる。
    public bool Equals(SomeExtension? other) => Property == other?.Property;
}

// 拡張の対象の例。
class SomeClass
{
    // (中身は適当。)
    public static int X = 123;

    private int _value;

    public int GetValue() => _value;
    public void SetValue(int value) => _value = value;
    public int GetValue(int index) => _value * index;
}

ちなみに、「インターフェイスの実装を持つ」には少し難題があって、 C# 13 時点では入らない可能性がかなり高いです。

普通の構造体 + Unsafe.As

拡張はラッパー構造体を使った実装になりそうです。 一時期は以下のような ref struct を使った実装になりそうだったんですが、 この案は結局没になりました。

var value = new SomeStruct();
var extension = new SomeExtension(ref value);

// 拡張プロパティを呼び出す。
extension.Property = 123;

// ちゃんと元インスタンスに値が反映。
Console.WriteLine(value.GetValue());

ref struct SomeExtension(ref SomeStruct @this)
{
    ref SomeStruct @this = ref @this;

    public int Property
    {
        get => @this.GetValue(); // ref で持ってるので、引数でもらった構造体に書き換えが反映される。
        set => @this.SetValue(value);
    }
}

// デモ用に構造体に変更。
struct SomeStruct
{
    private int _value;

    public int GetValue() => _value;
    public void SetValue(int value) => _value = value;
    public int GetValue(int index) => _value * index;
}

この案に変わって、普通の構造体 + Unsafe.As を使う路線で考えているそうです。

using System.Runtime.CompilerServices;

var value = new SomeStruct();

// Unsafe.As を使って、value 値が入っているの場所を無理やり SomeExtension で解釈。
ref var extension = ref Unsafe.As<SomeStruct, SomeExtension>(ref value);

// 拡張プロパティを呼び出す。
extension.Property = 123;

// extension の参照先が value なので、ちゃんと value が書き変わる。
Console.WriteLine(value.GetValue());

// 普通の構造体。
struct SomeExtension
{
    private SomeStruct @this;

    public int Property
    {
        get => @this.GetValue();
        set => @this.SetValue(value);
    }
}

// SomeStruct は先ほどと同じ。

型消去

Extensions は普通の型と同じように使えたりします。 (特に、explicit を付けた Extensions はむしろ「型を明示しないと使えない」状態になります。) なのでこれを拡張型(extension types)と呼んだりもします。

で、前節の通り Extensions のコンパイル結果はラッパー構造体だったりするわけですが、 このラッパー構造体への変換(Unsafe.As)はあくまでメンバー参照のタイミングで行われます。 メソッドの引数などに拡張型を書くと、実際には「元の型 + 属性」(いわゆる「型消去」方式)になる予定です。 例えば、以下のようなメソッドを書いたとして、

static int Sum(SomeExtension a, List<SomeExtension> b)
{
    var sum = a.Property;
    foreach (var x in b) sum += x.Property;
    return sum;
}

以下のような類のコードに置き換わる予定です。

static int Sum(
    // SomeExtension は属性の中にしか残らない。
    // 元の、 SomeStruct に置き換わる。
    [Extension(typeof(SomeExtension))] SomeStruct a,
    [Extension(typeof(SomeExtension))] List<SomeStruct> b)
{
    // メンバーアクセスするところで Unsafe.As
    var sum = Unsafe.As<SomeStruct, SomeExtension>(ref a).Property;
    foreach (var x in b) sum += Unsafe.As<SomeStruct, SomeExtension>(ref Unsafe.AsRef(in x)).Property;
    return sum;
}

変性を持っていない List<T> で、 List<SomeStruct>List<SomeExtension> に変換する手段は通常全くありません。 型消去で List<SomeExtension>List<SomeStruct> に置き換わることで、 List<SomeStruct> 型の変数を List<SomeExtension> 型の引数に渡せるようになっています。

メンバーのルックアップ(継承)

拡張型は元となる型との間には、クラスの継承関係と似た関係が成り立ちます。 なので、メンバーのルックアップのルールも「クラスの継承に準ずる」で行きたいそうです。 例えば、派生クラスから基底クラスのメンバーを何の修飾もなしで(this. とか base. が必須ではなく)参照できるように、 拡張型から元となる型のメンバーも修飾なしで参照できます。

おさらい的に、「継承があるときのルックアップ」の例をいくつか紹介しておきます。 (拡張型中で元となる型と同名のメンバーを書くとこれに準ずることになると思われます。)

近い側優先:

class Base
{
    public void M(int x) { }
}

class Derived : Base
{
    public new void M(int x) { }

    public void M()
    {
        // 近い側優先なので、Derived.M が呼ばれる。
        M(1);
    }
}

もうちょっとわかりにくい例:

class Base
{
    public void M(int x) { }
}

class Derived : Base
{
    public new void M(object x) { }

    public void M()
    {
        // わかりにくいけども、Derived.M(object) の方が呼ばれる。
        // 引数の型を考えると Base.M(int) が呼ばれそうに見えるけども、そうはならない。
        // (「元々はなかったけど後から Base の方に M(int) が追加された」みたいな状況で破壊的変更にならないようにするため。)
        M(1);
    }
}

メンバーのルックアップ(拡張同士)

あと、既存の拡張メソッドには以下のような優先度があります。

namespace Ex1
{
    static class AExtension
    {
        public static void M(this App1.A _) => Console.WriteLine("Extension in Ex1");
    }
}

namespace App1
{
    class A
    {
        public void M() => Console.WriteLine("Instance");
    }

    class Program
    {
        public static void Main()
        {
            // インスタンス メソッド優先。
            new A().M(); // Instance
        }
    }
}
namespace Ex1
{
    static class AExtension
    {
        public static void M(this App1.A _) => Console.WriteLine("Extension in Ex1");
    }
}

namespace App1
{
    class A;

    static class AExtension
    {
        public static void M(this A _) => Console.WriteLine("Extension in App1");
    }

    class Program
    {
        public static void Main()
        {
            // 同じ名前空間内の拡張メソッド優先。
            new A().M(); // in App1
        }
    }
}
using Ex1;

namespace Ex1
{
    static class AExtension
    {
        public static void M(this App1.A _) => Console.WriteLine("Extension in Ex1");
    }
}

namespace Ex2
{
    static class AExtension
    {
        public static void M(this App1.A _) => Console.WriteLine("Extension in Ex1");
    }
}

namespace App1
{
    using Ex2;

    class A;

    class Program
    {
        public static void Main()
        {
            // 内側で using した方優先。
            new A().M(); // in Ex2
        }
    }
}
namespace Ex1
{
    static class AExtension
    {
        public static void M(this App1.A _) => Console.WriteLine("Extension in Ex1");
    }
}

namespace Ex2
{
    static class AExtension
    {
        public static void M(this App1.A _) => Console.WriteLine("Extension in Ex1");
    }
}

namespace App1
{
    using Ex1;
    using Ex2;

    class A;

    class Program
    {
        public static void Main()
        {
            // 優劣がない場合はコンパイル エラー。
            new A().M();
        }
    }
}

新しい拡張型でも同様のルールになると思われます。

一方で、旧「拡張メソッド」と新「拡張型」に優劣をつけるかという議題もありますが、 現状は「優劣つけない」という方向で検討されています。 というか、新旧混在した時点でコンパイル エラーにしようかという話もあるみたいです。

namespace Ex1
{
    static class AExtension
    {
        public static void M(this App1.A _) => Console.WriteLine("old extension method");
    }
}

namespace Ex2
{
    implicit extension AExtension for A
    {
        public void M() => Console.WriteLine("new extension type");
    }
}

namespace App1
{
    using Ex1; // これが外にあってもエラーにする案もあり
    using Ex2;

    class A;

    class Program
    {
        public static void Main()
        {
            // 優劣を付けない(コンパイル エラーになる)。
            // 何なら新旧混在している時点でコンパイル エラーにする可能性濃厚。
            new A().M();
        }
    }
}

インターフェイス実装

ここまでの話は割かし C# 13 で入りそうな話なんですが、 最後に1つ、13では入らなさそうなのがインターフェイス実装の後付けです。

これまでの話どおり、ラッパー構造体を作る方針で少し考えてみましょう。

インターフェイス実装に関する部分だけ残して、以下のようにしたとします。

var value = new SomeClass { Value = 1 };
SomeExtension extension = value;

extension.Equals(new SomeClass { Value = 1 });

explicit extension SomeExtension for SomeClass : IEquatable<SomeExtension>
{
    public bool Equals(SomeExtension? other) => Value == other?.Value;
}

class SomeClass
{
    public int Value;
}

ラッパー構造体で展開するとしたら以下のようになります。

using System.Runtime.CompilerServices;

var value = new SomeClass { Value = 1 };
ref var extension = ref Unsafe.As<SomeClass, SomeExtension>(ref value);

var temp = new SomeClass { Value = 1 };

// こういう風に直接インターフェイス メンバーを呼ぶ分には特に問題なさげ。
extension.Equals(Unsafe.As<SomeClass, SomeExtension>(ref temp));

struct SomeExtension : IEquatable<SomeExtension>
{
    private SomeClass Value;
    public bool Equals(SomeExtension other) => Value.Value == other.Value?.Value;
}

class SomeClass
{
    public int Value;
}

この例はインターフェイス実装しているといっても、そもそもメンバーを直接呼んでいるので問題がないだけです。 問題は以下の状況。

  • インターフェイス型や object 型の変数で受けてボックス化する場合
  • ジェネリック メソッドに渡す場合

まず、インターフェイス型の変数で受けてみましょう。 ReferenceEqualsis 判定であまり期待通りとは言えない挙動を起こします。

using System.Runtime.CompilerServices;

var value = new SomeClass { Value = 1 };
ref var extension = ref Unsafe.As<SomeClass, SomeExtension>(ref value);

// インターフェイスに渡そうとすると、この実装だとボックス化が発生。
IEquatable<SomeExtension> boxedExtension = extension;

// インスタンスが一致しなくなる。
Console.WriteLine(ReferenceEquals(value, boxedExtension)); // false

// ダウンキャストが失敗する。
Console.WriteLine(boxedExtension is SomeClass); // false

ジェネリク メソッドでは、以下のように、元の型と拡張型の両方の型情報を使う必要がでてきます。

var value = new SomeClass { Value = 1 };
List<SomeClass> list = [new() { Value = 2 }, new() { Value = 1 }, new() { Value = 0 }];

// SomeClass のままだと IEquatable 制約を満たさなくて呼べない。
var i1 = IndexOf<SomeClass>>(list, value);

// これなら呼べるようになるはず。
// ただ、list は List<SomeClass> なので、やっぱり型消去が必要。
// 型引数が暗黙的に SomeClass と SomeExtension の2つに増えるような処理が必要。
var i2 = IndexOf<SomeExtension>(list, value);

static int IndexOf<T>(List<T> list, T value)
    where T : IEquatable<T>
{
    // 今の型システムだと T が通常の型か拡張型かを知るすべはなく、Unsafe.As 展開ができない。
    for (int i = 0; i < list.Count; i++)
        if (list[i].Equals(value))
            return i;
    return -1;
}

いずれも、C# コンパイラー上のトリックでは問題を解消できなさそうで、 .NET ランタイムの型システムに手を入れる必要が出てきそうです。 型システムに手を入れるとなると結構大ごとなので、C# 13 で実現する見込みは残念ながらほぼありません。