简介
C/C++程序员或多或少都有使用struct的经历,在C++中struct和class的区别不大,除了默认成员的可访问性,这点在C#中则截然不同。本文将力图说明C#中struct和class的区别以及如何正确的使用struct。
为什么需要struct?
众所周知,在java中并没有struct的概念,那么C#为何引入struct呢?最基本原因是可以创建值类型的类型,使在托管环境中有更好的性能。
区别于java,C#有值类型和引用类型的概念(java只有引用类型)。引用类型的实例分配在堆上,当对象没有被引用时被垃圾回收器回收;值类型的实例分配在栈上,当离开其作用域后内存被回收。值类型本身存储的是值本身,引用类型存储的是引用,C#语言提供的原始类型除了string类型其它都是值类型。
在C#中struct是值类型,class是引用类型,可以通过enum和struct关键字创建值类型的对象。使用值类型可以减少托管堆上对象的数量,从而减少垃圾回收器(GC)的负担,提高性能,值类型也有明显的缺点:通过值类型传递较大对象的开销比较昂贵、装箱和拆箱对性能造成影响。
Classes 和Structs
public struct Employeestruct { private int _fooNumber; public Employeestruct(int fooNumber) : this() { _fooNumber = fooNumber; } public string Name { get; set; } public int Age { get; set; } public string GetMessage() { return string.Format("{0}--{1}", Name, Age); } }
从上面可以看到,struct和class非常相似,不过它们还是有本质的区别,接下来我们就一一分析。
1. Structs 和Inheritance
Structs从System.ValueType继承而classes继承于System.Object或从System.Object继承的其他类型,当然System.ValueType也从System.Object继承(这不是重点)。Structs可以实现接口,但不能从另一个classes、structs继承,而且不能作为其他classes的基类。要知道,当你把structs作为接口使用时,就进行一次隐形装箱,因为接口作为引用类型。请看下面代码:
struct Foo : IFoo { int x; } IFoo iFoo = new Foo();
Foo的实例被创建并赋值给iFoo(装箱),当iFoo调用时实际上使用的是Foo装箱后的实例。
2. Constructors
尽管CLR允许,但是不允许在structs中定义无参的构造函数。对于值类型,编译器默认情况下不生成默认的构造函数,也不调用默认的构造函数,所以C#编译器不允许使用者定义无参的构造函数。由于structs不生成默认的构造函数,所以不能初始化字段。如下(错误):
Struct MyFoo { int x = 1; }
记住,编译器把初始化工作放在构造函数中,由于structs没有默认的构造函数,所以不能初始化字段。
有趣的是,你可以使用下面的语法:
Foo foo = new Foo();
通过之前的章节了解到,尽管初始化foo使用了new操作符,但是structs的实例分配到栈上。new Foo()不会调用无参的构造函数,仅仅是初始化该struct的字段为默认值。
struct Foo { int x; public Foo(int x) { this.x = x; } } Foo foo = new Foo();
注意,我已经重载了构造函数,然而我能够调用new Foo()。
3. Destructors
Structs不允许定义析构函数,如果你扔就去定义一个析构函数,编译器会立即提示一个错误,不过structs可以实现IDisposable接口。
4. Comparison against null
Structs不能和null进行比较,这点在.net framework2.0已不再是问题(可空类型),关于可空类型,超出本文讨论范围。
5. Readonly关键字
对于引用类型,readonly阻止再为变量赋值,但不阻止你修改当前引用对象的状态。对于值类型,readonly有点类似C++中的const关键字,它阻止你修改引用对象,也意味着不能再为他赋值,因为这会导致重新初始化一次。请看下面代码:
public class MyReferenceType { public int State { get; set; } } public struct MyValueType { public int State { get; set; } } public void TestMethod() { myReferenceType = new MyReferenceType(); // 错误 myReferenceType.State = 1234; // Ok myValueType = new MyValueType(); // 错误 myValueType.State = 1234; // 错误 }
foreach 语句和using语句的变量为隐式readonly,所以如果使用structs,将无法更改其状态。
何时使用 structs
通过前面的描述已经清楚classes和structs的区别,那我们来看下适合使用structs的场景:
l 实例使用起来像C#的基元类型
l 需要创建大量的、短暂实例(例如在循环体内)
l 实例不需要大量的传递
l 不需要从其他类型继承或不希望被其他类型继承
l 希望被人操作你实例的副本
不适合使用structs的场景:
l 实例过大,当实例过大时,传递实例会有很大的开销。微软建议structs的理想大小应该在16bytes以下
l 当回引起装箱、拆箱操作时。关于装箱、拆箱超出本文讨论范围