首页 / C# 入门教程 / using 语句与资源管理

C# 入门教程

using 语句与资源管理

本教程共 100 篇 · 第 76 篇 · 更新于 2026-07-31 · 约 11 分钟阅读

C#C# 入门教程资源管理IDisposableusing

76. using 语句与资源管理

本节目标:学完你理解为什么有些对象必须”显式释放”,并会用 using 声明让 C# 自动替你调用 Dispose。

前面讲 finally 时提到”必须执行的收尾逻辑”。对一类特殊的对象——文件、网络连接、数据库连接——光靠垃圾回收(GC)还不够,必须主动释放。C# 用 IDisposable 接口和 using 把这个动作变得几乎零负担。

先打个比方:借来的东西要还

把非托管资源想成图书馆的书。你借了书(打开文件、建立连接),看完了就得还(释放)。GC 像图书馆的”自动整理机器人”,它只定期收拾”没人借的书”,但不会主动替你把书还回原位——要还,得你自己去还。要是人人借了不还,书架迟早被占满,别人就借不到了。

using 就是 C# 替你安排的”自动还书机”:你借的时候登记一下,离开作用域时它自动帮你还,连中途被打断(抛异常)也照还不误。

为什么需要主动释放

很多资源分为两类:托管资源(由 CLR 管理的内存)和非托管资源(操作系统级的句柄,如文件句柄、套接字、数据库连接)。非托管资源 GC 管不到,你不主动还,它就一直占着,累积起来会耗尽系统资源。

Note

为什么不能指望 GC?因为 GC 只在”觉得需要”时才回收,时机完全不可预测。一个数据库连接拖到几分钟后才释放,期间连接池可能早已被占满,新请求直接失败。

这类对象都实现了 IDisposable 接口,里面只有一个方法:

public interface IDisposable
{
    void Dispose();   // 释放非托管资源
}
Note

规则很简单:用完实现了 IDisposable 的对象,就要调它的 Dispose()。常见的 Stream、SqlConnection、HttpClient 的底层都实现了它。

传统 using 语句块

最经典的写法是用 using (...) 把对象圈起来。离开这个花括号,Dispose 就会被自动调用——即使中途抛了异常也不例外。

namespace CSharpDemo;

using (var r = new MyResource())
{
    r.Use();
}
Console.WriteLine("离开 using 块后");

class MyResource : IDisposable
{
    public void Use() => Console.WriteLine("使用资源");
    public void Dispose() => Console.WriteLine("已释放资源");
}

输出顺序是:使用资源 → 已释放资源 → 离开 using 块后。这就保证了”用完即清”。

C# 8 的 using 声明(推荐)

C# 8 引入了一种更简洁的写法:把 using 直接放在变量声明前,不用花括号。它的释放时机是”离开当前作用域(方法/代码块)时”。

namespace CSharpDemo;

using var r = new MyResource();
r.Use();
Console.WriteLine("方法结束前……");
// 到这里 r 被自动释放

class MyResource : IDisposable
{
    public void Use() => Console.WriteLine("使用资源");
    public void Dispose() => Console.WriteLine("已释放资源");
}
Tip

using var 没有额外的缩进块,代码更扁平。这是 .NET 10 模板里最推荐的写法。注意:它会在”方法返回前”才释放,若想更早释放,仍用传统 using 块。

自己实现一个 IDisposable

看懂原理最好的办法是手写一个。下面这个类在 Dispose 里打一行日志,模拟释放动作。

namespace CSharpDemo;

using var file = new TempFile();
file.Write("hello");

class TempFile : IDisposable
{
    private bool _disposed;
    public void Write(string text) => Console.WriteLine($"写入:{text}");

    public void Dispose()
    {
        if (_disposed) return;     // 防止重复释放
        _disposed = true;
        Console.WriteLine("删除临时文件(释放非托管资源)");
    }
}
Note

_disposed 标记防止重复释放,是 IDisposable 的标准习惯。多次调 Dispose 不应报错。

多个资源的释放顺序

当多个 using var 排在一起时,释放顺序与声明顺序相反:后声明的先释放。

namespace CSharpDemo;

using var a = new Resource("A");
using var b = new Resource("B");
// 结束时先释放 B,再释放 A

class Resource : IDisposable
{
    private string _name;
    public Resource(string name) => _name = name;
    public void Dispose() => Console.WriteLine($"{_name} 已释放");
}

这个”后进先出”的顺序很合理:就像叠盘子,最后放上去的先拿走。当资源之间有依赖(B 依赖 A)时,这种顺序正好安全。

using 声明 vs using 语句块:何时用哪个

两种写法能力相同,释放时机不同:

写法释放时机缩进适用
using (var r = ...)离开花括号即释放多一层想”尽早释放”
using var r = ...离开当前方法/块时释放扁平一直用到方法结束

要”尽早释放”(比如大文件读完马上关,把句柄让给别人),用传统块;要”方法结束前一直可用”,用声明式。

namespace CSharpDemo;

// 传统块:file 在块结束即释放,块外不可用
using (var file = new TempFile("A"))
{
    file.Write("在块内使用");
}
// 此处 file 已释放,无法再访问

// 声明式:整个方法结束前都可用
using var file2 = new TempFile("B");
file2.Write("方法结束前一直可用");

class TempFile : IDisposable
{
    private string _name;
    public TempFile(string name) => _name = name;
    public void Write(string text) => Console.WriteLine($"[{_name}] {text}");
    public void Dispose() => Console.WriteLine($"[{_name}] 已释放");
}

哪些类型该用 using

经验法则:类型名字里带 Stream、Connection、Writer、Reader,或文档注明”实现 IDisposable”的,用完就用 using 圈起来。常见的如 FileStream、StreamReader、SqlConnection、HttpClient 底层。

namespace CSharpDemo;

// 读文本文件,离开 using 自动关闭
using var reader = new StreamReader("note.txt");
string? line = reader.ReadLine();
Console.WriteLine(line);

class StreamReader : IDisposable
{
    private string _path;
    public StreamReader(string path) => _path = path;
    public string? ReadLine() => "(示意内容)";
    public void Dispose() => Console.WriteLine($"已关闭 {_path}");
}
Note

上面的 StreamReader 是演示用的简化类,只为说明”用完即释放”的思路。真实代码直接用 System.IO.StreamReader 即可。

常见坑与注意事项

  1. 不要把 using 变量传出作用域:若把 using var 的局部变量 return 出去或存进字段,调用方拿到的其实是个”已经(或即将)被释放”的对象,用它就会出错。需要传出时,让调用方自己负责 using。
Warning

一旦对象被 using 接管,离开作用域必被 Dispose。千万别返回它、也别把它缓存到生命周期更长的容器里,否则”借来的书被自动还了,你还拿它当没还的用”。

  1. 误以为所有对象都要 using:只有实现 IDisposable/IAsyncDisposable 的才需要。普通 List<T>string 交给 GC 即可,强行 using 反而画蛇添足。
  2. 重复释放:用了 using 就别再手动调 Close()/Dispose(),using 结束会自动调一次。重复释放虽大多被忽略,但代码多余易误读。

忘记释放会怎样

不释放非托管资源,它们会一直占着系统句柄。短时间看不出,长时间运行或高并发时,句柄耗尽会导致程序变慢甚至崩溃。using 把这件事从”靠人记得”变成”编译器保证”。

Tip

把 using 当成”资源的保险栓”。凡是打开一个资源,顺手就 using 包上,比事后排查”句柄泄漏”省力一百倍。

using 背后等于什么

using 不是魔法,编译器会把它展开成 try/finally

{
    var r = new MyResource();
    try
    {
        r.Use();
    }
    finally
    {
        r.Dispose();   // 无论正常结束还是抛异常,都会执行
    }
}

所以 using 和”手写 try/finally 调 Dispose”完全等价,只是更短更不易漏写 finally。这也是为什么哪怕中途抛异常,资源也一定会被释放

Dispose 与终结器(简述)

IDisposable 让你主动释放;GC 还有个”终结器(finalizer)“作为最后兜底,在回收对象时跑。但终结器时机不确定、有性能损耗,所以正确做法是实现 IDisposable 主动释放,别依赖终结器。

异步也能释放:IAsyncDisposable

若资源释放本身是异步的(如异步关闭网络连接),可实现 IAsyncDisposable,并用 await using 声明,离开作用域时异步释放。

namespace CSharpDemo;

await using var resource = new AsyncResource();
// 离开作用域时调用 DisposeAsync

class AsyncResource : IAsyncDisposable
{
    public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
Tip

用了 using 就别再手动调 Close/Dispose,using 结束会自动调一次;重复释放虽大多被忽略,但代码多余易误读。

实战里的最佳实践小结

  • 打开资源,第一反应就是 using,别等”以后记得关”。
  • 需要提前让出资源,用传统 using (...) 块,把作用域缩到最小。
  • 不要在 using 作用域外泄露那个对象。
  • 自己写 IDisposable 时,用 _disposed 标记防重复释放。
  • 异步释放用 await using + IAsyncDisposable

小结

实现了 IDisposable 的对象用完了要 Dispose;using 语句块或 C# 8 的 using var 声明都能在离开作用域时自动调用它,异常也拦不住。优先用 using var,代码更干净。下一章进入”特性(Attribute)“——给代码贴标签的元数据包。