还不明白可空类型原理? 我可要挖到底了

一:背景

1. 讲故事

下决心作好自媒体到如今有一个月了,关注个人兄弟应该知道我产出了很多文章,号里的粉丝也多起来了,我也尽最大努力作到有问必回,如今是基础的、高深的问题都接踵而来,可我也只是一只小菜鸟,想飞也飞不动了(┬_┬),昨天号里有位朋友被面试官问到可空类型的原理,回答的很差,面试官也是,面就面呗,又给不了多少银子,还动不动就原理,哪有那么多原理,搞得双方都尴尬😄😄😄。面试

二:给我锄头我要挖到底

这种问题要怎么挖呢? 我在以前的文章也聊过,C#代码到机器码中间有两个编译过程,一个是csc编译后的IL代码,一个是jit编译后的native代码,因此搞懂IL代码和native代码就是咱们要深究的方向,我仍是把那篇文章的图拿过来。ide

为了方便演示,我就定义一个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<int32> num1,
        [1] valuetype [mscorlib]System.Nullable`1<int32> 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<int32>::.ctor(!0)
    IL_000a: ldloca.s 1
    IL_000c: initobj valuetype [mscorlib]System.Nullable`1<int32>
    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<int32> ,而后从valuetype 标记能够看到这玩意是个值类型,因此把上面的代码回转成C#代码就是下面这样。布局

{
        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就行了。this

这里你确定有一个疑问,为何num3输出10,而num4什么都没输出呢? 哈哈,这是由于Nullable的ToString()被重写了,再来看下ToString被重写成啥样了,代码以下:spa

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代码挖到这里就能够了。线程

2. 挖机器代码

要看num1和num2的机器代码,其实也就是看 Nullable<T> 的内存布局方式,这里我使用windbg,仍是使用 !clrstack -l 查看线程栈。code

int? num1 = 10;
     int? num2 =null;

0:007> ~0s
ntdll!ZwReadFile+0x14:
00007ffc`ec11aa64 c3              ret
0:000> !clrstack -l
OS Thread Id: 0x5364 (0)
        Child SP               IP Call Site
ConsoleApp4.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp4\Program.cs @ 21]
    LOCALS:
        0x00000018a9dfeaf8 = 0x0000000a00000001
        0x00000018a9dfeaf0 = 0x0000000000000000

00000018a9dfed08 00007ffcd5b66c93 [GCFrame: 00000018a9dfed08]

LOCALS中能够看到,num1和num2的线程栈上存放的内容分别是0x0000000a000000010x0000000000000000, 不过这值也挺奇怪的,一个是1一个是0。。。咱们用 dd 命令把地址转储出来。blog

0:000> dd 0x00000018a9dfeaf8 
00000018`a9dfeaf8  00000001 0000000a a9dfec08 00000018
0:000> dd 0x00000018a9dfeaf0 
00000018`a9dfeaf0  00000000 00000000 00000001 0000000a

在num1的内存区域中有一个十六进制值 0000000a ,这就是十进制的10,那前面的 00000001 是什么东西呢? 你们不要忘啦, int?是语法糖, 你如今看的是 Nullable<T> 哈。。。

看清楚啦,这个结构体里面有两个值类型字段,天然 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 -l
OS Thread Id: 0xa98 (0)
ConsoleApp4.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp4\Program.cs @ 25]
   LOCALS:
       0x000000a8395fedbc = 0x00000000000000ff
       0x000000a8395fedb8 = 0x00000000000000ff
       0x000000a8395fedb4 = 0x0000000000007fff
       0x000000a8395fedb0 = 0x0000000000007fff
       0x000000a8395fedac = 0x000000007fffffff
       0x000000a8395feda8 = 0x000000007fffffff

而后把最小的地址0x000000a8395feda8 转储出来。

0:000> dd 0x000000a8395feda8
000000a8`395feda8  7fffffff 7fffffff 00007fff 00007fff
000000a8`395fedb8  000000ff 000000ff 395feec8 000000a8
000000a8`395fedc8  395fefc8 000000a8 395fee00 000000a8
000000a8`395fedd8  d5b66c93 00007ffc 98e72d30 000001ee
000000a8`395fede8  76504140 00007ffc 00000000 00000000
000000a8`395fedf8  00000000 00007ffc 395feef0 000000a8
000000a8`395fee08  971d0b20 000001ee 00000000 00000000
000000a8`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 -l
OS Thread Id: 0x23f8 (0)
000000859a1fec60 00007ffc76630967 ConsoleApp4.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp4\Program.cs @ 32]
    LOCALS:
        0x000000859a1feca0 = 0x000002773cb32d70
        0x000000859a1fec98 = 0x000002773cb32d90

000000859a1feeb8 00007ffcd5b66c93 [GCFrame: 000000859a1feeb8] 
0:000> !do 0x000002773cb32d70
Name:        System.Int32[]
MethodTable: 00007ffcd2d58538
EEClass:     00007ffcd2ec5918
Size:        28(0x1c) bytes
Array:       Rank 1, Number of elements 1, Type Int32 (Print Array)
Fields:
None
0:000> !do 0x000002773cb32d90
Name:        System.Nullable`1[[System.Int32, mscorlib]][]
MethodTable: 00007ffcd3fb2058
EEClass:     00007ffcd30221a0
Size:        32(0x20) bytes
Array:       Rank 1, Number of elements 1, Type VALUETYPE (Print Array)
Fields:
None

0:000> !objsize 0x000002773cb32d70
sizeof(000002773cb32d70) = 32 (0x20) bytes (System.Int32[])
0:000> !objsize 0x000002773cb32d90
sizeof(000002773cb32d90) = 32 (0x20) bytes (System.Nullable`1[[System.Int32, mscorlib]][])

能够看到,一个是28byte,一个是32byte,多出来的就是那个hasValue哈,有一点要注意了,用!objsize打出来都是32byte,这是由于28byte要靠8对齐就变成32byte啦, 而后我把两个值类型转储出来,以下图:

四: 总结

挖到这里,不知道可挖到了面试官的盲区啦😄,总之int?就是 Nullable<T>, 并且可空比非可空多4个字节的空间,最后你们要看本身状况使用啦。

相关文章
相关标签/搜索