トップ 差分 一覧 ping ソース 検索 ヘルプ PDF RSS ログイン

プログラミングC# 第7版(2)



目次



記事一覧

キーワード

プログラミングC#(2) LINQ

[言語 まとめ C#][C#][C# サンプルコード][Effective C# 4.0][Universal Windows Platform][Visual Studio]



101 LINQ Samples

基本

  • 言語統合クエリ
  • 連携して動作するいくつかの言語要素から構成
    • クエリ式
    • LINQ演算子
    • LINQプロバイダ
  • .NET Framework

オブジェクト用

  • LINQ to Objects

データベース用

WCFデータサービスクライアント

あらゆるオブジェクトコレクションに対してLINQを使用してみると非常に便利

 クエリ式(クエリ構文)

  • 多くの種類のクエリを非常に自然な文法で記述できる
  • あらゆるLINQプロバイダに対して使用できる
例1
int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
IEnumerable<int> odd =
    from num in nums
    where num % 2 == 1
    select num;
foreach(var n in odd)
{
    Console.WriteLine($"{n}");
}
例2
var result = from book in books
             where book.Title.IndexOf("計算") > 0
             orderby book.Title descending
             select book.Title
 ;

foreach (string title in result)
{
    Console.WriteLine(title);
}
  • 項目を選択するためのコードが選択された項目に対して行う作業と明確に分離されている
  • より宣言的なスタイル
    • 方法ではなく何を取得するかに集中

from句

from 適用範囲 in データソース

データソース
  • クエリのソースを確定
  • IEnumerable/IEnumerable<T> インターフェース、またはその派生インターフェースを実装していることだけが条件
  • foreachで処理できるオブジェクトであれば利用できる
    • オブジェクト配列、データセット、XElementオブジェクト配列など
  • データソースに何が渡されたかによりプロバイダーが決定されるため開発者はプロバイダーを意識する必要はない
適用範囲
  • 範囲変数を指定
  • データソースから取り出した1つ1つの項目を一時的に格納するための仮変数

where句

  • データソースを絞り込み結果を選択するための条件式を指定するために使用する
  • 範囲変数を介して、プロパティにアクセスできる
  • 論理演算子を使用し、複合条件を指定できる。
  • 省略可
  • 複数回出現可

select句

  • すべてのクエリ式は、select句かgroup句で終わる
  • 出力を決定
  • 出力結果の型は、select句もしくはgroup句によって決まる
  • すべてのクエリ式が IEnumerable<T> となるわけではない
    • プロバイダによって決まる
    • var を結果利用することが一般的
  • 匿名型を使用して、select new { Title=book.Title, Price=book.Price }; 等とすることもできる。

 メソッド構文

  • クエリ式はコンパイラにより1つか複数のメソッド構文呼び出しに変換される
  • クエリ式はメソッド構文のシンタックスシュガーなのでクエリ式であらわされる内容は必ずメソッド構文で表現できる
例1
int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
IEnumerable<int> odd = nums
    .Where(num => num % 2 == 1)
    .OrderByDescending( book=> book.Title)
    .Select(num => num);

foreach (var n in odd)
{
    Console.WriteLine($"{n}");
}
例2
var result = books
            .Where( book => book.Title.IndexOf("計算") > 0 )
            .Select( book => book.Title)
 ;

foreach (string title in result)
{
    Console.WriteLine(title);
}
  • コンパイラはselect句の式をラムダ式に変換する
  • 範囲変数はラムダへの引数となる
  • Where,Select メソッドは LINQ演算子

let句

  • クエリ式で、サブ式の結果を格納して後の句で使用すると便利な場合があります。 let キーワードを使用すると、これを行うことができます
int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
IEnumerable<int> odd =
    from num in nums
    let r = num % 2
    where r == 1
    select num;

クエリ式のサポート

  • コンパイラは機械的にクエリ式を一連のメソッド呼び出しに変換
  • 必要なのはラムダを実引数として受け入れvoid以外の何かしらを返す
  • 本来の仕事はLINQプロバイダで行われている
  • Whereメソッドを持っていない型でも利用できるのは、LINQ to Objects が適切な拡張メソッドを定義しているから
  • using System.Linq; を宣言する必要がある

 遅延評価

  • 必要なときのみ動作するオブジェクトを返す

クエリの結果を取得しようとしたときのみ実際に処理が行われる

  • 無限シーケンスを処理できる
  • クエリが何度も評価されないように注意する必要がある

 LINQ、ジェネリックとIQueryable<T>

  • ほとんどのLINQプロバイダでは、ジェネリック型が使われている
  • LINQ to Objects では IEnumerable<T> が使われている
  • データベース用プロバイダでは、IQueryable<T> を使うものがある

標準LINQ演算子

  • 演算子とはLINQプロバイダが提供するクエリ機能のこと
  • コンポジションをサポートするよう設計されている
    • 演算子を自由に組み合わせて使用できる
    • 単純な項目に対して複雑なクエリを実行できる
    • ほとんどの演算子が項目の集合を表す型を引数に取るだけでなく結果として返す
    • LINKQ演算子は数学的な関数
  • 入力に影響を与えない
  • 結果は新しく生成される
  • 副作用を恐れずに任意の組み合わせで演算子をつなげられる
  • 複数のクエリで同じソースを繰り返し利用できる
  • 技術的な制約ではない

 フィルタ

Where

  • 個々の項目を引数としboolを返す関数を渡す
  • 遅延実行
  • クエリ式からは利用出来ないオーバーロード版がある
    • インデックスを伴うWhere演算子
int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
IEnumerable<int> odd 
  = nums.Where((r, idx) => { Console.WriteLine($"IDX:{idx}"); return r % 2 == 1; });
  • 条件に合ったオブジェクトがない場合空のシーケンスを生成

OfType<T>

  • 特定の型の項目だけを取り出したいときに便利
var strings = src.OfType<string>();
  • 条件に合ったオブジェクトがない場合空のシーケンスを生成

最終的に出力される項目を指定するラムダを渡すためにクエリの最後にselect句を書く

出力加工の意味

  • それぞれの項目から1つだけの情報を取り出したい
  • 取り出した情報を変形させる

データシェイプと匿名型

  • データアクセスのためにLINQプロバイダを利用している場合、フェッチするデータ量を減らすことが出来る
  • 必要なプロパティだけを含む匿名型のインスタンスを返す
var pq = from product in dbCtx.Products
  where (product.ListPrice > 3000)
  select new { product.Name, product.ListPrice, product.Size };

射影とマップ

  • Select演算子は射影と呼ばれることがあるが、mapと同じもの
  • Selectは概念的には Map Reduce の一部と同じ命令(LINQでは reduce に対して Aggregate と命名)

SelectMany

  • 複数のfrom句を持つクエリ式の中で使用される
  • クエリ式
int[] nums = { 1, 2, 3, 4, 5 };
char[] alphas = { 'a', 'b', 'c', 'd', 'e' };
IEnumerable<string> strs = from num in nums
                          from alpha in alphas
                          select alpha + num.ToString()
                          ;
  • 演算子
int[] nums = { 1, 2, 3, 4, 5 };
char[] alphas = { 'a', 'b', 'c', 'd', 'e' };
IEnumerable<string> strs = nums.SelectMany(num => alphas, (num,alpha) => alpha + num.ToString());

取得列を明示的に指定

var tables = from tbl in _context.Talbes
             select new {
               tbl.tabschema,
               tbl.tabname
             };

 並べ替え

orderby句

  • 並べ替えを定義できる
var q = from cource in Cource.Catalog
           orderby cource.PublicationDate ascending, cource.Duration descending
           select cource;

演算子

    • Orderby,OrderbyDescending,ThenBy,ThenByDescending

コンテインメントテスト

  • コレクションの中の項目を調べるために、さまざまな演算子が標準で定義

Contains

  • もっとも簡単な演算子
  • 指定した項目がソース中にあるか
  • 2つのオーバーロード
    • 項目を引数にとる
    • --IList<T>を実装するコレクションの場合、IList<T> のContains が利用される
    • 項目+IEqualityComparer<T> 型の引数をとる

Any

  • コレクションが特定の条件を満たす値を1つ以上含んでいるか
  • predicate をとる

Count

  • 条件にあう項目の個数を知りたい
  • predicateをとる
    • intを返す
condition.ResultCount = db.SearchConditionResults.Count(src => src.SearchConditionId == condition.Id);

LongCount

  • 非常に大きな値の個数
  • 64ビットで個数を返す

All

  • predicate をとる
  • predicateの条件に合わないものが1つもない場合 true

 特定の項目と部分選択

  • 項目を1つだけ返すクエリを記述したい
  • クエリが結果を1つだけ生成することが明らかな場合

Single 演算子を利用

    • クエリによって生成される唯一のオブジェクトが返される
    • 結果がないもしくは2つ以上
    • --InvalidOperationException

SingleOrDefault

    • 結果が1つもしくは結果がない場合規定値

var tables = (from table in DbContext.Tables
        where table.tabschema == SCHEMA_NAME
        &&    table.tabname == id
        select table).SingleOrDefault();

その他

機能 演算子
最初の1つの項目を取得 First,FirstOrDefault
最後の項目を取得 Last,LastOrDefault
指定した数の要素をスキップしてその他すべてを返す Skip
最初から指定数を取得し残りは捨てる Take
predicate に一致するまでの項目が捨てられる SkipWhile
predicate に一致しない項目が見つかるまでの項目が返される TakeWhile

空でない限りソースのコレクション全体が返される、空の場合 T型のゼロに相当する規定値参照型:null、数値型:0)を持つ一つの項目を含むシーケンスを返す,DefaultIfEmpty<T>

集約

機能 演算子
ソース中のすべての項目の値を処理
合計 Sum
合計÷個数 Avarage

Max,Min

  • 数値型のコレクションに備わる
double av = (new List<int>{12,123,5,6546,8,9,1}).Average();
  • あらゆる型の項目に対して動作するオーバーロード版
    • ラムダを引数に取る
var ttlAge = persons.Sum(person => person.Age);
var avAge = persons.Average(person => person.Age);
var mxAge = persons.Max(person => person.Age);
var mnAge = persons.Min(person => person.Age);

Aggregate

    • すべてを調べて1つの値を得る演算子を一般化
    • --合計、最大、平均を書き換える
var s2 = persons.Aggregate(0.0, (ttl, person) => ttl + person.Age);
var m2 = persons.Aggregate(0.0, (max, person) => max > person.Age ? max : person.Age);
var a2 = persons.Aggregate(new { ttl=0, cnt=0 }, (p1, person) => new { ttl = p1.ttl + person.Age,cnt = p1.cnt + 1 }, p2 => p2.ttl / p2.cnt);
    • 累積器
    • reduce と呼ばれる機能のLINQにおける名称
    • foldと呼ばれる場合もある

 集合演算子

  • 3つの演算子
  • 2つのソースをまとめる
  • 一般的な集合演算
  • 数学の集合とは異なる

Distinct

    • --重複を取り除く
    • --より集合に近い状態とする

Intersect

    • 両方に含まれる

Except

    • 一方にあるのにもう片方に含まれない

Union

    • どちらか、もしくは両方に含まれる

シーケンス全体を扱ったり、順序を維持するための演算子

Reverse

  • 要素の順序を反転

Concat

  • 2つのシーケンスを1つにまとめる
  • 順序は維持される

Zip

  • 2つのシーケンスを1つにまとめる
  • 要素をペアに処理する
string[] nums = { "1", "2", "3", "4", "5" };
string[] alphas = {"a", "b", "c", "d", "e" };
var zipped = nums.Zip(alphas, (num, alpha) => num + ":" + alpha);
foreach( string s in zipped)
{
    Console.WriteLine(s);
}
  • ソースの長さが異なる場合には、短い方の最後に到達で終了

SequenceEqual

  • 2つのソースの項目数、値がすべて同じ場合、true

 グループ化

group句

  • IGrouping<TKey,TItem> を実装する項目のコレクションを生成する
var persons = new List<Person> {
    new Person() { Team="A",Name="hoge" },
    new Person() { Team="B",Name="foo"  },
    new Person() { Team="A",Name="bar" },
    new Person() { Team="C",Name="fuga"},
};
var teamGroups = from person in persons
            group person by person.Team;
foreach(var group in teamGroups)
{
    Console.WriteLine(group.Key);
    foreach(var person in group)
    {
        Console.WriteLine(person.Name);
    }
}

項目の射影を使ったグループ式の展開

    • クエリ式はselectかgroupが最後の句となる必要がある
    • group句がある場合に、それが最後である必要はない
    • into キーワードによりクエリの残りで反復処理するための範囲変数が作られる
    • --orderbyやwhereなどほかの句でも利用できる
    • 規定の結果IGrouping<TKey,TItem>も変更できる
var teams = from person in persons
            group person by person.Team into team
            select $"Team:{team.Key}:{team.Count()}";

グループ射影を使ったグループ化クエリの展開

var teams = persons.GroupBy(person => person.Team)
            .Select(team => $"Team{team.Key}:{team.Count()}");
    • 同じ意味を直接的に表すオーバーロド
    • --2つのラムダをとる
    • ----1つめはグループ化条件
    • ----2つめはグループオブジェクトを生成するため
    • ------最初の引数にKeyが渡る
var teams = persons.GroupBy(person => person.Team,
            (teamName,team) => $"Team{teamName}:{team.Count()}");

キー、項目とグループの射影を使ったGroupBy演算子

    • 3つのラムダをとる
    • --どの項目によってグループ化するか
    • --個々の項目がグループの中でどのようにあらわされるか
    • --各オブジェクトを生成
var teams = persons.GroupBy(
                person => person.Team,
                person => person.Name,
                (teamName,names) => $"Team{teamName}:{names.Count()}");

複合グループキー

    • 複数のキーでグループ化したい場合、単純にキーに両方の値を入れる
var persons = new List<Person> {
    new Person() { Team="A",Name="hoge1",Lank="1" },
    new Person() { Team="A",Name="hoge2",Lank="1" },
    new Person() { Team="A",Name="hoge3",Lank="1" },
    new Person() { Team="B",Name="foo",Lank="2"  },
    new Person() { Team="A",Name="bar",Lank="2" },
    new Person() { Team="C",Name="fuga",Lank="3"},
};
var teamLankGroup = from person in persons
            group person by new { person.Team, person.Lank };
foreach (var teamLank in teamLankGroup)
{
    Console.WriteLine($"Team={teamLank.Key.Team},Lank={teamLank.Key.Lank},Count={teamLank.Count()}");
}

 Join

  • 異なるソースから関連性のあるデータをクエリから使用できるようにする
var teams = new List<Team> {
    new Team() {Code="A",Name="TeamA" },
    new Team() {Code="B",Name="TeamB" },
    new Team() {Code="C",Name="TeamC" }
};
var persons = new List<Person> {
    new Person() { Team="A",Name="hoge1"},
    new Person() { Team="B",Name="foo"},
    new Person() { Team="C",Name="fuga"}
};
var pt = from person in persons
         join team in teams
         on person.Team equals team.Code
         select new { PersonName=person.Name, TeamName=team.Name};
foreach(var p in pt)
{
    Console.WriteLine($"{p.PersonName},{p.TeamName}");
}

複合キーで結合する

var teams = new List<Team> {
    new Team() {Code="A",SubCode="1",Name="TeamASub1" },
    new Team() {Code="B",SubCode="1",Name="TeamBSub1" },
    new Team() {Code="C",SubCode="1",Name="TeamCSub1" }
};
var persons = new List<Person> {
    new Person() { Team="A",TeamSub="1",Name="hoge1"},
    new Person() { Team="B",TeamSub="1",Name="foo"},
    new Person() { Team="C",TeamSub="1",Name="fuga"}
};
var pt = from person in persons
         join team in teams
         on new { person.Team, person.TeamSub } equals new { Team=team.Code, TeamSub=team.SubCode }
         select new { PersonName=person.Name, TeamName=team.Name};
         
foreach(var p in pt)
{
    Console.WriteLine($"{p.PersonName},{p.TeamName}");
}
    • 結合用の複合キーに匿名型を利用している
    • プロパティを合わせる
    • --同一な構造を持つ同じアセンブリ内の匿名型は同じ型を共有する
    • --コンパイラは匿名型に対して各メンバすべてを比較するEqualsメソッドを生成する

 変換

シーケンスの型変換

var sourceSeq = sequence.Cast<Cource>();

Cast<T>

  • 変換不能な場合、例外をスローする

OfType<T>

  • Cast<T> とほとんど同じだが、間違った型が存在した場合、例外をスローせずフィルタする

AsEnumerable<T>

  • 何ら変更なくソースを返す
  • 他のLINQプロバイダーによって処理される可能性があるものを扱う場合でも、確実にLINQ to Objectsによって処理されるようにする
  • ここから先はクライアント側で処理をすると明示

AsQueryable<T>

  • AsEnumerable<T>の反対に、LINQ to Objectsの代わりに確実に自分で作成したクエリを使用したいようなシナリオ

AsParallel

  • Parallel LINQ(PLINQ)によって実行されるクエリを構築するためのParallelQuery<T> が返される

ToArray,ToList

  • 入力クエリを実行した結果のすべてを格納した配列、リストが返される

ToDictionary,ToLookup

  • 連続ルックアップをサポートした結果を生成
  • --IDictionay<TKey,TValue>,ILookup<TKey,TValue>が返される
var persons = new List<Person> {
    new Person() { Team="A",Name="hoge1"},
    new Person() { Team="A",Name="hoge2"},
    new Person() { Team="B",Name="foo"},
    new Person() { Team="C",Name="fuga"}
};
var ta = persons.ToLookup(person => person.Team);
foreach (var p in ta["A"])
{
    Console.WriteLine($"{p.Team},{p.Name}");
}

シーケンスの生成

Enumerable.Range

  • 2つのintをとり、2つ目の引数まで1づつ大きくなるIEnumerable<int>を返す

Enumerable.Repeat<T>

  • 型Tと回数を引数にとり、指定した回数生成する

Enumerable.Empty<T>

  • 要素のないIEnumerable<T>が返される

他のLINQの実装

 Entity Framework

LINQ to Entities

  • .NET Frameworkの一部
  • データベースとオブジェクト層を対応づけ
  • 複数のデータベースベンダ製品がサポート
  • IQueryable<T> を使用
    • IEnemerable<T> から派生しているのでLINQ to Objectsの演算子を利用できる
  • 匿名メソッドやデリゲートは使用できない

LINQ to SQL

  • SQL サーバー専用の設計
  • .NET API として設計
  • テーブルオブジェクトが、IQueryable<T> を実装

WCF データサービスクライアント

  • Open Data Protocol(OData)を使用してHTTP上でデータを表現したり消費する
  • XML もしくは JSON
  • IQueryable<T>を使用
  • 一部のLINQ演算子のみサポート

 Parallel LINQ

クエリの結果を取得する際、可能であれば使用可能なCPU資源を効率的に使用するため、スレッドプールを使ったマルチスレッドの評価が行われる

LINQ to XML

  • LINQプロバイダではない
  • XMLを作成すしたり、パースするためのAPI
  • XML文書に対して簡単にLINQクエリを実行できるように設計されている
  • XML文書を .NETオブジェクトモデルで表現することで実現
  • 完全に.NETに向けて設計されているため、C#技術と非常によく統合されている
  • LINQ to Objectsと同様な方法でクエリの定義、実行が可能

 Reactive Extensions

  • LINQ演算子が様々な型とともに動作する仕組みをうまく表している



YAGI Hiroto (piroto@a-net.email.ne.jp)
twitter http://twitter.com/pppiroto

Copyright© 矢木 浩人 All Rights Reserved.