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 == nullreturn 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方法使用的是反射技术。

原文地址:https://www.cnblogs.com/Jax/p/895909.html