阅读《Effective C#》一书的总结,第二部分,.Net资源管理。
.NET Resource Management
GC管理托管内存,默默地清除不再使用的内存,也会在每次运行时压缩托管堆。压缩托管堆能够将当前仍旧在使用的对象放在连续的内存中,因此空余空间也会是连续的内存。
对于托管堆以外的系统资源,GC不作处理。.Net提供了两种非托管资源生命周期的控制机制,即finalizer和IDisposable接口:
finalizer:只能保证指定类型对象分配的非托管资源被释放,无法确定预知其执行的时机。同时由于GC的分代机制,还会产生一些性能问题。
IDisposable:与finalizer配合,释放掉非托管资源,避免性能问题,详见第17条;
12 推荐使用成员初始化器而不是赋值语句
在C#中,声明成员变量的同时就进行初始化是一件很自然的事,如:
1 | public class MyClass |
成员的初始化器代码插入到构造函数之前执行;
有三种情况应避免使用成员初始化器:
当要初始化的值是
0
或者null
时。因为系统底层会默认将其初始化为0
或null
,使用初始化器会产生额外的开销;不同的构造函数将成员初始化为不同的值时。会出现对应成员被重复初始化(初始化器一次,构造函数内一次);
初始化对象需要做异常处理时。初始化器无法用
try
包裹,出现异常会传递到对象以外;
13 正确地初始化静态成员变量
C#中提供了静态初始化器和静态构造函数;
与实例的初始化器和构造函数相似,静态成员也可以使用静态初始化器,如果需要一些复杂的逻辑来初始化静态成员变量,则使用静态构造函数;
以下是分别用静态初始化器和静态构造函数实现单例模式的示例:
1 | public class MySingleton |
1 | public class MySingleton2 |
在应用程序作用域(AppDomain)内,在第一次访问类型之前,CLR会调用其静态构造函数;
类型的静态构造函数是唯一的,没有参数,且应当妥善处理可能的异常;
14 尽量减少重复的初始化逻辑
使用构造函数初始化器来实现一个构造函数调用另一个构造函数,以下是一个示例(也可以使用默认参数/可选参数来达到这一目的):
1 | public class MyClass |
为了使类型满足泛型的new()
约束,必须显式定义无参构造函数;
要保证每个成员变量在构造过程中只被初始化一次,应当尽可能早地初始化。创建某个类型的第一个实例时所进行的操作如下:
静态变量设置为0
执行静态变量初始化器
执行基类的静态构造函数
执行静态构造函数
实例变量设置为0
执行实例变量的初始化器
执行基类中合适的实例构造函数
执行实例构造函数
15 使用using和try/finally清理资源
使用了非托管系统资源的类型,必须显式使用IDisposable
接口的Dispose()
接口来释放。如果忘了调用,则会在终结器(finalizer)执行时被释放(这样这些资源会在内存中停留更长的时间);
使用using
语句能够以最简单的方式保证对象可以正常销毁,它将生成一个try/catch
块。以下两段代码生成的IL完全一致。
1 | SqlConnection myConnection = null; |
16 避免创建非必要的对象
若某个引用类型的局部变量用于将被频繁调用的例程中,那么应该将其提升为成员变量;
提供一个类,存放某个类型常用实例的单例对象(Color.Black
、Vector3.Up
等);
为不可变类型提供可变的创建对象(为String
提供StringBuilder
);
17 实现标准的销毁模式
.NET Framework中标准的销毁非托管资源的模式。这个标准的模式可以让使用者在正常使用时通过IDisposable
接口来销毁资源,也会使在使用者忘了调用时用终结器来销毁资源;
类继承体系中,基类应该实现IDisposable
接口并提供终结器作为最后保护。这两个处理都应该把具体的资源清理工作交给一个虚方法。子类仅在需要释放自身非托管资源时才覆写该虚方法,且覆写时应调用基类的版本;
实现IDisposable.Dispose()
,需要在该方法内做四件事情:
释放所有非托管资源
释放所有托管资源,包括所有事件监听程序
设定一个状态标志,表示该对象已经被销毁。若是在销毁后继续调用其共有方法,应抛出ObjectDisposed异常;
跳过终结操作,即调用
GC.SuppressFinalize(this)
;
为了避免终结器和Dispose()
方法中出现重复的内容(清理的逻辑代码),以及正确地处理子类和基类的清理方法。一种清理资源的模式是定义Dispose(bool disposing)
方法,参数表示是否清理托管资源(true表示从Dispose()调用,false表示从终结器调用),以下是一段示例代码:
1 | public class MyResourceHog : IDisposable |
1 | public class DerivedResourceHog : MyResourceHog |
(注:个人理解,上边的示例中遗漏掉重要的一步,即定义析构函数/终结器,在其内部调用Dispose(false)
)
不要在终结器或Dispose()
方法中做除了资源清理以外其它事情;
18 区分值类型和引用类型
值类型无法实现多态,因此其最佳用途就是存放应用程序中用到的数据(struct);
引用类型支持多态,因此可以用来定义应用程序的行为(class);
应当根据类型的用途来决定选择值类型还是引用类型:
从内存管理角度说,值类型更加高效,因为值类型不会带来太多堆上的碎片,也少了一些间接的指针操作;
从方法或属性中返回值类型时,将会发生复制,可以避免将内部引用暴露出去的危险;
值类型缺少面向对象技术的支持,如无法实现值类型的继承。值类型可以实现接口,但这需要装箱;
一般来说创建的类型都是引用类型而不是值类型。如果匹配以下四项要求,那么此时应选择值类型:
该类型的主要职责在于数据存储;
该类型的公有接口都是由访问其数据成员的属性定义的;
该类型绝对不会有派生类;
该类型永远都不需要多态支持;
19 保证0为值类型的有效状态
.NET系统默认初始化过程会将所有对象设置为0;
枚举必须将0设置为枚举值的一个有效选择;
在使用标志枚举时(为枚举增加[Flags]
属性),应确保0为有效值,且让其表示“所有标志都没有设置”的情况;
20 保证值类型的常量性和和原子性
常量性,创建之后就保持其值不变。有利于保证内部数据正确,并且天生线程安全,在基于hash的集合中也性能更佳;
原子性,对象是由多个相关字段组成的单一实体,当要修改其中一个字段则意味着要同时修改其它的字段。如Address
地址,修改了City
,则一定要同时修改ZipCode
和State
;
当需要返回一个可变的引用类型时,可以防御性地创建一个副本,然后返回副本,例如:
1 | // Immutable: A copy is made at construction. |
初始化一个常量类型时,可以定义一组合适的构造函数,或者使用工厂方法,或者引入一个可变的辅助类;