一:背景
1. 讲故事
下决心做好自媒体到现在有一个月了,关注我的兄弟应该知道我产出了不少文章,号里的粉丝也多起来了,我也尽最大努力做到有问必回,现在是基础的、高深的问题都接踵而来,可我也只是一只小菜鸟,想飞也飞不动了(┬_┬),昨天号里有位朋友被面试官问到可空类型的原理,回答的不好,面试官也是,面就面呗,又给不了多少银子,还动不动就原理,哪有那么多原理,搞得双方都尴尬。
二:给我锄头我要挖到底
这种问题要怎么挖呢? 我在之前的文章也聊过,C#代码到机器码中间有两个编译过程,一个是csc编译后的IL代码,一个是jit编译后的native代码,所以搞懂IL代码和native代码就是我们要深究的方向,我还是把那篇文章的图拿过来。
![fb551364b14c0cd9ba90572c89ac8e4a.png](https://i-blog.csdnimg.cn/blog_migrate/1d4514d1d929d3b0abde1faa1726e79a.jpeg)
为了方便演示,我就定义一个int?类型,接收非null和null两种情况。
static void Main(string[] args) { int? num1 = 10; int? num2 = null; Console.WriteLine("执行结束啦!"); Console.ReadLine(); }
1. 挖IL代码
挖IL代码简单,用ILSPY小工具就可以了,编译后生成的IL代码如下:
.method private hidebysig static void Main ( string[] args ) cil managed { // Method begins at RVA 0x2048 // Code size 36 (0x24) .maxstack 2 .entrypoint .locals init ( [0] valuetype [mscorlib]System.Nullable`1 num1, [1] valuetype [mscorlib]System.Nullable`1 num2 ) IL_0000: nop IL_0001: ldloca.s 0 IL_0003: ldc.i4.s 10 IL_0005: call instance void valuetype [mscorlib]System.Nullable`1::.ctor(!0) IL_000a: ldloca.s 1 IL_000c: initobj valuetype [mscorlib]System.Nullable`1 IL_0012: ldstr "执行结束啦!" IL_0017: call void [mscorlib]System.Console::WriteLine(string) IL_001c: nop IL_001d: call string [mscorlib]System.Console::ReadLine() IL_0022: pop IL_0023: ret} // end of method Program::Main
这IL代码还是非常易懂的,比汇编简单多啦(┬_┬),可以看到int ? 就是 System.Nullable ,然后从valuetype 标记可以看到这玩意是个值类型,所以把上面的代码回转成C#代码就是下面这样。
{ static void Main(string[] args) { //int? num1 = 10; //int? num2 = null; Nullable num3 = new Nullable(10); Nullable num4 = new Nullable(); Console.WriteLine("执行结束啦!"); Console.ReadLine(); }
很简单吧,那怎么输出num3和num4呢? 直接Console.WriteLine就好了。
![df5426e6ea4d014dde16aad586fcaf88.png](https://i-blog.csdnimg.cn/blog_migrate/89c0813bea0c3c1891dc8c5c1f34d964.jpeg)
这里你肯定有一个疑问,为什么num3输出10,而num4什么都没输出呢? 哈哈,这是因为Nullable的ToString()被重写了,再来看下ToString被重写成啥样了,代码如下:
public struct Nullable 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代码挖到这里就可以了。
2. 挖机器代码
要看num1和num2的机器代码,其实也就是看 Nullable 的内存布局方式,这里我使用windbg,还是使用 !clrstack -l 查看线程栈。
int? num1 = 10; int? num2 =null;0:007> ~0sntdll!ZwReadFile+0x14:00007ffc`ec11aa64 c3 ret0:000> !clrstack -lOS Thread Id: 0x5364 (0) Child SP IP Call SiteConsoleApp4.Program.Main(System.String[]) [C:dreamCsharpConsoleApp1ConsoleApp4Program.cs @ 21] LOCALS: 0x00000018a9dfeaf8 = 0x0000000a00000001 0x00000018a9dfeaf0 = 0x000000000000000000000018a9dfed08 00007ffcd5b66c93 [GCFrame: 00000018a9dfed08]
从LOCALS中可以看到,num1和num2的线程栈上存放的内容分别是0x0000000a00000001和 0x0000000000000000, 不过这值也挺奇怪的,一个是1一个是0。。。我们用 dd 命令把地址转储出来。
0:000> dd 0x00000018a9dfeaf8 00000018`a9dfeaf8 00000001 0000000a a9dfec08 000000180:000> dd 0x00000018a9dfeaf0 00000018`a9dfeaf0 00000000 00000000 00000001 0000000a
![be179ee1f3c8fed39f7489d8cd8f72de.png](https://i-blog.csdnimg.cn/blog_migrate/28460a69a61ace48ef317494a2e89441.jpeg)
在num1的内存区域中有一个十六进制值 0000000a ,这就是十进制的10,那前面的 00000001 是什么东西呢? 大家不要忘啦, int?是语法糖, 你现在看的是 Nullable 哈。。。
![b2e6121dd1350785c538ace0f183a219.png](https://i-blog.csdnimg.cn/blog_migrate/8b7e83994975ecf8f841e4d36668b1f7.jpeg)
看清楚啦,这个结构体里面有两个值类型字段,自然 00000001 就是 hasValue=true啦。 num2也就好理解了,两个默认值也就是两个0了。00000000 00000000。
三:有意外发现
1. int? 比 int 要占用更多的内存
如果你的内存数据量特别大的话,你就要当心了,int? 比 int 在x64上要多占4个字节,也就是多一倍,无论线程栈还是托管堆。
2. 为什么bool要占用 4字节空间?
<1> 线程栈上的演示
肯定有人比较疑惑,bool在C#中不就是一个字节嘛? 你怎么说是4个字节呢? 你要是问我,我只能说从windbg上看就是这样的,x64系统的线程栈上就是以4个字节为一个单位,你不信的话,我就在代码中定义不同字段的 值类型,你看看在线程栈上的分布不就好啦,以事实说话。
byte b1 = byte.MaxValue; byte b2 = byte.MaxValue; short b3 = short.MaxValue; short b4 = short.MaxValue; int b5 = int.MaxValue; int b6 = int.MaxValue;0:000> !clrstack -lOS Thread Id: 0xa98 (0)ConsoleApp4.Program.Main(System.String[]) [C:dreamCsharpConsoleApp1ConsoleApp4Program.cs @ 25] LOCALS: 0x000000a8395fedbc = 0x00000000000000ff 0x000000a8395fedb8 = 0x00000000000000ff 0x000000a8395fedb4 = 0x0000000000007fff 0x000000a8395fedb0 = 0x0000000000007fff 0x000000a8395fedac = 0x000000007fffffff 0x000000a8395feda8 = 0x000000007fffffff
然后把最小的地址0x000000a8395feda8 转储出来。
0:000> dd 0x000000a8395feda8000000a8`395feda8 7fffffff 7fffffff 00007fff 00007fff000000a8`395fedb8 000000ff 000000ff 395feec8 000000a8000000a8`395fedc8 395fefc8 000000a8 395fee00 000000a8000000a8`395fedd8 d5b66c93 00007ffc 98e72d30 000001ee000000a8`395fede8 76504140 00007ffc 00000000 00000000000000a8`395fedf8 00000000 00007ffc 395feef0 000000a8000000a8`395fee08 971d0b20 000001ee 00000000 00000000000000a8`395fee18 d5b66b79 00007ffc 00000000 00000000
对比一下可以看到上面的 7fffffff, 00007fff,000000ff就是相应的int,short,byte的MaxValue, 都是占用4个字节的空间,没问题吧。
<2> 托管堆演示
var arr1 = new int[] { 10 }; var arr2 = new int?[] { 14 };0:000> !clrstack -lOS Thread Id: 0x23f8 (0)000000859a1fec60 00007ffc76630967 ConsoleApp4.Program.Main(System.String[]) [C:dreamCsharpConsoleApp1ConsoleApp4Program.cs @ 32] LOCALS: 0x000000859a1feca0 = 0x000002773cb32d70 0x000000859a1fec98 = 0x000002773cb32d90000000859a1feeb8 00007ffcd5b66c93 [GCFrame: 000000859a1feeb8] 0:000> !do 0x000002773cb32d70Name: System.Int32[]MethodTable: 00007ffcd2d58538EEClass: 00007ffcd2ec5918Size: 28(0x1c) bytesArray: Rank 1, Number of elements 1, Type Int32 (Print Array)Fields:None0:000> !do 0x000002773cb32d90Name: System.Nullable`1[[System.Int32, mscorlib]][]MethodTable: 00007ffcd3fb2058EEClass: 00007ffcd30221a0Size: 32(0x20) bytesArray: Rank 1, Number of elements 1, Type VALUETYPE (Print Array)Fields:None0:000> !objsize 0x000002773cb32d70sizeof(000002773cb32d70) = 32 (0x20) bytes (System.Int32[])0:000> !objsize 0x000002773cb32d90sizeof(000002773cb32d90) = 32 (0x20) bytes (System.Nullable`1[[System.Int32, mscorlib]][])
可以看到,一个是28byte,一个是32byte,多出来的就是那个hasValue哈,有一点要注意了,用!objsize打出来都是32byte,这是因为28byte要靠8对齐就变成32byte啦, 然后我把两个值类型转储出来,如下图:
![200d2a6afd113602100e48b45c4d23a2.png](https://i-blog.csdnimg.cn/blog_migrate/0299bbfd3380bcc8eedb32193ce0a768.jpeg)
四: 总结
挖到这里,不知道可挖到了面试官的盲区啦,总之int?就是 Nullable, 而且可空比非可空多4个字节的空间,最后大家要看自己情况使用啦。