はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】IEnumerable<T>とyield returnのコンビネーションアタック

はじめに

今回はIEnumerableyield returnについての記事になります!

ネットサーフィンをしていたところ、以下のような記事を見かけました。

qiita.com

これを見た時にあまりのすごさにビックリしてしまいました。

この中で重要になってくるIEnumerable<T>yield returnについて取り上げてみたいと思います。

では早速見ていきましょう。

yield文

まずはyieldとは何かを見ていきます。

ステートメントで yield コンテキスト キーワードを使用した場合、メソッド、演算子、または get アクセサーが反復子であることを示します。

コンテキスト キーワード yield - C# リファレンス | Microsoft Docs


反復子の説明はこんな感じ。

反復子を使用して、リストや配列などのコレクションをステップ実行することができます。

C# でのコレクションの反復処理 | Microsoft Docs


なんとなくニュアンスが伝わったかもしれませんが、もっと簡単な言葉で書くとメソッドの処理を一時的に中断し、処理を呼び出し元に返すことができるということです。

以下のコードを見てみてください。

using System;
using System.Collections;

class Program
{
    static void Main()
    {
        foreach(var n in GetSomeNumbers())
        {
            Console.WriteLine(n);
        }
    }

    public static IEnumerable GetSomeNumbers()
    {
        Console.WriteLine("yield return 1");
        yield return 1;
        Console.WriteLine("yield return 2");
        yield return 2;
        Console.WriteLine("yield return 3");
        yield return 3;
    }
}


これを実行してみるとこんな出力になります。

yield return 1
1
yield return 2
2
yield return 3
3


ここから処理を一時中断,次回の呼び出し時に中断した箇所の次の行から再開されていることが分かります。

また処理を止めたいときはyield breakを使えばOKです。

public static IEnumerable GetSomeNumbers()
{
    Console.WriteLine("yield return 1");
    yield return 1;
    Console.WriteLine("yield break");
    yield break;
    Console.WriteLine("yield return 3");
    yield return 3;
}
yield return 1
1
yield break

遅延評価

下のコードを見てみてください。

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> numbers = GetSomeNumbers().ToList();

        foreach (var n in numbers) Console.WriteLine(n);
    }

    public static IEnumerable<int> GetSomeNumbers()
    {
        Console.WriteLine("yield return 1");
        yield return 1;
        Console.WriteLine("yield return 2");
        yield return 2;
        Console.WriteLine("yield return 3");
        yield return 3;
    }
}
yield return 1
yield return 2
yield return 3
1
2
3

これはリストをforeachして要素を取り出していますが、IEnumerableIEnumerable<T>を用いたときと異なる挙動をしています。

ListではToList()の段階で処理を行い、foreachの部分ではほとんどなにもしていません。

しかし2つ前のコードのようにforeachIEnumerable<T>型の変数を使った場合では、必要になった要素をそのたびに(IEnumeratorMoveNextが呼ばれるたびに)計算を行っています

これは遅延評価と呼ばれるもので、上手に使うことでメモリの節約・処理の分散をすることができることもあります。

ただし間違った使い方をすると余分に計算が増えたりと注意してください。

○ダメな例

using System;
using System.Collections;

class Program
{
    static void Main()
    {
        var numbers = GetSomeNumbers();

        // GetSomeNumbersが3回も実行されてしまう
        foreach (var n in numbers) Console.WriteLine(n);
        foreach (var n in numbers) Console.WriteLine(n);
        foreach (var n in numbers) Console.WriteLine(n);
    }

    public static IEnumerable GetSomeNumbers()
    {
        Console.WriteLine("yield return 1");
        yield return 1;
        Console.WriteLine("yield return 2");
        yield return 2;
        Console.WriteLine("yield return 3");
        yield return 3;
    }
}


○改善例

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        // この時点でGetSomeNumbersメソッドによる{1, 2, 3}が格納される
        List<int> numbers = GetSomeNumbers().ToList();

        // GetSomeNumbersメソッドは一切呼ばれない
        foreach (var n in numbers) Console.WriteLine(n);
        foreach (var n in numbers) Console.WriteLine(n);
        foreach (var n in numbers) Console.WriteLine(n);
    }

    public static IEnumerable<int> GetSomeNumbers()
    {
        Console.WriteLine("yield return 1");
        yield return 1;
        Console.WriteLine("yield return 2");
        yield return 2;
        Console.WriteLine("yield return 3");
        yield return 3;
    }
}

サンプル

これらを使った簡単なサンプルとして、平方数を求めるプログラムを書いてみましょう。

平方数とは自然数の自乗(二乗)で表される整数のことで、1, 4(=2*2), 9(=3*3),....で奴ですね。

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        foreach(var n in GetSquareNumber(10))
        {
            Console.WriteLine(n);
        }
    }

    /// <summary>
    /// 平方数を列挙する
    /// </summary>
    public static IEnumerable<int> GetSquareNumber(int n)
    {
        yield return 0;     // 一応0も平方数らしい
        for(int i = 1; i < n; i++)
        {
            yield return i * i;
        }
    }
}
0
1
4
9
16
25
36
49
64
81

さいごに

特に遅延評価は上手に使うとかなり強力なので、是非うまく活用してみてください。

ではまた。