C# 动态编程(基于 dynamic 类型)

一、动态编程的核心:dynamic 类型的引入

C# 4.0 引入 dynamic 类型,其核心是运行时绑定(动态绑定),而非编译时绑定(静态绑定)。

  • 静态绑定:编译时验证类型、成员是否存在,若不存在直接报错。
  • 动态绑定:编译时不验证,运行时才解析对象的类型、成员并执行调用。

二、为什么需要动态编程?

有些场景下,对象的结构在编译时无法确定,典型场景包括:

  • 从 XML/CSV、数据库、IE DOM 等动态数据源加载数据;
  • 调用 COM 组件(如 IDispatch 接口);
  • 调用动态语言(如 IronPython)定义的类型;
  • 简化反射代码的编写。

三、C# 动态对象的 4 种绑定方式

C# 4.0 中,dynamic 支持 4 种运行时绑定逻辑:

  • 针对 CLR 类型的反射:通过反射机制动态查找 / 调用类型成员;
  • 自定义 IDynamicMetaObjectProvider:自己实现动态对象的绑定逻辑;
  • COM 的 IUnknown/IDispatch 接口:与 COM 组件交互;
  • 动态语言类型:调用 IronPython 等动态语言定义的类型。

四、方式 1:用 dynamic 简化反射调用

反射的核心是 "运行时查找 / 调用类型成员",但原生反射代码繁琐;dynamic 可以用更简洁的语法实现反射功能

4.1 原生反射 vs dynamic 反射

原生反射(繁琐):

cs 复制代码
// 假设要调用 string 的 Length 属性
object str = "Hello";
Type type = str.GetType();
PropertyInfo prop = type.GetProperty("Length");
int length = (int)prop.GetValue(str); // 运行时获取值

dynamic 反射(简洁):

cs 复制代码
dynamic str = "Hello";
int length = str.Length; // 编译时不验证,运行时通过反射调用 Length

4.2 dynamic 反射的代码示例

cs 复制代码
using System;

dynamic data = "Hello! My name is Inigo Montoya";
Console.WriteLine(data);
data = (double)data.Length; // 字符串转 double(运行时执行装箱/拆箱)
data = data + 28.6;
if(data == 2.4 + 112 + 26.2)
{
    Console.WriteLine($"{data} makes for a long triathlon.");
}
else
{
    data.NonExistentMethodCallStillCompiles(); // 编译不报错,运行时抛异常
}

输出:

cs 复制代码
Hello! My name is Inigo Montoya
40.6 makes for a long triathlon.

4.3 dynamic 反射的注意事项

  • 编译时不验证,运行时抛异常 :若调用的成员不存在(如 data.NonExistentMethodCallStillCompiles()),运行时会抛出 Microsoft.CSharp.RuntimeBinder.RuntimeBinderException
  • 不支持扩展方法 :扩展方法是 "编译时绑定" 的语法糖,dynamic 是运行时绑定,因此无法直接调用扩展方法(需通过静态类显式调用)。
  • 返回值默认是 dynamic :调用 dynamic 对象的成员,返回值默认是 dynamic 类型(需显式转换为具体类型)。

五、dynamic 的核心原则和行为

dynamic 不是 "新类型",而是告诉编译器 "运行时再处理绑定" 的指令 ,其底层是 System.Object

5.1 dynamicobject 的关系
  • dynamic 本质是 System.Object:IL 代码中,dynamic 会被编译为 object
  • 隐式转换:任何类型都能隐式转换为 dynamic(值类型会自动装箱);
  • 显式转换:dynamic 转具体类型需要显式转换(值类型会自动拆箱)。
5.2 dynamic 的 "动态性" 体现
  • 基础类型可动态变更dynamic 变量的底层类型可以在运行时改变(如示例中 datastring 变为 double);
  • 运行时才验证成员:编译时不检查成员是否存在,运行时通过 "解释机制" 解析调用(底层依赖反射)。

六、方式 2:自定义动态对象(实现 IDynamicMetaObjectProvider

若内置的动态绑定逻辑不满足需求(如自定义 XML/JSON 动态解析),可以自己实现动态对象

C# 提供了 System.Dynamic.DynamicObject 抽象类(实现了 IDynamicMetaObjectProvider),只需重写对应的虚方法,即可自定义动态绑定逻辑。

6.1 自定义动态对象的核心:重写 DynamicObject 的方法

DynamicObject 提供了一系列虚方法,用于自定义动态行为,常用的有:

  • TryGetMember:自定义 "获取动态成员" 的逻辑;
  • TrySetMember:自定义 "设置动态成员" 的逻辑;
  • TryInvokeMember:自定义 "调用动态方法" 的逻辑;
  • TryConvert:自定义 "类型转换" 的逻辑;
  • TryGetIndex/TrySetIndex:自定义 "索引器" 的逻辑。

6.2 示例:实现 DynamicXml(动态解析 XML)

需求:通过 dynamic 语法直接访问 XML 元素(如 person.FirstName 对应 XML 中的 <FirstName> 节点)。

步骤 1:定义 DynamicXml 类(继承 DynamicObject

cs 复制代码
using System;
using System.Dynamic;
using System.Xml.Linq;

public class DynamicXml : DynamicObject
{
    // 包装的 XML 元素
    private XElement Element { get; set; }

    // 构造函数
    public DynamicXml(XElement element)
    {
        Element = element;
    }

    // 工厂方法:解析 XML 字符串
    public static DynamicXml Parse(string text)
    {
        return new DynamicXml(XElement.Parse(text));
    }


    // 重写:自定义"获取成员"的逻辑
    public override bool TryGetMember(
        GetMemberBinder binder, out object result)
    {
        bool success = false;
        result = null;

        // 查找与"成员名"匹配的 XML 子元素
        XElement firstDescendant = Element.Descendants(binder.Name).FirstOrDefault();
        if (firstDescendant != null)
        {
            // 若子元素有子节点,递归包装为 DynamicXml;否则返回文本值
            if (firstDescendant.Descendants().Any())
            {
                result = new DynamicXml(firstDescendant);
            }
            else
            {
                result = firstDescendant.Value;
            }
            success = true;
        }
        return success;
    }


    // 重写:自定义"设置成员"的逻辑
    public override bool TrySetMember(
        SetMemberBinder binder, object value)
    {
        bool success = false;
        XElement firstDescendant = Element.Descendants(binder.Name).FirstOrDefault();
        if (firstDescendant != null)
        {
            // 若值是 DynamicXml,取其包装的 XElement;否则直接设为文本
            if (value.GetType() == typeof(DynamicXml))
            {
                firstDescendant.ReplaceWith(((DynamicXml)value).Element);
            }
            else
            {
                firstDescendant.Value = value.ToString();
            }
            success = true;
        }
        return success;
    }
}

步骤 2:使用 DynamicXml

cs 复制代码
using System;

// 解析 XML 字符串
dynamic person = DynamicXml.Parse(@"
    <Person>
        <FirstName>Inigo</FirstName>
        <LastName>Montoya</LastName>
    </Person>");

// 直接通过"动态成员"访问 XML 元素
Console.WriteLine($"{person.FirstName} {person.LastName}");

输出:

cs 复制代码
Inigo Montoya

6.3 自定义动态对象的逻辑说明

当调用 person.FirstName 时:

  • 编译器识别到 persondynamic,不做编译时验证;
  • 运行时触发 DynamicXml.TryGetMember 方法;
  • TryGetMember 中,通过 binder.Name 获取成员名(FirstName);
  • 查找 XML 中对应的 <FirstName> 节点,返回其值;
  • 若找不到节点,TryGetMember 返回 false,运行时抛出 RuntimeBinderException

七、静态编程 vs 动态编程:对比与适用场景

|-------|--------------|-----------------------|
| 维度 | 静态编程(编译时绑定) | 动态编程(运行时绑定) |
| 编译时验证 | 验证类型、成员是否存在 | 不验证,运行时才检查 |
| 代码可读性 | 依赖强类型,可读性高 | 语法简洁(如动态 XML 访问) |
| 性能 | 编译时绑定,性能高 | 运行时反射 / 解释,性能较低 |
| 类型安全性 | 编译时保证,错误更早暴露 | 运行时才发现错误 |
| 适用场景 | 类型结构固定的场景 | 类型结构动态(XML/COM/ 动态语言) |

八、动态编程的扩展

8.1 动态编程的底层:CallSite 与表达式树

dynamic 调用的底层依赖 System.Runtime.CompilerServices.CallSite<T>

  • 编译时,编译器生成 CallSite 对象,用于封装动态调用的上下文;
  • 运行时,CallSite 会生成表达式树,将动态调用编译为 CIL 代码并缓存;
  • 后续相同的动态调用会复用缓存的 CIL 代码,减少反射开销。

8.2 动态编程的局限性

  • 性能:运行时绑定的性能比静态绑定低(反射 / 表达式树编译有开销);
  • 工具支持:IDE 无法提供 "智能感知"(编译时不知道成员);
  • 调试难度:错误在运行时暴露,调试需跟踪运行时状态。

九、小结

  • dynamic 是 C# 4.0 引入的 "运行时绑定" 语法糖,底层依赖反射 / 表达式树;
  • 动态编程适用于类型结构动态的场景(XML/COM/ 动态语言交互);
  • 可通过 DynamicObject 自定义动态对象,重写 TryGetMember/TrySetMember 等方法实现自定义绑定逻辑;
  • 动态编程的优势是语法简洁 ,劣势是性能低、类型不安全,需根据场景选择。
相关推荐
星火开发设计1 小时前
枚举类 enum class:强类型枚举的优势
linux·开发语言·c++·学习·算法·知识
喜欢吃燃面6 小时前
Linux:环境变量
linux·开发语言·学习
徐徐同学6 小时前
cpolar为IT-Tools 解锁公网访问,远程开发再也不卡壳
java·开发语言·分布式
LawrenceLan6 小时前
Flutter 零基础入门(二十六):StatefulWidget 与状态更新 setState
开发语言·前端·flutter·dart
m0_748229996 小时前
Laravel8.X核心功能全解析
开发语言·数据库·php
qq_192779877 小时前
C++模块化编程指南
开发语言·c++·算法
代码村新手7 小时前
C++-String
开发语言·c++
qq_401700417 小时前
Qt 中文乱码的根源:QString::fromLocal8Bit 和 fromUtf8 区别在哪?
开发语言·qt
EndingCoder9 小时前
案例研究:从 JavaScript 迁移到 TypeScript
开发语言·前端·javascript·性能优化·typescript