回顾总结

2019年8月23日

  1. 需要装箱的情况

    • 值类型重写的虚方法如果要调用基类的实现,就需要装箱传this指针引用给基方法
    • 如果重写基类的虚方法(不需要调用基类实现)就不需要装箱
    • 要调用非虚的,继承的方法(GetType)无论如何都要对值类型进行装箱
    • 将值类型转型为接口时,进行装箱. 因为接口变量是引用类型
  2. 使用接口方法修改已装箱值类型中的字段,在C#中,不用接口方法便无法做到.

    • ((IChangeBoxedPoint) o).Change(5,5); // 修改已装箱对象中的值
    • ((Point) o).Change(3, 3); // 值复制到栈上,只在栈上修改
  3. 值类型应该是不可变的,不应该定义任何会修改实例字段的成员,建议标记成readonly

  4. dynamic(动态绑定)基元类型

    • C#编译器允许将表达式/变量的类型标记为dynamic
    • 对于CLR, dynamicObject完全一致.
    • C#编译器会生成payload有效载荷代码,在运行时检查var的实际类型,调用对应的重载版本.
    • 编译器 不允许 写代码将表达式从Object隐式转成其他类型. 必须显示转型.
    • 编译器 允许 使用隐式转型语法将表达式从dynamic转型为其他类型.
      • 运行时,dynamic类型不兼容要转型的类型,则会抛出InvalidCastException.
      • 返回值result具有dynamic类型. 如果运行时调用M方法.返回类型是void,将抛出RuntimeBinderException异常.
    • 变量/表达式用dynamic会生成 payload代码,进行动态调度.
    • 不能定义对dynamic进行扩展的扩展方法.
    • 不能将lanmbda表达式和匿名方法作为实参传给dynamic方法调用. 因为编译器推断不了类型.
    • 在foreach或者using语句中的资源被指定了dynamic表达式, 编译器分别将表达式转型为非泛型System.IEnumerable接口或System.IDispose接口.
      • 转型失败会抛出RuntimeBinderException异常.
  5. var和dynamic

    • var只能用在方法内部声明局部变量,必须显式初始化此变量
    • 表达式不能转型为var,但是能转型dynamic,也无需初始化用dynamic声明的变量.
  6. dynamic可以显著简化一些反射的代码

    • 但是会对性能产生影响
    • 如果只是一两个地方需要动态行为,反射做法或许更高效
    • dynamic的一个限制是只能访问对象的实例成员,因为dynamic变量必须引用对象

什么时候需要装箱

  • 第一种情况: 将值类型的实例传给需要获取引用类型的方法.

要获取值类型的引用,实例就必须装箱.

未装箱值类型比引用类型更”轻”,归结于

  1. 不在托管堆上分配
  2. 没有堆上的每个对象都有的额外成员:类型对象指针同步块索引

其中,由于未装箱值类型没有同步块索引,所以不能使用System.Threading.Monitor(提供同步访问对象的机制)类型的方法(或C#lock语句),让多个线程同步对实例的访问.

  • 第二种情况: 值类型如果重写的虚方法(例如Equals,ToString…)方法要调用基类的实现时,会装箱,通过this指针将引用传给基方法.

虽然未装箱的值类型没有类型对象指针,但仍可以调用由类型继承或重写的虚方法.(比如Equals,GetHashCode,ToString).

值类型可以重写Equals, GetHashCode或者ToString的虚方法,CLR可以非虚地调用该方法,因为值类型是隐式密封的(即不存在多态性),没有任何类型能够从它们派生。如果你重写的虚方法要调用方法在基类中的实现,那么在调用基类的实现时,值类型实例就会装箱,以便通过this指针将对一个堆对象的引用传给基方法。

  • 第三种情况:

调用非虚的,继承的方法时(比如GetType或者MemberwiseClone),无论如何都要对值类型进行装箱, 因为这些方法由System.Object定义,要求this实参是指向堆对象的指针.

  • 第四种情况:

将值类型的未装箱实例 转型为某个接口时 要对实例进行装箱. 是因为接口变量必须包含对堆对象的引用.

using System;
using System.Collections;

namespace ConsoleApp1
{
    internal struct Point : IComparable
    {
        private Int32 m_x, m_y;
        public Point(Int32 x, Int32 y)
        {
            m_x = x;
            m_y = y;
        }
        // 重写从System.ValueType继承的ToString方法.
        public override string ToString()
        {
            // 将Point作为字符串返回. 调用ToString避免装箱
            return string.Format($"{m_x.ToString()}, {m_y.ToString()}");
        }
        // 实现类型安全的CompareTo方法
        public Int32 CompareTo(Point other)
        {
            // Math.sign方法用来判断一个数到底是正数、负数、还是零。
            // 利用勾股定理计算哪个Point离(0,0)更远
            return Math.Sign(
                Math.Sqrt(m_x * m_x + m_y * m_y) - Math.Sqrt(other.m_x * other.m_x + other.m_y * other.m_y));
        }
        // 实现IComparable接口的CompareTo方法
        public int CompareTo(object obj)
        {
            if (GetType() != obj.GetType()) throw new ArgumentException("obj is not a point");
            // 调用类型安全的CompareTo方法
            return CompareTo((Point) obj);
        }
    }


    class Program
    {
        static void Main(string[] args)
        {
            Point p1 = new Point(10,10);
            Point p2 = new Point(20,20);

            // 调用Point重写的ToString(虚方法)方法,不装箱p1
            Console.WriteLine(p1.ToString());

            // 调用GetType(非虚方法)时, 要对p1进行装箱
            // 调用非虚的,继承的方法时, 无论如何都要对值类型进行装箱
            // 因为这些方法由System.Object定义,要求this实参是指向堆对象的指针.
            Console.WriteLine(p1.GetType()); // 显示ConsoleApp1.Point

            // p1调用的是CompareTo(Point other),所以p2不需要装箱
            Console.WriteLine(p1.CompareTo(p2)); // 显示 -1

            // 装箱p1, 引用放到c中
            // 将值类型的未装箱实例 转型为某个接口时 要对实例进行装箱
            IComparable c = p1;
            Console.WriteLine(c.GetType()); // 显示ConsoleApp1.Point

            // 由于向CompareTo传递的不是Point变量,
            // 所以调用的是CompareTo(object obj),c不需要装箱,
            // 因为已经是引用了已装箱的Point
            Console.WriteLine(p1.CompareTo(c)); // 显示0

            // c是引用类型,不需要装箱
            // c调用的是IComparable的CompareTo(object obj)方法
            // 所以p2要装箱
            Console.WriteLine(c.CompareTo(p2)); // 显示-1

            // 对c拆箱, 字段复制到p2中
            p2 = (Point) c;
            Console.WriteLine(p2.ToString()); // 显示(10,10)  证明已经复制到栈
        }
    }
}

上述代码演示了涉及装箱和拆箱的几种情形

  1. 调用ToString
    • p1.ToString()时, p1不必装箱,因为ToString是从基类System.ValueType继承的虚方法.

通常,为了调用虚方法,CLR需要判断对象的类型来定位类型的方法表,由于p1是未装箱的值类型,,所以不存在 类型对象指针 这个成员. 但是JIT编译器发现Point重写了ToString方法,所以会 直接生产代码来直接(非虚地)调用重写的这个ToString方法 . 这里不存在多态性,没有类型能从它派生以提供虚方法的另一个实现. 但是如果Point.ToString方法内部调用了base.ToString() ,那么调会调用System.ValueType的ToString方法,值类型实例就会装箱.

  1. 调用GetType
    • 调用非虚方法GetType时,p1必须装箱. 因为Point的GetType方法从System.Object继承的,CLR必须使用指向类型的指针,这个指针只能通过装箱p1来获得.
  2. 调用CompareTo第一次
    • p1.CompareTo(p2)时,p1不用装箱,因为Point实现了CompareTo(Point other)方法,编译器直接调用它,并且传递的是值类型对象,不需要装箱.
  3. 转型为IComparable
    • p1转型为接口类型c时必须装箱. 因为接口被定义为引用类型.
  4. 调用CompareTo第二次
    • p1.CompareTo(c)时,传递的是接口变量c,所以编译器调用的是重载版本的CompareTo(object obj),要传递引用指针, c是引用了一个已装箱的Point,所以无序额外装箱.
  5. 调用CompareTo第三次
    • c.CompareTo(p2)时,c是引用堆上的已装箱Point对象,还是IComparable接口类型,只能调用接口的CompareTo(object obj)方法, 因此p2需要装箱.
  6. 转型为Point
    • 将c引用的堆上对象拆箱成Point复制到栈上的p2.

使用接口更改已装箱值类型中的字段(不应该这样做)

来看看你的理解程度,答出控制台输出什么.

using System;
using System.Collections;

namespace ConsoleApp1
{
    internal struct Point
    {
        private Int32 m_x, m_y;

        public Point(Int32 x, Int32 y)
        {
            m_x = x;
            m_y = y;
        }
        public void Change(Int32 x, Int32 y)
        {
            m_x = x;
            m_y = y;
        }
        public override string ToString()
        {
            // 将Point作为字符串返回. 调用ToString避免装箱
            return string.Format($"{m_x.ToString()}, {m_y.ToString()}");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Point p = new Point(1, 1);
            Console.WriteLine(p);

            p.Change(2, 2);
            Console.WriteLine(p);

            Object o = p;
            Console.WriteLine(o);

            ((Point) o).Change(3, 3);
            Console.WriteLine(o);
        }
    }
}

答案:
1,1
2,2
2,2
2,2

解析:
重点说下((Point) o).Change(3, 3);

Object o对象对于Change方法一无所知, 所以需要拆箱转型到Point.

  • 拆箱转型过程
    • 将已装箱的Point中的字段 复制到线程栈 上的一个 临时Point 中.
    • 这个栈上的Point的m_x和m_y字段会变成3,3
    • 但是在堆上已装箱的Point里的值不受这个Change调用的影响.所以最后输出的Object o是堆上的2,2

有的语言(比如C/C++)允许更改已装箱值中的字段,但是C#不允许. 不过,可以用接口欺骗C#,让它允许这个操作.代码如下:

using System;
using System.Collections;

namespace ConsoleApp1
{
    internal interface IChangeBoxedPoint
    {
        void Change(Int32 x, Int32 y);
    }
    internal struct Point : IChangeBoxedPoint
    {
        private Int32 m_x, m_y;
        public Point(Int32 x, Int32 y)
        {
            m_x = x;
            m_y = y;
        }
        public void Change(Int32 x, Int32 y)
        {
            m_x = x;
            m_y = y;
        }    
        public override string ToString()
        {
            // 将Point作为字符串返回. 调用ToString避免装箱
            return string.Format($"{m_x.ToString()}, {m_y.ToString()}");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Point p = new Point(1, 1);
            Console.WriteLine(p); // 1,1

            p.Change(2, 2);
            Console.WriteLine(p); // 2,2

            Object o = p;
            Console.WriteLine(o); // 2,2

            ((Point) o).Change(3, 3);
            Console.WriteLine(o); // 2,2

            // 将P转型接口,装箱
            // 在已装箱的值上调用Change, 堆上已装箱的值就变为4,4
            // 并没有引用指向这个已装箱值,即将被垃圾回收掉
            // 未装箱的Point p仍然是2,2
            ((IChangeBoxedPoint) p).Change(4,4);
            Console.WriteLine(p);// 2,2

            // 将引用类型o转成IChangeBoxedPoint,才能使用Change方法
            // 不需要装箱,因为o本来就是已装箱的Point
            // Change(5,5);正确修改了已装箱的值
            ((IChangeBoxedPoint) o).Change(5,5);
            Console.WriteLine(o); //  5,5
        }
    }
}

演示接口方法如何修改已装箱值类型中的字段,在C#中,不用接口方法便无法做到.

值类型应该是”不可变”(immutable). 也就是说我们不应该定义任何会修改实例字段的成员.建议将值类型字段都标记为readonly. 否则容易写出一个试图更改字段的方法,就会产生非预期的行为. 标记readonly后,编译时就会报错.前面的例子清楚的揭示了为什么这样做.

FCL的核心值类型(Byte,Int32,UInt32,Int64,UInt64,Single,Double,Decimal,BigInteger,Complex以及所有枚举)都是不可变的.

不可变(immutable): 即对象一旦被创建初始化后,它们的值就不能被改变,之后的每次改变都会产生一个新对象。

所以,对于不变对象来说,调用对象自身的任意方法,也不会改变该对象自身的内容。相反,这些方法会创建新的对象并返回,这样,就保证了不可变对象本身永远是不可变的。

dynamic(动态绑定)基元类型

C#是类型安全的编程语言. 意味着所有表达式都解析成类型的实例,编译器生成的代码只执行对该类型有效的操作.

类型安全的优势:

  1. 许多错误能在编译时检测到
  2. 能编译出更小,更快的代码. 是因为能在编译时进行更多预设,并在生成的IL和元数据中落实预设.

为了方便开发人员使用反射或者与其他组件通信.C#编译器允许将表达式/变量的类型标记为dynamic.

对于CLR, dynamic与Object完全一致.

dynamic 变量/表达式

变量/表达式 使用dynamic的话,编译器会生成payload有效载荷代码根据运行时具体的类型来决定执行的操作.

编译器生成特殊IL代码来描述所需操作,这种特殊的代码称为payload有效载荷.

在运行时,payload有效载荷代码根据dynamic表达式/变量引用的对象的实际类型来决定具体执行的操作.

static void Main(string[] args)
{
    // dynamic 局部变量:编译器会将dynamic转成System.Object.
    // 值类型会装箱.
    dynamic val;
    for (Int32 demo = 0;  demo<2 ;demo ++)
    {
        // dynamic 表达式 : 编译器会生成payload代码
        val = (demo == 0) ? (dynamic) 5 : (dynamic) "A";
        // 两个操作数的类型是dynamic
        val = val + val;
        // 传入dynamic类型参数, C#编译器会生成payload代码
        // 在运行时检查val的实际类型.
        // 调用对应的重载版本.
        M(val);
    }
}
private static void M(Int32 n)
{
    Console.WriteLine($"M(Int32):{n}");
}

private static void M(String s)
{
    Console.WriteLine($"M(String):{s}");
}

输出:
M(Int32):10
M(String):AA

由于val是dynamic类型,C#编译器生成payload代码在运行时检查value的实际类型,然后决定+操作符实际要做什么.

  1. 第一个循环中: val = 5(Int32值),结果是10 .
  2. M(val);传入dynamic类型参数, C#编译器会生成payload有效载荷代码,在运行时检查val的实际类型,调用对应的重载版本.

所有表达式都能隐式转型为dynamic,因为所有表达式最终都生成从Object派生的类型(值类型需要装箱).

正常情况下:

  1. 编译器 不允许 写代码将表达式Object隐式转成其他类型. 必须显示转型.
  2. 但是,编译器 允许 使用隐式转型语法将表达式dynamic转型为其他类型.
    • 虽然编译器允许省略显示转型,但CLR会在运行时验证来确保类型的安全性.
    • 运行时,dynamic类型不兼容要转型的类型,则会抛出InvalidCastException.
Object o1 = 123;    // OK: 从值类型隐式转型为Object,装箱
//Int32 n1 = o1;    // Error: 不允许从Object隐式转型到Int32

Int32 n2 = (Int32) o1; // OK: 显示转型, 拆箱

dynamic d1 = 123;    // OK: 从值类型隐式转为dynamic,装箱
Int32 n3 = d1;       // OK: 从dynamic隐式转为值类型,拆箱
// 编译出的IL代码会对值类型123进行装箱
dynamic d = 123;
// d引用了已装箱的Int32
var result = M(d); // var result 等同于 dynamic result

上述代码之所以能通过编译,是因为编译器不知道调用哪个方法,也不知道返回的类型,所以编译器假定result具有dynamic类型. 如果运行时调用M方法.返回类型是void,将抛出Microsoft.CSharp.RuntimeBinder.RuntimeBinderException异常.

var声明局部变量是一种简化语法,要求编译器根据表达式推断具体数据类型. 只能用在方法内部声明局部变量,必须显式初始化用var声明的变量.

表达式不能转型为var,但是能转型dynamic,也无需初始化用dynamic声明的变量.

dynamic 字段/方法参数/方法返回值

字段/方法参数/方法返回值 是dynamic类型, 编译器会将dynamic转成System.Object.

C#编译器会将该类型转换为System.Object,并在元数据中向字段/参数/返回类型应用System.Runtime.CompilerServices.DynamicAttribute的实例.

如果指定局部变量被指定为dynamic, 则变量类型也会成为Object,局部变量不会应用DynamicAttribute,因为限制在方法内部使用.

由于dynamic其实就是Object,所以方法签名不能仅靠dynamic和Object来区分.

dynamic 泛型类(引用类型),结构(值类型),接口,委托,方法的泛型类型实参

泛型类(引用类型),结构(值类型),接口,委托,方法泛型类型实参也可以是dynamic,编译器将dynamic转为Object. 向必要的元数据应用DynamicAttribute.

泛型方法是已经编译好的,会将类型视为Object,编译器不在泛型代码中生成payload代码.所以不会执行动态调度.

重要提示:

  1. 对于CLR, dynamic与Object完全一致.
  2. 变量/表达式用dynamic会生成 payload代码,进行动态调度.
  3. 不能定义对dynamic进行扩展的扩展方法.
  4. 不能将lanmbda表达式匿名方法作为实参传给dynamic方法调用. 因为编译器推断不了类型.
  5. foreach或者using语句中的资源被指定了dynamic表达式, 编译器分别将表达式转型为非泛型System.IEnumerable接口或System.IDispose接口.
    • 转型成功,就是用表达式,代码正常运行.
    • 失败就抛出Microsoft.CSharp.RuntimeBinderException异常.

dynamic 具体用法

什么是动态化(dynamification)?

  • 在为COM对象生成可由”运行时”调用的包装(warpper)程序集是,COM方法中使用任何VARIANT实际都转换成dynamic.
// 如果没有dynamic类型,就需要转型成Range类型,才能访问Value属性
((Range)execel.Cells[1,1]).Value = "Text";

// 由于excel.Cells[1,1]是dynamic类型,所以不需要显示转型.
execel.Cells[1,1].Value = "Text";

利用反射和dynamic的例子

Object target = "ABC";
Object arg    = "ff";

// 在目标上查找和希望的实参类型匹配的方法.
// 从目标target的String类型上查找 方法名MethodA,参数的类型String的方法信息
Type[]     argTypes = new Type[] {arg.GetType()};
MethodInfo method   = target.GetType().GetMethod("MethodA", argTypes);

// 在目标上调用方法,传递实参"ff"
Object[] arguments = new Object[] {arg};
Boolean  result    = Convert.ToBoolean(method.Invoke(target, arguments));


// 利用dynamic简化上述代码写法
dynamic target1 = "ABC";
dynamic arg1    = "ff";
result = target1.MethodA(arg1);

可以看到显著简化的语法.

payload代码

C#编译器会生成payload代码,在运行时根据对象的实际类型判断要持续什么操作.

这些payload代码使用了称为运行时绑定器(RuntimeBinder)的类.

C# 运行时绑定器(RuntimeBinder)的代码在Microsoft.CSharp.dll程序集中. 生成使用dynamic关键字就必须引用该程序集(默认的csc.rsp中已经引用了该程序集).

是这个程序集中的代码知道在运行时生成代码,在+操作符2个Int32执行加法,+操作符两个string时执行连接.
运行时绑定器(RuntimeBinder) 首先检查类型是否实现了IDynamicMetaObjectProvider接口. 如果是就调用接口的GetMetaObject方法, 返回的类型DynamicMetaObject的派生类型能处理对象的多有成员,方法和操作符绑定.

payload代码执行时,会在运行时生成动态代码; 这些代码进入驻留于内存的程序集,即”匿名寄宿的DynamicMethods程序集(Anonymously Housted DynamicMethods Assembly)”,作用是当特定的call site(发生调用处)使用具有相同运行时类型的动态参数发出大量调用时增强动态调度的性能.

C#内建的动态功能所产生的额外开销不容忽视

虽然动态功能能简化语法,但也要加载这些程序集以及额外的内存消耗,会对内存造成额外的影响.

  • Microsoft.CSharp.dll
  • System.dll
  • System.Core.dll
  • System.Dynamic.dll (如果使用dynamic与COM组件互操作)

什么时候使用dynamic

  1. 如果程序只是 一两个地方需要动态行为,传统(反射)做法或许更高效. 托管对象则调用反射方法,COM对象则进行手动类型转换.
  2. 如果在动态表达式中使用的对公对象未实现IDynamicMetaObjectProvider接口,C#编译器会将对象视为C#定义的普通类型的实例,利用反射在对象上执行操作.

dynamic的限制

dynamic的一个限制是只能访问对象的实例成员,因为dynamic变量必须引用对象.但有时需要动态调用运行时才能确定一个类型的静态成员.

实现动态调用类型的静态成员方法

作者实现的StaticMemberDynamicWrapperDynamicObject派生,实现了接口IDynamicMetaObjectProvider.

        /// <summary>
        /// 构造一个 'dynamic' 的实例派生类,来动态调用类型的静态成员
        /// </summary>
        internal sealed class StaticMemberDynamicWrapper : DynamicObject //P(132)
        {
            private readonly TypeInfo m_type;
            public StaticMemberDynamicWrapper(Type type) { m_type = type.GetTypeInfo(); }

            public override IEnumerable<String> GetDynamicMemberNames()
            {
                return m_type.DeclaredMembers.Select(mi => mi.Name);
            }

            public override bool TryGetMember(GetMemberBinder binder, out object result)
            {
                result = null;
                var field = FindField(binder.Name);
                if (field != null) { result = field.GetValue(null); return true; }

                var prop = FindProperty(binder.Name, true);
                if (prop != null) { result = prop.GetValue(null, null); return true; }
                return false;
            }

            public override bool TrySetMember(SetMemberBinder binder, object value)
            {
                var field = FindField(binder.Name);
                if (field != null) { field.SetValue(null, value); return true; }

                var prop = FindProperty(binder.Name, false);
                if (prop != null) { prop.SetValue(null, value, null); return true; }
                return false;
            }

            public override Boolean TryInvokeMember(InvokeMemberBinder binder, Object[] args, out Object result)
            {
                MethodInfo method = FindMethod(binder.Name, args.Select(a => a.GetType()).ToArray());
                if (method == null) { result = null; return false; }
                result = method.Invoke(null, args);
                return true;
            }

            private MethodInfo FindMethod(String name, Type[] paramTypes)
            {
                return m_type.DeclaredMethods.FirstOrDefault(mi => mi.IsPublic && mi.IsStatic && mi.Name == name && ParametersMatch(mi.GetParameters(), paramTypes));
            }

            private Boolean ParametersMatch(ParameterInfo[] parameters, Type[] paramTypes)
            {
                if (parameters.Length != paramTypes.Length) return false;
                for (Int32 i = 0; i < parameters.Length; i++)
                    if (parameters[i].ParameterType != paramTypes[i]) return false;
                return true;
            }

            private FieldInfo FindField(String name)
            {
                return m_type.DeclaredFields.FirstOrDefault(fi => fi.IsPublic && fi.IsStatic && fi.Name == name);
            }

            private PropertyInfo FindProperty(String name, Boolean get)
            {
                if (get)
                    return m_type.DeclaredProperties.FirstOrDefault(
                       pi => pi.Name == name && pi.GetMethod != null &&
                       pi.GetMethod.IsPublic && pi.GetMethod.IsStatic);

                return m_type.DeclaredProperties.FirstOrDefault(
                   pi => pi.Name == name && pi.SetMethod != null &&
                      pi.SetMethod.IsPublic && pi.SetMethod.IsStatic);
            }
        }

为了调用静态成员,传递想要操作的Type来构建上述类的实例, 将引用放到dynamic变量中, 再用实例成员语法调用所需静态成员.

dynamic stringType = new StaticMemberDynamicWrapper(typeof(String));
var r = stringType.Concat("A","B");// 动态调用String 的静态Concat方法
Console.WriteLine(r); // 显示AB