Selectメソッド
LINQで一番利用されている(と思う)Selectメソッドです。コレクションの全要素に対して処理が行われます。
例えば、適当な数値のコレクション全要素に対して3を掛けるような処理を考えてみましょう。
int[] values = { 1, 9, 5, 6, 8, 6, 2, 5, 3 }; //従来の方法 int[] res1 = new int[values.Length]; for (int i = 0; i < values.Length; ++i) { res1[i] = values[i] * 3; } //LINQ int[] res2 = values.Select(x => x * 3).ToArray();
Dim values As Integer() = {1, 9, 5, 6, 8, 6, 2, 5, 3} '従来の方法 Dim res1(values.Length) As Integer For i As Integer = 0 To values.Length - 1 res1(i) = values(i) * 3 Next 'LINQ Dim res2() As Integer = values.Select(Function(x) x * 3).ToArray()
このようにLINQの最大の特徴は、for文やforeach文といった繰り返し処理が無くなることです。これによって無駄に長いコードや不要な変数宣言が無くなります。
LINQを始める前に・・・
LINQを始める前にいくつか基礎知識として情報を紹介しておきます。
SQLとの関連
LINQ to Objectにおいて、構文がSQLと似ていますが、SQLと一切関係はありません。例えば、
int[] values = { 1, 2, 3, 4, 5 }; var q = from x in values where x % 2 == 0 orderby x select x * 3;
Dim values As Integer() = {1, 2, 3, 4, 5} Dim q = From x In values Where x Mod 2 = 0 Order By x Select x * 3
こんな感じでSQLでおなじみのキーワードが出てきますが全く関係ないです。ただ、意味としてはSQLとほぼ同じと考えて良いでしょう。
速度
よく「LINQは遅い」という記事を見ますが、どれくらい遅いか確認してみましょう。10万個の数値配列にすべて2をかけてListコレクションに再格納してみます。
int[] values = Enumerable.Range(1, 100000).ToArray(); //foreach { Stopwatch sw = new Stopwatch(); List<int> res = new List<int>(); sw.Start(); foreach (int v in values) { res.Add(v * 2); } sw.Stop(); Console.WriteLine(sw.Elapsed); } //LINQ { Stopwatch sw = new Stopwatch(); List<int> res = new List<int>(); sw.Start(); res = values.Select(x => x * 2).ToList(); sw.Stop(); Console.WriteLine(sw.Elapsed); }
Dim values As Integer() = Enumerable.Range(1, 100000).ToArray() 'For Each With Nothing Dim sw As New Stopwatch Dim res As New List(Of Integer) sw.Start() For Each v As Integer In values res.Add(v * 2) Next sw.Stop() Console.WriteLine(sw.Elapsed) End With 'LINQ With Nothing Dim sw As New Stopwatch Dim res As New List(Of Integer) sw.Start() res = values.Select(Function(x) x * 2).ToList() sw.Stop() Console.WriteLine(sw.Elapsed) End With
00:00:00.0014226 00:00:00.0049621
確かにforeach文の方が速度に分があるようです。しかし、10万回の処理でその差は0.004秒以下となり、そこまで気にするほどの遅さではありません。
三項演算子の有効活用
課題
例えば以下のような処理を考えます。
bool syoriFlg = true; string naiyo = ""; if (syoriFlg ) { naiyo = "hoge"; } else { naiyo = "piyo"; }
Dim syoriFlg As Boolean = True Dim naiyo As String = "" If syoriFlg Then naiyo = "hoge" Else naiyo = "piyo" End If
処理フラグの条件によって変数の値を変える場合です。
リファクタリング
可読性が落ちている原因は、やはり条件分岐です。条件分岐が増えれば増えるほどフローチャート上右に広がってしまいます。これらを改善するには2パターンあります。
①三項演算子を利用する方法
これは真偽がほぼほぼ同じ割合で発生する場合に有効です。
bool syoriFlg = true; string naiyo = syoriFlg ? "hoge" : "piyo";
Dim syoriFlg As Boolean = True Dim naiyo As String = If(syoriFlg, "hoge", "piyo")
三項演算子はうまく使えば非常に見やすくなりますので覚えておきましょう。
②初期値を設定する方法
発生頻度が明らかに傾いている場合、例えば真:偽=9:1とかの場合です。これは初期値をほぼ発生する方にしておき、あまり発生しない方を変更処理として記述します。
bool syoriFlg = true; if (!syoriFlg) naiyo = "piyo"; If
Dim naiyo As String = "hoge" If not syoriFlg Then naiyo = "piyo"
ラムダ式の基本
ラムダ式(lambda expression)自体は関数型言語で良く使われる機能の一つですが、デリゲートやLINQを利用した際のメソッド引数部分をより簡潔に記述できるようにとC#3.0(VB.NET9)から導入されました。
ラムダ式の構文は以下の通り、式形式と文形式の2種類があります。
//式形式 左辺 => 右辺 //文形式 左辺 => { 右辺 };
'式形式 Sub(左辺) 右辺 Function(左辺) 右辺 '文形式 Sub(左辺) 左辺 End Sub Function(左辺) 左辺 End Function
式形式は短くまとめやすいので、簡単な処理であればこちらの方が優位です。逆に複数行の少し複雑な処理をする場合は文形式の方がよいでしょう。
多分例を見ないとピンとこないので、適当な例を挙げてみましょう。例えば以下のような数値配列の各要素に2を掛ける処理を考えてみます。
int[] values = { 1, 2, 3, 4, 5 }; { int res = 0; foreach (int v in values) { res += v * 2; } Console.WriteLine(res); }
Dim values As Integer() = {1, 2, 3, 4, 5} Dim res As Integer = 0 For Each v As Integer In values res += v * 2 Next Console.WriteLine(res)
普通に書いたらこうなりますよね。
今回はLINQを用いたラムダ式を例とします。集計メソッドsumを利用します。sumは各要素の合計値を計算してくれるメソッドです。
C#の場合、ラムダ式が導入される前は匿名メソッドが利用されていましたので、併せて比較してみましょう。
//匿名メソッド int res = values.Sum(delegate (int v) { return v * 2; }); Console.WriteLine(res); //ラムダ式 int res = values.Sum(v => v * 2); Console.WriteLine(res);
匿名メソッドを利用したことで、foreachのループ処理が無くなりました。これでもかなりシンプルになりましたが、やはりデリゲートメソッドの部分が少しややこしいのが気になります。ラムダ式はこういった煩わしさから解放するために導入されました。
ポイントはreturnと{}が無くなり、シンプルになったことです。
VB.NETは匿名メソッドが存在しません。C#と異なり、Sub、Functionキーワードが必要になりますので、少し冗長になります。
Dim res As Integer = values.Sum(Function(v) v * 2) Console.WriteLine(res)
それでもFor Each文が無くなることで非常に読みやすくなります。
ポリモーフィズム(インターフェース)
インターフェースでポリモーフィズムを実現します。
インターフェースはクラスの規程のみを定めたものとなる為、抽象クラスと同様にポリモーフィズムを比較的容易に実現することが可能です。
継承の時と同じように、何かメッセージを表示する共通メソッドをインターフェースに定義しておき、それぞれのクラスに派生させる場合を考えてみます。
interface IClass { void ShowMessage(); } public class Class1 : IClass{ public void ShowMessage() { Console.WriteLine("クラス1"); } } public class Class2 : IClass { public void ShowMessage() { Console.WriteLine("クラス2"); } } public class Class3 : IClass { public void ShowMessage() { Console.WriteLine("クラス3"); } }
Interface IClass Sub ShowMessage() End Interface Public Class Class1 : Implements IClass Public Sub ShowMessage1() Implements IClass.ShowMessage Console.WriteLine("クラス1") End Sub End Class Public Class Class2 : Implements IClass Public Sub ShowMessage2() Implements IClass.ShowMessage Console.WriteLine("クラス2") End Sub End Class Public Class Class3 : Implements IClass Public Sub ShowMessage3() Implements IClass.ShowMessage Console.WriteLine("クラス3") End Sub End Class
これらもポリモーフィズムの実現により同じメソッド名で違った処理を呼ぶことが可能となります。
List<IClass> icls = new List<IClass>() { new Class1(), new Class2(), new Class3() }; foreach (IClass c in icls) { c.ShowMessage(); }
Dim icls As New List(Of IClass) From {New Class1(), New Class2(), New Class3()} For Each c As IClass In icls c.ShowMessage() Next
インデクサ
ユーザー定義型(構造体やクラス)のオブジェクトに対して、配列のような特定要素を取得・設定できる仕組みをインデクサと言います。VB.NETでは既定のプロパティとも言います。
インデクサはメソッドとプロパティの両方の性質を持ち合わせた機能です。
- getter/setterアクセサを設定する
- getter/setterアクセサのアクセス修飾子の設定や省略はプロパティと一緒
- インデクサ自体に名前をつけることはできない
- 添字の型は何でも良い
- 添字は2つ以上あっても良い
- オーバーロードできる
public class Test { private string[] msg = { "最初", "まんなか", "最後" }; public string this[int index] { get { return msg[index]; } set { msg[index] = value; } } public string this[int index1,int index2] { get { return msg[index1] + msg[index2]; } } } Test t = new Test(); Console.WriteLine(t[1]); Console.WriteLine(t[1,2]);
Public Class Test Private msg As String() = {"最初", "まんなか", "最後"} Default Public Property Item(ByVal index As Integer) As String Get Return msg(index) End Get Set(ByVal value As String) msg(index) = value End Set End Property Default Public Property Item(ByVal index1 As Integer, ByVal index2 As Integer) As String Get Return msg(index1) & msg(index2) End Get End Property End Class Dim t As New Test Console.WriteLine(t.Item(1)) Console.WriteLine(t.Item(1, 2))
最初 まんなか 最後
インデクサとよく似た機能で名前付きプロパティという機能があります。これはVB.NETの機能でC#には残念ながらありません。
sasaki816.hatenablog.com