编程语言
首页 > 编程语言> > 《Effective C#》笔记(2) - .NET的资源管理

《Effective C#》笔记(2) - .NET的资源管理

作者:互联网

理解并善用.NET的资源管理机制

.NET环境会提供垃圾回收器(GC)来帮助控制托管内存,这使得开发者无须担心内存泄漏等内存管理问题。尽管如此,但如果开发者能够把自己应该执行的那些清理工作做好,那么垃圾回收器会表现得更为出色。非托管的资源是需要由开发者控制的,例如数据库连接、GDI+对象、IO等;此外,某些做法可能会令对象在内存中所待的时间比你预想的更长,这些都是需要我们去了解、避免的。

GC的检测过程是从应用程序的根对象出发,把与该对象之间没有通路相连的那些对象判定为不可达的对象,也就是说,凡是无法从应用程序中的活动对象(live object)出发而到达的那些对象都应该得到回收。应用程序如果不再使用某个实体,那么就不会继续引用它,于是,GC就会发现这个实体是可以回收的。
垃圾回收器每次运行的时候,都会压缩托管堆,以便把其中的活动对象安排在一起,使得空闲的内存能够形成一块连续的区域。

针对托管堆的内存管理工作完全是由垃圾回收器负责的,但是除此之外的其他资源则必须由开发者来管理。
有两种机制可以控制非托管资源的生存期

在这两种方式中,应该优先考虑通过IDisposable接口来更为顺畅地将资源及时返还给系统,因为finalizer作为一种防护机制,虽然可以确保对象总是能够把非托管资源释放掉,但这种机制有一些缺陷

声明字段时,尽量直接为其设定初始值

类的构造函数有时不止一个,如果某个成员变量的初始化在构造函数进行,就会有忘记给某些成员变量设定初始值的可能性。为了彻底杜绝这种情况,无论是静态变量还是实例变量,最好都在声明的时候直接初始化,而不要等实现每个构造函数的时候再去赋值。

表面上看,在构造函数初始化和在声明的时候直接初始化等效,但实际上如果选择在声明的时候直接初始化,编译器会把由这些语句所生成的程序码放在类的构造函数之前。这些语句的执行时机比基类的构造函数更早,它们会按照本类声明相关变量的先后顺序来执行。

但也并不是说,如何时候都优先在声明的时候直接初始化,在下面三种情况下,声明的时候直接初始化是不建议的,甚至会带来问题:

  1. 把对象初始化为0或null。系统在执行开发者所编写的代码之前,本身就会生成初始化逻辑,以便把相关的内容全都设置成0,这是通过底层CPU指令来做的。这些指令会把整块内存全都设置成0,因此,你如果还要编写初始化语句,让编译器会添加相关指令,把那些内存再度清零,那就显得多余了。

  2. 如果不同的构造函数需要按照各自的方式来设定某个字段的初始值,那么就不应该再在声明的时候初始化了,因为它只适用于那些总是按相同方式来初始化的变量。
    就类似这样的写法:

public class MyClass
{
  private List<string> labels = new List<string>();
  
  public MyClass(int size)
  {
    labels = new List<string>(size);
  }
}

这会在构造类实例的过程中创建出两个不同的List对象,而且先创建出来的那个List马上就会被后创建的List取代,实际上等于是白创建了一次。这是因为字段的初始化语句会先于构造函数而执行,于是,程序在初始化labels字段时,会根据其初始化语句的要求创建出一个List,然后,等到执行构造函数时,又会根据其中的赋值语句创建出另一个List,并导致前一个List失效。
编译器所生成的代码相当于下面这样:

public class MyClass
{
  private List<string> labels;
  
  public MyClass(int size)
  {
    labels = new List<string>();
    labels = new List<string>(size);
  }
}
  1. 如果初始化变量的过程中有可能出现异常,那么就不应该使用初始化语句,而是应该把这部分逻辑移动到构造函数里面。由于成员变量的初始化语句不能包裹在try-catch块中,因此初始化的过程中一旦发生异常,就会传播到对象之外,从而令开发者无法在类里面加以处理,应该把这种初始化代码放在构造函数中,以便通过适当的代码将异常处理好。

用适当的方式初始化类中的静态成员

通过静态初始化语句或者静态构造函数都可以初始化类中的静态成员。如果只需给静态成员分配内存即可将其初始化,那么用一条简单的初始化语句就足够了,反之,若是必须通过复杂的逻辑才能完成初始化,则应考虑创建静态构造函数。
静态初始化语句与实例字段的初始化语句一样,静态字段的初始化语句也会先于静态构造函数而执行,并且有可能比基类的静态构造函数执行得更早。如果静态字段的初始化工作比较复杂或是开销比较大,那么可以考虑运用Lazy机制,将初始化工作推迟到首次访问该字段的时候再去执行。

静态构造函数是特殊的函数,会在初次访问该类所定义的其他方法、变量或属性之前执行,可以用来初始化静态变量、实现单例(singleton)模式,或是执行其他一些必要的工作,以便使该类能够正常运作。
当程序码初次访问应用程序空间(application space,也就是AppDomain)里面的某个类型之前,CLR会自动调用该类的静态构造函数。这种构造函数每个类只能定义一个,而且不能带有参数。

由于静态构造函数是由CLR自动调用的,因此必须谨慎处理其中的异常。如果异常跑到了静态构造函数外面,那么CLR就会抛出TypeInitialization-Exception以终止该程序。调用方如果想要捕获这个异常,那么情况将会更加微妙,因为只要AppDomain还没有卸载,这个类型就一直无法创建,也就是说,CLR根本就不会再次执行其静态构造函数,这导致该类型无法正确地加以初始化,并导致该类及其派生类的对象也无法获得适当的定义。因此,不要令异常脱出静态构造函数的范围。

不要创建无谓的对象

虽然垃圾回收器能够有效地管理应用程序所使用的内存,但在堆上创建并销毁对象仍需耗费一定的时间,因此应尽量避免过多地创建对象,也不要创建那些根本不用去重新构建的对象。此外,在函数中以局部变量的形式频繁创建引用类型的对象也是不合适的,应该把这些变量提升为成员变量,或是考虑把最常用的那几个实例设置成相关类型中的静态对象。

绝对不要在构造函数里面调用虚函数

这里有个构造函数里面调用虚函数的demo,运行后打印出的结果是"VFunc in B",还是"VFunc in B1",还是"Msg from main"?答案是"VFunc in B1"。

public class B
{
  protected B()
  {
    VFunc();
  }

  protected virtual void VFunc()
  {
    Console.WriteLine("VFunc in B");
  }
}

public class B1 : B
{
    private readonly string msg = "VFunc in B1";

    public B1(string msg)
    {
      this.msg = msg;
    }

    protected override void VFunc()
    {
      Console.WriteLine(msg);
    }

    public static void Init()
    {
      _ = new B1("Msg from main");
    }
}

为什么会这样呢,这要从构建某个类型的首个实例时系统所执行的操作说起,步骤如下:

  1. 把存放静态变量的空间清零。
  2. 执行静态变量的初始化语句。
  3. 执行基类的静态构造函数。
  4. 执行本类的静态构造函数。
  5. 把存放实例变量的空间清零。
  6. 执行实例变量的初始化语句。
  7. 适当地执行基类的实例构造函数。
  8. 执行本类的实例构造函数。

所以会先初始化B1.msg,然后执行基类B的构造函数。基类的构造函数调用了一个定义在本类中但是为派生类所重写的虚函数VFunc,于是程序在运行的时候调用的就是派生类的版本,因为对象的运行期类型是B1,而不是B。在C#语言中,系统会认为这个对象是一个可以正常使用的对象,因为程序在进入构造函数的函数体之前,已经把该对象的所有成员变量全都初始化好了。尽管如此,但这并不意味着这些成员变量的值与开发者最终想要的结果相符,因为程序仅仅执行了成员变量的初始化语句,而尚未执行构造函数中与这些变量有关的逻辑。

在构建对象的过程中调用虚函数有可能令程序中的数据混乱,也会让基类的代码严重依赖于派生类的实现细节,而这些细节是无法控制的,这种做法很容易出问题。所以应该避免这样做。

实现标准的dispose模式

dispose模式用于对非托管资源进行释放,托管资源是指受GC管理的内存资源,而非托管资源与之相对,则不受GC的管理,当使用完非托管资源后,必须显式释放它们。 最常用的非托管资源类型是包装操作系统资源的对象,如文件、窗口、网络连接或数据库连接。 虽然垃圾回收器可以跟踪封装非托管资源的对象的生存期,但无法了解如何发布并清理这些非托管资源。
比如System.IO.File中的FileStream,它属于.NET的类被GC管理,但它的内部又依赖了操作系统提供的API,因此可以看作是一个Wrapper, 因此要实现dispose模式,在自身被GC销毁的时候,释放文件句柄。

标准的dispose(释放/处置)模式既会实现IDisposable接口,又会提供finalizer,以便在客户端忘记调用IDisposable.Dispose()的情况下也可以释放资源。

在类的继承体系中,位于根部的那个基类应该做到以下几点:

继承体系中的子类应该做到以下几点:

下面两个类UnManaged与MyUnManaged作为非托管资源的示例,假设UnManaged类中直接使用了非托管资源:

public class UnManaged : IDisposable
{
  private bool alreadyDisposed;

  public void Dispose()
  {
    Dispose(true);
    GC.SuppressFinalize(this);
  }

  protected virtual void Dispose(bool isDisposing)
  {
    if (alreadyDisposed)
      return;
    if (isDisposing)
    {
      // free managed resource here
    }

    // free unmanaged resource here
    alreadyDisposed = true;
  }

  public void ExampleMethod()
  {
    if (alreadyDisposed)
      throw new ObjectDisposedException(nameof(UnManaged), "Call methods on disposed object");

    // do something
  }

  ~UnManaged()
  {
    Dispose(false);
  }
}

public class MyUnManaged : UnManaged
{
  private bool alreadyDisposedInDerived;

  protected override void Dispose(bool isDisposing)
  {
    if (alreadyDisposedInDerived)
      return;
    if (isDisposing)
    {
      // free managed resource here
    }

    // free unmanaged resource here

    base.Dispose(isDisposing); // call base.Disposes

    alreadyDisposedInDerived = true;
  }
}

UnManaged直接使用了非托管资源,因此需要析构函数。虽然前面提到存在析构函数的对象不会被GC立即回收,但作为一种防范机制是必须的,如果使用者忘调用Dispose,finalizer仍然确保非托管资源可以得到释放。尽管程序性能或许会因此而有所下降,但只要客户代码能够平常调用Dispose方法,就不会有这个问题。Dispose方法中通过GC.SuppressFinalize(this)来通知GC不必再执行finalizer。

实现IDisposable.Dispose()方法时,要注意以下四点:

  1. 把非托管资源全都释放掉。
  2. 把托管资源全都释放掉(这也包括不再订阅早前关注的那些事件)。
  3. 设定相关的状态标志,用以表示该对象已经清理过了。如果对象已经清理过了之后还有人要访问其中的公有成员,那么你可以通过此标志得知这一状况,从而令这些操作抛出ObjectDisposedException。
  4. 阻止垃圾回收器重复清理该对象。这可以通过GC.SuppressFinalize(this)来完成。

但finalizer中执行的操作与Dispose有所区别,它只应释放非托管资源,因此为了代码复用,添加了Dispose的重载方法protected virtual void Dispose(bool isDisposing),它声明为protected virtual,可以被子类重写。被IDisposable.Dispose()方法调用时,isDisposing参数是true,那么应该同时清理托管资源与非托管资源,finalizer中调用时isDisposing为false,则只应清理非托管资源。

还有另外一些注意事项:

参考书籍

《Effective C#:改善C#代码的50个有效方法(原书第3版)》 比尔·瓦格纳

标签:初始化,Effective,C#,托管,Dispose,finalizer,对象,NET,构造函数
来源: https://blog.csdn.net/zhixin9001/article/details/113447892