390 likes | 514 Views
LINQ ソースで GO!. In 名古屋 MS 系秋祭り 2013/09/21 Kouji Matsui (@kekyo2). 自己紹介. けきょ。 会社やってます。 Micoci とまどべん よっかいち。 主に Windows 。 C#, C++/CLI, ATL, C++0x, x86/x64 アセンブラ , WDM, Azure, TFS, OpenCV , Geo, JNI, 鯖管理 , MCP 少々 , 自作 PC, 昔マイコン , 複式簿記経理 最近 は WPF と Prism に足をツッコミ中。. LINQ 知ってますか?.
E N D
LINQソースでGO! In 名古屋MS系秋祭り 2013/09/21 Kouji Matsui (@kekyo2)
自己紹介 • けきょ。 • 会社やってます。 • Micociとまどべんよっかいち。 • 主にWindows。C#, C++/CLI, ATL, C++0x, x86/x64アセンブラ, WDM, Azure, TFS, OpenCV, Geo, JNI, 鯖管理, MCP少々, 自作PC, 昔マイコン, 複式簿記経理 • 最近はWPFとPrismに足をツッコミ中。
LINQ知ってますか? • ビデオチャット製品ではありませんw • アイドルグループではありませんww • .NET Framework 3.5 (C# 3.0)にて導入された、「統合言語クエリ」拡張です。 (Language Integrated Query) 人員一覧の中から、年齢が30歳以上、かつ女性の人員を抽出し、名前・苗字の順でソートする var results = from person in persons where (person.Age >= 30) && (person.IsFemale == true) orderbyperson.FirstName, person.LastName select person; foreach(varresult in results){ Console.WriteLine(“{0} {1}”, result.FirstName, result.LastName); } クエリ結果はforeachで列挙可能
LINQはどんな場面で使える? • 配列にクエリを掛ける Personクラスの配列 varpersons = new[] { new Person { FirstName=“Kouji”, LastName=“Matsui”, Age=41, IsFemale=false }, new Person { FirstName=“Mogeko”, LastName=“Moge”, Age=35, IsFemale=true }, new Person { FirstName=“Uhyo”, LastName=“Hidebu”, Age=31, IsFemale=true }, }; var results = from person in persons where (person.Age >= 30) && (person.IsFemale == true) orderbyperson.FirstName, person.LastName select person; 配列から抽出する
LINQはどんな場面で使える? • リストにクエリを掛ける リストにPersonを格納 varpersons = new List<Person>(); persons.Add( new Person { FirstName=“Kouji”, LastName=“Matsui”, Age=41, IsFemale=false }); persons.Add( new Person { FirstName=“Mogeko”, LastName=“Moge”, Age=35, IsFemale=true }); persons.Add( new Person { FirstName=“Uhyo”, LastName=“Hidebu”, Age=31, IsFemale=true }); var results = from person in persons where (person.Age >= 30) && (person.IsFemale == true) orderbyperson.FirstName, person.LastName select person; 抽出クエリ文は、配列の時と全く同じ
LINQはどんな場面で使える? • ジェネリックではないリストは駄目 ArrayListにPersonを格納 varpersons = newArrayList(); persons.Add( new Person { FirstName=“Kouji”, LastName=“Matsui”, Age=41, IsFemale=false }); persons.Add( new Person { FirstName=“Mogeko”, LastName=“Moge”, Age=35, IsFemale=true }); persons.Add( new Person { FirstName=“Uhyo”, LastName=“Hidebu”, Age=31, IsFemale=true }); // 構文エラー var result = from person inpersons where (person.Age >= 30) && (person.IsFemale == true) orderbyperson.FirstName, person.LastName select person; error CS1934: ソース型 'System.Collections.ArrayList' のクエリ パターンの実装が見つかりませんでした。'Where' が見つかりません。範囲変数 'person' の型を明示的に指定してください。
「Where」って何よ? • 「‘Where’ が見つかりません」… • そもそも、その先頭大文字の「Where」って何? • 実はLINQのクエリ構文は、メソッド構文に置き換えられてコンパイルされる。 // クエリ構文 var results = from person in persons where (person.Age >= 30) && (person.IsFemale == true) orderbyperson.FirstName, person.LastName select person; // メソッド構文(コンパイル時にはこのように解釈される) var results = persons. Where(person => (person.Age>= 30) && (person.IsFemale == true)). OrderBy(person => person.FirstName). ThenBy(person => person.LastName). Select(person => person); error CS1934: ソース型 'System.Collections.ArrayList' のクエリ パターンの実装が見つかりませんでした。'Where' が見つかりません。範囲変数 'person' の型を明示的に指定してください。
「Where」って何よ? • ArrayListクラスのドキュメントを確認。 Whereメソッドが無い。仕方ないか。
「Where」って何よ? • そりゃ失礼。では正常なList<T>クラスのドキュメントを確認。 やっぱり無いんですけど ( ゚Д゚)
「Where」は拡張メソッド • 拡張メソッドは、C#3.0にて導入された。 • staticクラス内のstaticメソッドの第一引数に「this」を修飾する事で定義できる。 string型に対して、「this」の修飾 // 拡張メソッドの例。クラス名は完全に任意 public static class SampleExtensions { // 文字列をintに変換する拡張メソッド public staticint ToInt32(this string stringValue) { return int.Parse(stringValue); } } public sealed class MainClass { public static void Main() { var string123 = “123”; var int123 = string123.ToInt32(); // 拡張メソッドの呼び出し }} string型インスタンスメソッドの呼び出しのように見える(書ける)
「Where」は拡張メソッド • Whereメソッドは、System.Linq.Enumerableクラスに定義されている。 // System.Linq.Enumerable public static class Enumerable { // Where拡張メソッド(擬似コード) public static IEnumerable<T> Where<T>( this IEnumerable<T> enumerable, Func<T, bool> predict) { foreach(varvalue in enumerable) { if (predict(value) == true) { yield return value; } } } } IEnumerable<T>インターフェイス型に対して「this」の修飾
それで? • でも、配列やリストは、IEnumerable<T>型じゃないよ? • .NETの配列は、IEnumerable<T>インターフェイスを自動的に実装している。 // 配列は、IEnumerable<T>を実装している varpersons = new[] { new Person { FirstName=“Kouji”, LastName=“Matsui” } }; IEnumerable<Person>personsEnumerable = persons; // OK • List<T>クラスは、IEnumerable<T>を実装している。 // List<T>は、IEnumerable<T>を実装している varpersons = new List<Person>(); persons.Add( new Person { FirstName=“Kouji”, LastName=“Matsui” }); IEnumerable<Person>personsEnumerable = persons; // OK
それで? • つまり、両方とも抽象基底インターフェイスとして、IEnumerable<T>インターフェイスを実装している。 System.Linq.Enumerableクラス IEnumerable<T> IEnumerable<T> Where<T>(this IEnumerable<T> enumerable, …) だから、どちらでも同じ Where拡張メソッドが使える!! T型の配列 T[] List<T>
ArrayListは? • なぜArrayListクラスは駄目なのか? • ArrayListクラスが実装しているインターフェイスは、IEnumerable<T>ではなく、IEnumerableインターフェイス。 • IEnumerableインターフェイスのWhere拡張メソッドは存在しない。 • じゃあ、全く使えないかというと、要するにT型を特定して、ジェネリックなIEnumerable<T>に変換すればいい。 // ArraListを用意 varpersons = newArrayList(); // (ArrayListに様々なインスタンスを追加) // すべての要素をキャスト(キャストに失敗すれば例外がスロー) IEnumerable<Person>persons2 = persons.Cast<Person>(); // OK // 又は、指定された型のインスタンスだけを抽出 IEnumerable<Person>persons3 = persons.TypeOf<Person>(); // OK Cast・TypeOfメソッドは、 「this IEnumerable」と定義された拡張メソッド。
ところで。 • クエリの結果をforeachで回してたっけ。 varresults = from person in persons where (person.Age >= 30) && (person.IsFemale == true) orderbyperson.FirstName, person.LastName select person; foreach(varresult inresults){ Console.WriteLine(“{0} {1}”, result.FirstName, result.LastName); } ぐるぐる • foreachで回すことができる条件は?
今さらforeach • foreachで回すことができるインスタンスは、IEnumerableインターフェイスを実装していること。 • IEnumerableって言うと、配列とか、リストだっけ… // 配列 varpersons = new[] { new Person { FirstName=“Kouji”, LastName=“Matsui”, Age=41, IsFemale=false }, new Person { FirstName=“Mogeko”, LastName=“Moge”, Age=35, IsFemale=true }, new Person { FirstName=“Uhyo”, LastName=“Hidebu”, Age=31, IsFemale=true }, }; // 配列を回してみた foreach(varperson inpersons){ Console.WriteLine(“{0} {1}”, person.FirstName, person.LastName); } 継承・実装関係 IEnumerable IEnumerable<T> なんだ、LINQクエリと一緒じゃん。 一緒、なのか?? ( ゚Д゚) T型の配列 T[] List<T>
配列・リスト・そしてLINQクエリ • IEnumerable<T>もOKなので、LINQクエリはforeachでそのまま回せる。 • 「逆に言えば」、LINQクエリはIEnumerable<T>インターフェイスを実装している? • と言う事は? // 実はLINQクエリの結果はIEnumerable<T> IEnumerable<Person> results = from person in persons where (person.Age >= 30) && (person.IsFemale == true) orderbyperson.FirstName, person.LastName select person; // 前段のLINQクエリに対して、更にLINQクエリを適用する IEnumerable<Person> results2 = from result inresults whereresult.LastName.StartsWith(“Suzuki”) == true select result; 更にこの結果に対してLINQクエリを…
効率は? • LINQクエリを数珠つなぎにして、効率悪くないの? • 悪いとも言えるし、変わらないともいえる。 • where絞り込み条件を完全に統合できるなら、その方が効率が良い。 クエリの意味が変わってしまわないように注意 // where条件をまとめる var results = from person in persons where (person.Age >= 30) && (person.IsFemale == true) && (result.LastName.StartsWith(“Suzuki”) == true) orderbyperson.FirstName, person.LastName select person;
起源を思い出せ • 効率が変わらないって? • クエリ構文は、メソッド構文に置き換えられてコンパイルされる。 varresults = persons. Where(person => (person.Age >= 30) && (person.IsFemale == true)). OrderBy(person => person.FirstName). ThenBy(person => person.LastName). Select(person => person); varresults2 = results. Where(result => result.LastName.StartsWith(“Suzuki”) == true). Select(result => result); クエリ構文だと、クエリが分割されているだけで効率が悪いように見えるが、実際はそれほどでもない。 // まとめると、単に連結されただけ。 varresults2= persons. Where(person => (person.Age >= 30) && (person.IsFemale == true)). OrderBy(person => person.FirstName). ThenBy(person => person.LastName). Select(person => person). // ← しいて言えばここが無駄 Where(result => result.LastName.StartsWith(“Suzuki”) == true). Select(result => result);
結局のところ • LINQクエリは、IEnumerable<T>インターフェイスを返すメソッドを数珠つなぎにしただけ。 これらは全て、Enumerableクラスに定義されている拡張メソッド群 // 全てが、IEnumerable<T>インターフェイスを利用した、拡張メソッド群の呼び出しで解決される。 var results2 = persons. Where(person => (person.Age >= 30) && (person.IsFemale == true)). OrderBy(person => person.FirstName). ThenBy(person => person.LastName). Select(person => person). Where(result => result.LastName.StartsWith(“Suzuki”) == true). Select(result => result); // System.Linq.Enumerableクラス public static IEnumerable<T> Where<T>(thisIEnumerable<T> enumerable, …); public static IEnumerable<T> Select<T>(thisIEnumerable<T> enumerable, …); public static IEnumerable<T> OrderBy<T>(thisIEnumerable<T> enumerable, …); public static IEnumerable<T> ThenBy<T>(thisIEnumerable<T> enumerable, …); OrderByとThenByは込み入った理由から本当はこの通りではないが、同じように理解してよい
一体、ソースの話はどこにww • 前節までに、LINQクエリの肝は「IEnumerable<T>インターフェイス」と、その拡張メソッド群であることが明らかになりました。というか、これを強くイメージしてほしかったので、長々と解説しました。 • 言い換えると、「IEnumerable<T>インターフェイスのインスタンスを返しさえすれば、フリーダムにやってOK」って事です。 < マダー?
IEnumerable<T>を返す • LINQで使える独自のメソッドを作りたい。例として、「指定された個数の乱数を返す」 LINQソースを考える。 個数がデカいとちょっと… // イケてない実装(配列を作って、乱数を格納して返す) publicIEnumerable<int> GetRandomNumbers(int count) { varr = new Random(); varresults = new int[count]; for (var index = 0; index < results.Length; index++) { results[index] = r.Next(); } return results; // 配列はIEnumerable<T>を実装しているのでOK } // こう使える varresults = fromrninGetRandomNumbers(1000000) // これって… where (rn % 2) == 0 selectrn;
オンザフライで乱数を生成(1) • 要するに、IEnumerable<T>で返せばいいのだから、配列やリストでなくても良い。IEnumerable<T>を実装した、独自のクラスを定義する。 • IEnumerable<T>は、IEnumerator<T>のファクトリとなっているので、これらを実装する。 // IEnumerable<int>を実装したクラスを定義 internal sealed class RandomNumberEnumerable : IEnumerable<int> { private readonlyintcount_; // 個数を記憶する publicRandomNumberIEnumerable(int count) { count_ = count; } publicIEnumerator<int> GetEnumerator() { // RandomNumberEnumeratorを作って返す(ファクトリメソッド) return new RandomNumberEnumerator(count_); } IEnumeratorIEnumerable.GetEnumerator() { // 非ジェネリック実装は、単にジェネリック実装を呼び出す return this.GetEnumerator(); } }
オンザフライで乱数を生成(2) • GetEnumerator()はIEnumerator<T>を返す必要があるので、そのためのクラスを準備。 // 乱数生成の本体クラス internal sealed class RandomNumberEnumerator : IEnumerator<int> { private readonly Random r_ = new Random(); private readonlyintcount_; private intremains_; publicRandomNumberEnumerator(int count) { count_ = count; remains_ = count; } public intCurrent { get; private set; } // 次があるかどうかを返す publicboolMoveNext() { if (remains_ >= 1) { remains_--; this.Current = r.Next(); // 次の値を保持 return true; } return false; } } 実際には、Resetメソッドも必要…
オンザフライで乱数を生成(3) • やっと完成。 // オンザフライ出来た! publicIEnumerable<int> GetRandomNumbers(int count) { // RandomNumberEnumerableクラスを生成 return new RandomNumberEnumerable(count); } // こう使える varresults = fromrninGetRandomNumbers(1000000) // メモリを過度に消費しない!! where (rn % 2) == 0 selectrn; め、面倒クサ過ぎる orz
yield return yieldを使うと、コンパイル時に、自動的に前述のような内部クラスが生成される (ステートマシンの生成) • C#2.0にて、「yield」予約語が導入された。 • これを使うと、IEnumerableインターフェイスの実装が劇的に簡単に!(つまり、前節の方法は、.NET 1.1までの方法) // イケてる実装 publicIEnumerable<int> GetRandomNumbers(int count) { varr = new Random(); for (var index = 0; index < count; index++) { yield return r.Next(); // yield returnって書くだけ!! } } // こう使える varresults = fromrninGetRandomNumbers(1000000) // 勿論、メモリ消費しない where (rn % 2) == 0 selectrn;
yieldを使って、拡張メソッド • 任意数のIEnumerable<T>インスタンスを結合する拡張メソッドを作る。 // 適当なstaticクラスに定義 publicstaticIEnumerable<T> Concats<T>( this IEnumerable<T> enumerable, paramsIEnumerable<T>[] rhss) { // まず、自分を全て列挙 foreach(varvalue in enumerable) { yieldreturn value; } // 可変引数群を列挙 foreach(varrhsinrhss) { // 個々の引数を列挙foreach(varvalue inrhs) { yieldreturn value; } } } 可変引数群を受け取る
yieldを使って、拡張メソッド // 配列 varpersons1 = new[] { new Person { FirstName=“Kouji”, LastName=“Matsui”, Age=41, IsFemale=false }, new Person { FirstName=“Mogeko”, LastName=“Moge”, Age=35, IsFemale=true }, new Person { FirstName=“Uhyo”, LastName=“Hidebu”, Age=31, IsFemale=true }, }; // リスト varpersons2 = new List<Person>(); persons2.Add(newPerson { FirstName=“Kouji”, LastName=“Matsui”, Age=41, IsFemale=false }); persons2.Add(newPerson { FirstName=“Mogeko”, LastName=“Moge”, Age=35, IsFemale=true }); persons2.Add(newPerson { FirstName=“Uhyo”, LastName=“Hidebu”, Age=31, IsFemale=true }); // 何らかのLINQクエリ varpersons3 = from person inpersonsX where (person.Age >= 30) && (person.IsFemale == true) select person; // 全部結合 varresults = persons1.Concats(persons2, persons3); この部分が可変引数群(rhss)
yieldでこんな事も可能 • yieldによって勝手にステートマシンが作られるので、逆手にとって… // 移動角度群を返すLINQソース publicstaticIEnumerable<double> EnemyAngles() { // 敵の移動角度を以下のシーケンスで返す yieldreturn0.0; yieldreturn32.0; yieldreturn248.0; yieldreturn125.0; yieldreturn66.0; yieldreturn321.0; // 10ステップはランダムな方角に移動 varr = new Random(); for (varindex = 0; index < 10; index++) { yieldreturnr.Next(360); } // 最後に少し動いて死亡 yieldreturn37.0; yieldreturn164.0; } foreachで回せば、これらの順で値が取得出来る。 もちろん、LINQクエリで値を加工することも可能 重要なのは、yieldを使う事で、返却する値を自由自在にコントロールできると言う事 単独で値を返したり、ループさせたり、それらを組み合わせたりもOK
必ずIEnumerable<T>? • LINQソースとなるためには、必ずIEnumerable<T>を返さなければならないのか? // 例えば、パラレルLINQクエリ var results = from person inpersons.AsParallel() where (person.Age >= 30) && (person.IsFemale == true) orderbyperson.FirstName, person.LastName select person; // メソッド構文 var results = persons. AsParallel(). Where(person => (person.Age >= 30) && (person.IsFemale == true)). OrderBy(person => person.FirstName). ThenBy(person => person.LastName). Select(person => person); AsParallelするだけで、あとは普通のLINQと変わらないよ?
必ずIEnumerable<T>? • パラレルLINQクエリの結果は、実はParallelQuery<T>型。 // 似ているようで、違うのか? ParallelQuery<T> results = persons. AsParallel(). Where(person => (person.Age >= 30) && (person.IsFemale == true)). OrderBy(person => person.FirstName). ThenBy(person => person.LastName). Select(person => person);
必ずIEnumerable<T>? • ParallelQuery<T>クラスは、IEnumerable<T>インターフェイスを実装している。 • じゃあ、Where拡張メソッドの呼び出しは、結局同じってこと?? System.Linq.Enumerableクラス IEnumerable<T> IEnumerable<T> Where<T>(this IEnumerable<T> enumerable, …) ParallelQuery<T>
ParallelEnumerableクラス • ParallelQuery<T>クラスに対応するWhere拡張メソッドは、Enumerableクラスではなく、ParallelEnumerableクラスに定義されている。 • C#コンパイラは、型がより一致する拡張メソッドを自動的に選択するため、ParallelQuery<T>に対してWhereを呼び出すと、ParallelEnumerable.Whereが呼び出される。 System.Linq.Enumerableクラス IEnumerable<T> IEnumerable<T> Where<T>(this IEnumerable<T> enumerable, …) ParallelQuery<T>の場合は、こっちのWhereが呼び出される。この実装がパラレルLINQを実現する。 System.Linq.ParallelEnumerableクラス ParallelQuery<T> ParallelQuery<T> Where<T>(this ParallelQuery<T>enumerable, …)
わざと似せている • ParallelEnumerableクラスには、Enumerableクラスに定義されているメソッドと同じシグネチャ(但し、IEnumerable<T> → ParallelQuery<T>)の、全く異なる実装が定義されている。 戻り値の型も、ParallelQuery<T>となっているので、 // System.Linq.ParallelEnumerableクラス public static ParallelQuery<T> AsParallel<T>(thisIEnumerable<T> enumerable); public static ParallelQuery<T> Where<T>(thisParallelQuery<T> enumerable, …); public static ParallelQuery<T> Select<T>(thisParallelQuery<T> enumerable, …); public static ParallelQuery<T> OrderBy<T>(thisParallelQuery<T> enumerable, …); public static ParallelQuery<T> ThenBy<T>(thisParallelQuery<T> enumerable, …); // メソッドの連結 ParallelQuery<T> results = persons. AsParallel(). Where(person => (person.Age >= 30) && (person.IsFemale == true)). OrderBy(person => person.FirstName). ThenBy(person => person.LastName). Select(person => person; これらの戻り値の型は、すべからくParallelQuery<T>型。 だから、全てParallelEnumerableの実装が使われる。 →これによって、クエリのパラレル実行が行われる。
まだある、似て異なる実装 • LINQto SQLやLINQ to EntitiesのデータベースコンテキストからLINQクエリを記述すると、IEnumerable<T>ではなく、IQueryable<T>が返される。 // LINQ to Entitiesに対して、LINQクエリを記述する var results = from person inpersonsContext// DBコンテキストがソース where (person.Age >= 30) && (person.IsFemale == true) orderbyperson.FirstName, person.LastName select person; IQueryable<T> // メソッド構文と戻り値の型 IQueryable<Person> results = personsContext. Where(person => (person.Age >= 30) && (person.IsFemale == true)). OrderBy(person => person.FirstName). ThenBy(person => person.LastName). Select(person => person);
まだある、似て異なる実装 • IQueryable<T>に対応するWhere拡張メソッドは、Enumerableクラスではなく、Queryableクラスに定義されている。 • 考え方はパラレルLINQと同じ。 System.Linq.Enumerableクラス IEnumerable<T> IEnumerable<T> Where<T>(this IEnumerable<T> enumerable, …) System.Linq.Queryableクラス IQueryable<T> IQueryable<T> Where<T>(this IQueryable<T>queryable, …) データベースにWHERE句を送信するための仕掛けを持った実装。
やっぱりLINQソースは • IEnumerable<T>を継承したクラスやインターフェイスを使うのか? • ReactiveExtensionライブラリが最後の常識を覆す。 // マウス移動イベント発生時に座標をフィルタする IObservable<Point>rx = Observable.FromEvent<MouseEventArgs>(window, “MouseMove”). Select(ev => ev.EventArgs.GetPosition(window)). Where(pos => (pos.X < 100) && (pos.Y < 100)); // rxの条件が満たされたときに実行する内容を記述 rx.Subscribe(pos => { window.Text = string.Format(“{0}, {1}”, pos.X, pos.Y); }); IObservable<T>は、IEnumerable<T>と全く関係がない。 しかし、WhereやSelect拡張メソッドを用意する事で、まるでLINQクエリのように見えるようにしている。
まとめ • ベーシックなLINQクエリは、すべからくIEnumerable<T>を使用する。その場合、Enumerableクラスに定義された拡張メソッドを使用して、LINQの機能を実現している。 • 独自の拡張メソッドを定義すれば、LINQ演算子を追加できる。 • 独自のLINQソースを作るなら、yield構文を使うと良い。 • 拡張されたLINQクエリ(パラレルLINQやLINQ to Entitiesなど)は、IEnumerable<T>を継承した、新たなクラスやインターフェイスを使用して、拡張メソッドを切り替えさせる事で、似て異なる動作を実現する。これにより、既存の拡張メソッドの動作に影響を与えることなく、かつ、容易に理解可能なAPIを生み出すことができる。 • IEnumerable<T>と全く関係のない型を使用したとしても、まるでLINQクエリのように見せることができる。これを「Fluent API」パターンと呼ぶ。Fluent APIを用意すれば、LINQクエリのようなフレンドリ感と、VS上でのサクサクタイピングが実現する。
完 ご静聴ありがとうございました m(_ _)m