Design Notes 2件追加

10/1 のは、nullable 参照型がジェネリクスに絡むときの話。 例えば以下のような、型推論とかについての検討。

using System.Collections.Generic;

class Program
{
    static void Main()
    {
        IEnumerable<string> nonNull = new[] { "" };
        IEnumerable<string?> nullable = new[] { default(string) };

        // 配列の要素の型推論。
        // これは、IEnumerable<string?>[] になってるみたい。
        var array = new[] { nonNull, nullable };

        // ジェネリック メソッドの型引数の型推論。
        // 配列の要素と似たような推論になるはず。
        // が、201/9/11 版の実装では IEnumerable<string> で推論されてる。
        // この挙動が変だよね、と言うのが議題。
        var ret = M(nonNull, nullable);
    }

    static T M<T>(T x, T y) => x;
}

10/3 の方は、null チェックのコンテキスト切り替えの再検討と、 IAsyncEnumerableインターフェイスの実装方法の決定について。

null チェックのコンテキスト切り替え

改めての説明になりますが、元々 null の存在を前提にしている C# にとって、 nullable 参照型(T だと非 null、T? で null 許容)の追加は、何も考えずにやると破壊的変更になってしまいます。 破壊的変更を極力避けている C# にとってそれはまずいので、null チェックの On/Off を切り替える仕組みを用意する予定です。

これまでのプレビュー版では、とりあえず属性ベースでコンテキスト切り替えが実装されていました。 NonNullTypes属性を付けたら On、付けていないかもしくはNonNullTypes(Warning = false)で Off。 が、そのやり方だと苦しそうということで、プリプロセッサ-でやる(#nonnullディレクティブみたいなものを追加する)ことを改めて検討しているそうです。

(これまでの C# だと、属性の有無によってコンパイラーの挙動がガラッと変わるというような実装をしたことがなく、 そういう「モラル的な意味」でもあまり良くないのは元々良くないんですが、技術的にも苦しそうなことがわかってきたとか。)

ということで、やるとしたら以下の3つのうちのどれかになるだろうということで、これらをそれぞれ検討。

  • 修飾子を作る
    • 非同期メソッドの async 修飾子みたいなの
  • 属性でやるなら、かなり特別扱いした「疑似属性」的なものになる
    • const を受け付けない(true, false の直指定しか受け付けない)とか、名前付き引数を認めないとか
  • ディレクティブを使う
    • #pragma warningと同じノリで、#nonnull disable#nonnull restoreで制御
    • プロジェクト全体の On/Off 制御のために、コンパイラー オプションも必要

で、とりあえず、ディレクティブを使ったアプローチで行ってみようという感じになっているみたいです。 (実際もう、pull request も出てて merge 済み。)

IAsyncEnumerableインターフェイスの実装方法

非同期ストリーム(awaityieldの混在と、非同期版 foreach)を実装するにあたって、インターフェイスをどうするかというのがずっと課題になっていました。 同期版だと、IEnumerable<T>(と、同じ名前のメソッドを持ってさえいればOK)を使うわけですが、 それの非同期版であるIAsyncEnumarable<T>はどういうメソッドを持つべきか。

結局、以下のように、IEnumerable<T>とほぼ同じで単にAsync語尾を付け、ValueTaskを返す作りにしたいとのこと。

public interface IAsyncEnumerable<out T>
{
    IAsyncEnumerator<T> GetAsyncEnumerator();
}

public interface IAsyncEnumerator<out T> : IAsyncDisposable
{
    ValueTask<bool> MoveNextAsync();
    T Current { get; }
}

public interface IAsyncDisposable
{
    ValueTask DisposeAsync();
}

他の選択肢としては、以下のようなものが検討されていました。

public interface IAsyncEnumerator<out T> : IAsyncDisposable
{
    ValueTask<bool> WaitForNextAsync();
    T TryGetNext(out bool success);
}

というのも、パフォーマンス的には後者の方がだいぶ良いことがわかっています。 ただ、これは今回新たに追加する非同期版だけの話ではなくて、 既存のIEnumerable<T>についても同じ課題を抱えています。

IEnumerable<T>の方で軽く試してみた感じ、かかるオーバーヘッドがほんとに倍くらい変わり得ます。 というのも、仮想メソッド呼び出しのコストはそこそこあるので、MoveNext/Currentの2回の呼び出しに分かれているより、TryGetNextの1回で済む方が明らかに速くなります。

上記のIAsyncEnumerator<T>では、WaitForNextAsyncTryGetNextの2つのメソッドがありますが、WaitForNextAsyncの方は呼び出しがかなり少なくなる想定なので、実質的にはこちらでも「TryGetNextの1個だけになるので速い」ということが言えます。

が、以下のようなデメリットもあります。

  • (foreachとかのコンパイラー生成に頼らず手動で)WaitForNextAsyncTryGetNextを使ったコードを書くのはかなり大変になる
    • .NET の仕様上、bool TryGetNext(out T) だと共変にできなくて、T TryGetNext(out bool)なのがキモい
    • 特に、Zip みたいに複数のenumerableが絡むとかなり大変
  • 同期版でもどの道同じ問題があるんだから、もしやるなら、同期版の方も含めて foreach の拡張を後から考えるべき
    • 同期版が MoveNext/Current なのに非同期版だけ TryGetNext にするのは差が大きすぎる

結局、デメリットがきつすぎるということで、同期版と同じMoveNext/Current型のインターフェイスにこだわりたいということになったようです。

ValueTask 実装

このIAsyncEnumerable<T>に関する検討を始めた当初は、Taskに掛かるコストが特に懸念されていました。 が、ValueTaskを使った最適化が進んだ結果、案外、ValueTask<bool> MoveNextAsync()にすれば低コストになりそうというのもわかってきた結果、上記の決断に至ったというのもありそうです。

そちらの検討も、corefx の方に issue が立っています。

前にちょっと書きましたが、.NET Core 2.1 世代で、ValueTaskTaskだけじゃなくて、IValueTaskSourceインターフェイスを受け付けるようになりました。 このインターフェイスを実装した独自のクラスを作ることで、非同期処理に掛かるコストを下げれる場合があります (作ったインスタンスをキャッシュ・再利用したり)。 非同期ストリーム(awaityieldの混在)の実装はまさにその場合に該当していて、 IValueTaskSourceを使ったコード生成をしようという流れになっています。

あと、IValueTaskSource自体は.NET Core 2.1世代(NuGetパッケージを参照すればそれ以外でも利用可能)で追加されましたが、このインターフェイスをちゃんと実装するのはそれなりに面倒です (いくつか、これを実装したクラスは現在もあるんですが、全部internalだったりします)。 そこで、汎用に使えるManualResetValueTaskSourceという実装クラスも、これを機にpublicにしたいという話もついでに出ています。