Effective-c#-2 .NET资源管理

12 使用成员初始化器

运行时机

成员初始化器要比任何构造方法早运行。

何时不推荐

  1. 当不需要有默认值的时候
    当要初始化为默认值的时候,不推荐还多余地做一个初始化。当不初始化的时候,系统直接把一整块内存置为0,如果自己画蛇添足偏要初始化,反而是多了个创建实例的消耗。

    1
    2
    MyValType myVal; // 被初始化为0
    MyValType myVal = new MyValType(); // 同样被初始化为0

    第一条直接把内存变为0,第二条则还要调用initobj这条IL命令。

  2. 当还会在某个构造方法再次被初始化的时候
    当然,不需要被初始化两次。

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

静态构造方法。

1
2
3
4
5
6
7
8
9
10
11
12
public class A{
private static readonly A instance;

void A()
{
instance = A();
}

private A(){
// ...
}
}
  • 静态构造方法由CLR自动调用
  • 不能接受任何参数
  • 必须处理任何可能会抛出的异常,否则(由于是CLR调用的)会使CLR终止整个程序
  • 如果内部吞下了异常,会导致创建该类型的代码以失败告终,直到该AppDomain停止

16 避免创建非必要的对象

在那种频繁调用的方法中尽量避免创建对象(比如游戏中每帧调用的Update方法)。

创建过多对象会给GC造成负担。

两种方法可以改良:

  1. 把常用的局部变量升级为成员变量
  2. 提供一个类,存放某个类型常用实例的单例对象(比如某种颜色的笔刷)

17 实现标准的销毁模式

资源分为两种,一种称为非托管资源,可以理解为非c#对象,如系统API中的socket或者数据库连接;另一种成为托管资源,为c#对象,可由GC回收。

当使用非托管资源后,应该显示地调用非托管资源的关闭资源方法。

关闭一个资源常有三种方法:

  1. 析构方法(终结器) 特点是无法主动调用,且被调用时机不明,一般作为补救措施用
  2. 实现了IDisposable接口的Dispose方法
  3. Close方法(如Socket)

一般而言他们的关系是:InternalDispose方法为虚方法且不公开,执行释放资源的任务,Dispose方法公开且调用Dispose方法,在有些地方,Dispose方法也常被命名为Close,析构方法的实现亦同样是调用InternalDispose方法。

Dispose方法的逻辑中还需有一句命令,告知GC在回收本对象时无需再调用析构函数,避免二次调用InternalDispose。

从下面代码中可简明地明白他们的关系:

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
public ASocketLikeClass: IDisposable
{
// 标志位,防止被多次执行dispose
private bool _isDisposed;

public void IDisposable.Dispose(){
this.InternalDispose();
// 通知GC,回收此对象的时候无需再调用析构方法
GC.SuppressFinalize(this);
}

protected virtual void InternalDispose()
{
if(_isDisposed)
{
throw new Error("This object has been disposed!!");
}
// ...
}

// a socket like function
public void Close()
{
this.Dispose();
}

~ASocketLikeClass(){
InternalDispose();
}
}

应该注意的是,在InternalDispose中,不要写和释放资源无关的任何代码,避免造成对象的生命周期紊乱。

18 区分值类型和引用类型

c#中,struct为值类型,class为引用类型。值类型不支持多态,而引用类型支持。

值对象有更好的隐蔽性

当作为某个函数的返回值时,值类型会被复制一遍,而引用类型会被直接返回。故出现一问题,即如果函数返回了某个私有的引用类型对象,那么就相当于把该对象暴露出去,外界可以随意更改该对象。

1
2
3
4
5
6
7
8
9
10
11
12
class A{
private PrivateClass _privateObject;

public PrivateClass GetInfo(){
return _privateObject;
}
}

// call it
A a = new A();
var obj = a.GetInfo();
obj.xxx = 999;

(代码1-1)

当PrivateClass是一个struct时,obj.xxx = 999语句并不会影响到a._privateObject的内容(因为修改的是它的副本),相反,当PrivateClass为一个class时,a._privateObject的内容就可能会被改变。

内存分配

值类型被分配在栈上,引用类型被分配在堆上。

当你需要创建一个数组的时候,数组内容若是值类型,会更加高效(但是可能会爆栈)。

这是因为当创建该数组的时候,若,CLR将在栈上直接分配数组长度 * 值类型大小那么大的内存空间。

而当内容为引用类型的时候,只会分配数组长度 * 引用指针大小的内存于堆中。后续对数组的初始化过程中,会不断地在堆上new出新对象,它们在内存里的分布不一定是连续的,有可能会让堆上充满碎片,让程序变慢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct A{
double field;
}
class B{
double field;
}

// use them
A[] arrA = new A[100]; // 在栈上直接分配空间 64bit * 100
B[] arrB = new B[100]; // 在栈上分配空间,给指针使用(假设为32位指针) 32bit * 100
for(uint i = 0; i < arrB.Length; i++)
{
arrB[i] = new B(); // 在堆上分配 32bit 空间,并将地址计入arrB[i]
}

两者之间的转换

当想把struct转成class的时候要小心。由于struct作为值类型,根据前文所说,当它从方法中被返回出来后,外界的修改并不会影响内部数据,因而其修改非永久性的。若有人利用这一特性,而又有人把struct改成了class,就有可能导致数据被误修改。

参见代码1-1,当PrivateClass是个struct时,原有程序员可随意对其内容进行改动。若此时有人将struct草率地改成class,那么就会导致A._privateObject的内容被永久改变。

19 保证0为值类型的初始状态

众所周知一个enum类型的值,默认值是0。但是如果你人为地让enum从1开始,不包括0,那么enum值被默认初始化成0之后,就成了一个非法值。

1
2
3
4
5
6
7
8
9
10
11
12
13
class A
{
public enum Status{
StatusA = 1,
StautsB = 2,
}

private Status status;
}

// use it
A a = new A();
// 此时,status的值为0,为一个非法值

为避免status为非法值,一般有两种处理方法:

  1. 利用默认初始化方法

    1
    private Status status = Status.StatusA;
  2. 给Status设置一个默认值0,如此,就会被CLR自动设置为None状态

    1
    2
    3
    4
    5
    6
    public enum Status
    {
    None = 0,
    StatusA = 1,
    StatusB = 2,
    }
Buy Me A Coffee / 捐一杯咖啡的钱
分享这篇文章~
0%
//