はじめに
.NET10 Preview3 で拡張メソッドが機能追加されました。
public static class __ExtensionMethodTest
{
extension(string word)
{
// インスタンスメソッドの場合、static を指定しない
internal int SumLength(string other)
{
return word.Length + other.Length;
}
// static メソッドを実装可能
internal static string MaxLength(string left, string right)
{
return left.Length > right.Length ? left : right;
}
// プロパティを実装可能
internal bool HasASCII
{
get
{
foreach (var n in word)
if (!char.IsAscii(n)) return false;
return true;
}
}
// 演算子の多重定義は不可
// public static string operator -(string left, string right)
// {
// throw new NotImplementedException();
// }
}
テストコード
テストコード
using Xunit;
public static class __ExtensionMethodTest
{
extension(string word)
{
// インスタンスメソッドの場合、static を指定しない
internal int SumLength(string other)
{
return word.Length + other.Length;
}
// static メソッドを実装可能
internal static string MaxLength(string left, string right)
{
return left.Length > right.Length ? left : right;
}
// プロパティを実装可能
internal bool HasASCII
{
get
{
foreach (var n in word)
if (!char.IsAscii(n)) return false;
return true;
}
}
// 演算子の多重定義は不可
// public static string operator -(string left, string right)
// {
// throw new NotImplementedException();
// }
}
[Fact]
static void SumLengthTest()
{
Assert.Equal(11, "Hello".SumLength("World!"));
}
[Fact]
static void MaxLengthTest()
{
Assert.Equal("World!", string.MaxLength("Hello", "World!"));
}
[Fact]
static void HasASCII()
{
Assert.True("Hello".HasASCII);
Assert.False("こんにちは".HasASCII);
}
[Fact]
static void NullReference()
{
string str = null!;
Assert.Throws<NullReferenceException>(() => str.SumLength("World!"));
Assert.Equal([], str.AsSpan());
int? nullNumber = null;
Assert.Throws<InvalidOperationException>(() => nullNumber!.Value.GetHashCode());
}
internal static void Dummy(this string _) { }
static void LikeInstanceMethod()
{
var str = "Hello";
Action act = str.Dummy;
}
static void ExtensionMethodArguments(this string str)
{
ArgumentNullException.ThrowIfNull(str);
if (str is null) throw new NullReferenceException(nameof(str));
if (str is null) throw new InvalidOperationException(nameof(str));
}
}
extension
キーワード
↑ の例でいう __ExtensionMethodTest
型の中に extension
キーワードがあります。個人的な感想ですが C# でこういう書き方をするのはかなり珍しいと思います。型の中でこういう書き方をするのは初登場じゃないでしょうか。
インスタンスメソッド、static
メソッド、プロパティと通常のクラスの中での書き方に近いものになりました。this
キーワードは使いませんが、これはあくまで拡張メソッド実装なのでそこは区別したかったのでしょう。
言われてみれば確かにこちらの書き方のほうがしっくりきます。というのも、this
キーワードによる第1引数の指定は割と奇妙なところで、static
メソッドなのにインスタンスメソッドのように振る舞います。
static void Dummy(this string _) { }
var str = "Hello";
Action act = str.Dummy;
static
メソッドですが、インスタンスメソッドとしてデリゲートに割り当てられます。これを使ったパフォーマンス改善のテクニックも存在します。
カリー化デリゲート https://ufcpp.net/study/csharp/functional/miscdelegateinternal/#curried-delegate
null
で例外にするか問題
個人的に以前から引っかかっていたこととして、this
としての引数が null
だった場合の処理をどうするかがありました。
nullable
がある現在ではコンパイラが警告してくれるため、そもそも null
チェックは省きます。
-
null
オブジェクトのインスタンスにアクセス:NullReferenceException
(ランタイムが例外を出す) - 値型の
Nullable<T>.Value
プロパティにアクセス:InvalidOperationException
- 拡張メソッド: どのように振る舞うべき?
値型の Nullable<T>.Value
プロパティで NullReferenceException
じゃないんかい! という感じですが、まあ実際のところはなんか例外が出ればよしとしましょう。
拡張メソッドの第1引数でどんな例外にするかは微妙ですが、強い例外保証(操作は失敗することがあるが、失敗した操作は副作用を起こさないことが保証され、すべてのデータは元の値を保持する)をするならなにか投げるといいでしょう。
static void ExtensionMethodArguments(this string str)
{
ArgumentNullException.ThrowIfNull(str); // パフォーマンス的にはこれが最善
if (str is null) throw new NullReferenceException(nameof(str));
if (str is null) throw new InvalidOperationException(nameof(str));
}
一方で例外にしないのもあります。
string str = null!;
Assert.Equal([], str.AsSpan());
拡張メソッドはインスタンスメソッドに見えるため、インスタンスメソッドと同じ挙動を期待しがちです。しかしながら ↑ の例では null
の場合空の配列が返ってくれば十分で、副作用もありません。利便性もそちらのほうがよさそうです。
この辺の判断は分かれそうで、インスタンスメソッドと拡張メソッドの一貫性を損なうかもしれません。
個人的に拡張メソッドは可視性を internal
にすることが多いです。アセンブリの外に漏れなければリファクタも簡単ですし、ある程度適当に設計する余地が生まれます。
拡張メソッドが目指す完成形
https://ufcpp.net/blog/2024/3/extensions/ によれば、C# の今後の方針としてメソッドに限らず型そのものを拡張したいとのこと。
// 拡張の構文例。
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;
}
今回(.net10 preview3)は比較的実装が楽なものから追加した印象です。
.net9 で ref
構造体がインターフェイスを継承できるようになったのも出どころはこれのようです。
おわりに
今回の .net10 preview3 の機能追加は注目度が高いようで、各地で記事を見かけました。大変よろしいです。
C# の目指す拡張メソッドの完成形は、あると便利だが急ぎではない機能といった印象で、完成まで時間がかかりそうです。一方で機能を細かく分割して少しずつ実装していく、実現に向けて前進しているところはなんか感心しました。
自分も見習って、塩漬けのプロジェクトを少しずつ前に進めようと思います。
関連
【C#】.NET10 Preview1 キタ━━(゚∀゚)━━!!
【C# .NET10 Preview1】値型の配列をスタックに作成する最適化の検証
【C# .NET10 Preview2】参照型がスタックに置かれる最適化
【C# .NET10 Preview3】null 条件付き代入
【C# .NET10 Preview3】参照型の小さな配列のスタック割り当て