首页 > Asp.net开发 > .NET中struct与class的区别

.NET中struct与class的区别

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

1.类型不同

我们先来看一段代码

[csharp] view plaincopy在CODE上查看代码片派生到我的代码片

  1. static void Main(string[] args)  
  2.       {  
  3.           TypeDemo();             
  4.           Console.ReadLine();  
  5.       }  
  6.   
  7.       // Reference type (because of 'class')   
  8.       class SomeClassRef { public Int32 x; }  
  9.   
  10.       // Value type (because of 'struct')   
  11.       struct SomeStructVal { public Int32 x; }  
  12.   
  13.       static void TypeDemo()  
  14.       {  
  15.           SomeClassRef r1 = new SomeClassRef(); // Allocated in heap   
  16.           SomeStructVal v1 = new SomeStructVal(); // Allocated on stack   
  17.           r1.x = 5; // Pointer dereference   
  18.           v1.x = 5; // Changed on stack   
  19.           Console.WriteLine("r1=" + r1.x.ToString()); // Displays "5"   
  20.           Console.WriteLine("v1=" + v1.x.ToString()); // Also displays "5"   
  21.           // The left side of Figure 5­2 reflects the situation   
  22.           // after the lines above have executed.   
  23.           SomeClassRef r2 = r1; // Copies reference (pointer) only   
  24.           SomeStructVal v2 = v1; // Allocate on stack & copies members   
  25.           r1.x = 8; // Changes r1.x and r2.x   
  26.           v1.x = 9; // Changes v1.x, not v2.x   
  27.           Console.WriteLine("r1=" + r1.x.ToString()); // Displays "8"   
  28.           Console.WriteLine("r2=" + r2.x.ToString()); // Displays "8"   
  29.           Console.WriteLine("v1=" + v1.x.ToString()); // Displays "9"   
  30.           Console.WriteLine("v2=" + v2.x.ToString()); // Displays "5"   
  31.           // The right side of Figure 5­2 reflects the situation   
  32.           // after ALL of the lines above have executed.   
  33.       }  

该代码执行结果如下

从代码中,我们可以看到,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等都是值类型,都具有相同的特性。因为值类型的赋值是数据拷贝,引用类型是引用拷贝,所以两者在作为函数参数传传递时就会有很大的差异。值类型作参数的,其原始值不会受影响,而引用类型作参数的,其原始值会受影响。测试代码如下

[csharp] view plaincopy在CODE上查看代码片派生到我的代码片

  1. static void Main(string[] args)  
  2.       {            
  3.           TestFunc();  
  4.           Console.ReadLine();  
  5.       }    
  6. atic void TestFunc()  
  7.       {  
  8.           SomeClassRef r = new SomeClassRef();  
  9.           r.x = 1;  
  10.           SomeStructVal v = new SomeStructVal();  
  11.           v.x = 1;  
  12.   
  13.           RefFunc(r);  
  14.           ValFunc(v);  
  15.   
  16.           Console.WriteLine(r.x);  
  17.           Console.WriteLine(v.x);  
  18.       }  
  19.   
  20.       static void RefFunc(SomeClassRef r)  
  21.       {  
  22.           r.x = 100;  
  23.       }  
  24.   
  25.       static void ValFunc(SomeStructVal v)  
  26.       {  
  27.           v.x = 100;  
  28.       }  


从结果可以看到,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。下面是测试代码

[csharp] view plaincopy在CODE上查看代码片派生到我的代码片

  1. private const int TIME_MAX = 100000000;  
  2.   
  3.       static void Main(string[] args)  
  4.       {            
  5.           TestStruct();  
  6.           TestClass();           
  7.           Console.ReadLine();  
  8.       }  
  9.   
  10. atic void TestStruct()  
  11.       {  
  12.           Stopwatch sw = new Stopwatch();  
  13.           sw.Start();  
  14.           for (int i = 0; i < TIME_MAX; i++)  
  15.           {  
  16.               SomeStructVal v1 = new SomeStructVal();  
  17.               v1.x = i;  
  18.           }  
  19.           sw.Stop();  
  20.           Console.WriteLine("Struct time:" + sw.ElapsedMilliseconds.ToString());  
  21.       }  
  22.   
  23.       static void TestClass()  
  24.       {  
  25.           Stopwatch sw = new Stopwatch();  
  26.           sw.Start();  
  27.           for (int i = 0; i < TIME_MAX; i++)  
  28.           {  
  29.               SomeClassRef r1 = new SomeClassRef();  
  30.               r1.x = i;  
  31.           }  
  32.           sw.Stop();  
  33.           Console.WriteLine("Class time:" + sw.ElapsedMilliseconds.ToString());  
  34.       }  


测试结果见下图

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

5.装箱与拆箱

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

[csharp] view plaincopy在CODE上查看代码片派生到我的代码片

  1. static void BoxAndUnbox_struct()  
  2.        {  
  3.            ArrayList a = new ArrayList();  
  4.            SomeStructVal v; // Allocate a SomeStructVal (not in the heap).   
  5.            for (Int32 i = 0; i < 10; i++)  
  6.            {  
  7.                v = new SomeStructVal();  
  8.                v.x = i; // Initialize the members in the value type.   
  9.                a.Add(v); // Box the value type and add the   
  10.                // reference to the Arraylist.   
  11.            }  
  12.            SomeStructVal v2 = (SomeStructVal)a[0];  
  13.            Console.WriteLine(v2.x);  
  14.        }  
  15.   
  16.        static void BoxAndUnbox_class()  
  17.        {  
  18.            ArrayList a = new ArrayList();  
  19.            SomeClassRef r; // Allocate SomeClassRef in the heap.   
  20.            for (Int32 i = 0; i < 10; i++)  
  21.            {  
  22.                r = new SomeClassRef();  
  23.                r.x = i; // Initialize the members in the value type.   
  24.                a.Add(r); //add the reference to the Arraylist.   
  25.            }  
  26.            SomeClassRef r2 = (SomeClassRef)a[0];  
  27.            Console.WriteLine(r2.x);            
  28.        }  


编译后,使用IL查看EXE

BoxAndUnbox_class函数

  1. .method private hidebysig static void  BoxAndUnbox_class() cil managed  
  2. {  
  3.   // 代码大小       75 (0x4b)  
  4.   .maxstack  2  
  5.   .locals init ([0] class [mscorlib]System.Collections.ArrayList a,  
  6.            [1] class ClassAndStruct.Program/SomeClassRef r,  
  7.            [2] int32 i,  
  8.            [3] class ClassAndStruct.Program/SomeClassRef r2,  
  9.            [4] bool CS$4$0000)  
  10.   IL_0000:  nop  
  11.   IL_0001:  newobj     instance void [mscorlib]System.Collections.ArrayList::.ctor()  
  12.   IL_0006:  stloc.0  
  13.   IL_0007:  ldc.i4.0  
  14.   IL_0008:  stloc.2  
  15.   IL_0009:  br.s       IL_0026  
  16.   IL_000b:  nop  
  17.   IL_000c:  newobj     instance void ClassAndStruct.Program/SomeClassRef::.ctor()  
  18.   IL_0011:  stloc.1  
  19.   IL_0012:  ldloc.1  
  20.   IL_0013:  ldloc.2  
  21.   IL_0014:  stfld      int32 ClassAndStruct.Program/SomeClassRef::x  
  22.   IL_0019:  ldloc.0  
  23.   IL_001a:  ldloc.1  
  24.   IL_001b:  callvirt   instance int32 [mscorlib]System.Collections.ArrayList::Add(object)  
  25.   IL_0020:  pop  
  26.   IL_0021:  nop  
  27.   IL_0022:  ldloc.2  
  28.   IL_0023:  ldc.i4.1  
  29.   IL_0024:  add  
  30.   IL_0025:  stloc.2  
  31.   IL_0026:  ldloc.2  
  32.   IL_0027:  ldc.i4.s   10  
  33.   IL_0029:  clt  
  34.   IL_002b:  stloc.s    CS$4$0000  
  35.   IL_002d:  ldloc.s    CS$4$0000  
  36.   IL_002f:  brtrue.s   IL_000b  
  37.   IL_0031:  ldloc.0  
  38.   IL_0032:  ldc.i4.0  
  39.   IL_0033:  callvirt   instance object [mscorlib]System.Collections.ArrayList::get_Item(int32)  
  40.   IL_0038:  castclass  ClassAndStruct.Program/SomeClassRef  
  41.   IL_003d:  stloc.3  
  42.   IL_003e:  ldloc.3  
  43.   IL_003f:  ldfld      int32 ClassAndStruct.Program/SomeClassRef::x  
  44.   IL_0044:  call       void [mscorlib]System.Console::WriteLine(int32)  
  45.   IL_0049:  nop  
  46.   IL_004a:  ret  
  47. } // end of method Program::BoxAndUnbox_class  

可以看到,IL代码中没有一个box或unbox的操作,只有pop一类的操作。可以判定并没有进行装箱和拆箱。

再来看BoxAndUnbox_struct函数

  1. .method private hidebysig static void  BoxAndUnbox_struct() cil managed  
  2. {  
  3.   // 代码大小       84 (0x54)  
  4.   .maxstack  2  
  5.   .locals init ([0] class [mscorlib]System.Collections.ArrayList a,  
  6.            [1] valuetype ClassAndStruct.Program/SomeStructVal v,  
  7.            [2] int32 i,  
  8.            [3] valuetype ClassAndStruct.Program/SomeStructVal v2,  
  9.            [4] bool CS$4$0000)  
  10.   IL_0000:  nop  
  11.   IL_0001:  newobj     instance void [mscorlib]System.Collections.ArrayList::.ctor()  
  12.   IL_0006:  stloc.0  
  13.   IL_0007:  ldc.i4.0  
  14.   IL_0008:  stloc.2  
  15.   IL_0009:  br.s       IL_002e  
  16.   IL_000b:  nop  
  17.   IL_000c:  ldloca.s   v  
  18.   IL_000e:  initobj    ClassAndStruct.Program/SomeStructVal  
  19.   IL_0014:  ldloca.s   v  
  20.   IL_0016:  ldloc.2  
  21.   IL_0017:  stfld      int32 ClassAndStruct.Program/SomeStructVal::x  
  22.   IL_001c:  ldloc.0  
  23.   IL_001d:  ldloc.1  
  24.   IL_001e:  box        ClassAndStruct.Program/SomeStructVal  
  25.   IL_0023:  callvirt   instance int32 [mscorlib]System.Collections.ArrayList::Add(object)  
  26.   IL_0028:  pop  
  27.   IL_0029:  nop  
  28.   IL_002a:  ldloc.2  
  29.   IL_002b:  ldc.i4.1  
  30.   IL_002c:  add  
  31.   IL_002d:  stloc.2  
  32.   IL_002e:  ldloc.2  
  33.   IL_002f:  ldc.i4.s   10  
  34.   IL_0031:  clt  
  35.   IL_0033:  stloc.s    CS$4$0000  
  36.   IL_0035:  ldloc.s    CS$4$0000  
  37.   IL_0037:  brtrue.s   IL_000b  
  38.   IL_0039:  ldloc.0  
  39.   IL_003a:  ldc.i4.0  
  40.   IL_003b:  callvirt   instance object [mscorlib]System.Collections.ArrayList::get_Item(int32)  
  41.   IL_0040:  unbox.any  ClassAndStruct.Program/SomeStructVal  
  42.   IL_0045:  stloc.3  
  43.   IL_0046:  ldloca.s   v2  
  44.   IL_0048:  ldfld      int32 ClassAndStruct.Program/SomeStructVal::x  
  45.   IL_004d:  call       void [mscorlib]System.Console::WriteLine(int32)  
  46.   IL_0052:  nop  
  47.   IL_0053:  ret  
  48. } // 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中。所以两者已经不是一个对象了。

下面是测试代码

  1. static void Unbox()  
  2.        {  
  3.            SomeStructVal v = new SomeStructVal();  
  4.            v.x = 1;  
  5.            Object o = v;  
  6.            SomeStructVal v2 = (SomeStructVal)o;  
  7.            v2.x = 11;  
  8.            SomeStructVal v3 = (SomeStructVal)o;  
  9.            Console.WriteLine("v2.x="+v2.x.ToString());  
  10.            Console.WriteLine("o.x="+v3.x.ToString());  
  11.        }  


结果如下图


下面是IL的代码

  1. .method private hidebysig static void  Unbox() cil managed  
  2. {  
  3.   // 代码大小       104 (0x68)  
  4.   .maxstack  2  
  5.   .locals init ([0] valuetype ClassAndStruct.Program/SomeStructVal v,  
  6.            [1] object o,  
  7.            [2] valuetype ClassAndStruct.Program/SomeStructVal v2,  
  8.            [3] valuetype ClassAndStruct.Program/SomeStructVal v3)  
  9.   IL_0000:  nop  
  10.   IL_0001:  ldloca.s   v  
  11.   IL_0003:  initobj    ClassAndStruct.Program/SomeStructVal  
  12.   IL_0009:  ldloca.s   v  
  13.   IL_000b:  ldc.i4.1  
  14.   IL_000c:  stfld      int32 ClassAndStruct.Program/SomeStructVal::x  
  15.   IL_0011:  ldloc.0  
  16.   IL_0012:  box        ClassAndStruct.Program/SomeStructVal  
  17.   IL_0017:  stloc.1  
  18.   IL_0018:  ldloc.1  
  19.   IL_0019:  unbox.any  ClassAndStruct.Program/SomeStructVal  
  20.   IL_001e:  stloc.2  
  21.   IL_001f:  ldloca.s   v2  
  22.   IL_0021:  ldc.i4.s   11  
  23.   IL_0023:  stfld      int32 ClassAndStruct.Program/SomeStructVal::x  
  24.   IL_0028:  ldloc.1  
  25.   IL_0029:  unbox.any  ClassAndStruct.Program/SomeStructVal  
  26.   IL_002e:  stloc.3  
  27.   IL_002f:  ldstr      "v2.x="  
  28.   IL_0034:  ldloca.s   v2  
  29.   IL_0036:  ldflda     int32 ClassAndStruct.Program/SomeStructVal::x  
  30.   IL_003b:  call       instance string [mscorlib]System.Int32::ToString()  
  31.   IL_0040:  call       string [mscorlib]System.String::Concat(string,  
  32.                                                               string)  
  33.   IL_0045:  call       void [mscorlib]System.Console::WriteLine(string)  
  34.   IL_004a:  nop  
  35.   IL_004b:  ldstr      "o.x="  
  36.   IL_0050:  ldloca.s   v3  
  37.   IL_0052:  ldflda     int32 ClassAndStruct.Program/SomeStructVal::x  
  38.   IL_0057:  call       instance string [mscorlib]System.Int32::ToString()  
  39.   IL_005c:  call       string [mscorlib]System.String::Concat(string,  
  40.                                                               string)  
  41.   IL_0061:  call       void [mscorlib]System.Console::WriteLine(string)  
  42.   IL_0066:  nop  
  43.   IL_0067:  ret  
  44. } // 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://www.devba.com/index.php/archives/5773.html | 开发吧

报歉!评论已关闭.