可空类型
关于可空类型,如果我们手动显式模仿,会编译出错:
从图中可以看到,原来事情没有这么简单,最后还是回到了原来的问题上,null不能给值类型赋值,这个时候,你可能就比较好奇。
我们的FCL中定义的类怎么就能逃过编译器呢?
①:我们用ILdasm看下标准的 Nullable 的IL代码。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Nullable<Int32> i = null; 6 } 7 }
②:下面我们再将Nullable<Int32> i = null 改成 Nullable<Int32> i = 0,看看il代码是怎么样的。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Nullable<Int32> i = 0; 6 } 7 }
下面我们比较比较这两张图不一样的地方。
《1》 当 Nullable<Int32> i = 0 的时候,发现Nullable被实例化了(instance),并且还调用了其构造函数(ctor(!0)),
这种情况我们看Nullable的结构体定义,发现是非常合乎情理的。
《2》当 Nullable<Int32> i = null 的时候,从IL代码上看,只是调用了initobj指令,并没有实例化,也没有调用构造函数,
再看看这个指令的意思:将位于指定地址的对象的所有字段初始化为空引用或适当的基元类型的 0。
①:既然是”初始化“操作,那我应该也可以写成这样:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Nullable<Int32> i = new Nullable<Int32>(); 6 } 7 }
②:既然是“初始化”,那么作为null的Nullable应该可以调用实例方法并不报错,这就如指令说的一样,如果成功,那就
说明null只是Nullable的一种状态,不能跟“类”中的空引用混淆。
从上面的三张图上可以看出,也许答案就在这个里面,编译器和CLR作为“特等公民”在底层做了很多我们看不到的东西,
这其中就像上图一样给我们多加了一种”可空状态“,只是如何做的,我们看不到而已。
《3》既然说到null,我也很好奇的看看到底“类”下面的null是什么情况。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Program p = null; 6 } 7 }
ldnull的意思是:将空引用推送到计算堆栈上。
可以看到,既然没有new,也就不会在堆中分配内存,而这里是将null放入到线程栈中
{ static void Main(string[] args) { //int? num1 = 10; //int? num2 = null; Nullable<int> num3 = new Nullable<int>(10); Nullable<int> num4 = new Nullable<int>(); Console.WriteLine("执行结束啦!"); Console.ReadLine(); }
简单吧,那怎么输出num3和num4呢? 直接Console.WriteLine
就好了。
这里你肯定有一个疑问,为什么num3输出10,而num4什么都没输出呢? 哈哈,这是因为Nullable的ToString()被重写了,再来看下ToString被重写成啥样了,代码如下:
public struct Nullable<T> where T : struct { private bool hasValue; internal T value; [NonVersionable] [__DynamicallyInvokable] public Nullable(T value) { this.value = value; hasValue = true; } [__DynamicallyInvokable] public override string ToString() { if (!hasValue) { return ""; } return value.ToString(); } }
可以看到ToString方法里要么返回空字符串要么返回你在构造函数中塞入的value,这这么简单,IL代码挖到这里就可以了。
int? 比 int 要占用更多的内存
如果你的内存数据量特别大的话,你就要当心了,int? 比 int 在x64上要多占4个字节,也就是多一倍,无论线程栈还是托管堆。