Visual Basic 初級講座 [改訂版]
VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

第40回 動的言語ランタイム

2021/8/2

この記事が対象とする製品・バージョン

VB2019 Visual Basic 2019 対象です。
VB2017 Visual Basic 2017 対象です。
VB2015 Visual Basic 2015 対象です。
VB2013 Visual Basic 2013 対象です。
VB2012 Visual Basic 2012 対象です。
VB2010 Visual Basic 2010 対象です。
VB2008 Visual Basic 2008 × 対象外です。
VB2005 Visual Basic 2005 × 対象外です。
VB.NET 2003 Visual Basic.NET 2003 × 対象外です。
VB.NET 2002 Visual Basic.NET (2002) × 対象外です。
VB6対応 Visual Basic 6.0 × 対象外です。

 

目次

 

1.動的言語

VBやC#などの言語では、事前に厳密に型を定義する必要があるのに対し、JavaScriptなどいくつかの言語は実行時に柔軟に、プロパティやメソッドを追加できます。

たとえば、次のJavaScriptのプログラムでは空のオブジェクト型の変数 myObj を宣言し、 xプロパティ、 yプロパティ、それに Addメソッドを追加しています。

var myObj = {};
myObj.x = 2;
myObj.y = 3;
myObj.Add = function() { 
                return this.x + this.y; 
            }.bind(myObj);

var result = myObj.Add();
alert(result);

x や y や Add を事前に定義して置く必要はなく、プログラム実行時にこのように追加していけるのが特徴です。このような言語を「動的言語」と呼びます。

VBやC#の言語設計はこれとは異なり、事前に型やプロパティ・メソッドを定義しておくことで、何か間違っていることに早めに気が付いたり、結果の予測を向上させることでバグを減らすという価値観の方を重視しています。これを「静的型付け」の言語、「静的言語」と呼びます。

ところが、VB2010の時に、動的言語の機能が使用できる動的言語ランタイム(DLR)がフレームワークに導入された影響で、VBでも限定的ではありますが動的言語のようなプログラムも記述できるようになりました。

VBはもともとが静的言語なので、あまり幅広く活用される機能ではありませんが、XMLやJSONなど処理では、動的言語のようなアプローチで直接XMLやJSONの値を扱うことで、プログラムを簡潔な記述できるようになる場合があります。

 

VBで動的言語ランタイムの機能を使用するには、System.Dynamic名前空間(読み方:Dynamic=ダイナミック)の機能を使用します。

たとえば、VBでもVB2010以降であれば次のようにプログラム中にxプロパティ、yプロパティ、Addメソッドを追加することが可能です。

これ例を実行するには Option Strict が Off である必要があります。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim myObj As Object = New Dynamic.ExpandoObject
myObj.x = 2
myObj.y = 3
myObj.Add = Function()
                Return myObj.x + myObj.y
            End Function

Dim result = myObj.Add.Invoke
Debug.WriteLine(result)

Option Strict が Off の場合 だけこの例を実行できます。

Debug.WriteLineで出力される場所

 

これで普段は静的型付け言語のメリットを享受しつつ、必要な時には System.Dynamic名前空間を経由して動的言語ランタイムの力を使ったプログラムをすることもできます。主役になるのは DynamicObjectクラスとExpandoObjectクラスです。実際に使ってみるとわかるのですが、VBの動的言語機能は融通が聞かないところがあり、いろいろ使いにくい面もあります。

今回は、動的言語ランタイムの使い方を説明します。

発展 発展学習  -  C# の dynamic

発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。

C# で動的言語ランタイムの機能を使用するにはキーワード dynamic を使用します。VBにはこれに該当するキーワードはなく、Option Strict が Off であれば、いつでも動的言語ランタイムの機能を使用できます。

 

2.遅延バインディング

動的言語ランタイム導入以前から、VBには、もともと Option Strictという独特な機能があり、Option Strict が Off の場合、Objectかたを使うと、事前に定義されていないメンバーへの呼び出しを記述することが可能です。これを遅延バインディングと呼びます。

Option Strict を On/Off する方法は Option Strict を On/Off する方法 を参照してください。

たとえば、次のプログラムはObject型にありもしないメソッドStartやStopを呼び出していますがOption Strict Offの場合実行可能です。実行がStartメソッドに差し掛かると、MissingMemberExceptionの例外になります。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim o As New Object

o.Start

'1~1000000 の逆数をすべて合計します。
Dim total = Enumerable.Range(1, 1000000).Aggregate(Function(seed, i) (1 / i) + seed)

o.Stop

遅延バインディングとは関係ありませんが、Aggregateについて少し補足しておきます。この例の Aggregate は LINQで集計操作を行う Aggregate のメソッド形式の使い方です。合計値'(seed)と数値(i)を引数にもつラムダ式を指定すると、このラムダ式が数値の数だけ(この例では1000000回)呼び出されます。2回目以降の呼び出しでは前回の戻り値が引数の合計値(seed)に渡されます。最後の戻り値が集計結果になります。

 

変数 o に StopWatchクラスのインスタンスを設定するとこの例はエラーなしで実行できるようになります。

StopWatchクラスには StartメソッドとStopメソッドがあるからです。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim o As Object = New StopWatch

o.Start

'1~1000000 の逆数をすべて合計します。
Dim total = Enumerable.Range(1, 1000000).Aggregate(Function(seed, i) (1 / i) + seed)

o.Stop

Dim time = o.Elapsed.TotalSeconds

Debug.WriteLine($"集計処理に{time}秒かかりました。")
'Debug.WriteLine("集計処理に" & time & "秒かかりました。") '←VB2013以前の場合

Option Strict が Off の場合 だけこの例を実行できます。

Debug.WriteLineで出力される場所

これを実行すると私の環境では「集計処理に0.0286168秒かかりました。」と表示されました。100万個の逆数を計算して合計するのにたった0.02秒しかかからないとは流石コンピューターです。

 

このようにOption Strict Off と Object 型による遅延バインディングは、メンバーを実際に呼び出そうとしたときにはじめてそのメンバーの定義を探しに行きます。コンパイル時点ではメンバーが存在している必要はありません。コンパイル後も該当するメンバーが存在しなくてもその部分までは実行できるし、該当するメンバーが存在すれば、それをそのまま呼び出してくれます。

 

この遅延バインディング機能は、プログラム時点でメンバーの情報が明らかでないというレアな状況の時には役に立ちます。たとえば、プラグインのような仕組みを実装する場合やCOMオブジェクトを利用する場合などです。

※どちらの場合も、遅延バインディング以外の代替策(インターフェース、CCW)があります。

 

動的言語ランタイムの機能は、コンパイル時点でメンバーの存在していなくてもよいという点で、この遅延バインディングと同じです。そこで、遅延バインディングとともに使うものとして導入されました。

※この点でC#では動的言語ランタイムのために dynamic という新しいキーワードが導入されたのとアプローチが大きく異なります。C#にはもともと遅延バインディング機能もありませんでした。

というわけですから、Option Strict Off と Object型 が動的言語ランタイムを使用するうえで活躍することになります。

Option Strict は普段 On にしている方も多いと思うので、動的言語ランタイムの機能を使いたい場合は、そのプログラムファイルだけ独立させて、ファイルの先頭に Option Strict Off と記述することで、そのファイルだけ Option Strict Off にするという使い方が良いかもしれません。

 

3.ExpandoObject

3-1.Objectとの違い

System.Dynamic.ExpandoObjectクラス(読み方:ExpandoObject=エクスパンドオブジェクト)は、存在しないメンバーを呼び出したときに、そのメンバーを自動的に作成し、メンバーが存在するかのように振る舞います。実際にはこれは一種のトリックであり、本当にメンバーが追加されるわけではありませんが、プログラムする側からはそのようなことが起こっているように動作します。

  存在しないメンバーを呼び出そうとすると
Object MissingMemberException の例外が発生
ExpandoObject そのメンバーを自動的に作成
(しているかのように振る舞う)

 

ExpandoObjectのこの機能は、遅延バインディングを利用して実現されているので、Option Strict を Off にしたうえで、Objectクラスを経由して機能を使用する必要があります。(言語仕様上存在するかどうかわからないメンバーの呼び出しをコンパイルできるのはObjectクラスのみ)

メモ メモ  - ExpandoObject の スペルに注意。 o が付きますよ。

ExpandObject ではなく、ExpandoObject ですのでご注意ください。

 

 

3-2.動的プロパティ

簡単な例を紹介しましょう。

次の例ではExpandoObjectを使って存在しないプロパティ Tekito と Desuyo を読み書きします。エラーにならず、そのプロパティが存在するかのように動作します。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim o As Object = New Dynamic.ExpandoObject

o.Tekito = "Hello"
o.Desuyo = 123

o.Tekito = o.Tekito.ToUpper
o.Desuyo += 100


Debug.WriteLine($"{o.Tekito} {o.Desuyo}")
'Debug.WriteLine(o.Tekito & " " & o.Desuyo) '←VB2013以前の場合

Option Strict が Off の場合 だけこの例を実行できます。

Debug.WriteLineで出力される場所

この例を実行すると HELLO 223 と表示されます。

1行目で 変数o が Object 型で宣言されているのがポイントで、これは必須です。前述したように、VBでは、コンパイル時点で存在を確認できないメンバーの呼び出しを記述するには Object 型を対象にすることが必須であり、ExpandoObject クラスの機能を使っても例外ではありません。そのため、Dim o As Object = New Dynamic.ExpandoObject のようなあまり見かけない書き方になります。

 

3-3.動的メソッド

メソッドの場合は、もう少し複雑です。メソッドの定義はラムダ式で行い、呼び出すにはInvokeを使用するという特別ルールがあります。

次の例では Addメソッドを動的に定義し、呼び出しています。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim o As Object = New Dynamic.ExpandoObject

o.Add = Function(x As Integer, y As Integer) As Integer
            Return x + y
        End Function

Dim result = o.Add.Invoke(2, 3)

Debug.WriteLine(result)

Option Strict が Off の場合 だけこの例を実行できます。

Debug.WriteLineで出力される場所

 

特徴的なInvokeは赤い字で示しておきました。

Invoke を付けないで o.Add(2, 3) のように呼び出すと次のようなメッセージで MissingMemberException の例外が発生します。

System.MissingMemberException: 'No default member found for type 'VB$AnonymousDelegate_0(Of Integer,Integer,Integer)'.'

このエラーは動的言語ランタイムの問題ではなく、VBでのObject型の振る舞いの問題のようです。

次のように、ExpandoObjectを使用しないで、この現象を発生させることもできます。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim addMethod = Function(x As Integer, y As Integer) As Integer
                    Return x + y
                End Function

Dim iMethod = addMethod
Dim oMethod As Object = addMethod

Dim result1 = iMethod(1, 2) '呼び出し成功
Dim result2 = oMethod.Invoke(3, 4) '呼び出し成功
Dim result3 = oMethod(5, 6) 'MissingMemberException

Option Strict が Off の場合 だけこの例を実行できます。

 

つまり、オブジェクト型から遅延バインディングを利用して直接メソッドを呼び出すことはできないということですね。

 

3-4.仕組み

ExpandoObjectは、追加されたメンバーを内部ではDictionaryとして保持しています。

For Eachなどを利用してこの内部のDictionaryに直接アクセスすることができます。

たとえば、次のプログラムではExpandoObjectに追加されているメンバーの一覧を列挙します。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim o As Object = New Dynamic.ExpandoObject

o.Name = "徳川家康"
o.Age = 19
o.Say = Sub() Debug.WriteLine("こんにちは")

For Each member As KeyValuePair(Of String, Object) In o
    Debug.WriteLine(member.Key & ": " & member.Value.ToString)
Next

Option Strict が Off の場合 だけこの例を実行できます。

Debug.WriteLineで出力される場所

 

この例を実行すると次のように表示されます。

Name: 徳川家康
Age: 19
Say: VB$AnonymousDelegate_0

最後の Say は自動生成された型の名前が表示されています。

 

4.DynamicObject

4-1.Objectとの違い

System.Dynamic.DynamicObjectクラス(読み方:DynamicObject=ダイナミックオブジェクト)は、メンバーを呼びすと、必ず TryGetMemberメソッドが呼び出されます。メンバーが呼び出されたときにどうするのかはTryGetMemberメソッド内で自分で記述することができます。

ExpandoObjectのところでも紹介した表に DynamicObjectを加えて再掲します。

  存在しないメンバーを呼び出そうとすると
Object MissingMemberException の例外が発生
ExpandoObject そのメンバーを自動的に作成
(しているかのように振る舞う)
DynamicObject TryGetMemberメソッドが呼び出されます。
(既に定義済のメンバーでも呼び出されます。)

 

4-2.継承の例

DynamicObject は ExpandoObject とは異なり、継承して使用することが前提となっています。

初級講座では自分でクラスを継承する方法は扱いませんが、ここでは簡単に例を紹介しておきます。

この例では何が呼び出されても "OK" を返します。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Public Class MyDynamic
    Inherits Dynamic.DynamicObject

    Public Overrides Function TryGetMember(binder As Dynamic.GetMemberBinder, ByRef result As Object) As Boolean

        Debug.WriteLine(binder.Name & " が呼び出されました。")

        'とりあえず、何でも OK と返しておきます。
        result = "OK"

        'メンバーが存在してることを伝えます。(Falseにすると呼び出し側でMissingMemberException)
        Return True

    End Function
End Class

まず Class ~ End Class でクラスを定義します。名前は何でもよいですが、ここでは MyDynamic としておきました。

次のようの Inherits がポイントです。この記述だけ この MyDynamicクラスは DynamicObject の全機能を引き継ぎます。

そして、TryGetMemberメソッドは自分で記述する必要があります。親クラスである DynamicObjectクラスが持っているメソッドを子クラスであるMyDinamicクラスで書き換えるためにキーワード Overrides を使って TryGetMember メソッドを定義しています。

第1引数の binder には、何が呼び出されたかという情報が渡されます。Nameプロパティを見るとメンバー名を確認できます。第2引数 result には呼び出し結果を設定します。戻り値には、このメンバーが存在している場合はTrue、存在していない場合は False を設定します。

MyDynamic の呼び出し側は次のようになります。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim o As Object = New MyDynamic

Debug.WriteLine(o.MemberA) 'OK を表示します。
Debug.WriteLine(o.MemberB) 'OK を表示します。

Option Strict が Off の場合 だけこの例を実行できます。

Debug.WriteLineで出力される場所

 

存在しないプロパティ MemberA と MemberB を呼び出していますが、TryGetMember側ではメンバー名にかかわらず常に "OK" を返すようにプログラムしているので、どちらも OK となります。

実行すると次のように表示されます。

MemberA が呼び出されました。
OK
MemberB が呼び出されました。
OK

 

5.それで、これは何の役に立ちますか?

ExpandoObject と DynamicObject はユニークな機能ですが、あまり幅広く使われているわけではありません。

階層化された様々な定義を扱うときに役に立つようで、この記事の冒頭にも書いたように具体的には XML と JSON の処理で活用されることがあります。

XMLとJSONはどちらもデータをテキストで表現する方式のことで、アプリケーション間の通信(WebAPI)などでよく使用されます。

たとえば次のテキストはJSONで表現されたデータです。

{
    "Name": "徳川家康",
    "Note": "初代江戸幕府将軍",
    "Father": {
        "Name": "松平弘忠",
        "Note": "安城松平家8代当主"
    }
}

 

動的言語ラインタイムが導入されたときのMicrosoft 技術者のブログでも、ExpandoObjectとDynamicObjectが役に立つ例として階層化されたXMLの処理が挙げられています。(C#のものですが)

Dynamic in C# 4.0: Introducing the ExpandoObject | Visual Studio Blog (microsoft.com)

Dynamic in C# 4.0: Creating Wrappers with DynamicObject | Visual Studio Blog (microsoft.com)

JSONの処理を簡単にする JSON.NET というパッケージではJSONを読み込んだ後、DynamicObject のようにJSONのデータにアクセスする機能が定義されており、これはかなり多くのプログラマーに活用されています。

JSON読み込み時の活用例

 

少し実例も紹介しておきましょう。ExnpandoObjectの内容をJSON化する汎用の ExpandoToJsonメソッド(内容は後述)を作成しておけば、次のようにシンプルに JSON の内容をプログラムに記述することができます。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim image As Object = New Dynamic.ExpandoObject

image.Width = 800
image.Height = 600
image.Title = "View from 15th Floor"
image.Thumbnail = New Dynamic.ExpandoObject
image.Thumbnail.Url = "http://www.example.com/image/481989943"
image.Thumbnail.Height = 125
image.Thumbnail.Width = 100
image.Animated = False
image.IDs = {116, 943, 234, 38793}

Dim json As String = ExpandoToJson(image)
Debug.WriteLine(json)

Option Strict が Off の場合 だけこの例を実行できます。

Debug.WriteLineで出力される場所

 

実行すると次の通り表示されます。

{
    "Width": 800,
    "Height": 600,
    "Title": "View from 15th Floor",
    "Thumbnail": {
        "Url": "http://www.example.com/image/481989943",
        "Height": 125,
        "Width": 100
    }
,
    "Animated": false,
    "IDs": [116,943,234,38793]
}

 

ExpandoToJsonメソッドの例はこちらです。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

''' <summary>
''' ExpandoObjectの内容をJSON化します。
''' 簡易的に作成したプログラムですので、実用で使用する際には十分テストしてください。
''' </summary>
Private Function ExpandoToJson(o As Dynamic.ExpandoObject, Optional indentLevel As Integer = 0) As String
    Dim result As New System.Text.StringBuilder
    Dim outerIndent As String = New String(" ", (indentLevel) * 4)
    Dim innerIndent As String = New String(" ", (indentLevel + 1) * 4)
    Dim dic As IDictionary(Of String, Object) = o
    Dim memberCount As Integer = dic.Count
    Dim count As Integer = 0

    result.AppendLine(outerIndent & "{")

    'Expandoオブジェクトから内容を1つずつ取り出して書き込みます。
    For Each member As KeyValuePair(Of String, Object) In dic
        result.Append(innerIndent & """" & member.Key & """: ")
        If TypeOf member.Value Is Dynamic.ExpandoObject Then
            '▼オブジェクトの場合
            result.Append(ExpandoToJson(member.Value, indentLevel + 1))
        ElseIf TypeOf member.Value Is Array Then
            '▼配列の場合、 [ ] で囲んで , で区切って書き込みます。
            Dim arr As New List(Of Object)
            result.Append("[")
            For Each value In member.Value
                If Information.IsNumeric(value) Then
                    arr.Add(value)
                Else
                    arr.Add("""" & value & """")
                End If
            Next
            result.Append(String.Join(",", arr))
            result.Append("]")
        Else
            '▼単一の値の場合
            If TypeOf member.Value Is Boolean Then
                '▼True/Falseの場合、true または false を書き込みます。
                result.Append(member.Value.ToString.ToLower)
            ElseIf Information.IsNumeric(member.Value) Then
                '▼数値の場合、そのまま書き込みます。
                result.Append(member.Value)
            Else
                '▼その他の場合、 " で囲んで書き込みます。
                result.Append("""" & member.Value & """")
            End If
        End If

        count += 1
        If count = memberCount Then
            '最後の項目の場合は、改行だけ書き込みます。
            result.AppendLine("")
        Else
            result.AppendLine(",")
        End If

    Next

    result.AppendLine(outerIndent & "}")

    Return result.ToString
End Function

Option Strict が Off の場合 だけこの例を実行できます。

Debug.WriteLineで出力される場所

 

ここで紹介した例は、すべてのJSONに対応できるわけではなく、一番外側が配列の場合や単一の値のみからなるJSONには対応していません。