非同期処理が出てくる中で、yield
が出てきて、はて、C#でどうだっけ?と思ったので書いてみます。yield
は JavaScriptとかで出てきました。ちなみに、アメリカに住んでいると、信号機のところにYeildって書いてあって、その意味は状況を見て曲がっていいよです。
C# の yield
C#のYield は、IEnumerable<T>
や、IEnumerator<T>
が戻り値になるようなメソッドで使用可能です。簡単な例を示すとこんな感じです。
こういうメソッドを書いておいて、
static IEnumerable<int> Generate()
{
for (int i = 0; i < 10; i++)
{
yield return i;
Thread.Sleep(TimeSpan.FromSeconds(1));
}
}
こんな風に使います
static void Display()
{
foreach (int i in Generate())
{
Console.WriteLine($"Number: {i}");
}
}
すると、1秒ごとに
Number: 0
Number: 1
:
といった感じで表示されていきます。IEnumerable<T>
は、シンプルなイテレーターを表すインターフェイスで、実装クラスはGetEnumerator()
を実装する必要があります。IEnumerator は、Dispose(), MoveNext(), Reset() の三つのメソッドをもっていて、yield のところで待ち受けをしているのですが、MoveNext() が呼ばれたタイミングで、yieldのところで値が返ります。
この仕組みはLinqでも使われているものです。
これと似たメソッドは下記のように書けますが、1秒ごとに待たせるというのは難しくて、メソッドが呼ばれた時点で、表示がかかる前に、このメソッドが10秒時間がかかってから、一気にすべての行が表示されます。
static IEnumerable<int> GenerateWithCollection()
{
var numbers = new List<int>();
for (int i = 0; i < 10; i++)
{
numbers.Add(i);
Thread.Sleep(TimeSpan.FromSeconds(1));
}
return numbers;
}
Map を実装する
これを使って何か実装してみましょう。昔私はScalaを書いていて、MapやFlatMapが懐かしいなと思っていました。C#だと、Linqで書けるのですが、あえてMapを自分で実装してみましょう。うお、めっちゃ簡単や!
public static class Extensions
{
public static IEnumerable<U> Map<T, U>(this IEnumerable<T> self, Func<T, U> func)
{
foreach (var item in self)
{
var converted = func(item);
yield return converted;
}
}
}
じゃあ使ってみよう。ちなみに、これを実行すると何も起こりませんw。なぜなら、yieldで書いたものは遅延評価になるからです。Linqが遅延評価なのも同じインターフェイスを使っているからです。
private static void TestOriginalMap()
{
var names = new List<string>
{
"ushio",
"yamada",
"kim"
};
var namesWithRespect = names.Map(item =>
{
var withRespect = $"{item} sama";
Console.WriteLine(withRespect);
return withRespect;
});
}
じゃあ、しっかり要素にアクセスしてみましょう。
private static void TestOriginalMap()
{
var names = new List<string>
{
"ushio",
"yamada",
"kim"
};
var namesWithRespect = names.Map(item =>
{
var withRespect = $"{item} sama";
Console.WriteLine(withRespect);
return withRespect;
});
foreach (var name in namesWithRespect)
{
Console.WriteLine($"{name} sama de gozaimasu ne.");
}
}
実行結果
ushio sama
ushio sama sama de gozaimasu ne.
yamada sama
yamada sama sama de gozaimasu ne.
kim sama
kim sama sama de gozaimasu ne.
非同期型のIEnumerable
非同期型のものもあります。IAsyncEnumerable<T>
を返す型を定義すると、asyncメソッドの中でyieldを使うことができます。その際、イテレータは、foreach
にawaitキーワードをつけてループを回すことができます。
static async IAsyncEnumerable<int> GenerateAsync()
{
for (int i = 0; i < 10; i++)
{
yield return i;
await Task.Delay(TimeSpan.FromSeconds(1));
}
}
static async Task DisplayAsync()
{
await foreach (var i in GenerateAsync())
{
Console.WriteLine($"NumberAsync: {i}");
}
}