しばたテックブログ

気分で書いている技術ブログです。

VB.NETでのDLRと遅延バインディングの関係

本エントリはVisual Basic Advent Calendar 2014 - Qiitaの12/13日の記事になります。

VB.NETにおけるDLR(動的言語ランタイム)と遅延バインディングの関係について適当に語ります。

遅延バインディングを有効にするには

はじめに、VB.NETで遅延バインディングを有効にするには以下の設定をする必要があります。

  1. Option Strict Offにする。
  2. 変数の型をObject型にする。
    • Option Infer Onの場合は明示的にAs Objectと型宣言をする。

この設定は昔からのものなのでVB使いにとってはおなじみのものかと思います。

動的オブジェクトを扱うには

次に、VB.NETでDLRを使用し動的オブジェクトを扱うには遅延バインディングを有効にするのと同様の設定をする必要があります。

  1. Option Strict Offにする。
  2. 変数の型をObject型にする。
    • Option Infer Onの場合は明示的にAs Objectと型宣言をする。

C#の場合はdynamicキーワードを使って変数を宣言するだけなのに比べるとVB.NETの場合は結構めんどうくさいですね。

DLRと遅延バインディングの関係

DLRと遅延バインディングの関係を調べるために以下の単純なコンソールアプリで検討してみます。
ターゲットフレームワークは.NET Framework 4.5.2です。

サンプルプログラム

Program.vb (Main)

Option Strict Off
Imports System.Dynamic
''' <summary>
''' サンプルプログラムです。
''' </summary>
Module Program

    ''' <summary>
    ''' プログラムのエントリポイントです。
    ''' </summary>
    Public Function Main(args As String()) As Integer

        '例1) 通常のクラスを普通に宣言した場合
        Dim Obj1 As Class1 = New Class1()
        Obj1.Key = 1
        Obj1.Value = "Value1"
        Console.WriteLine(String.Format("Key={0}, Value={1}", Obj1.Key, Obj1.Value))

        '例2) 通常のクラスをObject型として宣言した場合(遅延バインディング)
        Dim Obj2 As Object = New Class1()
        Obj2.Key = 2
        Obj2.Value = "Value2"
        Console.WriteLine(String.Format("Key={0}, Value={1}", Obj2.Key, Obj2.Value))

        '例3) Dynamic型のクラスを使用した場合(動的オブジェクトの使用)
        Dim Obj3 As Object = New Class1Dynamic()
        Obj3.Key = 3
        Obj3.Value = "Value3"
        Console.WriteLine(String.Format("Key={0}, Value={1}", Obj3.Key, Obj3.Value))

        Console.ReadLine()
        Return 0
    End Function

End Module

Class1.vb

''' <summary>
''' テスト用のクラスです。
''' </summary>
''' <remarks>
''' 単純にキーと値のプロパティを持つだけのクラスです。
''' </remarks>
Public Class Class1

    Public Property Key As Integer

    Public Property Value As String

End Class

Class1Dynamic.vb

Imports System.Dynamic
''' <summary>
''' テスト用のDynamicなクラスです。
''' </summary>
''' <remarks>
''' コードの単純化のためにDynamicObjectを継承させています。
''' プロパティとその値をDictionaryに保持するだけのクラスです。
''' </remarks>
Public Class Class1Dynamic
    Inherits DynamicObject

    Private _Values As Dictionary(Of String, Object) = New Dictionary(Of String, Object)()

    ''' <summary>
    ''' TryGetMemberメソッドのオーバーライドです。
    ''' </summary>
    Public Overrides Function TryGetMember(binder As GetMemberBinder, ByRef result As Object) As Boolean
        If _Values.ContainsKey(binder.Name) Then
            result = _Values(binder.Name)
            Return True
        End If
        Return False
    End Function

    ''' <summary>
    ''' TrySetMemberメソッドのオーバーライドです。
    ''' </summary>
    Public Overrides Function TrySetMember(binder As SetMemberBinder, value As Object) As Boolean
        If _Values.ContainsKey(binder.Name) Then
            _Values(binder.Name) = value
        Else
            _Values.Add(binder.Name, value)
        End If
        Return True
    End Function

End Class

で、実行結果はこんな感じでコンソールに出力されます。

Key=1, Value=Value1
Key=2, Value=Value2
Key=3, Value=Value3

逆アセンブル

このプログラムをILSpyでデコンパイルしてみると以下の様なコードになります。
(わかりやすさのため生のILではなくVB.NETの文法に戻した形で表示しています)

Imports Microsoft.VisualBasic.CompilerServices
Imports System
Imports System.Dynamic
Imports System.Runtime.CompilerServices

Namespace DynamicTest
    Friend Module Program
        <STAThread()>
        Public Function Main(args As String()) As Integer
            '例1)
            Dim [class] As Class1 = New Class1()
            AddressOf [class].Key = 1
            AddressOf [class].Value = "Value1"
            Console.WriteLine(String.Format("Key={0}, Value={1}", AddressOf [class].Key, AddressOf [class].Value))

            '例2)遅延バインディング
            Dim instance As Object = New Class1()
            NewLateBinding.LateSet(instance, Nothing, "Key", New Object()() { 2 }, Nothing, Nothing)
            NewLateBinding.LateSet(instance, Nothing, "Value", New Object()() { "Value2" }, Nothing, Nothing)
            Console.WriteLine(String.Format("Key={0}, Value={1}", RuntimeHelpers.GetObjectValue(NewLateBinding.LateGet(instance, Nothing, "Key", New Object(0 - 1) {}, Nothing, Nothing, Nothing)), RuntimeHelpers.GetObjectValue(NewLateBinding.LateGet(instance, Nothing, "Value", New Object(0 - 1) {}, Nothing, Nothing, Nothing))))

            '例3) 動的オブジェクトの使用
            Dim instance2 As Object = New Class1Dynamic()
            NewLateBinding.LateSet(instance2, Nothing, "Key", New Object()() { 3 }, Nothing, Nothing)
            NewLateBinding.LateSet(instance2, Nothing, "Value", New Object()() { "Value3" }, Nothing, Nothing)
            Console.WriteLine(String.Format("Key={0}, Value={1}", RuntimeHelpers.GetObjectValue(NewLateBinding.LateGet(instance2, Nothing, "Key", New Object(0 - 1) {}, Nothing, Nothing, Nothing)), RuntimeHelpers.GetObjectValue(NewLateBinding.LateGet(instance2, Nothing, "Value", New Object(0 - 1) {}, Nothing, Nothing, Nothing))))

            Console.ReadLine()
            Return 0
        End Function
    End Module
End Namespace

結果を見ると例2) 遅延バインディング、例3) 動的オブジェクトの使用の場合がNewLateBindingクラスのNewLateBinding.LateSetおよびNewLateBinding.LateGetを使用してオブジェクトのプロパティにアクセスする様になっており、全く差異の無いコードになっていることが分かります。

Microsoft.VisualBasic.CompilerServices.NewLateBinding

このNewLateBindingクラス(Microsoft.VisualBasic.CompilerServices.NewLateBinding)について、Reference Source(.NET 4.5.2)からソースを見てみると以下の様な感じになっています。

LateSetメソッド

        'UNDONE: temporary entry point to get the compiler hookup working.
        <DebuggerHiddenAttribute(), DebuggerStepThroughAttribute()> _
        Public Shared Sub LateSet( _
                ByVal Instance As Object, _
                ByVal Type As Type, _
                ByVal MemberName As String, _
                ByVal Arguments() As Object, _
                ByVal ArgumentNames() As String, _
                ByVal TypeArguments As Type())

#If Not NEW_BINDER Then
            LateBinding.LateSet(Instance, Type, MemberName, Arguments, ArgumentNames)
            Return
#End If
            Dim idmop As IDynamicMetaObjectProvider = IDOUtils.TryCastToIDMOP(Instance)
            If idmop IsNot Nothing AndAlso TypeArguments Is Nothing Then
                IDOBinder.IDOSet(idmop, MemberName, ArgumentNames, Arguments)
            Else
                ObjectLateSet(Instance, Type, MemberName, Arguments, ArgumentNames, TypeArguments)
                Return
            End If
        End Sub

LateGetメソッド

        <DebuggerHiddenAttribute(), DebuggerStepThroughAttribute()> _
        Public Shared Function LateGet( _
                ByVal Instance As Object, _
                ByVal Type As System.Type, _
                ByVal MemberName As String, _
                ByVal Arguments As Object(), _
                ByVal ArgumentNames As String(), _
                ByVal TypeArguments As Type(), _
                ByVal CopyBack As Boolean()) As Object

#If Not NEW_BINDER Then
            Return LateBinding.LateGet(Instance, Type, MemberName, Arguments, ArgumentNames, CopyBack)
#End If

            If Arguments Is Nothing Then Arguments = NoArguments
            If ArgumentNames Is Nothing Then ArgumentNames = NoArgumentNames
            If TypeArguments Is Nothing Then TypeArguments = NoTypeArguments

            Dim BaseReference As Container
            If Type IsNot Nothing Then
                BaseReference = New Container(Type)
            Else
                BaseReference = New Container(Instance)
            End If

            Dim InvocationFlags As BindingFlags = BindingFlags.InvokeMethod Or BindingFlags.GetProperty

            If BaseReference.IsCOMObject AndAlso Not BaseReference.IsWindowsRuntimeObject Then
                'UNDONE:  BAIL for now -- call the old binder.
                Return LateBinding.LateGet(Instance, Type, MemberName, Arguments, ArgumentNames, CopyBack)
                'Return BaseReference.InvokeCOMMethod2(MemberName, Arguments, ArgumentNames, CopyBack, InvocationFlags)
            Else
                Dim idmop As IDynamicMetaObjectProvider = IDOUtils.TryCastToIDMOP(instance)
                If idmop IsNot Nothing AndAlso TypeArguments Is NoTypeArguments Then
                    Return IDOBinder.IDOGet(idmop, MemberName, Arguments, ArgumentNames, CopyBack)
                Else
                    Return ObjectLateGet(Instance, Type, MemberName, Arguments, ArgumentNames, TypeArguments, CopyBack)
                End If
            End If
        End Function 'LateGet

メソッド内部の細かい部分までは説明しませんが、どちらのメソッドでも対象のオブジェクトがIDynamicMetaObjectProviderインターフェイスを実装しているかをチェックし、IDynamicMetaObjectProviderインターフェイスを実装していればDynamic型のオブジェクトとして処理、そうでなければ遅延バインディングとして処理しています。*1

VB.NETでのDLRと遅延バインディング

ここまでの例からわかる様にVB.NETにおいてDLRは遅延バインディングを拡張する様な形で導入されています。
「DLRと遅延バインディング」というよりは「遅延バインディングの新機能としてのDLR」といった方がより実態に近いのかもしれません。

私見

VB.NETでのDLRの使用がこの様な形で導入されたのは互換性を重視した結果だと思われます。
とはいえ、実際に開発する側からするとOption StrictのOn/Offはソース単位でしか指定できず非常に不便なのと、DLRだけを選択的に使用することが出来ないのが致命的に辛いです。
C#と同様にdynamicキーワード*2を導入し、DLRの使用と従来の遅延バインディングを明確に分離することは出来なかったのものなのかと心底思います。

この点においてはVB.NETは本当に残念だと思います。

参考資料

*1:このサンプルでは触れてませんが、メソッド呼び出しにおいてはNewLateBinding.LateCallが呼び出され同様のチェックが行われます

*2:またはそれに類するもの