0%

《Effective C#》笔记 .Net资源管理

阅读《Effective C#》一书的总结,第二部分,.Net资源管理。

.NET Resource Management

GC管理托管内存,默默地清除不再使用的内存,也会在每次运行时压缩托管堆。压缩托管堆能够将当前仍旧在使用的对象放在连续的内存中,因此空余空间也会是连续的内存。

对于托管堆以外的系统资源,GC不作处理。.Net提供了两种非托管资源生命周期的控制机制,即finalizer和IDisposable接口:

  • finalizer:只能保证指定类型对象分配的非托管资源被释放,无法确定预知其执行的时机。同时由于GC的分代机制,还会产生一些性能问题。

  • IDisposable:与finalizer配合,释放掉非托管资源,避免性能问题,详见第17条;

12 推荐使用成员初始化器而不是赋值语句

在C#中,声明成员变量的同时就进行初始化是一件很自然的事,如:

1
2
3
4
5
public class MyClass 
{
// declare the collection, and initialize it.
private List<string> labels = new List<string>();
}

成员的初始化器代码插入到构造函数之前执行;

有三种情况应避免使用成员初始化器:

  • 当要初始化的值是0或者null时。因为系统底层会默认将其初始化为0null,使用初始化器会产生额外的开销;

  • 不同的构造函数将成员初始化为不同的值时。会出现对应成员被重复初始化(初始化器一次,构造函数内一次);

  • 初始化对象需要做异常处理时。初始化器无法用try包裹,出现异常会传递到对象以外;

13 正确地初始化静态成员变量

C#中提供了静态初始化器和静态构造函数;

与实例的初始化器和构造函数相似,静态成员也可以使用静态初始化器,如果需要一些复杂的逻辑来初始化静态成员变量,则使用静态构造函数;

以下是分别用静态初始化器和静态构造函数实现单例模式的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MySingleton 
{
private static readonly MySingleton theOneAndOnly = new MySingleton();

public static MySingleton TheOnly
{
get { return theOneAndOnly; }
}

private MySingleton()
{
}

// remainder elided

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MySingleton2 
{
private static readonly MySingleton2 theOneAndOnly;

static MySingleton2()
{
theOneAndOnly = new MySingleton2();
}

public static MySingleton2 TheOnly
{
get { return theOneAndOnly; }
}

private MySingleton2()
{
}

// remainder elided

}

在应用程序作用域(AppDomain)内,在第一次访问类型之前,CLR会调用其静态构造函数;

类型的静态构造函数是唯一的,没有参数,且应当妥善处理可能的异常;

14 尽量减少重复的初始化逻辑

使用构造函数初始化器来实现一个构造函数调用另一个构造函数,以下是一个示例(也可以使用默认参数/可选参数来达到这一目的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MyClass 
{

// collection of data
private List<ImportantData> coll;

// Name of the instance:
private string name;
public MyClass() :
this(0, "")
{
}

public MyClass(int initialCount) :
this(initialCount, string.Empty)
{
}

public MyClass(int initialCount, string name)
{
coll = (initialCount > 0) ?
new List<ImportantData>(initialCount) :
new List<ImportantData>();
this.name = name;
}
}

为了使类型满足泛型的new()约束,必须显式定义无参构造函数;

要保证每个成员变量在构造过程中只被初始化一次,应当尽可能早地初始化。创建某个类型的第一个实例时所进行的操作如下:

  1. 静态变量设置为0

  2. 执行静态变量初始化器

  3. 执行基类的静态构造函数

  4. 执行静态构造函数

  5. 实例变量设置为0

  6. 执行实例变量的初始化器

  7. 执行基类中合适的实例构造函数

  8. 执行实例构造函数

15 使用using和try/finally清理资源

使用了非托管系统资源的类型,必须显式使用IDisposable接口的Dispose()接口来释放。如果忘了调用,则会在终结器(finalizer)执行时被释放(这样这些资源会在内存中停留更长的时间);

使用using语句能够以最简单的方式保证对象可以正常销毁,它将生成一个try/catch块。以下两段代码生成的IL完全一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SqlConnection myConnection = null;
// Example Using clause:
using (myConnection = new SqlConnection(connString))
{
myConnection.Open();
}

// example Try / Catch block:
try
{
myConnection = new SqlConnection(connString);
myConnection.Open();
}
finally
{
myConnection.Dispose();
}

16 避免创建非必要的对象

若某个引用类型的局部变量用于将被频繁调用的例程中,那么应该将其提升为成员变量;

提供一个类,存放某个类型常用实例的单例对象(Color.BlackVector3.Up等);

为不可变类型提供可变的创建对象(为String提供StringBuilder);

17 实现标准的销毁模式

.NET Framework中标准的销毁非托管资源的模式。这个标准的模式可以让使用者在正常使用时通过IDisposable接口来销毁资源,也会使在使用者忘了调用时用终结器来销毁资源;

类继承体系中,基类应该实现IDisposable接口并提供终结器作为最后保护。这两个处理都应该把具体的资源清理工作交给一个虚方法。子类仅在需要释放自身非托管资源时才覆写该虚方法,且覆写时应调用基类的版本;

实现IDisposable.Dispose(),需要在该方法内做四件事情:

  1. 释放所有非托管资源

  2. 释放所有托管资源,包括所有事件监听程序

  3. 设定一个状态标志,表示该对象已经被销毁。若是在销毁后继续调用其共有方法,应抛出ObjectDisposed异常;

  4. 跳过终结操作,即调用GC.SuppressFinalize(this)

为了避免终结器和Dispose()方法中出现重复的内容(清理的逻辑代码),以及正确地处理子类和基类的清理方法。一种清理资源的模式是定义Dispose(bool disposing)方法,参数表示是否清理托管资源(true表示从Dispose()调用,false表示从终结器调用),以下是一段示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class MyResourceHog : IDisposable
{
// Flag for already disposed
private bool alreadyDisposed = false;
// Implementation of IDisposable.
// Call the virtual Dispose method.
// Suppress Finalization.
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
// Virtual Dispose method
protected virtual void Dispose(bool isDisposing)
{
// Don't dispose more than once.
if (alreadyDisposed)
return;
if (isDisposing)
{
// elided: free managed resources here.
}
// elided: free unmanaged resources here.
// Set disposed flag:
alreadyDisposed = true;
}
public void ExampleMethod()
{
if (alreadyDisposed)
throw new ObjectDisposedException(
"MyResourceHog",
"Called Example Method on Disposed object");
// remainder elided.
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DerivedResourceHog : MyResourceHog
{
// Have its own disposed flag.
private bool disposed = false;
protected override void Dispose(bool isDisposing)
{
// Don't dispose more than once.
if (disposed)
return;
if (isDisposing)
{
// TODO: free managed resources here.
}
// TODO: free unmanaged resources here.
// Let the base class free its resources.
// Base class is responsible for calling
// GC.SuppressFinalize( )
base.Dispose(isDisposing);
// Set derived class disposed flag:
disposed = true;
}
}

(注:个人理解,上边的示例中遗漏掉重要的一步,即定义析构函数/终结器,在其内部调用Dispose(false)

不要在终结器或Dispose()方法中做除了资源清理以外其它事情;

18 区分值类型和引用类型

值类型无法实现多态,因此其最佳用途就是存放应用程序中用到的数据(struct);

引用类型支持多态,因此可以用来定义应用程序的行为(class);

应当根据类型的用途来决定选择值类型还是引用类型:

  • 从内存管理角度说,值类型更加高效,因为值类型不会带来太多堆上的碎片,也少了一些间接的指针操作;

  • 从方法或属性中返回值类型时,将会发生复制,可以避免将内部引用暴露出去的危险;

  • 值类型缺少面向对象技术的支持,如无法实现值类型的继承。值类型可以实现接口,但这需要装箱;

一般来说创建的类型都是引用类型而不是值类型。如果匹配以下四项要求,那么此时应选择值类型:

  1. 该类型的主要职责在于数据存储;

  2. 该类型的公有接口都是由访问其数据成员的属性定义的;

  3. 该类型绝对不会有派生类;

  4. 该类型永远都不需要多态支持;

19 保证0为值类型的有效状态

.NET系统默认初始化过程会将所有对象设置为0;

枚举必须将0设置为枚举值的一个有效选择;

在使用标志枚举时(为枚举增加[Flags]属性),应确保0为有效值,且让其表示“所有标志都没有设置”的情况;

20 保证值类型的常量性和和原子性

常量性,创建之后就保持其值不变。有利于保证内部数据正确,并且天生线程安全,在基于hash的集合中也性能更佳;

原子性,对象是由多个相关字段组成的单一实体,当要修改其中一个字段则意味着要同时修改其它的字段。如Address地址,修改了City,则一定要同时修改ZipCodeState

当需要返回一个可变的引用类型时,可以防御性地创建一个副本,然后返回副本,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Immutable: A copy is made at construction. 
public struct PhoneList2
{
private readonly Phone[] phones;
public PhoneList2(Phone[] ph)
{
phones = new Phone[ph.Length];
// Copies values because Phone is a value type.
ph.CopyTo(phones, 0);
}

public IEnumerable<Phone> Phones
{
get
{
return phones;
}
}
}

初始化一个常量类型时,可以定义一组合适的构造函数,或者使用工厂方法,或者引入一个可变的辅助类;

REFERENCE

《C#高效编程 改进C#代码的50个行之有效的办法》