• 【读书笔记】.NET本质论第四章Programming with Type(Part One)


    在上一章中主要探讨的是CTS中的类型,基本上是类型的“静态结构”,本章将主要涉及类型的运行时结构。你定义了一个类型,然后实例化它,那么它在内存中的布局到底是什么样子的呢?声明一个类型到底占多少内存?是分配在栈上还是堆上?这些都是本章需要讨论的话题。不过这一篇先说一些简单的问题。

    一个类型的实例要么是一个对象,要么是一个值,这要看这个类型是如何定义的。一般来讲,自定义的类的实例都是对象,而所有直接或间接从System.ValueType类派生的类型却是值类型(在这里,请区分类与类型的区别)。

    好,那我这样是不是可以定义一个值类型了呢:

       1: public class MyValueType : ValueType
       2: {
       3:     //some code
       4: }

    嗯,这样竟然是不行的,C#编译器将会给出如下的错误(而且你发现没,在Vs里输入上面的代码时,输入ValueType的时候却没有智能感知,也没有代码着色):

    'MyValueType' cannot derive from special class 'System.ValueType'

    哦,原来把System.ValueType当做一个特殊类处理了。

    既然不能从System.ValueType继承,那我们有什么办法定义值类型啊:

       1: public struct MyValueType
       2: {
       3:  
       4: }

    就这样,你的MyValueType就是一个值类型了,使用ILDasm看看:

    .class public sequential ansi sealed beforefieldinit MyValueType
    extends [mscorlib]System.ValueType
    {
    }
    延伸阅读
    也许你比较了上面的IL代码与一个普通的用class定义的类型的区别,除了MyValueType从System.ValueType继承,而通常定义的一个class从System.Object外,我们还发现一个sequential元数据,而在class的对应位置应该是一个auto元数据。这是什么意思呢?
    实际上这属于CLR控制类型中字段布局的问题,你写一个类型,在写代码的时候,字段的排列肯定是有顺序的,那么CLR如何安排这些字段呢?有三种布局模式,实际上也是LayoutKind枚举的三个成员:
       1: [Serializable, ComVisible(true)]
       2: public enum LayoutKind
       3: {
       4:     //CLR自动控制内存布局
       5:     Auto = 3,
       6:     //使用偏移量在代码中显式控制布局
       7:     Explicit = 2,
       8:     //按照开发人员书写代码时的字段顺序控制
       9:     Sequential = 0
      10: }
    如果使用class定义一个类型,C#编译器默认会使用LayoutKind.Auto,而如果定义一个struct,C#编译器默认会使用LayoutKind.Sequential。因为.NET中的struct的存在实际上是为了与哪些非托管代码交互的,这个时候就必须知道你的内存布局是个啥样子的,如果你使用Auto这种方式,那布局就是由CLR自动控制,你不知道CLR到底是如何自动的,也就无法交互了。不过,如果你确定你定义的这个struct不会与非托管代码交互,你也可以使用如下这样的代码覆盖C#编译器的默认设置了:
       1: [StructLayout(LayoutKind.Sequential)]
       2: public class MyObject
       3: { 
       4:  
       5: }
       6: [StructLayout(LayoutKind.Auto)]
       7: public struct MyValueType
       8: {
       9: }
    下面是对应的IL代码:
       1: .class public sequential ansi beforefieldinit MyObject
       2:     extends [mscorlib]System.Object
       3: {
       4: }
       5:  
       6: .class public auto ansi sealed beforefieldinit MyValueType
       7:     extends [mscorlib]System.ValueType
       8: {
       9: }
    现在换了个儿,class的使用sequential,而struct使用auto(注意,LayoutKind和StructLayout都来自System.Runtime.InteropServices命名空间,请添加对应的using指令)。
    CLR还允许你使用LayoutKind.Explicit配合FieldOffsetAttribute显式的指定每个字段的偏移(不过一般请不要这样做)。代码如下:
       1: [StructLayout(LayoutKind.Explicit)]
       2: public struct MyValueType
       3: {
       4:     [FieldOffset(0)]
       5:     public int A;
       6:  
       7:     [FieldOffset(4)]
       8:     public int B;
       9: }
    关于这个的更多示例可以参见MSDN
    
    

    原来这个struct也是用.class元数据描述的,它还继承了System.ValueType。不过这个MyValueType却已经加上了sealed,这样你就不能再从MyValueType派生了。还有没有其他方法定义值类型呢?有,那就是枚举:

       1: public enum Color
       2: { 
       3:     Red,
       4:     White,
       5:     Black
       6: }

    再看看编译器为上面的代码做了些什么事情:

       1: .class public auto ansi sealed Color
       2:     extends [mscorlib]System.Enum
       3: {
       4:     .field public static literal valuetype Color Black = int32(2)
       5:     .field public static literal valuetype Color Red = int32(0)
       6:     .field public specialname rtspecialname int32 value__
       7:     .field public static literal valuetype Color White = int32(1)
       8: }

    原来是定义了一个从System.Enum派生的类型啊,从System.Enum的代码我们可以看到,System.Enum是一个抽象类,但这个抽象类是从System.ValueType派生而来的。 所以枚举也是一个值类型。

    值类型一定是分配在栈上么?

    记得园子里也有相关的讨论。一般书上都讲值类型是分配在栈上的,而引用类型是分配在堆上的。不过这要看值类型的使用方式,如果值类型作为方法的局部变量或者方法的参数,那么值类型才分配在栈上,而如果值类型作为引用类型的字段,那么该值类型则分配在堆上:

       1: public class MyObject
       2: {
       3:     //作为引用类型的字段使用,该值类型会分配在堆上
       4:     private int _objectField;
       5:     //valueParameter作为方法的参数分配在栈上
       6:     public void Test(int valueParameter)
       7:     {
       8:         //作为方法的局部变量,分配在栈上
       9:         int localVar = 5;
      10:     }
      11: }

    那值类型里如果“有一个引用类型”,该引用类型也分配在栈上么?

    实际上“有一个引用类型”这个说法本来就不正确,值类型里是指向这个引用类型实例的引用(指针),这个指针指向的是堆上的引用类型所在的内存区域。

  • 相关阅读:
    .net core 在 Docker 上的部署
    js 运算的内置函数
    vux 项目的构建
    微信小程序开发资料
    HttpClient 调用WebAPI时,传参的三种方式
    jsplumb 中文教程
    MyEclipse最新版-版本更新说明及下载
    如何用VSCode调试Vue.js
    vs2017开发Node.js控制台程序
    Objc的底层并发API
  • 原文地址:https://www.cnblogs.com/yuyijq/p/1534791.html
Copyright © 2020-2023  润新知