CLR笔记:5.基元,引用和值类型

  5.1基元类型

  编译器(C#)直接支持的任何数据类型都称为基元类型(primitive type),基元类型直接映射到FCL中存在的类型。可以认为 using string = System.String;自动产生。

  FCL中的类型在C#中都有相应的基元类型,但是在CLS中不一定有,如Sbyte,UInt16等等。

  C#允许在“安全”的时候隐式转型——不会发生数据丢失,Int32可以转为Int64,但是反过来要显示转换,显示转换时C#对结果进行截断处理。

  unchecked和check控制基元类型操作

  C#每个运算符都有2套IL指令,如+对应Add和Add.ovf,前者不执行溢出检查,后者要检查并抛出System.OverflowException异常。

  溢出检查默认是关闭的,即自动对应Add这样的指令而不是Add.ovf。

  控制C#溢出的方法:

  1.使用 /check+编译器开关

  2.使用操作符checked和unchecked:

            int b = 32767;      // Max short value
            //b = checked((short)(b + 32767));      throw System.OverflowException
            b = (short)checked(b + 32767);          //return -2

  这里,被注释掉的语句肯定会检查到溢出,运行期抱错;而第二句是在Int32中检查,所以不会溢出。注意这两条语句只是为了说明check什么时候发挥作用,是两条不同语义的语句,而不是一条语句的正误两种写法。

  3.使用 checked和unchecked语句,达到与check操作符相同的效果:

            int b = 32767;      // Max short value

            checked
            {
                b = b + 32767;
            }

            return (short)b;

System.Decimal类型在C#中是基元,但在CLR中不是,所以check对其无效。

  5.2 引用类型和值类型

  引用类型从托管堆上分配内存,值类型从一个线程堆栈分配。

  值类型不需要指针,值类型实例不受垃圾收集器的制约

  struct和enum是值类型,其余都是引用类型。这里,Int32,Boolean,Decimal,TimeSpan都是结构。

  struct都派生自System.ValueType,后者是从System.Object派生的。enum都派生自System.Enum,后者是从System.ValueType派生的。

  值类型都是sealed的,可以实现接口。

  new操作符对值类型的影响:C#确保值类型的所有字段都被初始化为0,如果使用new,则C#会认为实例已经被初始化;反之也成立。

            SomeVal v1 = new SomeVal();
            Int32 a1 = v1.x;            //已经初始化为0

            SomeVal v2;
            Int32 a2 = v2.x;            //编译器报错,未初始化

  使用值类型而不是引用类型的情况:

  1.类型具有一个基元类型的行为:不可变类型,其成员字段不会改变

  2.类型不需要从任何类型继承

  3.类型是sealed的

  4.类型大小:或者类型实例较小(<16k);或者类型实例较大,但不作为参数和返回值使用

  值类型有已装箱和未装箱两种形式;引用类型总是已装箱形式。

  System.ValueType重写了Equals()方法和GetHashCode()方法;自定义值类型也要重写这两个方法。

  引用类型可以为null;值类型总是包含其基础类型的一个值(起码初始化为0),CLR为值类型提供相应的nullable。

copy值类型变量会逐字段复制,从而损害性能,copy引用类型只复制内存地址。

  值类型的Finalize()方法是无效的,不会在垃圾自动回收后执行——就是说不会被垃圾收集。

  CLR控制类型字段的布局:System.Runtime.InteropServices.StructLayoutAttribute属性,LayoutKind.Auto为自动排列(默认),CLR会选择它认为最好的排列方式;LayoutKind.Sequential会按照我们定义的字段顺序排列;LayoutKind.Explicit按照偏移量在内存中显示排列字段。

    [System.Runtime.InteropServices.StructLayout(LayoutKind.Auto)]
    struct SomeVal
    {
        public Int32 x;
        public Byte b;
    }

  Explicit排列,一般用于COM互操作

    [StructLayout(LayoutKind.Explicit)]
    struct SomeVal
    {
        [FieldOffset(0)]
        public Int32 x;

        [FieldOffset(0)]
        public Byte b;
    }

  5.3 值类型的装箱和拆箱

  boxing机制:

   1.从托管堆分配内存,包括值类型各个字段的内存,以及两个额外成员的内存:类型对象指针和同步块索引。

   2.将值类型的字段复制到新分配的堆内存。

   3.返回对象的地址。

   ——这样一来,已装箱对象的生存期 超过了 未装箱的值类型生存期。后者可以重用,而前者一直到垃圾收集才回收。

  unboxing机制:

   1.获取已装箱对象的各个字段的地址。

   2.将这些字段包含的值从堆中复制到基于堆栈的值类型实例中。

——这里,引用变量如果为null,对其拆箱时抛出NullRefernceException异常;拆箱时如果不能正确转型,则抛出InvalidCastException异常。

   装箱之前是什么类型,拆箱时也要转成该类型,转成其基类或子类都不行,所以以下语句要这么写:

                Int32 x = 5;
                Object o = x;
                Int16 y = (Int16)(Int32)o;

   拆箱操作返回的是一个已装箱对象的未装箱部分的地址。

   大多数方法进行重载是为了减少值类型的装箱次数,例如Console.WriteLine提供多种类型参数的重载,从而即使是Console.WriteLine(3);也不会装箱。注意,也许WriteLine会在内部对3进行装箱,但无法加以控制,也就默认为不装箱了。我们所要做的,就是尽可能的手动消除装箱操作。

   可以为自己的类定义泛型方法,这样类型参数就可以为值类型,从而不用装箱。

   最差情况下,也要手动控制装箱,减少装箱次数,如下:

                Int32 v = 5;
                Console.WriteLine("{0}, {1}, {2}", v, v, v);    //要装箱3次

                Object o = v;   //手动装箱
                Console.WriteLine("{0}, {1}, {2}", o, o, o);    //仅装箱1次

  由于未装箱的值类型没有同步块索引,所以不能使用System.Threading.Monitor的各种方法,也不能使用lock语句。

  值类型可以使用System.Object的虚方法Equals,GetHashCode,和ToString,由于System.ValueType重写了这些虚方法,而且希望参数使用未装箱类型。即使是我们自己重写了这些虚方法,也是不需要装箱的——CLR以非虚的方式直接调用这些虚方法,因为值类型不可能被派生。

值类型可以使用System.Object的非虚方法GetType和MemberwiseClone,要求对值类型进行装箱

  值类型可以继承接口,并且该值类型实例可以转型为这个接口,这时候要求对该实例进行装箱

  5.4使用接口改变已装箱值类型

    interface IChangeBoxedPoint
    {
        void Change(int x);   
    }

    struct Point : IChangeBoxedPoint
    {
        int x;

        public Point(int x)
        {
            this.x = x;
        }

        public void Change(int x)
        {
            this.x = x;
        }

        public override string ToString()
        {
            return x.ToString();
        }

        class Program
        {
            static void Main(string[] args)
            {
                Point p = new Point(1);

                Object obj = p;

                ((Point)obj).Change(3);
                Console.WriteLine(obj);       //输出1,因为change(3)的对象是一个临时对象,并不是obj

                ((IChangeBoxedPoint)p).Change(4);
                Console.WriteLine(p);         //输出1,因为change(4)的对象是一个临时的装箱对象,并不是对p操作

                ((IChangeBoxedPoint)obj).Change(5);
                Console.WriteLine(obj);         //输出5,因为change(5)的对象是(IChangeBoxedPoint)obj装箱对象,于是使用接口方法,修改引用对象obj
            }
        }
    }

5.5 对象相等性和身份标识

  相等性:equality

  同一性:identity

  System.Object的Equal方法实现的是同一性,这是目前Equal的实现方式,也就是说,这两个指向同一个对象的引用是同一个对象:

    public class Object
    {
        public virtual Boolean Equals(Object obj)
        {
            if (this == obj) return true;   //两个引用,指向同一个对象

            return false;
        }
    }

  但现实中我们需要判断相等性,也就是说,可能是具有相同类型与成员的两个对象,所以我们要重写Equal方法:

    public class Object
    {
        public virtual Boolean Equals(Object obj)
        {
            if (obj == null) return false;   //先判断对象不为null

            if (this.GetType() != obj.GetType()) return false;  //再比较对象类型

            //接下来比较所有字段,因为System.Object下没有字段,所以不用比较,值类型则比较引用的值

            return true;
        }
    }

  如果重写了Equal方法,就又不能测试同一性了,于是Object提供了静态方法ReferenceEquals()来检测同一性,实现代码同重写前的Equal()。

  检测同一性不应使用C#运算符==,因为==可能是重载的,除非将两个对象都转型为Object。

  System.ValueType重写了Equals方法,检测相等性,使用反射技术——所以自定义值类型时,还是要重写这个Equal方法来提高性能,不能调用base.Equals()。

重写Equals方法的同时,还需要:

   让类型实现System.IEquatable<T>接口的Equals方法。

   运算符重载==和!=

   如果还需要排序功能,那额外做的事情就多了:要实现System.IComparable的CompareTo方法和System.IComparable<T>的CompareTo方法,以及重载所有比较运算符<,>,<=,>=

  5.6 对象哈希码

   重写Equals方法的同时,要重写GetHashCode方法,否则编译器会有警告。

   ——因为System.Collection.HashTable和Generic.Directory的实现中,要求Equal的两个对象要具有相同的哈希码。

   HashTable/Directory原理:添加一个key/value时,先获取该键值对的HashCode;查找时,也是查找这个HashCode然后定位。于是一旦修改key/value,就再也找不到这个键值对,所以修改的做法是,先移除原键值对,在添加新的键值对。

   不要使用Object.GetHashCode方法来获取某个对象的唯一性。FCL提供了特殊的方法来做这件事:

using System.Runtime.CompilerServices;

            RuntimeHelpers.GetHashCode(Object o)

  这个GetHashCode方法是静态的,并不是对System.Object的GetHashCode方法重写。

  System.ValueType实现的GetHashCode方法使用的是反射技术。

你可能感兴趣的:(CLR笔记:5.基元,引用和值类型)