Modern C# 入門の備忘録 - Linq入門編


this.Is何

  • C#歴4ヶ月になったぼくが今まで覚えてきたことを適当に書きなぐっていくやつ。
  • 覚えるまでに躓いた部分とかを重点的に書いていきたい。
  • C#を始めたばかり、または使ってるけど詳しいことは分かってない人向けです。
  • まだまだ経験が浅いので全体的に間違っている可能性が高いです。
  • でももし何か間違ったことを書いてたらC#の神様が瞬速で訂正してくれると思うので、ぼくの理解も深まってみんなハッピー!

って感じを目指してがんばります。

C#とかいう言語

  • C#はマイクロソフトの……これはもういいよね。
  • とりあえずC++のようなJavaのような文法に近い言語。だった。昔は。
  • ラムダ式とかLinqが導入された後のいわゆるC#3.0以降のModern C#になるとかなり独自路線を突っ走った新言語のような、そうでもないような。
  • 静的型付けのオブジェクト指向を骨組みに、関数型言語とか動的言語の利点を取り込んでメリットだけ享受しよう!みたいな、わりと貪欲な進化を続けてきた感じの。

C#勉強してた時に感じた問題点

大きい進化をし続けてるっていうことは、その分ゴミの生産量も多い。既に過去のものになったやり方とかが今もネットで解説記事が掲載されてるんですよね。でも、勉強してる時はそれが既に推奨されないものなのかどうか判断がつかない。名前付きデリゲートの作り方とかマジで覚える必要なかったよね
最先端のコード以外覚える価値は薄いのに、その最先端の機能の解説がどこも「古い方法と比較したときの改善点」という視点で書かれていて、それらを理解するためにはまず古いものを理解しておかないといけなくなってたりする。結局ぼくも再帰的に最古の時代のものから順にたどって覚える羽目になりました。
とりあえず書ければいいっていうなら最速で覚えられる言語だと思います。きちんと覚えるにはわりと学習コスト高い。

なので、

今回は新しい技術から覚えようとしても理解しやすいように書いていけたらいいなと思います。


本題

入ります。

とりあえずLinqしよ!

C#といえば、Linqです。
LinqがあるからC#をやってるようなもんです。
今日はLinqについて書いていこうと思います。
内部の実装の仕組みなどについては書きません、主にLinqの思想、使い方とその考え方について書きます。
Linqを使いましょうとは直接は書きませんが、この記事でLinqを使うメリットそのものを感じ取ってもらえたらいいなって思います。
あ、ぼくの中でクエリ式は滅んだのでメソッドチェインで書いていきますよろしくお願いします。
何か間違ってることが書いてあったら全力でツッコミお願いします。

Linqとは?

Linqっていうのは、SQLの

ではありません。SQLは全く関係ありません。(あ、メソッド名の由来とか、チョットは、関係、あるけど、使う上でほぼ関係ないのは事実です)

Linqっていうのは

Linqはコレクション(配列やリスト)をターゲットに処理を行うメソッドを集めたライブラリです。
一言で言うと、今までループなどを使って自前でやっていたよくある処理を代替してくれる存在です。

Linqっていうのは、関数の

Linqは関数型言語を参考に作られたものです。(だよね?)
関数型言語の使えたらうれしい機能を一部C#に持ってきたのがLinqだと思っておけばいいです。
あ、今回関数がどうとかいろいろテキトーなこと言ってますけど、関数の定義とかは若干曖昧にしてます。
実はちゃんとした関数型言語はさわったことないですゴメンナサイ

Linqを覚えよう

とりあえずSelectとWhereだけ覚えておけばなんとかなるのじゃ

(本当です)

Selectの使い方

Selectは、コレクションの中身を全て別のものに変換して次に渡すやつです。

//[intのコレクション]→
.Select(x => x.ToString()) // (int x)をToString()でstringに変換
//→[stringのコレクション]

コレクションの値を一括して変換したい場面は非常に多いのでこれが最も使用頻度の高いメソッドです。
Selectメソッドの引数は、元のコレクションから要素を一つ引数に取って、好き勝手変換した結果をreturnする関数です。
(正しく言うと関数じゃなくてデリゲートなんだけど、やってることが関数のそれなので関数と呼ぶことにする。デリゲートっていう名前わかりづらい。簡単に言うと、メソッドそのものを変数として引数に渡したり代入したりするものがデリゲート。
(int)型、(string)型、(intを引数に取ってstringを返すメソッド)型←これがデリゲート、みたいな感じ。)

Whereの使い方

Whereは、コレクションの値を検証して、条件を満たしたものだけ通すメソッドです。

//[intのコレクション]→
.Where(x => x > 10) // (int x)が10より大きいものだけ通す
//→[intのフィルターされたコレクション]

コレクションをフィルターしたい場面、ありますよね。主には、nullが紛れ込んでいたら弾きたい場面とか。
Whereメソッドの引数は、コレクションから要素を一つ受け取って、値をチェックしてboolを返す関数です。

その他のメソッド

は、ほとんど上の2つと同じように使える。
実際SelectとWhereの使い方さえ覚えていればだいたいなんとかなる。
SelectManyはちょっと覚えておいた方がいいかもしれない。

Linqの中身のこと

遅延評価って?

「Linqは遅延評価である」なんて色んな場所に書いてありますが、遅延評価ってどういうこと?とか、なんで遅延評価なんだ?みたいな疑問を持った人もいると思います。
なので、ここではLinqの遅延評価というのがどういうものなのか書いてみます。

関数であるということ

たとえば、以下のような以下略。

ToaruClass
int nanika = 10;

これに、こんな、

ToaruClass
int Value = nanika;
int GetValue()
{
    return nanika;
}

変数とメソッドを用意します。
ここで、nanika = 20;と値を変更したとします。
するとこのとき、
var result1 = Value;
var result2 = GetValue();
この二つの中身はどうなっているでしょうか?
result1: 10
result2: 20
こうなります。
そんな当たり前の事がなんなんだ?って話ですが、
この二つのresultの結果が異なる理由がLinqの遅延評価の本質です。
関数(メソッド)は実行された時に初めて値を計算します。そして、Linqのメソッドの引数は関数です。
Linqのメソッドは値が通る時に初めてその値を処理するので、遅延評価以外にはなりえないのです。
値そのものを実際に要求しているのはforeachステートメントなどの列挙機構なので、値が要求されるまで関数が実行されないのは自然な挙動なのです。

Linqクエリはそれ自体が関数

以下の様なLinqクエリを作ったとします。

ToaruQuery
var query = source // stringのListだとする
    .Where(x => !string.IsNullOrEmpty(x)) // 値がnullや空文字だったら除外する
    .Select(x => "[" + x + "]"); // []で囲む文字列に加工する

このLinqクエリは遅延評価です。queryの中身を取り出す時に初めて評価されます。
どういうことかというと、このクエリは以下のように表現することが出来ます。
「stringのListを引数に取り、空要素を除外してから全要素を"[]"で囲む形に変換したものを返す'関数'である」
関数なら、実行した時に値を計算しますよね。つまり、そういうことです。
なので、Linqのクエリ式は以下のように言い換えることが出来ます。
「Linqのクエリ式というのは関数の動的な構築である。」

Linqとデバッガ

初めてLinqの式をステップ実行したとき、カーソルの飛び方が意味不明すぎて面食らったことを覚えてます。
しかし上で書いたように、Linqのクエリ式は関数の構築であると考えてみると、その動きも納得できます。
たとえば上の式で、var query = の部分では動的に関数を組み立てたものを代入して、foreachで回すときにその関数を実行して値を評価していく
そう考えると、Linq式をステップ実行したときにステップする順序もしっくりきませんか?

ToList()で即時評価?

ToList()のやってることを文にしてみましょう。
「Linqクエリという関数を実行して、得られた値を並べてリストにする」
関数、実行してますね。なのでその時点で評価されています。

Linqの実行順序

もうわかったと思いますが、Linqで連結された関数はそれ自体が一つの関数とみなせるものになります。
よって、以下の様なクエリ

var query = source // 適当なコレクションに
    .Select(x => x) // 何もしない
    .Select(x => x) // Selectを
    .Select(x => x); // 3連打

を列挙すると、各メソッドの実行順序は、

(source[0]に対して)Select1> Select2> Select3> (source[1]に対して)Select1> Select2> Select3> ...

という順番になります。Linqのメソッド一つずつループが回るわけではないんですね。
つまり、途中でWhereとかを挟むと、それ以降の処理が行われなくなるので、
無駄なメソッドの実行が省略され、効率が良くなります。
もしループで処理しようとしたらあたまがおかしくなりそうな複雑な変換式をLinqで組んでも、わりと効率よく処理してくれます。

その他どうでもいいこと

Linq to Object

ここまで説明してきたのは全てコレクション処理ライブラリとしてのLinq、正式名称Linq to Objectと呼ばれるものです。
が、使うのは9割これだと思うので、これだけ覚えておけばいいんです。
というより、Linq自体が同じ書き方でいろんなターゲットを同じようにに処理できる仕組みとして設計されているので、本当にこれだけ覚えておけばいいんです。
ほかの種類のLinqには、書いたクエリがSQLクエリに変換されるLinq to SQLとかがあったりします。

Linqと型推論の話

Linqのメソッドは本来ジェネリック型引数を必要とします。
たとえば、Select()intのコレクションをToString()してstringのコレクションに変換する場合、本当は

.Select<int, string>((int x) => x.ToString())

って書かないといけません。
ですが、元になるコレクションの型がintだと判れば、別に引数がintであることを明記しなくてもわかります。
同様に、return x.ToString();の戻り値がstringであることも明確です。
なので、このときは.Select(x => x.ToString())と書くだけで動作します。
Linqのチェーン全てで型推論が伝播していくので、一つも型引数を書く必要はありません。
Linqと型推論の相性は最高です。
ちなみに、nullを戻り値にする場合、型を推論することが出来ないので、

.Select(x => null) // なんの意味があるのか謎

みたいなのは型引数を省略することは出来ません。
ただし、

.Select(x => null as string) // string型のnullになる

こんな感じで書くと型推論が効くようになります。

Linqの引数には副作用を書かないように気をつける?

副作用を避ける という一言はわりと色んな所で見かけますが、副作用って実際にはどういうもので、どうして避けなければならないのか?
関数型言語でいう「関数」というものが「値を渡すと計算された結果が返ってくる」という当然っぽい文で説明できるためには、「値を渡すと計算された結果が返ってこなければならない」のです。
そして、関数が渡した値以外のものに干渉してしまうと、「値を渡しても計算された結果が返ってこないかもしれない」ということになる可能性が出てきます。
こういうものを避けるために、「関数は関数の中で閉じている必要がある」すなわち「副作用を排除する」ということになります。
例えば極端な例。

int count = 0; // 外側に変数
var sideEffect = Enumerable.Range(0, 10) // [0, 1, 2,,,9] の配列みたいなの
    .Where(_ => (5 > count++)); // 外の変数にアクセス!

これを列挙すると、1回目は0から4の値が出力されます。2回目からは当然何も出力されません。
同じ入力に対して同じ出力が返ってこないのは大抵の場合好ましくない結果を生みます。
これがもし意図した結果であっても、後々単純なミスでのバグを生みやすい危険な存在となります。
こういう事を最初からやらないことでバグを回避するというノウハウが「副作用を避ける」の一言で表現されています。
まあ、関数型言語でも副作用を認めるものだとクロージャを悪用してインクリメントに使う例とかありますし、
C#もラムダ式の外部変数キャプチャ機能とかは副作用どんとこいみたいな存在ですし……(そもそもオブジェクト指向プログラミングって副作用を全力で活用するプログラミング方式のことだと思ってました)
副作用を避けるべきなのは、あくまでLinqの一連のクエリ中での副作用です。ここに副作用を混ぜると意図しないバグを生み出しやすいのです。
副作用を回避するには、とりあえずラムダ式の内部で外の変数にはアクセスしない。 ラムダ式の内部で副作用のある関数を実行しない。この二つを守ればとりあえず大丈夫。な気がする。

Skip()はなるべく根本に置いたほうがいい話

以下の二つの式を見てみます。

Skip()する場所が違うだけ
var skip1 = Enumerable.Range(0, 4)
    .Select(x => x) // Select1 とりあえずなにもしない
    .Select(x => x) // Select2 こっちもなにもしない
    .Skip(2);

var skip2 = Enumerable.Range(0, 4)
    .Select(x => x)
    .Skip(2)
    .Select(x => x);

この二つを評価するとどちらも[ 2, 3 ]が得られます。
しかし、この二つの式でSelectの実行回数と順序は以下のようになります。

skip1: Select1, Select2, Select1, Select2, Select1, Select2, Select1, Select2
skip2: Select1, Select1, Select1, Select2, Select1, Select2

流れるようにクエリを書いてるとわりとskip1のように書いてしまいがちですが避けましょう。
Skip()Where()などのフィルターメソッドは内容が変わらない範囲でなるべく根元でカットした方が効率が良くなります。当然ですね。
こういうのを考慮していかないとLinq遅いって言われちゃう。

ちなみに、この例でSkip()の代わりにTake()を置いた場合、どの場所に置いてもSelectの評価回数は同じになります。
Skip()とTake()は真逆の存在みたいなものですが、実際の挙動は少し違いのようなものがあります。
Skip()は、「入った値を指定された数だけ破棄して続きを通す」メソッドです。
Take()は、「通った値が指定された数に到達したら、強制的にシーケンスを終了にする」メソッドです。
Take()はフィルターする訳じゃなくて、その時点で列挙を終了させることで以降の無駄な評価が行われないようになってるわけですね。

まとまらない

なんか自分でも何を書いてるのかわからなくなってきたぞ!
とりあえず強引にまとめました

  • Linqは超簡単にコレクションを加工できる便利メソッド群である
  • Linqのメソッドは関数である
  • Linqのクエリ式は関数を合成して作った関数である
  • Linqは関数に値を通して処理していくので自然と遅延評価になる
  • C#は良い言語
  • 他言語にもLinqの移植ライブラリあるから使えるよ!

以上になりますありがとうございました。