MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Visual Basic动态类型与ExpandoObject使用

2021-12-016.9k 阅读

Visual Basic 动态类型基础

在传统的静态类型语言中,变量的类型在编译时就已经确定,并且在变量的生命周期内不能改变。例如,在 Visual Basic 中声明一个 Integer 类型的变量:

Dim num As Integer
num = 10

这里 num 变量在声明时就确定为 Integer 类型,后续只能存储 Integer 类型的值。如果尝试将一个字符串赋值给它,如 num = "Hello",会在编译时抛出错误。

然而,动态类型允许变量在运行时确定其类型。Visual Basic 从 .NET 4.0 开始引入了 Dynamic 类型。使用 Dynamic 类型声明的变量,编译器不会对其进行类型检查,而是将类型检查推迟到运行时。例如:

Dim dynamicVar As Dynamic
dynamicVar = 10
Console.WriteLine(dynamicVar.GetType().Name) '输出: Int32
dynamicVar = "Hello"
Console.WriteLine(dynamicVar.GetType().Name) '输出: String

在上述代码中,dynamicVar 变量先被赋值为整数 10,此时它的运行时类型为 Int32。随后又被赋值为字符串 "Hello",其运行时类型变为 String。这种灵活性在一些场景下非常有用,比如处理来自动态数据源(如 JSON、XML 解析后的数据)或与 COM 组件交互时。

动态类型的优势与场景

  1. 与动态数据源交互 当从 JSON 或 XML 格式的数据源读取数据时,数据的结构和类型可能是不确定的。例如,从一个 JSON 字符串解析得到的数据可能包含不同类型的值。使用动态类型可以方便地处理这种情况。假设我们有如下 JSON 字符串:
{
    "name": "John",
    "age": 30,
    "isStudent": false
}

在 Visual Basic 中使用动态类型来处理这个 JSON 数据(这里假设使用 Newtonsoft.Json 库来解析 JSON):

Imports Newtonsoft.Json

Module Module1
    Sub Main()
        Dim jsonString As String = "{ ""name"": ""John"", ""age"": 30, ""isStudent"": false }"
        Dim dynamicData As Dynamic = JsonConvert.DeserializeObject(jsonString)
        Console.WriteLine("Name: " & dynamicData.name)
        Console.WriteLine("Age: " & dynamicData.age)
        Console.WriteLine("Is Student: " & dynamicData.isStudent)
    End Sub
End Module

在这个例子中,dynamicData 变量被声明为 Dynamic 类型,它可以方便地访问 JSON 对象中的不同属性,而无需预先定义复杂的类型结构。

  1. 与 COM 组件交互 COM 组件通常具有动态的接口,其方法和属性在编译时难以精确确定。使用动态类型可以简化与 COM 组件的交互。例如,与 Microsoft Excel COM 组件交互:
Imports System.Runtime.InteropServices

Module Module1
    Sub Main()
        Dim excelApp As Dynamic = Marshal.GetActiveObject("Excel.Application")
        Dim workbook As Dynamic = excelApp.Workbooks.Open("C:\test.xlsx")
        Dim worksheet As Dynamic = workbook.Sheets(1)
        Dim cellValue As String = worksheet.Cells(1, 1).Value
        Console.WriteLine("Cell value: " & cellValue)
        workbook.Close()
        excelApp.Quit()
    End Sub
End Module

这里通过 Dynamic 类型,我们可以轻松地调用 Excel COM 组件的各种方法和属性,而无需繁琐地定义互操作类型。

动态类型的局限性与注意事项

  1. 编译时类型检查缺失 由于动态类型在编译时不进行类型检查,代码中可能存在的类型错误只有在运行时才能发现。例如,假设我们有一个动态对象 dynamicObj,我们错误地调用了一个不存在的方法:
Dim dynamicObj As Dynamic
dynamicObj = New Object()
dynamicObj.NonExistentMethod() '运行时会抛出异常

在静态类型语言中,这种错误会在编译时被捕获,而动态类型下只有运行到这一行代码时才会抛出异常,这可能导致调试困难。

  1. 性能问题 动态类型的灵活性是以性能为代价的。由于编译器无法在编译时进行优化,运行时需要进行额外的类型检查和绑定操作。例如,对动态类型的属性访问或方法调用,在运行时需要查找对象的实际类型来确定如何执行操作,而静态类型的属性访问和方法调用在编译时就可以确定直接的内存地址或调用方式。因此,在性能敏感的场景下,应谨慎使用动态类型。

ExpandoObject 简介

ExpandoObject 是 .NET 框架提供的一个类,它允许在运行时动态地添加、删除和修改对象的属性和方法。ExpandoObject 实现了 IDynamicMetaObjectProvider 接口,这使得它能够与动态类型系统无缝集成。在 Visual Basic 中,我们可以使用 ExpandoObject 来创建具有动态属性的对象。例如:

Imports System.Dynamic

Module Module1
    Sub Main()
        Dim expando As New ExpandoObject()
        Dim expandoDict As IDictionary(Of String, Object) = CType(expando, IDictionary(Of String, Object))
        expandoDict.Add("Name", "Alice")
        expandoDict.Add("Age", 25)
        Console.WriteLine("Name: " & CType(expando, Dynamic).Name)
        Console.WriteLine("Age: " & CType(expando, Dynamic).Age)
    End Sub
End Module

在上述代码中,我们首先创建了一个 ExpandoObject 实例 expando。然后通过将其转换为 IDictionary(Of String, Object) 类型,我们可以方便地添加属性。之后,通过将 expando 转换为 Dynamic 类型,我们可以像访问普通对象属性一样访问动态添加的属性。

ExpandoObject 的使用场景

  1. 动态构建对象模型 在一些动态数据处理场景中,我们可能需要根据运行时的条件构建对象模型。例如,在开发一个数据报表生成工具时,用户可以选择要在报表中显示的字段。我们可以使用 ExpandoObject 来动态构建报表数据对象。
Imports System.Dynamic

Module Module1
    Sub Main()
        Dim userSelectedFields As String() = {"Name", "Email"}
        Dim reportData As New ExpandoObject()
        Dim dataDict As IDictionary(Of String, Object) = CType(reportData, IDictionary(Of String, Object))
        For Each field In userSelectedFields
            If field = "Name" Then
                dataDict.Add("Name", "Bob")
            ElseIf field = "Email" Then
                dataDict.Add("Email", "bob@example.com")
            End If
        Next
        Console.WriteLine("Name: " & CType(reportData, Dynamic).Name)
        Console.WriteLine("Email: " & CType(reportData, Dynamic).Email)
    End Sub
End Module

这里根据用户选择的字段动态地向 ExpandoObject 添加属性,灵活地构建了报表数据对象。

  1. 模拟动态对象行为 在单元测试中,有时需要模拟具有动态行为的对象。ExpandoObject 可以很好地满足这个需求。例如,假设我们有一个方法需要一个具有特定属性的对象作为参数,我们可以使用 ExpandoObject 来创建一个模拟对象。
Imports System.Dynamic

Module Module1
    Sub ProcessData(data As Dynamic)
        Console.WriteLine("Processing data with name: " & data.Name)
    End Sub

    Sub Main()
        Dim mockData As New ExpandoObject()
        Dim mockDict As IDictionary(Of String, Object) = CType(mockData, IDictionary(Of String, Object))
        mockDict.Add("Name", "Mocked Data")
        ProcessData(CType(mockData, Dynamic))
    End Sub
End Module

在这个例子中,ExpandoObject 创建的 mockData 模拟了一个具有 Name 属性的对象,用于测试 ProcessData 方法。

在 Visual Basic 中深入使用 ExpandoObject

  1. 动态方法添加 除了动态添加属性,ExpandoObject 还可以动态添加方法。我们可以通过定义一个 Delegate 并将其添加到 ExpandoObject 来实现。例如:
Imports System.Dynamic

Module Module1
    Delegate Function AddNumbers(num1 As Integer, num2 As Integer) As Integer

    Sub Main()
        Dim expando As New ExpandoObject()
        Dim expandoDict As IDictionary(Of String, Object) = CType(expando, IDictionary(Of String, Object))
        Dim addDelegate As New AddNumbers(Function(num1, num2) num1 + num2)
        expandoDict.Add("Add", addDelegate)
        Dim result As Integer = CType(expando, Dynamic).Add(5, 3)
        Console.WriteLine("Result of addition: " & result)
    End Sub
End Module

在上述代码中,我们定义了一个 AddNumbers 委托,然后将其作为方法 Add 添加到 ExpandoObject 中。通过 ExpandoObject 的动态调用机制,我们可以像调用对象的方法一样调用这个动态添加的方法。

  1. 嵌套属性与层次结构 ExpandoObject 可以构建复杂的嵌套属性结构。例如,我们可以创建一个具有子对象的 ExpandoObject
Imports System.Dynamic

Module Module1
    Sub Main()
        Dim parentExpando As New ExpandoObject()
        Dim parentDict As IDictionary(Of String, Object) = CType(parentExpando, IDictionary(Of String, Object))
        Dim childExpando As New ExpandoObject()
        Dim childDict As IDictionary(Of String, Object) = CType(childExpando, IDictionary(Of String, Object))
        childDict.Add("ChildProperty", "Value of child property")
        parentDict.Add("ChildObject", childExpando)
        Console.WriteLine("Child property value: " & CType(parentExpando, Dynamic).ChildObject.ChildProperty)
    End Sub
End Module

这里我们创建了一个 parentExpando 对象,并在其中添加了一个 ChildObject 属性,该属性的值是另一个 ExpandoObject childExpando。通过这种方式,我们可以构建类似 JSON 嵌套结构的动态对象层次。

ExpandoObject 与其他类型的转换

  1. 转换为自定义类型 有时候我们可能需要将 ExpandoObject 转换为自定义的类型。例如,我们有一个定义好的类 Person
Class Person
    Public Property Name As String
    Public Property Age As Integer
End Class

我们可以将 ExpandoObject 转换为 Person 类型。假设 ExpandoObject 具有 NameAge 属性:

Imports System.Dynamic

Module Module1
    Sub Main()
        Dim expando As New ExpandoObject()
        Dim expandoDict As IDictionary(Of String, Object) = CType(expando, IDictionary(Of String, Object))
        expandoDict.Add("Name", "Charlie")
        expandoDict.Add("Age", 35)
        Dim person As New Person()
        For Each kvp In expandoDict
            Dim propInfo = person.GetType().GetProperty(kvp.Key)
            If propInfo IsNot Nothing Then
                propInfo.SetValue(person, kvp.Value)
            End If
        Next
        Console.WriteLine("Name: " & person.Name)
        Console.WriteLine("Age: " & person.Age)
    End Sub
End Module

在上述代码中,我们通过反射获取 Person 类的属性,并将 ExpandoObject 中的属性值设置到 Person 对象的相应属性上,从而实现了从 ExpandoObject 到自定义类型的转换。

  1. 转换为字典类型ExpandoObject 转换为字典类型是很常见的操作。由于 ExpandoObject 实现了 IDictionary(Of String, Object) 接口,我们可以直接进行转换:
Imports System.Dynamic

Module Module1
    Sub Main()
        Dim expando As New ExpandoObject()
        Dim expandoDict As IDictionary(Of String, Object) = CType(expando, IDictionary(Of String, Object))
        expandoDict.Add("City", "New York")
        expandoDict.Add("Country", "USA")
        Dim normalDict As Dictionary(Of String, Object) = New Dictionary(Of String, Object)(expandoDict)
        For Each kvp In normalDict
            Console.WriteLine(kvp.Key & ": " & kvp.Value)
        Next
    End Sub
End Module

这里将 ExpandoObject 转换为普通的 Dictionary(Of String, Object),方便进行进一步的字典操作。

ExpandoObject 的性能考量

  1. 属性访问性能 ExpandoObject 的属性访问性能相对较低。因为每次访问属性时,它需要在内部的字典结构中查找属性名,而不像普通对象那样可以直接通过内存偏移量访问属性。例如,对于一个具有大量属性的 ExpandoObject,连续访问属性的操作会比普通对象慢。
Imports System.Dynamic
Imports System.Diagnostics

Module Module1
    Sub Main()
        Dim expando As New ExpandoObject()
        Dim expandoDict As IDictionary(Of String, Object) = CType(expando, IDictionary(Of String, Object))
        For i As Integer = 0 To 10000
            expandoDict.Add("Property" & i, i)
        Next
        Dim sw As New Stopwatch()
        sw.Start()
        For i As Integer = 0 To 10000
            Dim value = CType(expando, Dynamic)("Property" & i)
        Next
        sw.Stop()
        Console.WriteLine("ExpandoObject access time: " & sw.ElapsedMilliseconds & " ms")


        Dim normalObj As New System.Dynamic.ExpandoObjectProperties()
        For i As Integer = 0 To 10000
            normalObj("Property" & i) = i
        Next
        sw.Restart()
        For i As Integer = 0 To 10000
            Dim value = normalObj("Property" & i)
        Next
        sw.Stop()
        Console.WriteLine("Normal object access time: " & sw.ElapsedMilliseconds & " ms")
    End Class

    Class ExpandoObjectProperties
        Private properties As New Dictionary(Of String, Object)()
        Public Default Property Item(key As String) As Object
            Get
                Return properties(key)
            End Get
            Set(value As Object)
                properties(key) = value
            End Set
        End Property
    End Class
End Module

在这个示例中,我们对比了 ExpandoObject 和一个自定义的具有类似字典结构属性访问的类的性能。可以看到,ExpandoObject 的属性访问时间明显更长。

  1. 内存占用 ExpandoObject 由于需要维护内部的字典结构来存储属性,在存储大量属性时会占用较多的内存。每个属性在字典中都需要占用一定的内存空间,包括属性名和属性值。如果属性值是复杂对象,内存占用会更高。因此,在处理大量动态属性且对内存敏感的场景下,需要谨慎使用 ExpandoObject

动态类型与 ExpandoObject 的结合使用

  1. 动态访问 ExpandoObject 动态类型与 ExpandoObject 结合可以发挥出强大的功能。我们可以通过动态类型更方便地访问 ExpandoObject 的属性和方法。例如:
Imports System.Dynamic

Module Module1
    Sub Main()
        Dim expando As New ExpandoObject()
        Dim expandoDict As IDictionary(Of String, Object) = CType(expando, IDictionary(Of String, Object))
        expandoDict.Add("Message", "Hello from ExpandoObject")
        Dim dynamicExpando As Dynamic = expando
        Console.WriteLine(dynamicExpando.Message)
    End Sub
End Module

这里将 ExpandoObject 赋值给一个 Dynamic 类型的变量 dynamicExpando,通过 dynamicExpando 可以像访问普通对象属性一样访问 ExpandoObject 的动态属性,代码更加简洁直观。

  1. 动态创建和操作 ExpandoObject 层次结构 结合动态类型,我们可以更灵活地创建和操作 ExpandoObject 的嵌套层次结构。例如,我们可以动态地决定是否创建子对象并添加属性:
Imports System.Dynamic

Module Module1
    Sub Main()
        Dim rootExpando As New ExpandoObject()
        Dim rootDict As IDictionary(Of String, Object) = CType(rootExpando, IDictionary(Of String, Object))
        Dim createChild As Boolean = True
        If createChild Then
            Dim childExpando As New ExpandoObject()
            Dim childDict As IDictionary(Of String, Object) = CType(childExpando, IDictionary(Of String, Object))
            childDict.Add("ChildProp", "Value of child property")
            rootDict.Add("ChildObject", childExpando)
        End If
        Dim dynamicRoot As Dynamic = rootExpando
        If createChild Then
            Console.WriteLine("Child property value: " & dynamicRoot.ChildObject.ChildProp)
        End If
    End Sub
End Module

在上述代码中,根据 createChild 的值动态地创建 ExpandoObject 的子对象,并通过动态类型方便地访问子对象的属性。

在实际项目中应用动态类型与 ExpandoObject

  1. Web 应用开发 在 Web 应用开发中,当处理来自客户端的动态表单数据时,动态类型和 ExpandoObject 可以发挥作用。例如,在 ASP.NET Web Forms 中,假设我们有一个表单,用户可以自由选择要提交的字段。我们可以将表单数据解析为 ExpandoObject
Imports System.Dynamic
Imports System.Web.UI

Partial Class _Default
    Inherits Page

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Load
        If IsPostBack Then
            Dim formData As New ExpandoObject()
            Dim formDict As IDictionary(Of String, Object) = CType(formData, IDictionary(Of String, Object))
            For Each key In Request.Form.AllKeys
                formDict.Add(key, Request.Form(key))
            Next
            Dim dynamicForm As Dynamic = formData
            Console.WriteLine("Field1 value: " & dynamicForm.Field1) '假设表单中有 Field1 字段
        End If
    End Sub
End Class

这里将表单数据动态地添加到 ExpandoObject 中,并通过动态类型访问特定字段的值。

  1. 插件式架构开发 在插件式架构开发中,动态类型和 ExpandoObject 可以用于插件之间的交互。例如,主程序提供一个扩展点,插件可以通过这个扩展点向主程序注册动态属性和方法。主程序可以使用动态类型来调用这些插件提供的功能。
Imports System.Dynamic
Imports System.Collections.Generic

Module MainModule
    Private plugins As New List(Of ExpandoObject)()

    Sub RegisterPlugin(plugin As ExpandoObject)
        plugins.Add(plugin)
    End Sub

    Sub Main()
        Dim plugin1 As New ExpandoObject()
        Dim plugin1Dict As IDictionary(Of String, Object) = CType(plugin1, IDictionary(Of String, Object))
        plugin1Dict.Add("PluginName", "Plugin 1")
        plugin1Dict.Add("Execute", New Action(Sub() Console.WriteLine("Plugin 1 executed")))
        RegisterPlugin(plugin1)


        For Each plugin In plugins
            Dim dynamicPlugin As Dynamic = plugin
            Console.WriteLine("Plugin name: " & dynamicPlugin.PluginName)
            dynamicPlugin.Execute()
        Next
    End Sub
End Module

在这个示例中,插件通过 ExpandoObject 提供动态属性和方法,主程序通过动态类型调用这些插件功能,实现了插件式架构中的灵活交互。

总结动态类型与 ExpandoObject 的关系与应用策略

动态类型和 ExpandoObject 是 Visual Basic 中强大的工具,它们相互配合可以解决很多动态数据处理和灵活编程的问题。动态类型提供了运行时确定类型的灵活性,而 ExpandoObject 则允许在运行时动态构建对象结构。

在应用中,我们需要根据具体场景权衡使用。对于与动态数据源交互、与 COM 组件交互等场景,动态类型可以简化代码编写。而在需要动态构建对象模型、模拟动态对象行为时,ExpandoObject 是很好的选择。同时,要注意它们的局限性,如动态类型的编译时类型检查缺失和性能问题,以及 ExpandoObject 的属性访问性能和内存占用问题。

通过合理地运用动态类型与 ExpandoObject,我们可以提升代码的灵活性和适应性,在一些特定场景下提高开发效率,但也需要谨慎使用,以确保代码的健壮性和性能。在实际项目中,结合具体需求进行深入分析和测试,选择最合适的编程方式。