【C# .NET10 Preview3】拡張メソッドの機能追加

# はじめに

[.NET10 Preview3](https://dotnet.microsoft.com/ja-jp/download/dotnet/10.0) で拡張メソッドが機能追加されました。

```cs
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();
        // }
    }
```

## テストコード

<details>
<summary>テストコード</summary>

```cs
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));
    }
}
```

</details>

## `extension` キーワード

↑ の例でいう `__ExtensionMethodTest` 型の中に `extension` キーワードがあります。個人的な感想ですが C# でこういう書き方をするのはかなり珍しいと思います。型の中でこういう書き方をするのは初登場じゃないでしょうか。  
インスタンスメソッド、`static` メソッド、プロパティと通常のクラスの中での書き方に近いものになりました。`this` キーワードは使いませんが、これはあくまで拡張メソッド実装なのでそこは区別したかったのでしょう。

言われてみれば確かにこちらの書き方のほうがしっくりきます。というのも、`this` キーワードによる第１引数の指定は割と奇妙なところで、`static` メソッドなのにインスタンスメソッドのように振る舞います。

```cs
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` じゃないんかい！ という感じですが、まあ実際のところはなんか例外が出ればよしとしましょう。

拡張メソッドの第１引数でどんな例外にするかは微妙ですが、強い例外保証（操作は失敗することがあるが、失敗した操作は副作用を起こさないことが保証され、すべてのデータは元の値を保持する）をするならなにか投げるといいでしょう。

```cs
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));
}
```

一方で例外にしないのもあります。

```cs
string str = null!;
Assert.Equal([], str.AsSpan());
```

拡張メソッドはインスタンスメソッドに見えるため、インスタンスメソッドと同じ挙動を期待しがちです。しかしながら ↑ の例では `null` の場合空の配列が返ってくれば十分で、副作用もありません。利便性もそちらのほうがよさそうです。

この辺の判断は分かれそうで、インスタンスメソッドと拡張メソッドの一貫性を損なうかもしれません。  
個人的に拡張メソッドは**可視性を `internal` にする**ことが多いです。アセンブリの外に漏れなければリファクタも簡単ですし、ある程度適当に設計する余地が生まれます。

## 拡張メソッドが目指す完成形

<https://ufcpp.net/blog/2024/3/extensions/> によれば、C# の今後の方針としてメソッドに限らず型そのものを拡張したいとのこと。

```cs
// 拡張の構文例。
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# の目指す拡張メソッドの完成形は、あると便利だが急ぎではない機能といった印象で、完成まで時間がかかりそうです。一方で機能を細かく分割して少しずつ実装していく、実現に向けて前進しているところはなんか感心しました。  
自分も見習って、塩漬けのプロジェクトを少しずつ前に進めようと思います。

[.NET10 Preview3](https://dotnet.microsoft.com/ja-jp/download/dotnet/10.0)

関連  
[【C#】.NET10 Preview1 ｷﾀ━━(ﾟ∀ﾟ)━━!!](https://qiita.com/abetakahiro123/items/2153df9795cd0f12da47)  
[【C# .NET10 Preview1】値型の配列をスタックに作成する最適化の検証](https://qiita.com/abetakahiro123/items/9113ee6347580c5d0e39)  
[【C# .NET10 Preview2】参照型がスタックに置かれる最適化](https://qiita.com/abetakahiro123/items/cedd77ba25de1b8db8d1)  
[【C# .NET10 Preview3】null 条件付き代入](https://qiita.com/abetakahiro123/items/db05ae0cab986183cfab)  
[【C# .NET10 Preview3】参照型の小さな配列のスタック割り当て](https://qiita.com/abetakahiro123/items/339412e890dfe6274941)