演练:创建并使用动态对象(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
库的项目。
你可以完成其中一个,也可以同时完成两个;如果要完成两个,则顺序无关紧要。
先决条件
- 安装了具有 .NET Core 桌面开发工作负载的 Visual Studio 2019 版本 16.9 或更高版本。 选择此工作负载时,将自动安装 .NET 5 SDK。
注意
以下说明中的某些 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# 或 Visual Basic,然后依次选择“控制台应用程序”和“下一步”。
在“配置新项目”对话框中,输入
DynamicSample
作为“项目名称”,然后选择“下一步” 。在“其他信息”对话框中,为“目标框架”选择“.NET 5.0 (当前)”,然后选择“创建”。
创建新项目。
在“解决方案资源管理器”中,右键单击 DynamicSample 项目,然后选择“添加”>“类”。 在“名称”框中,键入
ReadOnlyFile
,然后选择“添加”。这将添加一个包含 ReadOnlyFile 类的新文件。
在 ReadOnlyFile.cs 或 ReadOnlyFile.vb 文件的顶部,添加以下代码以导入 System.IO 和 System.Dynamic 命名空间。
using System.IO; using System.Dynamic;
Imports System.IO Imports System.Dynamic
自定义动态对象使用一个枚举来确定搜索条件。 在类语句的前面,添加以下枚举定义。
public enum StringSearchOption { StartsWith, Contains, EndsWith }
Public Enum StringSearchOption StartsWith Contains EndsWith End Enum
更新类语句以继承
DynamicObject
类,如以下代码示例所示。class ReadOnlyFile : DynamicObject
Public Class ReadOnlyFile Inherits 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; }
' 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
将下面的
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
在
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
在
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
保存并关闭文件。
创建示例文本文件
在“解决方案资源管理器”中,右键单击 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.vb 文件(如果使用 Visual Basic)或 Program.cs 文件(如果使用 Visual C#)。
将以下代码添加到
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
保存文件,然后按 Ctrl+F5 生成并运行应用程序。
调用动态语言库
以下演练创建的项目将访问以动态语言 IronPython 编写的库。
创建自定义动态类
在 Visual Studio 中,选择“文件”>“新建”>“项目”。
在“创建新项目”对话框中,选择 C# 或 Visual Basic,然后依次选择“控制台应用程序”和“下一步”。
在“配置新项目”对话框中,输入
DynamicIronPythonSample
作为“项目名称”,然后选择“下一步” 。在“其他信息”对话框中,为“目标框架”选择“.NET 5.0 (当前)”,然后选择“创建”。
创建新项目。
安装 IronPython NuGet 包。
如果使用 Visual Basic,请编辑 Program.vb 文件。 如果使用 Visual C#,请编辑 Program.cs 文件。
在文件的顶部,添加以下代码以从 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
在 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.")
在用于加载 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
保存文件,然后按 Ctrl+F5 生成并运行应用程序。