チュートリアル: 動的オブジェクトの作成と使用 (C# および Visual Basic)

動的オブジェクトは、プロパティやメソッドなどのメンバーを、コンパイル時ではなく実行時に公開します。 これにより、静的な型や書式に一致しない構造体と連携するオブジェクトを作成することができます。 たとえば、動的オブジェクトを使用して HTML ドキュメント オブジェクト モデル (DOM) を参照することもできます。HTML DOM には、有効な HTML マークアップ要素と属性の任意の組み合わせを含めることができます。 各 HTML ドキュメントは一意であるため、特定の HTML ドキュメントのメンバーは実行時に決定されます。 HTML 要素の属性を参照するための一般的な方法は、属性の名前を要素の GetProperty メソッドに渡す方法です。 HTML 要素 <div id="Div1">id 属性を参照するには、まず <div> 要素への参照を取得し、その後 divElement.GetProperty("id") を使用します。 動的オブジェクトを使用する場合は、id 属性を divElement.id として参照できます。

動的オブジェクトを使用すると、IronPython や IronRuby などの動的言語にも簡単にアクセスできます。 動的オブジェクトを使用して、実行時に解釈される動的スクリプトを参照することもできます。

動的オブジェクトを参照するには、遅延バインディングを使用します。 C# では、遅延バインディング オブジェクトの型は dynamic として指定します。 Visual Basic では、遅延バインディング オブジェクトの型は Object として指定します。 詳しくは、「dynamic」および「事前バインディングと遅延バインディング」をご覧ください。

カスタムの動的オブジェクトは、System.Dynamic 名前空間内のクラスを使用して作成できます。 たとえば、ExpandoObject を作成し、実行時にそのオブジェクトのメンバーを指定することもできます。 また、DynamicObject クラスを継承する、独自の型を作成することもできます。 その後、DynamicObject クラスのメンバーをオーバーライドして、実行時の動的機能を提供することができます。

この記事には、次の 2 つの独立したチュートリアルがあります。

  • テキスト ファイルの内容をオブジェクトのプロパティとして動的に公開する、カスタム オブジェクトを作成する。

  • IronPython ライブラリを使用するプロジェクトを作成する。

これらのいずれかまたは両方を実行することができます。両方を実行する場合、順序は関係ありません。

前提条件

  • .NET デスクトップ開発 ワークロードがインストールされている Visual Studio 2019 バージョン 16.9 以降。 このワークロードを選択すると、.NET 5.0 SDK が自動的にインストールされます。

注意

次の手順で参照している Visual Studio ユーザー インターフェイス要素の一部は、お使いのコンピューターでは名前や場所が異なる場合があります。 これらの要素は、使用している Visual Studio のエディションや独自の設定によって決まります。 詳細については、「IDE をカスタマイズする」をご覧ください。

  • 2 番目のチュートリアルでは、IronPython for .NET をインストールします。 最新バージョンを取得するには、ダウンロード ページに移動します。

カスタム動的オブジェクトの作成

最初のチュートリアルでは、テキスト ファイルの内容を検索するカスタム動的オブジェクトを定義します。 動的プロパティにより、検索するテキストが指定されます。 たとえば、呼び出しコードで dynamicFile.Sample が指定された場合、動的クラスは "Sample" で始まるすべての行をファイルから取得し、それらを含んだ文字列のジェネリック リストを返します。 検索では、大文字と小文字を区別しません。 動的クラスでは、2 つの省略可能な引数もサポートされています。 最初の引数は、検索オプションの列挙値です。この引数では、動的クラスが行の先頭、行の末尾、または行内の任意の場所から一致を検索することを指定します。 2 番目の引数は、動的クラスが検索の前に各行から先頭と末尾の空白をトリミングすることを指定します。 たとえば、呼び出しコードで dynamicFile.Sample(StringSearchOption.Contains) と指定された場合、動的クラスは行内の任意の場所にある "Sample" を検索します。 呼び出しコードで dynamicFile.Sample(StringSearchOption.StartsWith, false) と指定された場合、動的クラスは、各行の先頭にある "Sample" を検索し、先頭と末尾のスペースは削除しません。 既定では、動的クラスは各行の先頭で一致を検索し、先頭と末尾のスペースを削除します。

カスタムの動的クラスを作成するには

  1. Visual Studio を起動します。

  2. [新しいプロジェクトの作成] を選択します。

  3. [新しいプロジェクトの作成] ダイアログで、[C#] または [Visual Basic] を選択し、 [コンソール アプリケーション] を選択して、 [次へ] を選択します。

  4. [新しいプロジェクトの構成] ダイアログで、 [プロジェクト名] に「DynamicSample」と入力し、 [次へ] を選択します。

  5. [追加情報] ダイアログで、 [ターゲット フレームワーク][.NET 5.0 (Current)] を選んでから、 [作成] を選択します。

    新しいプロジェクトが作成されます。

  6. ソリューション エクスプローラー で、DynamicSample プロジェクトを右リックし、 [追加] > [クラス] を選択します。 [名前] ボックスに「ReadOnlyFile」と入力し、 [追加] を選択します。

    ReadOnlyFile クラスを含んだ新しいファイルが追加されます。

  7. ReadOnlyFile.cs または ReadOnlyFile.vb のファイルの先頭に、次のコードを追加して System.IO および System.Dynamic の名前空間をインポートします。

    using System.IO;
    using System.Dynamic;
    
    Imports System.IO
    Imports System.Dynamic
    
  8. カスタム動的オブジェクトでは、列挙型を使用して検索条件を決定します。 クラス ステートメントの前に、次の列挙定義を追加します。

    public enum StringSearchOption
    {
        StartsWith,
        Contains,
        EndsWith
    }
    
    Public Enum StringSearchOption
        StartsWith
        Contains
        EndsWith
    End Enum
    
  9. 次のコード例に示すように、クラス ステートメントを更新して DynamicObject クラスを継承します。

    class ReadOnlyFile : DynamicObject
    
    Public Class ReadOnlyFile
        Inherits DynamicObject
    
  10. ReadOnlyFile クラスに次のコードを追加して、 ファイル パスのプライベート フィールドと、ReadOnlyFile クラスのコンス トラクターを定義します。

    // Store the path to the file and the initial line count value.
    private string p_filePath;
    
    // Public constructor. Verify that file exists and store the path in
    // the private variable.
    public ReadOnlyFile(string filePath)
    {
        if (!File.Exists(filePath))
        {
            throw new Exception("File path does not exist.");
        }
    
        p_filePath = filePath;
    }
    
    ' Store the path to the file and the initial line count value.
    Private p_filePath As String
    
    ' Public constructor. Verify that file exists and store the path in 
    ' the private variable.
    Public Sub New(ByVal filePath As String)
        If Not File.Exists(filePath) Then
            Throw New Exception("File path does not exist.")
        End If
    
        p_filePath = filePath
    End Sub
    
  11. 次の GetPropertyValue メソッドを ReadOnlyFile クラスに追加します。 GetPropertyValue メソッドは検索条件を (入力として) 受け取り、テキスト ファイルから検索条件に一致する行を返します。 ReadOnlyFile クラスによって提供される動的メソッドは、GetPropertyValue メソッドを呼び出して、それぞれの結果を取得します。

    public List<string> GetPropertyValue(string propertyName,
                                         StringSearchOption StringSearchOption = StringSearchOption.StartsWith,
                                         bool trimSpaces = true)
    {
        StreamReader sr = null;
        List<string> results = new List<string>();
        string line = "";
        string testLine = "";
    
        try
        {
            sr = new StreamReader(p_filePath);
    
            while (!sr.EndOfStream)
            {
                line = sr.ReadLine();
    
                // Perform a case-insensitive search by using the specified search options.
                testLine = line.ToUpper();
                if (trimSpaces) { testLine = testLine.Trim(); }
    
                switch (StringSearchOption)
                {
                    case StringSearchOption.StartsWith:
                        if (testLine.StartsWith(propertyName.ToUpper())) { results.Add(line); }
                        break;
                    case StringSearchOption.Contains:
                        if (testLine.Contains(propertyName.ToUpper())) { results.Add(line); }
                        break;
                    case StringSearchOption.EndsWith:
                        if (testLine.EndsWith(propertyName.ToUpper())) { results.Add(line); }
                        break;
                }
            }
        }
        catch
        {
            // Trap any exception that occurs in reading the file and return null.
            results = null;
        }
        finally
        {
            if (sr != null) {sr.Close();}
        }
    
        return results;
    }
    
    Public Function GetPropertyValue(ByVal propertyName As String,
                                     Optional ByVal StringSearchOption As StringSearchOption = StringSearchOption.StartsWith,
                                     Optional ByVal trimSpaces As Boolean = True) As List(Of String)
    
        Dim sr As StreamReader = Nothing
        Dim results As New List(Of String)
        Dim line = ""
        Dim testLine = ""
    
        Try
            sr = New StreamReader(p_filePath)
    
            While Not sr.EndOfStream
                line = sr.ReadLine()
    
                ' Perform a case-insensitive search by using the specified search options.
                testLine = UCase(line)
                If trimSpaces Then testLine = Trim(testLine)
    
                Select Case StringSearchOption
                    Case StringSearchOption.StartsWith
                        If testLine.StartsWith(UCase(propertyName)) Then results.Add(line)
                    Case StringSearchOption.Contains
                        If testLine.Contains(UCase(propertyName)) Then results.Add(line)
                    Case StringSearchOption.EndsWith
                        If testLine.EndsWith(UCase(propertyName)) Then results.Add(line)
                End Select
            End While
        Catch
            ' Trap any exception that occurs in reading the file and return Nothing.
            results = Nothing
        Finally
            If sr IsNot Nothing Then sr.Close()
        End Try
    
        Return results
    End Function
    
  12. GetPropertyValue メソッドの後に、DynamicObject クラスの TryGetMember メソッドをオーバーライドする次のコードを追加します。 TryGetMember メソッドは、動的クラスのメンバーが要求され、引数が指定されていない場合に呼び出されます。 binder 引数には、参照されているメンバーに関する情報が含まれます。result 引数は、指定したメンバーに対して返された結果を参照します。 TryGetMember メソッドはブール値を返します。要求されたメンバーが存在する場合には true を返し、その他の場合には false を返します。

    // Implement the TryGetMember method of the DynamicObject class for dynamic member calls.
    public override bool TryGetMember(GetMemberBinder binder,
                                      out object result)
    {
        result = GetPropertyValue(binder.Name);
        return result == null ? false : true;
    }
    
    ' Implement the TryGetMember method of the DynamicObject class for dynamic member calls.
    Public Overrides Function TryGetMember(ByVal binder As GetMemberBinder,
                                           ByRef result As Object) As Boolean
        result = GetPropertyValue(binder.Name)
        Return If(result Is Nothing, False, True)
    End Function
    
  13. TryGetMember メソッドの後に、DynamicObject クラスの TryInvokeMember メソッドをオーバーライドする次のコードを追加します。 TryInvokeMember メソッドは、動的クラスのメンバーが引数を使用して要求された場合に呼び出されます。 binder 引数には、参照されているメンバーに関する情報が含まれます。result 引数は、指定したメンバーに対して返された結果を参照します。 args 引数には、メンバーに渡される引数の配列が含まれます。 TryInvokeMember メソッドはブール値を返します。要求されたメンバーが存在する場合には true を返し、その他の場合には false を返します。

    TryInvokeMember メソッドのカスタム バージョンは、1 つ目の引数として、前の手順で定義した StringSearchOption 列挙からの値を受け付けます。 TryInvokeMember メソッドは、2 つ目の引数としてブール値を受け付けます。 引数の一方または両方が有効な値であれば、それらが GetPropertyValue メソッドに渡され、結果が取得されます。

    // Implement the TryInvokeMember method of the DynamicObject class for
    // dynamic member calls that have arguments.
    public override bool TryInvokeMember(InvokeMemberBinder binder,
                                         object[] args,
                                         out object result)
    {
        StringSearchOption StringSearchOption = StringSearchOption.StartsWith;
        bool trimSpaces = true;
    
        try
        {
            if (args.Length > 0) { StringSearchOption = (StringSearchOption)args[0]; }
        }
        catch
        {
            throw new ArgumentException("StringSearchOption argument must be a StringSearchOption enum value.");
        }
    
        try
        {
            if (args.Length > 1) { trimSpaces = (bool)args[1]; }
        }
        catch
        {
            throw new ArgumentException("trimSpaces argument must be a Boolean value.");
        }
    
        result = GetPropertyValue(binder.Name, StringSearchOption, trimSpaces);
    
        return result == null ? false : true;
    }
    
    ' Implement the TryInvokeMember method of the DynamicObject class for 
    ' dynamic member calls that have arguments.
    Public Overrides Function TryInvokeMember(ByVal binder As InvokeMemberBinder,
                                              ByVal args() As Object,
                                              ByRef result As Object) As Boolean
    
        Dim StringSearchOption As StringSearchOption = StringSearchOption.StartsWith
        Dim trimSpaces = True
    
        Try
            If args.Length > 0 Then StringSearchOption = CType(args(0), StringSearchOption)
        Catch
            Throw New ArgumentException("StringSearchOption argument must be a StringSearchOption enum value.")
        End Try
    
        Try
            If args.Length > 1 Then trimSpaces = CType(args(1), Boolean)
        Catch
            Throw New ArgumentException("trimSpaces argument must be a Boolean value.")
        End Try
    
        result = GetPropertyValue(binder.Name, StringSearchOption, trimSpaces)
    
        Return If(result Is Nothing, False, True)
    End Function
    
  14. ファイルを保存して閉じます。

サンプルのテキスト ファイルを作成するには

  1. ソリューション エクスプローラー で、DynamicSample プロジェクトを右クリックし、 [追加] > [新しい項目] を選択します。 [インストールされたテンプレート] ペインで [全般] をクリックし、 [テキスト ファイル] テンプレートを選択します。 [名前] ボックスで、既定の名前である TextFile1.txt をそのままにし、 [追加] をクリックします。 新しいテキスト ファイルがプロジェクトに追加されます。

  2. TextFile1.txt ファイルに次のテキストをコピーします。

    List of customers and suppliers
    
    Supplier: Lucerne Publishing (https://www.lucernepublishing.com/)
    Customer: Preston, Chris
    Customer: Hines, Patrick
    Customer: Cameron, Maria
    Supplier: Graphic Design Institute (https://www.graphicdesigninstitute.com/)
    Supplier: Fabrikam, Inc. (https://www.fabrikam.com/)
    Customer: Seubert, Roxanne
    Supplier: Proseware, Inc. (http://www.proseware.com/)
    Customer: Adolphi, Stephan
    Customer: Koch, Paul
    
  3. ファイルを保存して閉じます。

カスタム動的オブジェクトを使用するサンプル アプリケーションを作成するには

  1. ソリューション エクスプローラー で、Visual Basic をお使いの場合は Program.vb ファイルを、Visual C# をお使いの場合は Program.cs ファイルをダブルクリックします。

  2. Main プロシージャに次のコードを追加して、TextFile1.txt ファイル用に ReadOnlyFile クラスのインスタンスを作成します。 このコードは、遅延バインディングを使用して動的メンバーを呼び出し、"Customer" という文字列を含んだテキスト行を取得します。

    dynamic rFile = new ReadOnlyFile(@"..\..\..\TextFile1.txt");
    foreach (string line in rFile.Customer)
    {
        Console.WriteLine(line);
    }
    Console.WriteLine("----------------------------");
    foreach (string line in rFile.Customer(StringSearchOption.Contains, true))
    {
        Console.WriteLine(line);
    }
    
    Dim rFile As Object = New ReadOnlyFile("..\..\..\TextFile1.txt")
    For Each line In rFile.Customer
        Console.WriteLine(line)
    Next
    Console.WriteLine("----------------------------")
    For Each line In rFile.Customer(StringSearchOption.Contains, True)
        Console.WriteLine(line)
    Next
    
  3. ファイルを保存し、Ctrl+F5 キーを押してアプリケーションをビルドし、実行します。

動的言語ライブラリの呼び出し

次のチュートリアルでは、動的言語 IronPython で記述されたライブラリにアクセスするプロジェクトを作成します。

カスタムの動的クラスを作成するには

  1. Visual Studio で、 [ファイル] > [新規] > [プロジェクト] の順に選択します。

  2. [新しいプロジェクトの作成] ダイアログで、[C#] または [Visual Basic] を選択し、 [コンソール アプリケーション] を選択して、 [次へ] を選択します。

  3. [新しいプロジェクトの構成] ダイアログで、 [プロジェクト名] に「DynamicIronPythonSample」と入力し、 [次へ] を選択します。

  4. [追加情報] ダイアログで、 [ターゲット フレームワーク][.NET 5.0 (Current)] を選んでから、 [作成] を選択します。

    新しいプロジェクトが作成されます。

  5. IronPython NuGet パッケージをインストールします。

  6. Visual Basic をお使いの場合は、Program.vb ファイルを編集します。 Visual C# をお使いの場合は、Program.cs ファイルを編集します。

  7. ファイルの先頭に次のコードを追加して、IronPython ライブラリと System.Linq 名前空間から Microsoft.Scripting.Hosting および IronPython.Hosting 名前空間をインポートします。

    using System.Linq;
    using Microsoft.Scripting.Hosting;
    using IronPython.Hosting;
    
    Imports Microsoft.Scripting.Hosting
    Imports IronPython.Hosting
    Imports System.Linq
    
  8. Main メソッドで、IronPython ライブラリをホストする新しい Microsoft.Scripting.Hosting.ScriptRuntime オブジェクトを作成するための次のコードを追加します。 ScriptRuntime オブジェクトは、IronPython ライブラリ モジュール random.py を読み込みます。

    // Set the current directory to the IronPython libraries.
    System.IO.Directory.SetCurrentDirectory(
       Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) +
       @"\IronPython 2.7\Lib");
    
    // Create an instance of the random.py IronPython library.
    Console.WriteLine("Loading random.py");
    ScriptRuntime py = Python.CreateRuntime();
    dynamic random = py.UseFile("random.py");
    Console.WriteLine("random.py loaded.");
    
    ' Set the current directory to the IronPython libraries.
    System.IO.Directory.SetCurrentDirectory(
        Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) &
           "\IronPython 2.7\Lib")
    
    ' Create an instance of the random.py IronPython library.
    Console.WriteLine("Loading random.py")
    Dim py = Python.CreateRuntime()
    Dim random As Object = py.UseFile("random.py")
    Console.WriteLine("random.py loaded.")
    
  9. Random.py モジュールを読み込むコードの後に、整数の配列を作成する次のコードを追加します。 配列は random.py モジュールの shuffle メソッドに渡されます。このメソッドは、配列内の値をランダムに並べ替えします。

    // Initialize an enumerable set of integers.
    int[] items = Enumerable.Range(1, 7).ToArray();
    
    // Randomly shuffle the array of integers by using IronPython.
    for (int i = 0; i < 5; i++)
    {
        random.shuffle(items);
        foreach (int item in items)
        {
            Console.WriteLine(item);
        }
        Console.WriteLine("-------------------");
    }
    
    ' Initialize an enumerable set of integers.
    Dim items = Enumerable.Range(1, 7).ToArray()
    
    ' Randomly shuffle the array of integers by using IronPython.
    For i = 0 To 4
        random.shuffle(items)
        For Each item In items
            Console.WriteLine(item)
        Next
        Console.WriteLine("-------------------")
    Next
    
  10. ファイルを保存し、Ctrl+F5 キーを押してアプリケーションをビルドし、実行します。

関連項目