演练:使用 C# 创建和使用动态对象

动态对象会在运行时(而非编译时)公开属性和方法等成员。 动态对象使你能够创建对象以处理与静态类型或格式不匹配的结构。 例如,可以使用动态对象来引用 HTML 文档对象模型 (DOM),该模型包含有效 HTML 标记元素和特性的任意组合。 由于每个 HTML 文档都是唯一的,因此在运行时将确定特定 HTML 文档的成员。 引用 HTML 元素的特性的常用方法是,将该特性的名称传递给该元素的 GetProperty 方法。 若要引用 HTML 元素 <div id="Div1">id 特性,首先获取对 <div> 元素的引用,然后使用 divElement.GetProperty("id")。 如果使用动态对象,则可以将 id 特性引用为 divElement.id

动态对象还提供对 IronPython 和 IronRuby 等动态语言的便捷访问。 可以使用动态对象来引用在运行时解释的动态脚本。

使用晚期绑定引用动态对象。 将后期绑定对象的类型指定为 dynamic。有关详细信息,请参阅 dynamic

可以使用 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”,但不会删除前导空格和尾随空格。 动态类的默认行为是在每行的开头搜索匹配项,并删除前导空格和尾部空格。

创建自定义动态类

启动 Visual Studio。 选择“创建新项目”。 在“创建新项目”对话框中,选择 C#,然后依次选择“控制台应用程序”和“下一步”。 在“配置新项目”对话框中,输入 DynamicSample 作为“项目名称”,然后选择“下一步” 。 在“其他信息”对话框中,为“目标框架”选择“.NET 7.0 (当前)”,然后选择“创建”。 在“解决方案资源管理器”中,右键单击 DynamicSample 项目,然后选择“添加”>“类”。 在“名称”框中,键入 ReadOnlyFile,然后选择“添加”。 在 ReadOnlyFile.cs 或 ReadOnlyFile.vb 文件的顶部,添加以下代码以导入 System.IOSystem.Dynamic 命名空间。

using System.IO;
using System.Dynamic;

自定义动态对象使用一个枚举来确定搜索条件。 在类语句的前面,添加以下枚举定义。

public enum StringSearchOption
{
    StartsWith,
    Contains,
    EndsWith
}

更新类语句以继承 DynamicObject 类,如以下代码示例所示。

class ReadOnlyFile : DynamicObject

将以下代码添加到 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;
}
  1. 将下面的 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;
}

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;
}

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;
}

保存并关闭该文件。

创建示例文本文件

在“解决方案资源管理器”中,右键单击 DynamicSample 项目,然后选择“添加”>“新建项目” 。 在“已安装的模板”窗格中,选择“常规”,然后选择“文本文件”模板。 保留“名称”框中的默认名称 TextFile1.txt,然后选择“添加”。 将以下文本复制到 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

保存并关闭文件。

创建一个使用自定义动态对象的示例应用程序

在“解决方案资源管理器”中,双击 Program.cs 文件。 将以下代码添加到 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);
}

保存文件,然后按 Ctrl+F5 生成并运行应用程序。

调用动态语言库

以下演练创建的项目将访问以动态语言 IronPython 编写的库。

创建自定义动态类

在 Visual Studio 中,选择“文件”>“新建”>“项目”。 在“创建新项目”对话框中,选择 C#,然后依次选择“控制台应用程序”和“下一步”。 在“配置新项目”对话框中,输入 DynamicIronPythonSample 作为“项目名称”,然后选择“下一步” 。 在“其他信息”对话框中,为“目标框架”选择“.NET 7.0 (当前)”,然后选择“创建”。 安装 IronPython NuGet 包。 编辑 Program.cs 文件。 在文件的顶部,添加以下代码以从 IronPython 库和 System.Linq 命名空间导入 Microsoft.Scripting.HostingIronPython.Hosting 命名空间。

using System.Linq;
using Microsoft.Scripting.Hosting;
using IronPython.Hosting;

在 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.");

在用于加载 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("-------------------");
}

保存文件,然后按 Ctrl+F5 生成并运行应用程序。

请参阅