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インターフェイスの実装方法
非同期ストリーム(await
とyield
の混在と、非同期版 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>
では、WaitForNextAsync
とTryGetNext
の2つのメソッドがありますが、WaitForNextAsync
の方は呼び出しがかなり少なくなる想定なので、実質的にはこちらでも「TryGetNext
の1個だけになるので速い」ということが言えます。
が、以下のようなデメリットもあります。
-
(
foreach
とかのコンパイラー生成に頼らず手動で)WaitForNextAsync
とTryGetNext
を使ったコードを書くのはかなり大変になる -
同期版でもどの道同じ問題があるんだから、もしやるなら、同期版の方も含めて
foreach
の拡張を後から考えるべき- 同期版が
MoveNext
/Current
なのに非同期版だけTryGetNext
にするのは差が大きすぎる
- 同期版が
結局、デメリットがきつすぎるということで、同期版と同じMoveNext
/Current
型のインターフェイスにこだわりたいということになったようです。
ValueTask 実装
このIAsyncEnumerable<T>
に関する検討を始めた当初は、Task
に掛かるコストが特に懸念されていました。
が、ValueTask
を使った最適化が進んだ結果、案外、ValueTask<bool> MoveNextAsync()
にすれば低コストになりそうというのもわかってきた結果、上記の決断に至ったというのもありそうです。
そちらの検討も、corefx の方に issue が立っています。
- Proposal: Public APIs for C# 8 async streams #32640
- Proposal: Implement IAsyncDisposable on various BCL types #32665
- Proposal: ManualResetValueTaskSource{Logic} types #32664
前にちょっと書きましたが、.NET Core 2.1 世代で、ValueTask
がTask
だけじゃなくて、IValueTaskSource
インターフェイスを受け付けるようになりました。
このインターフェイスを実装した独自のクラスを作ることで、非同期処理に掛かるコストを下げれる場合があります
(作ったインスタンスをキャッシュ・再利用したり)。
非同期ストリーム(await
とyield
の混在)の実装はまさにその場合に該当していて、
IValueTaskSource
を使ったコード生成をしようという流れになっています。
あと、IValueTaskSource
自体は.NET Core 2.1世代(NuGetパッケージを参照すればそれ以外でも利用可能)で追加されましたが、このインターフェイスをちゃんと実装するのはそれなりに面倒です
(いくつか、これを実装したクラスは現在もあるんですが、全部internalだったりします)。
そこで、汎用に使えるManualResetValueTaskSource
という実装クラスも、これを機にpublicにしたいという話もついでに出ています。