zoukankan      html  css  js  c++  java
  • .NET中struct与class的区别

    在.net中的struct与class有很多相似之处,比如可以直接new,对于成员可以直接XX.field,以至于有不少程序员在用时,将其混在一起,分不清有何区别。这两者有何区别呢?

    1.类型不同

    我们先来看一段代码

      static void Main(string[] args)
            {
                TypeDemo();           
                Console.ReadLine();
            }
    
            // Reference type (because of 'class') 
            class SomeClassRef { public Int32 x; }
    
            // Value type (because of 'struct') 
            struct SomeStructVal { public Int32 x; }
    
            static void TypeDemo()
            {
                SomeClassRef r1 = new SomeClassRef(); // Allocated in heap 
                SomeStructVal v1 = new SomeStructVal(); // Allocated on stack 
                r1.x = 5; // Pointer dereference 
                v1.x = 5; // Changed on stack 
                Console.WriteLine("r1=" + r1.x.ToString()); // Displays "5" 
                Console.WriteLine("v1=" + v1.x.ToString()); // Also displays "5" 
                // The left side of Figure 5­2 reflects the situation 
                // after the lines above have executed. 
                SomeClassRef r2 = r1; // Copies reference (pointer) only 
                SomeStructVal v2 = v1; // Allocate on stack & copies members 
                r1.x = 8; // Changes r1.x and r2.x 
                v1.x = 9; // Changes v1.x, not v2.x 
                Console.WriteLine("r1=" + r1.x.ToString()); // Displays "8" 
                Console.WriteLine("r2=" + r2.x.ToString()); // Displays "8" 
                Console.WriteLine("v1=" + v1.x.ToString()); // Displays "9" 
                Console.WriteLine("v2=" + v2.x.ToString()); // Displays "5" 
                // The right side of Figure 5­2 reflects the situation 
                // after ALL of the lines above have executed. 
            }
    
    该代码执行结果如下


    从代码中,我们可以看到,r1和v1都是new出来的,同时其字段x都赋值为5.所以输出的是r1=5,v1=5。

    接着又定义了r2和v2,并将r2赋值为r1,v2赋值为v1。之后,将字段x分别赋值为8和9,接紧接着输出各自的x。结果是r1和r2的x值相同,都变成了后来赋值的8。v1和v2的x值不一样,r1是第一次赋值的5,r2是后面赋值的9.

    那引起这种差异的原因是什么呢?我们先用IL来查看一下生成的EXE。


    从IL中我们可以看到,SomeClassRef是继承自System.Object,是Object,是引用类型。SomeStructVal是继承自System.ValueType,是值类型。

    也就是说class是引用类型,struct是值类型。

    引用类型得到的是地址(或者是指针),这样在r2=r1的赋值过程中,其实是将r1的地址赋给了r2,所以r1和r2指向的其实指向的是同一个对象。而对象是存储在heap中。

    值类型是直接存在Thread Statck中。在r2=r1的赋值过程中,是直接的内存数据拷贝。

    简单的说,引用类型的赋值,始终只有一个对象,一个数据存储空间。值类型的赋值,赋值几次就有几个对象,几个存储空间。可以参看下图。


    在ThreadStatck中存储着r1、r2、v1、v2。r1和r2指向的是Managed Heap中的object。而v1和v2始终在Thread Statck中,其字段x是紧接v1或v2之后。

    2.函数传参影响不同

    我们常见的基本类型如Int32、bool、byte等都是值类型,都具有相同的特性。因为值类型的赋值是数据拷贝,引用类型是引用拷贝,所以两者在作为函数参数传传递时就会有很大的差异。值类型作参数的,其原始值不会受影响,而引用类型作参数的,其原始值会受影响。测试代码如下

      static void Main(string[] args)
            {          
                TestFunc();
                Console.ReadLine();
            }  
    static void TestFunc()
            {
                SomeClassRef r = new SomeClassRef();
                r.x = 1;
                SomeStructVal v = new SomeStructVal();
                v.x = 1;
    
                RefFunc(r);
                ValFunc(v);
    
                Console.WriteLine(r.x);
                Console.WriteLine(v.x);
            }
    
            static void RefFunc(SomeClassRef r)
            {
                r.x = 100;
            }
    
            static void ValFunc(SomeStructVal v)
            {
                v.x = 100;
            }


    从结果可以看到,r为class,为引用类型,在经过RefFunc函数后,其字段x的值变成了100。v为struct,为值类型,在经过ValFunc后,其字段x的值仍然为原始值1.

    3.继承性不同

    class可以继承,struct不可以继承。


    可以看到,SomeStructValChild继承SomeStructVal,编译无法通过。提示说无法从密封类型中派生。这么看来,struct被当成了密封类型。其实所有的值类型都是密封(sealed)类型。比如Boolean, Char, Int32, UInt64, Single, Double, Decimal。大家可以参看一下《CLR via C# 第四版》中的一段原文:


    有人会说,这里是用class来继承SomeStructVal的,一个是class,一个是struct,两者不一样,当然不能继承。那我们试试struct。


    同样编译不能通过,提示说SomeStructVal不是接口。

    当然除了继承性这样类的特性外,还有很多类的特性在struct中也是不可以使用的。比如类的虚方法、重载等。

    4.GC的触发

    从前面的图中,我们可以看到,SomeClassRef是在Managed Heap中开辟了一块空间来存储,而在Managed Headp开辟了空间,就必然会触发垃圾回收(GC)。在Thread Statck中则不会触发GC。

    所以如果不断的去new一个SomeClassRef和new一个SomeStructVal,两者的耗时会有很大的差距。可以推测,SomeStructVal的时间肯定会少于SomeClassRef。下面是测试代码

      private const int TIME_MAX = 100000000;
    
            static void Main(string[] args)
            {          
                TestStruct();
                TestClass();         
                Console.ReadLine();
            }
     
    static void TestStruct()
            {
                Stopwatch sw = new Stopwatch();
                sw.Start();
                for (int i = 0; i < TIME_MAX; i++)
                {
                    SomeStructVal v1 = new SomeStructVal();
                    v1.x = i;
                }
                sw.Stop();
                Console.WriteLine("Struct time:" + sw.ElapsedMilliseconds.ToString());
            }
    
            static void TestClass()
            {
                Stopwatch sw = new Stopwatch();
                sw.Start();
                for (int i = 0; i < TIME_MAX; i++)
                {
                    SomeClassRef r1 = new SomeClassRef();
                    r1.x = i;
                }
                sw.Stop();
                Console.WriteLine("Class time:" + sw.ElapsedMilliseconds.ToString());
            }

    测试结果见下图


    可以看到,class的时间消耗是struct的3倍多。当然这里的耗时还有class在Managed Headp中开辟空间时需要引用户指针(Type object ptr)和同步块索引(Sync block Index)。

    5.装箱与拆箱

    对于值类型,如果要转换成Object,会有一个装箱(box),再从Object转换成值类型时,会有一个拆箱的操作。而对于引用类型,则没有装箱和拆箱的过程。这也就引出了struct和class的装箱和拆箱的差别。下面是测试代码

     static void BoxAndUnbox_struct()
            {
                ArrayList a = new ArrayList();
                SomeStructVal v; // Allocate a SomeStructVal (not in the heap). 
                for (Int32 i = 0; i < 10; i++)
                {
                    v = new SomeStructVal();
                    v.x = i; // Initialize the members in the value type. 
                    a.Add(v); // Box the value type and add the 
                    // reference to the Arraylist. 
                }
                SomeStructVal v2 = (SomeStructVal)a[0];
                Console.WriteLine(v2.x);
            }
    
            static void BoxAndUnbox_class()
            {
                ArrayList a = new ArrayList();
                SomeClassRef r; // Allocate SomeClassRef in the heap. 
                for (Int32 i = 0; i < 10; i++)
                {
                    r = new SomeClassRef();
                    r.x = i; // Initialize the members in the value type. 
                    a.Add(r); //add the reference to the Arraylist. 
                }
                SomeClassRef r2 = (SomeClassRef)a[0];
                Console.WriteLine(r2.x);          
            }

    编译后,使用IL查看EXE

    BoxAndUnbox_class函数

    .method private hidebysig static void  BoxAndUnbox_class() cil managed
    {
      // 代码大小       75 (0x4b)
      .maxstack  2
      .locals init ([0] class [mscorlib]System.Collections.ArrayList a,
               [1] class ClassAndStruct.Program/SomeClassRef r,
               [2] int32 i,
               [3] class ClassAndStruct.Program/SomeClassRef r2,
               [4] bool CS$4$0000)
      IL_0000:  nop
      IL_0001:  newobj     instance void [mscorlib]System.Collections.ArrayList::.ctor()
      IL_0006:  stloc.0
      IL_0007:  ldc.i4.0
      IL_0008:  stloc.2
      IL_0009:  br.s       IL_0026
      IL_000b:  nop
      IL_000c:  newobj     instance void ClassAndStruct.Program/SomeClassRef::.ctor()
      IL_0011:  stloc.1
      IL_0012:  ldloc.1
      IL_0013:  ldloc.2
      IL_0014:  stfld      int32 ClassAndStruct.Program/SomeClassRef::x
      IL_0019:  ldloc.0
      IL_001a:  ldloc.1
      IL_001b:  callvirt   instance int32 [mscorlib]System.Collections.ArrayList::Add(object)
      IL_0020:  pop
      IL_0021:  nop
      IL_0022:  ldloc.2
      IL_0023:  ldc.i4.1
      IL_0024:  add
      IL_0025:  stloc.2
      IL_0026:  ldloc.2
      IL_0027:  ldc.i4.s   10
      IL_0029:  clt
      IL_002b:  stloc.s    CS$4$0000
      IL_002d:  ldloc.s    CS$4$0000
      IL_002f:  brtrue.s   IL_000b
      IL_0031:  ldloc.0
      IL_0032:  ldc.i4.0
      IL_0033:  callvirt   instance object [mscorlib]System.Collections.ArrayList::get_Item(int32)
      IL_0038:  castclass  ClassAndStruct.Program/SomeClassRef
      IL_003d:  stloc.3
      IL_003e:  ldloc.3
      IL_003f:  ldfld      int32 ClassAndStruct.Program/SomeClassRef::x
      IL_0044:  call       void [mscorlib]System.Console::WriteLine(int32)
      IL_0049:  nop
      IL_004a:  ret
    } // end of method Program::BoxAndUnbox_class
    
    可以看到,IL代码中没有一个box或unbox的操作,只有pop一类的操作。可以判定并没有进行装箱和拆箱。

    再来看BoxAndUnbox_struct函数

    .method private hidebysig static void  BoxAndUnbox_struct() cil managed
    {
      // 代码大小       84 (0x54)
      .maxstack  2
      .locals init ([0] class [mscorlib]System.Collections.ArrayList a,
               [1] valuetype ClassAndStruct.Program/SomeStructVal v,
               [2] int32 i,
               [3] valuetype ClassAndStruct.Program/SomeStructVal v2,
               [4] bool CS$4$0000)
      IL_0000:  nop
      IL_0001:  newobj     instance void [mscorlib]System.Collections.ArrayList::.ctor()
      IL_0006:  stloc.0
      IL_0007:  ldc.i4.0
      IL_0008:  stloc.2
      IL_0009:  br.s       IL_002e
      IL_000b:  nop
      IL_000c:  ldloca.s   v
      IL_000e:  initobj    ClassAndStruct.Program/SomeStructVal
      IL_0014:  ldloca.s   v
      IL_0016:  ldloc.2
      IL_0017:  stfld      int32 ClassAndStruct.Program/SomeStructVal::x
      IL_001c:  ldloc.0
      IL_001d:  ldloc.1
      IL_001e:  box        ClassAndStruct.Program/SomeStructVal
      IL_0023:  callvirt   instance int32 [mscorlib]System.Collections.ArrayList::Add(object)
      IL_0028:  pop
      IL_0029:  nop
      IL_002a:  ldloc.2
      IL_002b:  ldc.i4.1
      IL_002c:  add
      IL_002d:  stloc.2
      IL_002e:  ldloc.2
      IL_002f:  ldc.i4.s   10
      IL_0031:  clt
      IL_0033:  stloc.s    CS$4$0000
      IL_0035:  ldloc.s    CS$4$0000
      IL_0037:  brtrue.s   IL_000b
      IL_0039:  ldloc.0
      IL_003a:  ldc.i4.0
      IL_003b:  callvirt   instance object [mscorlib]System.Collections.ArrayList::get_Item(int32)
      IL_0040:  unbox.any  ClassAndStruct.Program/SomeStructVal
      IL_0045:  stloc.3
      IL_0046:  ldloca.s   v2
      IL_0048:  ldfld      int32 ClassAndStruct.Program/SomeStructVal::x
      IL_004d:  call       void [mscorlib]System.Console::WriteLine(int32)
      IL_0052:  nop
      IL_0053:  ret
    } // end of method Program::BoxAndUnbox_struct
    
    可以看到在ArrayList的add方法时,有一个box的操作,然后再pop进去。

    这里是因为ArrayList的Add方法是针对object的,而SomeStructVal是值类型,所以必须装箱成object,然后再add进去。

    在装箱时,值类型会依据ThreadStack中的该值所占的控件在Managed Heap中开辟相应的空间,并装数据拷贝进去(头部还会增加Type ojbect Ptr和Sync block Index)。

    这在无形中就增加了空间的消耗,所以使用ArrayList等需要将值类型进行装箱的操作,必须加以考量。当然ArrayList一般可以用List<T>来代替。在泛型(<T>)的List中,直接针对的是T类型,所以如果List的泛型T是值类型,将不会有装箱的操作,可以认为是直接在ThreadStack中开辟空间存数据。

    由于有了装箱,所以在拿ArrayList的数据时,必然需要拆箱。

    需要注意的是,拆箱并不是简单的引用Managed Heap中的数据,而是将该数据拷贝到了Thread Stack中。所以两者已经不是一个对象了。

    下面是测试代码

     static void Unbox()
            {
                SomeStructVal v = new SomeStructVal();
                v.x = 1;
                Object o = v;
                SomeStructVal v2 = (SomeStructVal)o;
                v2.x = 11;
                SomeStructVal v3 = (SomeStructVal)o;
                Console.WriteLine("v2.x="+v2.x.ToString());
                Console.WriteLine("o.x="+v3.x.ToString());
            }

    结果如下图


    下面是IL的代码

    .method private hidebysig static void  Unbox() cil managed
    {
      // 代码大小       104 (0x68)
      .maxstack  2
      .locals init ([0] valuetype ClassAndStruct.Program/SomeStructVal v,
               [1] object o,
               [2] valuetype ClassAndStruct.Program/SomeStructVal v2,
               [3] valuetype ClassAndStruct.Program/SomeStructVal v3)
      IL_0000:  nop
      IL_0001:  ldloca.s   v
      IL_0003:  initobj    ClassAndStruct.Program/SomeStructVal
      IL_0009:  ldloca.s   v
      IL_000b:  ldc.i4.1
      IL_000c:  stfld      int32 ClassAndStruct.Program/SomeStructVal::x
      IL_0011:  ldloc.0
      IL_0012:  box        ClassAndStruct.Program/SomeStructVal
      IL_0017:  stloc.1
      IL_0018:  ldloc.1
      IL_0019:  unbox.any  ClassAndStruct.Program/SomeStructVal
      IL_001e:  stloc.2
      IL_001f:  ldloca.s   v2
      IL_0021:  ldc.i4.s   11
      IL_0023:  stfld      int32 ClassAndStruct.Program/SomeStructVal::x
      IL_0028:  ldloc.1
      IL_0029:  unbox.any  ClassAndStruct.Program/SomeStructVal
      IL_002e:  stloc.3
      IL_002f:  ldstr      "v2.x="
      IL_0034:  ldloca.s   v2
      IL_0036:  ldflda     int32 ClassAndStruct.Program/SomeStructVal::x
      IL_003b:  call       instance string [mscorlib]System.Int32::ToString()
      IL_0040:  call       string [mscorlib]System.String::Concat(string,
                                                                  string)
      IL_0045:  call       void [mscorlib]System.Console::WriteLine(string)
      IL_004a:  nop
      IL_004b:  ldstr      "o.x="
      IL_0050:  ldloca.s   v3
      IL_0052:  ldflda     int32 ClassAndStruct.Program/SomeStructVal::x
      IL_0057:  call       instance string [mscorlib]System.Int32::ToString()
      IL_005c:  call       string [mscorlib]System.String::Concat(string,
                                                                  string)
      IL_0061:  call       void [mscorlib]System.Console::WriteLine(string)
      IL_0066:  nop
      IL_0067:  ret
    } // end of method Program::Unbox
    

    我们可以看到在  IL_0012:  box    IL_0019:  unbox.any    IL_0029:  unbox.any 三个地方进行了装箱和拆箱的操作。

    将v赋值给o时执行了装箱操作,再将o转换成v2时执行了拆箱操作。但对v2.x进行赋值后,并没有影响o的值(o转换成为v3输出)。

    综上,我们可以看到struct和class两者有很大的差别,而这差别根本是因为值类型和引用类型的差别。

    下面总结了struct使用的一些条件,仅供参考

    1.该类型作为基本类型,且其成员都是基本的类型,没有引用类型。并且该类型不会继承其他类型,也不会被其他类型继承。
    2.实例占用的字节数很小(一般是小于等于16字节)
    3.如果实例的字节数大于16,但不会作为参数在方法中传递,或者从方法中返回时。

    转载请注明出处

    http://blog.csdn.net/xxdddail/article/details/36862275

  • 相关阅读:
    127.0.0.1:8080 不在以下 request 合法域名列
    [SAP] 34. System Manager
    [SAP] 33. Deployment and instance management
    [Angular] State provider solutions
    [Cloud Architect] 9. Securing Access to Cloud Services
    [Git] Squash commits
    [Angular] Overlay CDK
    [Cloud Architect] 8. Infrastructure as Code
    [DevOps] Terraform Remote State Management
    [DevOps] Using Terraform to Create an EC2 Instance
  • 原文地址:https://www.cnblogs.com/sparkleDai/p/7605000.html
Copyright © 2011-2022 走看看