在.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