演练:创建并使用动态对象(C# 和 Visual Basic)

动态对象会在运行时(而非编译时)公开属性和方法等成员。 这使你能够创建对象以处理与静态类型或格式不匹配的结构。 例如,可以使用动态对象来引用 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。 有关详细信息,请参阅动态早期绑定和晚期绑定

可以使用 System.Dynamic 命名空间中的类来创建自定义动态对象。 例如,可以创建 ExpandoObject 并在运行时指定该对象的成员。 还可以创建继承 DynamicObject 类的自己的类型。 然后,可以替代 DynamicObject 类的成员以提供运行时动态功能。

本文包含两个独立的演练:

  • 创建一个自定义对象,该对象会将文本文件的内容作为对象的属性动态公开。

  • 创建使用 IronPython 库的项目。

你可以完成其中一个,也可以同时完成两个;如果要完成两个,则顺序无关紧要。

先决条件

注意

以下说明中的某些 Visual Studio 用户界面元素在计算机上出现的名称或位置可能会不同。 这些元素取决于你所使用的 Visual Studio 版本和你所使用的设置。 有关详细信息,请参阅个性化设置 IDE

  • 对于第二个演练,安装 IronPython for .NET。 转到其下载页以获取最新版本。

创建自定义动态对象

第一个演练定义了搜索文本文件内容的自定义动态对象。 动态属性指定要搜索的文本。 例如,如果调用代码指定 dynamicFile.Sample,则动态类将返回一个字符串泛型列表,其中包含该文件中以“Sample”开头的所有行。 搜索不区分大小写。 动态类还支持两个可选参数。 第一个参数是一个搜索选项枚举值,它指定动态类应在行的开头、行的结尾或行中任意位置搜索匹配项。 第二个参数指定动态类应在搜索之前去除每行中的前导空格和尾部空格。 例如,如果调用代码指定 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 (当前)”,然后选择“创建”。

    创建新项目。

  6. 在“解决方案资源管理器”中,右键单击 DynamicSample 项目,然后选择“添加”>“类”。 在“名称”框中,键入 ReadOnlyFile,然后选择“添加”。

    这将添加一个包含 ReadOnlyFile 类的新文件。

  7. 在 ReadOnlyFile.cs 或 ReadOnlyFile.vb 文件的顶部,添加以下代码以导入 System.IOSystem.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 方法的自定义版本期望第一个参数为上一步骤中定义的 StringSearchOption 枚举中的值。 TryInvokeMember 方法期望第二个参数为一个布尔值。 如果这两个参数有一个或全部为有效值,则将它们传递给 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. 在“解决方案资源管理器”中,双击 Program.vb 文件(如果使用 Visual Basic)或 Program.cs 文件(如果使用 Visual C#)。

  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 (当前)”,然后选择“创建”。

    创建新项目。

  5. 安装 IronPython NuGet 包。

  6. 如果使用 Visual Basic,请编辑 Program.vb 文件。 如果使用 Visual C#,请编辑 Program.cs 文件。

  7. 在文件的顶部,添加以下代码以从 IronPython 库和 System.Linq 命名空间导入 Microsoft.Scripting.HostingIronPython.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 生成并运行应用程序。

请参阅