LoginSignup
7
5

【C#】yield returnで、遅延評価されるコレクションを作る

Last updated at Posted at 2019-04-15

やりたいこと

yield returnとかいうわけのわからないreturnが出てきた。
ちょっと調べると、LINQとも、かかわりが深いらしい。どういうものなのか、知りたい。

yield returnはどういうものか?

yield returnを使うと、「コレクション」を簡単に作れるのがよいところらしい。ざっとまとめると、

歴史

  • コレクションは、「foreach (string str in StringList) {}」みたいな書き方ができて便利だが、これ(コレクション)を作るのは結構大変だった(らしい)。
  • それに対して、C# 2.0から、簡単にコレクションを作れる「イテレーター構文」というのが追加された。それが、yield returnを使う構文。

使い方や使う際の制約など

  • yield returnやyield breakを使うと、foreach文で使えるコレクションを返すメソッドやプロパティを簡単に実装することができる。
  • yield returnを含むブロックを「イテレーター ブロック」という。
  • イテレータブロックは、戻り値の型が以下のうちのいずれかにする必要がある。
    • System.Collections.IEnumerator
    • System.Collections.Generic.IEnumerator
    • System.Collections.IEnumerable
    • System.Collections.Generic.IEnumerable
  • return の変わりに yield return というキーワードを使う。
  • break の変わりに yield break というキーワードを使う。

以上、こちら参照

その他、特筆点

  • yield returnを使ったイテレータブロックで作成されるコレクションは、「遅延評価」される。
  • 「遅延評価」とは、必要になった要素から、必要になった分だけ計算するということ。式が書かれたところではなく、イテレータブロックで作成するコレクションを実際に使うところで、処理が走る。
  • 具体的には、下記を参照。

yield returnでコレクションを作る

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

namespace ConsoleApp3
{
    class Program
    {
        static void Main(string[] args)
        {
            // 1回目
            foreach (string name in GetFriendsNames())
            {
                Console.WriteLine("名前は");
                Console.WriteLine(name);
            }

            Console.WriteLine("---");

            // 2回目
            IEnumerable<string> names = GetFriendsNames();
            foreach (string name in names)
            {
                Console.WriteLine("もう一度、名前は");
                Console.WriteLine(name);
            }

            Console.WriteLine("---");

            // 3回目
            foreach (string name in names)
            {
                Console.WriteLine("三度、名前は");
                Console.WriteLine(name);
            }

            Console.ReadKey();
        }

        /// <summary>
        /// イテレーターブロック
        /// </summary>
        private static IEnumerable<string> GetFriendsNames()
        {
            Console.WriteLine("john");
            yield return "ジョン";
            Console.WriteLine("paul");
            yield return "ポール";
            Console.WriteLine("george");
            yield return "ジョージ";
            Console.WriteLine("ringo");
            yield return "リンゴ";
        }

    }
}

実行結果
image.png

イテレータブロックの基本動作

下記のイテレータブロックの結果として、"ジョン""ポール""ジョージ""リンゴ"の4つの文字列を含むコレクションができあがる。

private static IEnumerable<string> GetFriendsNames()
{
    Console.WriteLine("john");
    yield return "ジョン";
    Console.WriteLine("paul");
    yield return "ポール";
    Console.WriteLine("george");
    yield return "ジョージ";
    Console.WriteLine("ringo");
    yield return "リンゴ";
}

コレクションのできるタイミング

コレクションを作成するGetFriendsNames()が実行されるのは、
「IEnumerable names = GetFriendsNames();」の行ではなく
コレクションを使う「foreach (string name in names)」の行。

// 2回目
IEnumerable<string> names = GetFriendsNames();
foreach (string name in names)
{
    Console.WriteLine("もう一度、名前は");
    Console.WriteLine(name);
}

しかも、
GetFriendsNames()のメソッドをまず上から下まで実行してコレクションを作ってから、foreachでそれを使う、ではなく、

  • GetFriendsNames()のメソッドで、コレクションの要素1個目の"ジョン"を作成(yield return)する
  • foreachの1週目を実施
  • 次にコレクションの要素2個目の"ポール"を作成
  • foreachの2週目を実施
  • ・・・・

という感じで、**「必要になった要素から、必要になった分だけ計算する」**ということをする。

また、結果から分かるように、**「必要な時に、毎回同じ計算を行う」**ことになっている。
上の例だと、GetFriendsNames()で作るコレクションを使う箇所が来るたびに、毎回GetFriendsNames()を実行することになる。

同じことを、Listをnewして行う

同じようなこと(コレクションを作成して、foreachで回すこと)を、Listをnewして行うと、下記のようになる。

// Listを生成
var nameList = new List<string>();
nameList.Add("ミック・ジャガー");
nameList.Add("キース・リチャーズ");
foreach (string name in nameList)
{
    Console.WriteLine("ストーンズ");
    Console.WriteLine(name);
}

このときは、Listの実態を「var nameList = new List();
の行で作成しているため、ヒープにこのList用のメモリを確保している。
そうすると、メモリを食う代わりに、例えば同じforeachを2回書いたとしても、同じ計算は2回行われない。

// Listを生成
var nameList = new List<string>();
nameList.Add("ミック・ジャガー");
nameList.Add("キース・リチャーズ");
foreach (string name in nameList)
{
    Console.WriteLine(name);
}
foreach (string name in nameList)
{
    // ↑すでにメモリ上にあるnameListを使うので、再度リスト作成することはない
    Console.WriteLine(name);
}

遅延評価とそうでないものの違い

イテレータブロック(yield returnを使い、遅延評価されるもの)と通常のコレクション作成(遅延評価されないもの)の違いは、まとめると下記。

種類 処理(計算)実施タイミング 処理回数 メモリ使用 イメージ
イテレータブロックによるコレクション(遅延評価) 作成するコレクションを使用するとき 使用時毎回 確保しない メモリは食わないが、作ったコレクションを何回も使うときはCPUめっちゃ使う
通常のリスト(遅延評価なし) コレクションを作成するとき 作成時一回 ヒープ上に確保 最初一回リスト作ればそれで作成終わるが、リストが大きいとめっちゃメモリ食う

LINQと、遅延評価したくないときの.ToArray()

LINQは、このyield return(遅延評価)で作られている。
なので、上のストーンズのコレクションに対して、

// まったく意味ないが
var mick = nameList
                .Where(x => x == "ミック・ジャガー");
Console.WriteLine(mick.First());

などとしても、「var mick = nameList.Where・・」の行の時点では、メモリ上にリストはない。

これを、もし「var mick = nameList.Where・・」の時点でListとしてメモリに保持しておきたい、となった場合は、下記のようにする。

var mick = nameList
                .Where(x => x == "ミック・ジャガー")
                .ToArray();

こうすると、この行の時点でWhereの処理が実際に実行され、メモリにリストが保持される。

その他

上の例では、yield returnを、メソッドの中で上から何個も並べているが、普通はforなどのループの中で使われると思われる。

遅延評価されるコレクションの使いどころ

上記のように、yield return と書かれていたらどういうことが行われるのか、は何となくわかった。
が、これをどういうときに使うとうれしいのか、がまだよくわかってなかった。

画面のViewModelのコンストラクタで大きなコレクションを取ると、画面表示時に重くて固まったように見えるときに、遅延評価させるようにして、画面固まらないようにするのか?でもそれが達成できても、何度も同じ読み込みをされると毎回ロードに時間がかかるし、、、とかを考えて、答えが出てなかったのだが、

そういうときに、下記記事を見た。

上記記事では、「無限リスト」、つまり何件あるかわからないリスト作成をするときに遅延評価が使えるとあった。

で、例として、グーグルの検索結果の「全部で何件あるかは不明だが、現在1ページ目」の表示を挙げていた。なるほど...

確かに、無限かもしれないリストの取得処理をyield returnの遅延評価で書いておいて、それの何件目を返す、というメソッドを作っておけば、そういことが実現できそう。

やっと、なるほどと思える使い道を知ることができた。ありがとうございます。

参考

イテレーター
https://ufcpp.net/study/csharp/sp2_iterator.html

LINQ と遅延評価
https://ufcpp.net/study/csharp/sp3_lazylist.html

yieldのメリットを解説する
遅延評価の使い道を見つけたサイト。
https://qiita.com/Tadahiro_Yamamura/items/26a925d9a670f01bf497#%E7%84%A1%E9%99%90%E3%83%AA%E3%82%B9%E3%83%88%E3%81%AE%E4%BD%9C%E6%88%90

7
5
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5