読者です 読者をやめる 読者になる 読者になる

C# 3.0 と List 生成演算子

.NET

このあたりの話,今風の言語を使っている人から見れば,「何をいまさら」なんでしょうけど……

以下では,長さに制限のない単方向リストのことをリストと呼んでいます.ここで言う「リスト」は,.NET の List<T> クラスや IList<T> インターフェイスそのものではなく,IEnumerable<T> に対応します.

リストを作る

C# の匿名メソッドは,便利ではあるのですが,いくつか機能に制限もあって,それが時折気になることがあります.
私がよくでくわすのが,匿名メソッド中で yield が使えないという制限です.yield を使って作りたいあるリスト (イテレータ) があって,でもそのリストはひとつのメソッド内でしか使わないとします.このとき,わざわざリストを作るために新しいメソッドを外に書かなければならないのはちょっと不便です.そういうときは,標準で存在するリスト処理を組み合わせて,望むリストを作れないか考えてみます.

生成演算子

Standard Query Operators には,3 つの 生成演算子 があります.

  • Range<T>
  • Repeat<T>
  • Empty<T>

基本的な戦略は,生成演算子で種となるリストを生成し,それを変換しながら複雑なリストを生成するというものです *1
いくつか基本的なパターンがあるので見てみましょう.

(有限) 数列の生成

Enumerable.Range<T> は,活用場面の多い生成演算子で,1 ずつ増加する有限の単調数列を生成することができます.

var seq1 = Enumerable.Range(1, 10); // 1, 2, 3, ... , 10
var seq1x = Enumerable.Range(100, 10); // 100, 101, 102, ... , 109

Enumerable.Range はオーバーロードされておらず,そのままでは減少列を作れません*2.しかし,先ほどの seq1 を元に,たとえば以下のように減少列を生成することができます.

var seq2 = from x in seq1 select 3 - 2 * x; // 1, -1, -3, -5, ... , -17

これで一般項が分かっている数列は大丈夫です.
以下のような数列による配列の初期化は,Fortran では当たり前のように使われていましたが,C# でもやっと一行で書けるようになりました.

var array = Enumerable.Range(1, 10).Select(x => x*x).ToArray();

整数リストを元に,別のリストを生成することができます.

var seq3 = from t in seq1
           let theta = t * 0.1
           select new { X = Math.Cos(theta), Y = Math.Sin(theta) };

例外条件を加えると,元のリストをさらに複雑なリストに加工することもできます.

var seq4 = from year in Enumerable.Range(1000, 10000)
           where year % 400 == 0
             || (year % 100 != 0 && year % 4 == 0)
           select year;

組み合わせの列挙

直積操作でリスト同士の組み合わせを列挙することができます.ネストした foreach で yield 文を使っても同じリストを作れますが,こちらの方がよりシンプルです.

var seq5 = from x in seq1
           from y in seq1
           select new { x, y };

依存関係をもつ組み合わせも可能です.特に(全手)探索の問題で有効なので,憶えておきましょう.

var seq6 = from x in seq1
           from y in Enumerable.Range(1,x)
           select new { x, y };

単一オブジェクトからリストを作る

Standard Query Operators にはリストやリスト同士の操作がたくさん用意されています.しかし,リストから取り出された状態にある単一オブジェクトは,それ自体はリストではないため,Standard Query Operators で直接扱えません.こんなときは,オブジェクトを,そのオブジェクトを含んだリストに昇格させます.
これは Repeat を 1 回だけ用いることで可能です.

var s = "Hello";
var seq7 = Enumerable.Repeat(s,1);

ちょっとトリッキーなので,専用のメソッドを定義しても良いかもしれません.

空リスト

何もする必要がないときや,計算を打ち切りたいときは,null を返す代わりに空のリストを返しましょう.これは Enumerable.Empty<T>() から得ることができます.

無限リスト

さて,Standard Query Operators の生成演算子は,いずれも有限長のリストしか生成できません.海外の C# blog を読んでいても,無限に 1 を返すような単純な生成演算子を自分で作り,それを元に変換している例をよく目にするので,やはりこれは自作するしかなさそうです.
無限リストを作るもうひとつの方法として,前回ちらっとでてきた unfold を使うという手もあります.unfold は,初期値と関数を与え,n 項目までから n + 1 項目を作り出します.ここで関数には停止条件を含めることができますが,永遠に停止しない関数を渡すことで無限リストの生成にも使えるというわけです.unfold は再帰的なリストを作るのに便利なので,標準であってくれてもよさそうですが,こちらもどうも自作するしかなさそう,というのが前回のお話でした.

まとめ

「匿名メソッド中で yield を使いたいなぁ」と思ったときは,うまいこと今ある部品を組み合わせて作れないか考えてみましょう.たいていは何とかなります.ただし空気も読みましょう.
それが再利用すべき歯車であれば,外部にライブラリとして残すことも考えましょう.例えばファイルを開いて中身を一行ずつ文字列リストにして返すような処理は,外部に置いておいた方がよさそうです. Extension Method が使えないか考えてみるのも良いでしょう.

*1:もちろん,Base Class Library (BCL) が返す様々なリストを種に使うことも可能です

*2:個人的には簡単な等差数列はサポートして欲しいと思っています