.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 52 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 52 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之后。
我们常见的基本类型如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
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。