UfcppSample icon indicating copy to clipboard operation
UfcppSample copied to clipboard

Extensions

Open ufcpp opened this issue 1 year ago • 7 comments
trafficstars

csharplang 8697 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-14.0/extensions.md

配信で取り上げた回 https://github.com/ufcpp-live/UfcppLiveAgenda/issues/102

たぶん、C# 14 だと「プロパティの追加」と「静的メンバーの追加」だけかな。 メソッドも「新文法」に。

↑ その後、拡張 operator は入った。

コンストラクターとかは C# 15 に回ったとのこと。

ExtensionMarkerAttribute とかあるみたいなんだけどどう使ってるのか調べる。

過去

https://ufcpp.net/blog/2024/3/extensions/ (この時代の実装は「やってみたけどダメだった」撤回)

ufcpp avatar Jun 12 '24 13:06 ufcpp

昔ちょこっと書いたの:

要求1: 静的メンバー

C# 3.0 の拡張メソッドはインスタンス メソッドでだけ「既存の型に追加したように見える書き方」ができます。 123.Extension() とはできても、int.Extension() とはできません。

既存の型に静的メソッドを足せなくて困る例としては、ポリフィル(古い環境向けに最新機能を移植して使えるようにするような用途)とかでしょうか。

例えば割かし最近(.NET 8、2023年11月正式リリース)、TimeProviderという型が入って以下のような書き方ができるようになりました。

TimeProvider tp = TimeProvider.System;

// 1秒待つ。
// TimeProvider を差し替えて、単体テストとかでは一瞬で終わるようにしたい。
await Task.Delay(TimeSpan.FromSeconds(1), tp);

TimeProvider クラス自体は、.NET 8 より前のランタイムでも使えるようにできます。 (実際、Microsoft.Bcl.TimeProvider という NuGet パッケージが公式提供されています。) 以下のような書き方で、「なければ追加、あれば type forward」みたいなことができます。

#if NET8_0_OR_GREATER

using System;
using System.Runtime.CompilerServices;

[assembly: TypeForwardedTo(typeof(TimeProvider))]

#else

namespace System;

public class TimeProvider
{
    // 互換実装
}

#endif

インスタンス メソッドの追加風のことも、拡張メソッドでできました。

using System.Runtime.CompilerServices;

namespace System.Threading.Tasks;

public static class TaskExtensions
{
    // GetAwaiter メソッドがあると C# 5.0 の await が使える。
    public static TaskAwaiter GetAwaiter(this Task task)
    {
        // 今はインスタンス メソッドで GetAwaiter があるけど、
        // .NET Framework 4.0 以前はなかったので自作が必要。
        return task.GetAwaiter();
    }
}

ですが、先ほどの例に出てきた Task.Delay(TimeSpan, TimeProvider) は既存の型への静的メソッドの追加なので、 これまでの C# ではどうやってもできませんでした。

ufcpp avatar Jun 12 '24 13:06 ufcpp

13から外れた。 Unsafe.As 路線は JIT を混乱させて文字通り安全じゃないらしく、急遽路線変更。

ufcpp avatar Jul 14 '24 02:07 ufcpp


// this 形式の拡張メソッドとの互換性
//
// C# 14 で作った extension ブロック形式の拡張メソッドは、
// C# 13 以前のコンパイラーからも普通に呼べる。
//
// 以下のように書いた場合、
// B 側には A 側の public static void M(this int x) と全く同じメソッドが生成される。
//
// this 形式から生成されるメソッドに付いてた [Extension] 属性は、extension 形式から生成されるものにも付く。
// 一方で、extension 形式特有の「メタデータ用の unspeakable な型」も追加で生成される。
// 要は、新旧両方のメタデータを生成してる。

static class A
{
    public static void M(this int x) { }

    /* 内部的には以下のようなメソッドと同じ。

    [Extension]
    public static void M(int x) { }
     */
}

static class B
{
    extension(int x)
    {
        public void M() { }
    }

    /* こいつからも以下のようなメソッドは生成されてる。

    [Extension]
    public static void M(int x) { }
     */
}

ufcpp avatar Oct 27 '25 12:10 ufcpp

static class Ex
{
    /// <summary>
    /// aaa
    /// </summary>
    /// <param name="s">iii</param>
    public static void M(this string s)
    {
    }

    // ここに <param> doc コメント書く想定あるはずだけど、現時点、VS では反映されなさそう。
    // (利用側で "".M1 のヒント見ても s の説明が出ない。)

    /// <param name="s">uuu</param>
    extension(string s)
    {
        /// <summary>
        /// eee
        /// </summary>
        public void M1() { }
    }
}

ufcpp avatar Oct 27 '25 12:10 ufcpp

// 修飾子とか属性とか盛る

using System.Diagnostics.CodeAnalysis;

class AAttribute : Attribute;

struct S<T1, T2>;

static class Ex
{
    extension<[A] TKey, TValue>([NotNull][A] scoped ref S<TKey, TValue>? x)
        where TKey : notnull, IComparable<TKey>
        where TValue : unmanaged, IUtf8SpanFormattable
    {
        public static int Q => 1;
    }
}

ufcpp avatar Oct 27 '25 12:10 ufcpp

// 残念仕様。

static class Ex
{
    extension<T>(T x)
    {
        public void M<U>() { }
    }

    /*
     * ↓ みたいなコードに展開される。
     * x.M(); で型推論できない。x.M<int, string>(); みたいに2個とも型引数指定必要。
        public static void M<T, U>(this T x) { }
     
     */
}

ufcpp avatar Oct 27 '25 12:10 ufcpp

static class A
{
    extension(int x)
    {
        public void M() { }
    }

    extension(int x)
    {
        // 同じ型に対する extension は同一グループ扱いで、名前が被るとコンパイル エラー。
        public void M() { }
    }
}

static class B
{
    extension(int x)
    {
        public void M() { }
    }

    extension(ref int x)
    {
        // ref/in だけが違うものも同一グループ扱いになる。
        public void M() { }
    }


    extension(byte x)
    {
        public void M() { } // extension(int).M とは共存可能だけど、
    }

    extension(in byte x)
    {
        public void M() { } // これが in byte と byte の共存は不可。
    }
}

static class C
{
    // static だと引数を持たなくなるんで別の型でも区別できなくなる。
    extension(int)
    {
        public static void M() { }
    }

    extension(string)
    {
        public static void M() { } // コンパイル エラー
    }
}

ufcpp avatar Oct 27 '25 12:10 ufcpp

Ex.get_Property とか Ex.op_Plus とかでも呼べる。

ufcpp avatar Nov 07 '25 01:11 ufcpp

var e = IEnumerable<string>.M(out object o); // What type is e? Does this line compile?

public static class Ext
{
    extension<T>(IEnumerable<T>)
    {
        public static IEnumerable<T> M(out T t) => ...;
    }
}

object の方になってそう。

ufcpp avatar Nov 07 '25 01:11 ufcpp

extension(T) の中に T? の operator 書くのダメらしい。

S1? s1 = new S1();
s1 = +s1; // Error: no matching operator
s1 = +s1.Value; // Ok

public struct S1;

public static class Extensions
{
    extension(S1)
    {
        public static S1? operator +(S1? x) => x;
    }
}

extension の部分がそもそも T? なら大丈夫。

    extension(S1?)
    {
        public static S1? operator +(S1? x) => x;
    }

ufcpp avatar Nov 07 '25 06:11 ufcpp

    // <> ない、引数 x に使わない型 Y があっても平気。
    extension<X, Y>(X x)
    {

    }

特に演算子書くのにこれが必要っぽい。

    extension<X, Y>(X)
    {
        public static X operator +(X x, Y y) => default;
    }

ちなみに、呼ぶ側:

int.M(); // エラー。

int.M<int, int>(); // これなら書けちゃう…

static class Accessors
{
    // <> ない、引数 x に使わない型 Y があっても平気。
    extension<X, Y>(X x)
    {
        public static void M() { }
    }
}

ufcpp avatar Nov 07 '25 06:11 ufcpp

式ツリーの中でだけ使えない extension もあるらしい。 &&||。 対応するファクトリメソッドが Expression 型にないとのこと。

ufcpp avatar Nov 07 '25 06:11 ufcpp

extern と UnsafeAccessor も使えるらしい。

using System.Runtime.CompilerServices;

using var stream = new MemoryStream();

var x = stream._buffer;

Console.WriteLine(x is null);

static class Accessors
{
    extension(MemoryStream stream)
    {
        [UnsafeAccessor(UnsafeAccessorKind.Field)]
        public extern ref byte[] _buffer();
    }

}

ufcpp avatar Nov 07 '25 06:11 ufcpp

in の有無でオーバーロードあり事案:


int.Static(); // こっちは ambiguous

1.Instance(); // こっちは (int x) の方が選ばれる

static class E1
{
    extension(int x)
    {
        public static void Static() { }
        public void Instance() { }
    }
}
static class E2
{
    extension(in int i)
    {
        public static void Static() { }
        public void Instance() { }
    }
}

ufcpp avatar Nov 07 '25 06:11 ufcpp

readonly 修飾子ダメ。

ufcpp avatar Nov 07 '25 07:11 ufcpp

// この A と B、全く同じ拡張メソッドのはずなんだけど、A だけエラーになる。
// (そういう仕様。)
// 確かに元々 B の形で書けても、呼ぶ側では弁別できなくて拡張メソッドとしては呼べない。

static class A
{
    extension(int x)
    {
        public void M() { }
    }

    extension(ref int x)
    {
        public void M() { }
    }
}

static class B
{
    public static void M(this int xx) { }

    public static void M(this ref int xx) { }
}

ufcpp avatar Nov 16 '25 07:11 ufcpp