VB.NETでラムダ式やらデリゲートを扱う際のちょっとした注意点

#はじめに
VB.NETにて､LambdaやDelegateを取り扱う際にC#とは別の挙動をすることがあるのでまとめてみました｡

尚､書き手はC#を主に扱っているので､VB.NETのサンプルコードに不手際があるかもしれませんので､何か気づかれましたらコメントをいただければ幸いです｡

#VB.NETにできて､C#にできないこと:ラムダに対する型推論

##C#の場合

C#では､ラムダ式に対する推論は許容されません｡

```csharp:Invalid.cs
var add = (int x, int y) => x + y;
```

これは､ラムダ式の解釈が決定的では無いことに起因します｡上記の場合､推論しうる型は､

* System.Func<int,int,int>
* System.Linq.Expression<Func<int,int,int>>
* 独自に定義されたdelegate
* 独自に定義されたdelegateのExpression

のように､一意に決定できないので､推論が失敗しコンパイルエラーが発生するコトになります｡

##VB.NETの場合

しかしながら､VB.NETでは､C#で許容されなかったラムダ式に対する推論が許容されます

```vb.net:Valid.vb
Dim add = Function(x As Integer, y As Integer) x + y
```

この場合､addの型はどのように解釈されるのでしょうか?コンパイルしたモノをILSpyでデコンパイルしてみましょう

```vb.net:SampleA.vb
Imports System

Module Module1
    Sub Main()
        Dim add = Function(x As Integer, y As Integer) x + y
    End Sub
End Module
```

デコンパイル結果は以下のようになります｡(一部抜粋)

```vb.net:Decompiled.vb
Imports Microsoft.VisualBasic.CompilerServices
Imports System
Imports System.Runtime.CompilerServices

Namespace ConsoleApplication5
	Friend Module Module1
		<STAThread()>
		Public Sub Main()
			Dim add As VB$AnonymousDelegate_0(Of Integer, Integer, Integer) = Function(x As Integer, y As Integer) x + y
		End Sub
	End Module
End Namespace
```

さて､デコンパイル結果を眺めてみると､元のソースコードで定義した覚えの無い､"VB$AnonymousDelegate_0(Of Integer, Integer, Integer)"というデリゲートができあがっています｡これを､を追いかけてみると､以下のように定義されています｡

```vb.net:GeneratedDelegate.vb
<DebuggerDisplay("<generated method>", Type = "<generated method>"), CompilerGenerated()>
Friend Delegate Function VB$AnonymousDelegate_0(Of TArg0, TArg1, TResult)(x As TArg0, y As TArg1) As TResult
```

以上から､VB.NETでは､ラムダ式を推論させた場合､このような隠しデリゲートをコンパイル時に生成していると言うことがわかります｡

従って､一般的なSystem.FuncやSystem.Actionを使いたい場合は､下記のような書き方が必要になるでしょう

```vb.net:SampleB.vb
Imports System

Module Module1
    Sub Main()
        'CTypeを利用する
        Dim add = CType(Function(x As Integer, y As Integer) x + y, Func(Of Integer, Integer, Integer))

        '変数の型を明示する
        Dim sayHello As Action = Sub() Console.WriteLine("hello world")

    End Sub
End Module
```

#コンパイル時に生成された匿名デリゲート型からの変換
さて､それでは､先の例で示したコンパイラが自動生成した匿名デリゲート型は他のデリゲート型へ変換可能なのでしょうか?それを見ていきたいと思います｡
以下にサンプルを示します｡

```vb.net:ConvertSample.vb
Imports System

Friend Delegate Sub MyAction()
Friend Delegate Function MyFunction(x As Integer, y As Integer) As Integer
Module Module1

    Sub Main()
        Dim anonymousAction = Sub() Console.WriteLine("hello world")
        Dim action As Action = anonymousAction
        Dim myAction As MyAction = anonymousAction

        Dim anonymousFunction = Function(x As Integer, y As Integer) x + y
        Dim func As Func(Of Integer, Integer, Integer) = anonymousFunction
        Dim myFuncion As MyFunction = anonymousFunction

    End Sub
End Module
```

このように､匿名デリゲート型は引数及び戻り値が一致していれば､暗黙的に変換可能であることがわかります｡
但し､これは､匿名デリゲート型に対する特別な対応で有り､以下のように明示的なデリゲート型を変換することは許容されていないので､注意が必要です｡

```vb.net:Invalid.vb
Imports System

Friend Delegate Sub MyAction()
Friend Delegate Function MyFunction(x As Integer, y As Integer) As Integer
Module Module1

    Sub Main()
        Dim action As Action = Sub() Console.WriteLine("hello world")
        Dim myAction As MyAction = CType(action, MyAction)

        Dim func As Func(Of Integer, Integer, Integer) = Function(x As Integer, y As Integer) x + y
        Dim myFunc As MyFunction = CType(func, MyFunction)

    End Sub
End Module
```

##コンパイル結果がどうなるのか
それでは､先に検証したような変換を行った場合､コンパイラはどのようにコンパイルするのか検証してみたいと思います｡

先の例をデコンパイルした結果は下記のようになります｡
(一部改変)

```vb.net:Decompiled.vb
Imports System

Friend Delegate Sub MyAction()
Friend Delegate Function MyFunction(x As Integer, y As Integer) As Integer
Module Module1

    Sub Main()
        Dim anonymousAction = Sub() Console.WriteLine("hello world")
        Dim action As Action = New Action(AddressOf anonymousAction.Invoke)
        Dim myAction As MyAction = New MyAction(AddressOf anonymousAction.Invoke)

        Dim anonymousFunction = Function(x As Integer, y As Integer) x + y

        Dim func As Func(Of Integer, Integer, Integer) = _
            New Func(Of Integer, Integer, Integer)(AddressOf anonymousFunction.Invoke)

        Dim myFuncion As MyFunction = New MyFunction(AddressOf anonymousFunction.Invoke)

    End Sub
End Module
```

ここからわかるように､元になるデリゲートをラップする形で他のデリゲートを生成しています｡

この結果､変換先のデリゲートは"デリゲートを呼ぶデリゲート"となるコトから､実行効率に若干のオーバーヘッドが存在することになります｡

##呼び出しにかかるオーバーヘッドを検証する
最後に､どの程度のオーバーヘッドが介在するのか以下のサンプルコードで確認してみました｡

```vb.net:Sample.vb
Imports System
Imports System.Diagnostics
Imports System.IO

Module Module1
    
    Sub Main()

        Dim add = Function(x As Integer, y As Integer) x + y


        Const cnt As Long = CType(Integer.MaxValue / 4, Long)

        Dim addA = Function(x As Long, y As Long) x + y
        Dim addB As Func(Of Long, Long, Long) = addA

        Dim accum = addA(0, 0)
        Console.WriteLine(accum)
        accum = addB(10, 20)
        Console.WriteLine(accum)

        accum = 0

        Dim watch = New Stopwatch()


        Using wtr = New StreamWriter("result.csv", False)

            wtr.WriteLine("Category,Elapsed(ms)")

            For j = 0 To 10

                watch.Restart()
                For i = 0 To cnt
                    accum = addA(accum, i)
                Next
                watch.Stop()
                wtr.WriteLine("Anonymous," + watch.ElapsedMilliseconds.ToString())

                FullCollect()

                accum = 0L
                watch.Restart()
                For i = 0 To cnt
                    accum = addB(accum, i)
                Next
                watch.Stop()
                wtr.WriteLine("Func," + watch.ElapsedMilliseconds.ToString())

            Next


        End Using



    End Sub

    Sub FullCollect()

        GC.Collect()
        GC.WaitForPendingFinalizers()
        GC.Collect()

    End Sub

End Module
```

そして､その結果は以下の通りになります

| TrialCount | Via System.Func(ms)| Via Anonymous(ms) |
|:-----------|-------------:|------------:|
|0|2320|1793
|1|2316|1736
|2|2299|1729
|3|2282|1715
|4|2277|1705
|5|2281|1708
|6|2285|1711
|7|2281|1714
|8|2286|1713
|9|2283|1715
|10|2280|1714

このように､10回の試行に対して､匿名デリゲート型から呼び出した場合は平均で1,723ms､他方変換したSystem.Func経由の場合は2,290msとなり､気にするほどでは無いとは思いますが､さりとて誤差とは言えない程度の差があることがわかりました｡

#まとめ
VB.NETはC#と異なり､型推論を用いてラムダ式を参照させることや､そのラムダ式を同一の引数と戻り値を持つ他のデリゲートへ暗黙的に変換可能となっています｡
これらを満たすために､コンパイラが後ろ側で何をしているのか知ることは､何かあった際の一助となるかなと思いまとめてみました｡