UfcppSample
UfcppSample copied to clipboard
Extensions
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/ (この時代の実装は「やってみたけどダメだった」撤回)
昔ちょこっと書いたの:
要求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# ではどうやってもできませんでした。
13から外れた。 Unsafe.As 路線は JIT を混乱させて文字通り安全じゃないらしく、急遽路線変更。
// 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) { }
*/
}
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() { }
}
}
// 修飾子とか属性とか盛る
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;
}
}
// 残念仕様。
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) { }
*/
}
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() { } // コンパイル エラー
}
}
Ex.get_Property とか Ex.op_Plus とかでも呼べる。
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 の方になってそう。
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;
}
// <> ない、引数 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() { }
}
}
式ツリーの中でだけ使えない extension もあるらしい。
&& と ||。
対応するファクトリメソッドが Expression 型にないとのこと。
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();
}
}
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() { }
}
}
readonly 修飾子ダメ。
// この 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) { }
}