什么是dynamic类型?

微软给出的官方文档中这样解释:在通过 dynamic 类型实现的操作中,该类型的作用是绕过编译时类型检查。 改为在运行时解析这些操作。 dynamic 类型简化了对 COM API(例如 Office Automation API)、动态 API(例如 IronPython 库)和 HTML 文档对象模型 (DOM) 的访问。在大多数情况下,dynamic 类型与 类型的行为类似。 但是,如果操作包含 dynamic 类型的表达式,那么不会通过编译器对该操作进行解析或类型检查。 编译器将有关该操作信息打包在一起,之后这些信息会用于在运行时评估操作。 在此过程中,dynamic类型的变量会编译为 类型的变量。 因此,dynamic 类型只在编译时存在,在运行时则不存在。

dynamic的出现让C#具有了弱语言类型的特性。编译器在编译的时候不再对类型进行检查,编译期默认dynamic对象支持你想要的任何特性。

下例中生成的类型是一致的:

dynamic dyn = \"Fode\";           obj = \"Fode\";
// Rest the mouse pointer over dyn and obj to see their
  // types at compile time.
  System.Console.WriteLine(dyn.GetType());
  System.Console.WriteLine(obj.GetType());

其输出结果都是String类型,可知CLR可以正确的识别出dynamic是哪种类型,在反编译看看其生成的IL代码:

IL_0000: nop
 IL_0001: ldstr \"Fode\"
 IL_0006: stloc.0
 IL_0007: ldstr \"Fode\"
 IL_000c: stloc.1
 IL_000d: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
 IL_0012: pop
 IL_0013: ret

JIT编译器将dynamic识别为String类型,并将其推算到运算栈中(IL代码中 ldstr(将新对象引用推送到存储在元数据中的字符串文字)、(stloc.*)从评估堆栈的顶部弹出当前值,并将其存储在索引*处的本地变量列表中),不同IL代码也不所谓,前文只是介绍dynamic这个类型关键字,只需要你知道他的类型是绕过编译器就可以,如以下操作, 类型就会报编译的错误。但是,对于 dyn + 3,不会报告任何错误。 在编译时不会检查包含 dyn 的表达式,原因是 dyn 的类型为 dynamic。

   dynamic dyn = \"Fode\";
     obj = \"Fode\";
   dyn = dyn + 3;
   obj = obj + 3; //这句代码编译器会报错

dynamic是 work 4.0的新特性。dynamic的出现让C#具有了若语言的特性。编译器在编译时候不再对该类型进行检查,编译器默认dynamic对象支持开发者想要的任何特征。比如,即使你对 GetStudent()方法返回的对象一无所知,也可以像以下执行代码的调用,编译器不会报错:

static void Main(string[] args)
  {
   dynamic dyn = GetStudent();

   //正确的操作
   Console.WriteLine(dyn.Age);
   Console.WriteLine(dyn.Name);
   dyn.PrintName();

   //错误的操作
   //Console.WriteLine(dyn.Birthday); //该对象没有包含该属性
   //dyn.PrintAge(); //这行代码会报错误,访问级别不够
   Console.ReadKey();
  }
  static Student GetStudent()
  {
   Student student = new Student();
   student.Age = 21;
   student.Name = \"Fode\";
   return student;
  }

  class Student
  {
   public String Name { get; set; }
   public Int32 Age { get; set; }

   public void PrintName()
   {
    Console.WriteLine(this.Name);
   }

   private void PrintAge()
   {
    Console.WriteLine(this.Age);
   }
  }

如果运行时dyn对象不包含指定的特性(属性、字段、方法等),运行时会抛出一个运行时的错误 RuntimeBinderException。

注意:有人可能会将var关键字与dynamic进行比较。实际上,var和dynamic完全是两回事,两个不同的概念。var实际上是编译期间抛给我门的“语法糖”,一旦被编译,编译器会自动匹配var变量的实际类型,并用实际类型来替换给变量的声明,这看上去就好像我们在编码的时候用实际类型进行声明一样,优点也显而易见,当【赋值方】类型发生变化时,【实现方】无需改变其类型,因为var会自动去适配。而dynamic被编译后,实际上是一个 类型,只不过编译器会对dynamic类型进行特殊处理,让它在编译期间不进行任何的类型检查,而是将类型检查放到了运行期。这从VS这个IDE就能看出,在编辑窗口中,var支持【智能感知】,因为vs能推断出var类型的实际类型;而dynamic声明的变量却不支持【智能感知】,因为对其运行期的类型一无所知。对dynamic变量使用【智能感知】会提示\"此操作将在运行时解析\"。

BB了这么久,重点来了,利用好了动态类型dynamic的这个特性,可以简化C#中的反射语法,更高深的优化将在以后的博客推出。在dynamic出现之前,我们先用基础反射获取一个类中的方法,并执行它:

static void Main()
  {
   DynamicObj obj = new DynamicObj();
   var fun = obj.GetType().GetMethod(nameof(obj.CallFun));
   Int32 result = (Int32)fun.Invoke(obj, new  [] { \"Fode\" });
   Console.WriteLine(result);
   Console.ReadKey();
  } 
 
  public class DynamicObj
  {
   public Int32 CallFun(String str)
   {
    return str.Length;
   }
  }

其结果没有什么好讲的,就是一个用反射调用 CallFun() 方法的例子,而用dynamic之后,代码看上去更简洁了,并且在可控制的范围内减少了一次拆箱的操作,代码如下:

 dynamic dyn = new DynamicObj();
 Int32 result = dyn.CallFun(\"Fode\");
 Console.WriteLine(result);

可能我们会对这样的简化不以为然,毕竟代码看起来并没有减少多少,但是,如果考虑到效率兼优美两个特性,那么dynamic的优势就显现出来了。对上面的代码个执行10000000次,在进行分析,如下所示:

CodeTimer.Time(\"使用dynamic\", 10000000, () => { //执行里面的代码10000000次
    dynamic dyn = new DynamicObj();
    Int32 result = dyn.CallFun(\"Fode\");
   });

   CodeTimer.Time(\"使用基础反射\", 10000000, () => { //执行里面的代码10000000次
    DynamicObj obj = new DynamicObj();
    var fun = obj.GetType().GetMethod(nameof(obj.CallFun));
    Int32 result = (Int32)fun.Invoke(obj, new  [] { \"Fode\" });
   });
   Console.ReadKey();

其运行结果如下所示:

\"\"

从以上结果看出,使用dynamic使用时间为481ms,基础反射使用时间为3063ms,CPU和时间上相差了5倍多,测试器 CodeTimer 的代码随后会贴出。

总结:

可以看到虽然用dynamic优化后的反射跟基础反射的相比,效率虽然在同一个数量级上。可是基础反射却没有dynamic代码简洁,因此建议:始终使用dynamic来简化反射实现(前提你知道你要是实现的类型),在往后的随笔,将会提出用 Tree和Emit技术深度优化反射。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。

收藏 打印