佐々木屋

技術的なことから趣味まで色々書きます

型推論

暗黙的に型付けする変数のことを型推論と言います。

C#はvarキーワードを利用します。

//通常の書き方
TestClass cls = new TestClass();

//型推論
var cls = new TestClass();

VB.NETはDimキーワードのみにしてAs以降の型を指定しないと型推論となります。

'通常の書き方
Dim cls As TestClass = New TestClass()

'型推論
Dim cls = New TestClass()

型推論を使うことで、変数clsは暗黙的にTestClass型となります。


VB.NETユーザーのみ補足

型推論はVB6時代のVariant型と同じような記述の仕方なので、VB.NETユーザーは少し抵抗があるかもしれません。しかし、Variant型とDimによる型推論は全く別物です。型推論コンパイル時に右辺に設定されているデータ型に応じて変数の型が決定するのです。

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秒以下となり、そこまで気にするほどの遅さではありません。

文法

LINQの文法は最初のSQLとの関連で紹介した「クエリ構文」と、速度で紹介した「ラムダ式を利用したメソッド構文」がありますが、現在は後者である「ラムダ式を利用したメソッド構文」が主流です。

三項演算子の有効活用

課題

例えば以下のような処理を考えます。

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"

ラムダ式の基本

LINQの話をする前にラムダ式を説明しておきます。

ラムダ式(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