#はじめに
VB.NETにて、LambdaやDelegateを取り扱う際にC#とは別の挙動をすることがあるのでまとめてみました。
尚、書き手はC#を主に扱っているので、VB.NETのサンプルコードに不手際があるかもしれませんので、何か気づかれましたらコメントをいただければ幸いです。
#VB.NETにできて、C#にできないこと:ラムダに対する型推論
##C#の場合
C#では、ラムダ式に対する推論は許容されません。
var add = (int x, int y) => x + y;
これは、ラムダ式の解釈が決定的では無いことに起因します。上記の場合、推論しうる型は、
- System.Func
- System.Linq.Expression>
- 独自に定義されたdelegate
- 独自に定義されたdelegateのExpression
のように、一意に決定できないので、推論が失敗しコンパイルエラーが発生するコトになります。
##VB.NETの場合
しかしながら、VB.NETでは、C#で許容されなかったラムダ式に対する推論が許容されます
Dim add = Function(x As Integer, y As Integer) x + y
この場合、addの型はどのように解釈されるのでしょうか?コンパイルしたモノをILSpyでデコンパイルしてみましょう
Imports System
Module Module1
Sub Main()
Dim add = Function(x As Integer, y As Integer) x + y
End Sub
End Module
デコンパイル結果は以下のようになります。(一部抜粋)
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)"というデリゲートができあがっています。これを、を追いかけてみると、以下のように定義されています。
<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を使いたい場合は、下記のような書き方が必要になるでしょう
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
#コンパイル時に生成された匿名デリゲート型からの変換
さて、それでは、先の例で示したコンパイラが自動生成した匿名デリゲート型は他のデリゲート型へ変換可能なのでしょうか?それを見ていきたいと思います。
以下にサンプルを示します。
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
このように、匿名デリゲート型は引数及び戻り値が一致していれば、暗黙的に変換可能であることがわかります。
但し、これは、匿名デリゲート型に対する特別な対応で有り、以下のように明示的なデリゲート型を変換することは許容されていないので、注意が必要です。
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
##コンパイル結果がどうなるのか
それでは、先に検証したような変換を行った場合、コンパイラはどのようにコンパイルするのか検証してみたいと思います。
先の例をデコンパイルした結果は下記のようになります。
(一部改変)
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
ここからわかるように、元になるデリゲートをラップする形で他のデリゲートを生成しています。
この結果、変換先のデリゲートは"デリゲートを呼ぶデリゲート"となるコトから、実行効率に若干のオーバーヘッドが存在することになります。
##呼び出しにかかるオーバーヘッドを検証する
最後に、どの程度のオーバーヘッドが介在するのか以下のサンプルコードで確認してみました。
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#と異なり、型推論を用いてラムダ式を参照させることや、そのラムダ式を同一の引数と戻り値を持つ他のデリゲートへ暗黙的に変換可能となっています。
これらを満たすために、コンパイラが後ろ側で何をしているのか知ることは、何かあった際の一助となるかなと思いまとめてみました。