c# 高级编程 16章339页 【反射、元数据、动态编程】【动态编程】

380 阅读1分钟

为反射 使用 动态语言扩展

前面 一直 使用反射 来读取 元数据。还可以 使用 反射 从编译时 还不清楚的类型中 动态创建 实例

  • 编译器在编译时 不知道这种类型
    • 程序集 是动态加载的,没有添加引用
  • 在运行期间,实例化 对象,调用方法
  • 知道 如何使用 Reflection API 后, C# 关键字 dynamic 可以完成 相同的操作
    • C# 4.0 以来,就有dynamic关键字

示例

  • 要使用 反射 动态创建 Calculator 实例
  • 并使用两种方式 调用 Calculator 实例的方法
namespace CalculatorLib
{
    // This project can output the Class library as a NuGet Package.
    // To enable this option, right-click on the project and select the Properties menu item. In the Build tab select "Produce outputs on build".
    public class Calculator
    {
        public double Add(double x, double y) => x + y;
        public double Subtract(double x, double y) => x - y;
    }
}
using Microsoft.CSharp.RuntimeBinder;
using System;
using System.Reflection;

namespace ClientApp
{
    class Program
    {
        private const string CalculatorTypeName = "CalculatorLib.Calculator";

        static void Main(string[] args)
        {
            if (args.Length != 1)
            {
                ShowUsage();
                return;
            }
            UsingReflection(args[0]);
            UsingReflectionWithDynamic(args[0]);
        }

        private static void ShowUsage()
        {
            Console.WriteLine($"Usage: {nameof(ClientApp)} path");
            Console.WriteLine();
            Console.WriteLine("Copy CalculatorLib.dll to an addin directory");
            Console.WriteLine("and pass the absolute path of this directory when starting the application to load the library");
        }

        private static void UsingReflectionWithDynamic(string addinPath)
        {
            double x = 3;
            double y = 4;
            dynamic calc = GetCalculator(addinPath);
            double result = calc.Add(x, y);
            Console.WriteLine($"the result of {x} and {y} is {result}");

            try
            {
                result = calc.Multiply(x, y);
            }
            catch (RuntimeBinderException ex)
            {
                Console.WriteLine(ex);
            }
        }

        private static void UsingReflection(string addinPath)
        {
            double x = 3;
            double y = 4;
            object calc = GetCalculator(addinPath);

            object result = calc.GetType().GetMethod("Add").Invoke(calc, new object[] { x, y });
            Console.WriteLine($"the result of {x} and {y} is {result}");
        }

        private static object GetCalculator(string addinPath)
        {
            Assembly assembly = Assembly.LoadFile(addinPath);
            return assembly.CreateInstance(CalculatorTypeName);
        }
    }
}

其中:

调用方法之前,需要实例化 Calculator

        private static object GetCalculator(string addinPath)
        {
            Assembly assembly = Assembly.LoadFile(addinPath);
            return assembly.CreateInstance(CalculatorTypeName);
        }
  • Assembly.LoadFile() 动态加载 程序集 (也就是 调用者 事先没有添加 对这个程序集的 引用)
    • 参数是:Calculator的库的路径
    • 返回值:Assembly 实例
  • Assembly的实例方法 CreateInstance()
    • 参数是:类名 (包括命名空间)
    • 返回值:实例

用 Reflection API 来调用 Calculator的方法:

优点是:

  • 类型 不需要 在编译期间 可用
  • 只要 把库复制到 指定目录中,就可以在 稍后添加它

为了 使用 反射 调用成员, 需要用GetType() 得到 实例的Type对象

object calc = GetCalculator(addinPath);
//这里传递"Add"这个名字,编译时并不检查这个方法是否存在
object result = calc.GetType().GetMethod("Add").Invoke(calc, new object[] { x, y });
  • MethodInfo对象的Invoke()方法
    • 第一个参数:需要调用成员的类型实例
    • 其他参数:
      • object[]
      • 任意多个参数,传给方法

用 动态类型 调用 Calculator的方法:

  • 将 Calculator 实例 赋给dynamic 类型的变量
dynamic calc = GetCalculator(addinPath);

double result = calc.Add(x, y);

优点:

  • 语法简单
  • 在特定情形下有优势
    • 反射。例如这个例子中,

缺点:

  • 看起来像是用强类型访问方式调用一个方法,但实际并不是
    • 没有在编译时进行检查
    • 有问题的话,运行时才会暴露
    • 与强类型方式访问对象相比dynamic类型有更多开销

dynamic 类型

dynamic 类型 允许 编写 忽略编译期间 类型检查 的代码

  • 编译器假定,给 dynamic 类型的对象 定义的任何操作 都是有效的
  • 如果该操作无效,则在代码运行之前 不会检测该错误
  • 运行时 错误为 RuntimeBinderException

  • dynamic 类型的两个限制
    • 不支持扩展方法
    • LINQ不能用
  • dynamic 类型 后台的实现:
    • 编译器在编译为IL时做了很多事,产生了更多IL代码
    • CallSite 类用来在运行时查找

dynamic 类型是有用的,但它是有代价的

比较 dynamicvar

  • dynamic的对象 可以 在运行期间 改变其类型
    • 可以改变
    • 可以改变多次
    • 这不同于 把对象的类型强制转换为另一种兼容的类型
    • 这里 甚至可以把它从int 变成 Person类
  • 使用var时,对象类型的确定会延迟,但类型一旦确定,就不能改变

DynamicObject 和 ExpandoObject

要创建自己的动态对象,有两种办法:

  1. 从 DynamicObject 中派生
    • 需要做的工作比较多
    • 必须重写几个方法
  2. 使用ExpandoObject
    • 是立即可用的密封类

DynamicObject 示例

重写了以下方法:

  • TrySetMember
    • 给对象 添加 新方法、属性或字段
    • 本例 将添加的东西 存在Dictionary里
    • SetMemberBinder的Name 用于 标识Dictionary里的东西
  • TryGetMember
    • 根据GetMemberBinder的Name 从Dictionary里检索东西
using System;
using System.Collections.Generic;
using System.Dynamic;

namespace DynamicSample
{
    public class WroxDynamicObject : DynamicObject
    {
        private Dictionary<string, object> _dynamicData = new Dictionary<string, object>();

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            bool success = false;
            result = null;
            if (_dynamicData.ContainsKey(binder.Name))
            {
                result = _dynamicData[binder.Name];
                success = true;
            }
            else
            {
                result = "Property Not Found!";
            }

            return success;
        }

        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            _dynamicData[binder.Name] = value;
            return true;
        }

        public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
        {
            dynamic method = _dynamicData[binder.Name];
            result = method((DateTime)args[0]);
            return result != null;
        }
    }
}

  • 可以直接使用FirstName和LastName 就好像它们一直存在一样。
  • 其实是DynamicObject处理了绑定
            dynamic dyn;
            dyn = new WroxDynamicObject();
            dyn.FirstName = "Bugs";
            dyn.LastName = "Bunny";
            Console.WriteLine(dyn.GetType());
            Console.WriteLine($"{dyn.FirstName} {dyn.LastName}");
            dynamic dyn;
            Func<DateTime, string> GetTomorrow = today => today.AddDays(1).ToString("d");
            dyn.GetTomorrowDate = GetTomorrow;
            Console.WriteLine("Tomorrow is {0}", dyn.GetTomorrowDate(DateTime.Now));