前言
重构代码的时候,会遇到长参数的方法,此时就需要使用“引入参数对象”来封装这些参数。大多数时候,这些参数都是简单类型,而且所有参数的值占用的空间也不是非常的大,此时使用对象真的好吗?对象的特性是堆上分配、地址引用,看似很好,但是分配一个对象需要的一些额外成员(类型对象指针、同步块索引)以及需要对基类型进行计算,这些开销值得吗?如果你感觉不值得,那结构体(struct)就是你需要找的答案了。
定义描述
1、 结构体(Struct),值类型,继承自System.ValueType,在线程栈上分配内存,每一个实例都有自己的内存地址,不同实例互不影响。
2、 由于在栈上分配内存,所以实例不受垃圾回收器的控制,缓解了托管堆中的压力,减少了应用程序在其生存期内需要进行的垃圾回收次数,从而也提高了程序的性能。
3、 不能继承也不能是基类型,是sealed类型的,但是可以实现接口。
有些人会感觉不能继承基类有点“Low”,从面向对象的观点来说是有点。但是从结构体的特性上看,其实一点都不“Low”。如果能继承基类,按照继承规则,当获取实例的时候,就需要对所有的基类进行计算,这是有开销的。作为一个值类型,是为了给你提供轻便使用的,这些开销,绝对不接受。
4、结构体是没有null这种初始状态的,所以不要用null来对结构体的实例来进行判断。如果你想通过null来判断结构体是否已经实例化,可以使用可空类型Nullable。
不可变
1、建议把struct定义为和基元类型一样是不可变的,因为不可变可以减少一些不必要的问题。所谓的不可变就是不要让struct的实例的成员在struct外部被修改,内部定义的方法是可以修改的。如果要修改一个struct的实例,就重新构造一个新的实例(可参考DateTime, 看看它的实现)。
如下:
public class Struct_1_4 { public static void Run() { BroomCloset bc = new BroomCloset(1); changeBroom(bc);// 修改为10
Console.WriteLine(bc.Broom);// 输出0
} private static void changeBroom(BroomCloset bc) { bc.Broom = 10; } struct BroomCloset { private readonly int _mop; public BroomCloset(int mop) { _mop = mop; Broom = 0; } public int Mop { get { return _mop; } } public int Broom { get; set; } } }
通过方法修改了结构体的实例,但是最后并没有达到我们的预期,究其原因还是因为struct是按照值传递的。其实大家也知道只要把方法的参数改成添加ref关键字就可以了,这样就变成按照引用传递了。所谓的传引用只不过是把实例的地址作为参数传递过去进行计算罢了。.method private hidebysig static void changeBroom(valuetype StructAndClass.Struct_1_4/BroomCloset& bc) cil managed
声明和初始化
1、必须用new 初始化,如果仅仅是声明的话,可以不用new。如果不用new进行初始化,是不能调用属性或者方法的。其实公共字段也一样,如果不对字段进行初始化(赋值操作)也是不能使用的。
字段未赋值就使用,会提示字段不存在;实例未初始化就使用,提示实例不存在。
下面的实例代码展示了上面的描述,使用中一定要注意:
struct BroomCloset { public int Dustpan; public int Broom { get; set; } } public static void Run() { BroomCloset bc1; Console.WriteLine(bc1.Dustpan);// 报错:使用了可能未赋值的变量"Dustpan"
bc1.Dustpan = 12; Console.WriteLine(bc1.Dustpan);// 输出12
bc1.Broom = 10;// 报错:使用了未赋值的局部变量"bc1"
bc1 = new BroomCloset(); bc1.Broom = 12; Console.WriteLine(bc1.Broom);// 输出12
}
struct 和class都可以使用new关键字进行创建实例,但是他们的内部执行方式却是不一致的。从IL中可以看出struct的new是调用initobj StructAndClass.Struct_1_4/BroomCloset
对struct内的成员进行初始化。
如果还是不好理解,可以定义一个带有构造函数的struct,你会发现,如果你在构造函数内不对所有成员变量进行初始化,就会报错。
struct Room { public int Window; public Room(int window,int door) { Window = window; Door = door; } public int Door { get; set; } }
使用前考虑
虽然结构体不受垃圾回收器控制的这一特性,让它具有高性能的特质,但是如果整个系统中都用结构体来代替类,也是非常不合适的,以下情况需要考虑进去。
1、 由于值类型是按照值方式传递的,所以在传入方法参数或者返回方法返回值的时候,会造成字段复制,这一点是会造成性能损失的。
2、 值类型变量之间的赋值操作,由于是按值传递,所以会执行一次逐字段的复制操作。
3、 由于System.ValueType也继承自System.Object,所以如果方法的参数是object类型的,结构体是可以作为参数传递并使用的,此时就会出现装箱和拆箱的操作。
public static void Run() { BroomCloset bc2 = new BroomCloset(1); showObj(bc2); // 装箱
} private static void showObj(object obj) { Console.WriteLine("showObj"); if (obj is BroomCloset) { Console.WriteLine(((BroomCloset)obj).Dustpan); // 拆箱
} }
使用时机
1、 类型具有基元类型的行为,主要是不可变。
2、 类型不需要从其他任何类型继承,也不会派生出其他任何类型。
3、 类型的实例较小(Jeffery 建议16字节或者更小),这个需要自己把握一下。但是如果需要传递大数据(比如要传递一个数据库中的多条数据列表等等),还是不要使用结构体。
延伸一点
1、在使用方法的时候,如果是值类型,最好使用对应的值类型的重载方法,以减少装箱和拆箱操作带来的性能损失。
2、枚举的继承链是这样的,System.Enum -> System.ValueType -> System.Object, 所以按照辈分来说类是结构体的叔辈,结构体是枚举的叔辈。
3、使用dynamic可以简化语法,但是使用dynamic产生的额外开销也是不容忽视的。使用dynamic的时候,在运行时需要把Microsoft.CSharp.dll、System.dll、System.Core.dll加载到AppDomain中,加载这些程序集会产生额外的开销。如果程序中只是一、二个地方需要使用dynamic,使用传统的方法性能或许会更好。