Chapter 2: .NET Resource Management (Items 11-17)

完整覆盖第3版Item 11-17:GC与resource ownership、成员/静态初始化、去重构造逻辑、allocation、构造期virtual dispatch和标准Dispose模式。

学习目标

  • 能区分GC managed-memory reclamation与file/socket/native handle的deterministic release,并写出每个resource的owner
  • 能设计member、constructor、static与lazy initialization的单一truth path,验证所有public construction entries保持invariant
  • 能分析constructor virtual dispatch、unnecessary allocation和错误Dispose/finalizer模式,并以phase与ownership修正

机制总览

对象生命周期与外部资源责任实验

  1. 1

    初始化

    成员初始值与构造链应共享一个事实来源,静态状态按类型语义初始化。

  2. 2

    使用

    短命对象控制分配,长寿资源的 owner 不被回调或缓存意外延长。

  3. 3

    释放

    IDisposable 负责确定释放外部资源,using/finally 覆盖异常路径。

先按顺序建立机制,再进入实验切换阶段并检查失效证据。

章级决策实验

对象生命周期与外部资源责任实验

选择生命周期阶段,区分 GC 管理的内存与必须确定释放的资源。

选择推理阶段

当前阶段 · 初始化

成员初始值与构造链应共享一个事实来源,静态状态按类型语义初始化。

可核验证据

构造路径测试、nullable 分析与静态初始化顺序。

资源管理的核心不是多写清理代码,而是让每个资源只有一个可证明的 owner 和一条覆盖所有退出路径的释放协议。

失效—证据矩阵

对象生命周期与外部资源责任实验

初始化

典型失效

重复赋值让不同构造入口形成不同不变量。

核验证据

构造路径测试、nullable 分析与静态初始化顺序。

使用

典型失效

捕获、缓存和装箱制造隐性保留与 GC 压力。

核验证据

allocation profile、heap path 与 owner graph。

释放

典型失效

依赖 finalizer 时文件、句柄或连接长期占用。

核验证据

故障注入后的 Dispose 次数与句柄计数。

每个判断都必须能落到观测、测试或产物,不能只凭代码表面推测。

为什么Resource问题首先是Ownership问题

GC回答“某块managed memory是否仍reachable”,不回答“file handle何时关闭”“谁归还pool lease”或“borrowed service能否被当前对象释放”。因此本章把allocation、initialization和cleanup看作同一条lifecycle:创建前定义valid state,使用时保持owner,结束时由owner确定性释放。

先预测:GC最终会回收对象,是否意味着file handle可以不Dispose;class保存一个IDisposable field,是否必然拥有它;有Dispose是否必然需要finalizer?三个答案都是否。

Managed Memory与Deterministic Resource(Item 11)

Item 11: Understand .NET Resource Management

GC通过reachability回收managed memory并压缩部分heap;非托管handle、socket lease、subscription和pooled buffer不由GC按业务时机释放。不要以GC.Collect()作为正常资源管理,也不要仅因对象大就随意置null;先缩短真实reference lifetime并用memory/profile evidence验证。

Owned IDisposable field应由container级联释放;borrowed dependency由外部scope拥有,当前类型不得擅自Dispose。这个区分比“字段是否实现IDisposable”更重要。

分步1 / 3

切换managed、owned、borrowed、native与pooled资源

Initialization只有一个Source of Truth(Items 12-14)

Item 12: Prefer Member Initializers to Assignment Statements

与constructor参数无关、每条construction path相同的safe default放在field/property initializer,减少overload漏赋值。Required domain state仍由constructor/required member验证;不要为了追求initializer把I/O或复杂failure藏到field expression。

Item 13: Use Proper Initialization for Static Class Members

简单static value用inline initializer;必须顺序化、校验或捕获runtime condition时用static constructor,但它最多运行一次,失败会让type在当前runtime context持续不可用。External I/O与可重试startup更适合host composition或Lazy<T>配显式failure policy。

Item 14: Minimize Duplicate Initialization Logic

让public constructors链到一个canonical constructor,或让factory调用同一private creation path。重复assignment会在新增field时产生“某个overload忘了初始化”的silent invalid state。现代primary constructor、required/init member可减少样板,但仍需一处定义invariant。

public sealed class Report
{
    private readonly List<Line> _lines = [];
    public Report(string name) : this(name, SystemClock.Instance) { }
 
    internal Report(string name, IClock clock)
    {
        Name = string.IsNullOrWhiteSpace(name)
            ? throw new ArgumentException("A name is required.", nameof(name))
            : name;
        CreatedAt = clock.UtcNow;
    }
}
分步1 / 3

切换member、constructor、static、lazy与duplicate路径

Allocation、Construction与Cleanup(Items 15-17)

Item 15: Avoid Creating Unnecessary Objects

先以allocation profile确认hot path,再减少temporary string、closure、boxing或重复materialization。Cache只适合immutable或有清晰eviction/tenant boundary的结果,pool只适合创建昂贵且ownership可严格归还的resource;无界cache与错误pool比原allocation更危险。

byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
try { ReadFrame(buffer); }
finally { ArrayPool<byte>.Shared.Return(buffer, clearArray: true); }

Item 16: Never Call Virtual Functions in Constructors

Base constructor运行时derived fields尚未完成初始化,virtual call却可dispatch到derived override,暴露partial object。Constructor只建立自身invariant;需要polymorphic activation时,使用factory在完整构造后调用显式Start,或把variation作为constructor collaborator注入。

Item 17: Implement the Standard Dispose Pattern

Sealed type拥有managed disposable resource时通常实现简单、幂等的Dispose并级联owner fields。可继承type使用public nonvirtual Dispose()加protected virtual Dispose(bool);直接native handle优先交给SafeHandle,避免自己编写脆弱finalizer。Async cleanup用IAsyncDisposable,但不要让Dispose承担必须成功的business commit。

public sealed class ExportSession(Stream output) : IDisposable
{
    private Stream? _output = output;
 
    public void Dispose()
    {
        Interlocked.Exchange(ref _output, null)?.Dispose();
    }
}
分步1 / 3

切换constructor、factory、allocation与dispose案例

本章回顾:从创建到释放只有一条可证明的Lifecycle

  1. GC回收managed memory;resource owner负责scarce/native/leased resource的及时释放。
  2. Member initializer承载共同safe default,canonical constructor承载参数validation,static I/O移到hosted composition。
  3. 所有public construction entries必须建立同一invariant并具有明确失败语义。
  4. Allocation optimization需要profile;cache/pool同时引入lifetime与eviction contract。
  5. Constructor不得virtual dispatch;对象完整后再activate。
  6. Dispose按ownership级联并幂等,native fallback优先SafeHandle。

练习

问题 1:一个类接收DI提供的HttpClient并自行创建FileStream,应如何释放?

问题 2:五个constructor重复设置defaults,static constructor还访问远程配置,如何重构?

问题 3:可继承类型直接持有native handle并在constructor调用virtual Start,有哪些问题?

术语表

名词解释

本章出现的专业名词,用大白话再讲一遍。

resource owner
canonical construction path
partial object
idempotent disposal
SafeHandle boundary

讨论

评论区加载中…