CodeGym /课程 /C# SELF /关闭流和释放资源 ( using)

关闭流和释放资源 ( using)

C# SELF
第 36 级 , 课程 0
可用

1. 在C#里怎么正确释放资源?

想象一下操作系统就像个很严的图书管理员。你借了本书(打开了文件/流),看着看着,然后……忘了还!管理员就不乐意了:“怎么书还在你这儿?!”流也是一样。打开的流会占用资源:文件描述符、一块内存,还可能把文件锁住让别的程序用不了。

如果你不关流,后果可能从“有点不对劲”到“全崩了,谁都写不了这个文件”。如果程序里有一堆没关的流——系统资源就会“泄漏”,最后直接挂掉。

危险在哪?

  • 文件没关,命令没写到磁盘上(比如写入时——数据可能还在缓冲区里)。
  • 文件被别的进程锁住——同事和其他程序都要骂人了。
  • 描述符限制:Windows/Linux里进程能开的文件/流数量有限。

IDisposable接口

所有操作非托管资源(流、文件、数据库、socket)的类都必须实现IDisposable接口。


public interface IDisposable
{
    void Dispose();
}

Dispose()方法里一般会释放所有可怜的资源:文件终于关了,连接断了,内存释放了。

流就是那种手里攥着各种重要资源的对象:文件、连接、内存。如果你不关它,文件可能一直被锁着(你的Word会说“文件被其他应用打开!”),系统也会没内存。所以用完流一定要释放。

在.NET里,流都实现了IDisposable接口。意思就是:你得关它们,调用Dispose()(或者直接用using块包起来)。

2. 关闭流的几种方式:从危险到靠谱

方式1. “手动”关闭:别这么干!

这是老掉牙的写法。虽然我以前举过例子,但现在没人这么写了:P


var stream = new FileStream("file.txt", FileMode.Open);
// 操作流
stream.Close(); // 或者 stream.Dispose()

问题:如果打开和关闭之间出错/抛异常,文件就一直开着被锁住。就像你闻到披萨味儿,拿着书直接冲出图书馆……

方式2. 用try...finally


FileStream stream = null;
try
{
    stream = new FileStream("file.txt", FileMode.Open);
    // 操作流
}
finally
{
    if (stream != null)
        stream.Dispose();
}

这种写法靠谱:finally无论出不出错都会执行。但说实话,写起来真麻烦。

方式3. 又美又安全:using操作符

经典语法(using ( ... ) { ... }


using (var stream = new FileStream("file.txt", FileMode.Open))
{
    // 操作流
}
// 这里stream.Dispose()会自动调用!

关键点——using块里的代码都能用流,块结束时文件一定会关掉(哪怕抛异常)。

方式4. 现代语法

现代语法(using var


using var stream = new FileStream("file.txt", FileMode.Open);
// 操作流
// ... Dispose会在变量离开作用域时自动调用

太棒了!不用多写大括号和缩进。

底层是怎么实现的?

using操作符其实就是编译器帮你把代码变成try...finally。开个玩笑:“using帮你写干净代码——说不定以后还会喝咖啡刷Stack Overflow呢?”

经典和现代using的区别

经典using块 using var(声明)
样子
using (var x = ...) { ... }
using var x = ...; ...
作用域 在块的大括号里 到当前块(方法、循环等)结束
简洁度 稍微啰嗦点 简洁,缩进少
从哪个版本开始 C# 1.0 C# 8.0及以上

3. 什么是using声明?

5年前,大家见到了操作IDisposable对象的新写法,更简洁。

using声明——就是你不用写块,直接用using声明变量,它会在当前块(比如方法或循环)结束时自动释放,而不是在额外大括号结束时释放。


using var stream = new FileStream("file.txt", FileMode.Open);
// 操作流
Console.WriteLine(stream.Length);

// 这里文件还开着!

// ... 方法结尾
// stream.Dispose()会在这自动调用

和经典using的主要区别:

  • 不用大括号,不会多出一层代码块。
  • 变量能用到整个块结束(通常是方法,有时是循环、类,如果在类级别声明)。
  • 资源释放只会在块执行完时发生。

为啥这很酷?

  • 嵌套少了——代码短多了,也更好读。
  • 多个资源更好管理——可以连着声明好几个using变量,等方法结束一起释放。
  • 更不容易出错——不会漏掉Dispose()的调用范围。

4. 对比:经典 vs. 现代using

来看看代码对比。

经典写法


using (var reader = new StreamReader("input.txt"))
{
    using (var writer = new StreamWriter("output.txt"))
    {
        string line;
        while ((line = reader.ReadLine()) != null)
        {
            writer.WriteLine(line.ToUpper());
        }
    }
} // 这里两个文件都会被关掉

现代写法(C# 8+)


using var reader = new StreamReader("input.txt");
using var writer = new StreamWriter("output.txt");

string line;
while ((line = reader.ReadLine()) != null)
{
    writer.WriteLine(line.ToUpper());
}
// 两个文件都会在方法结束时关掉

是不是简单多了? 尤其嵌套多的时候——现代写法真的省事。

5. Dispose()啥时候会被调用?

很多同学会犯个错:以为Dispose会在用完那行代码后立刻调用——其实不是!

看这个例子:


void MyMethod()
{
    using var fileStream = new FileStream("data.bin", FileMode.Open);
    // ... 很多代码,可能还有循环和嵌套调用
    // fileStream还开着!

    // 这里还能用fileStream
}
// 这里,方法的},fileStream.Dispose()才会被调用

重点:如果你在循环里声明using变量,Dispose会在每次循环后调用。


foreach (var path in filePaths)
{
    using var reader = new StreamReader(path);
    // 用reader干活
} // 每次循环后reader.Dispose()会被调用(文件关掉)

6. 迁移老代码时的坑

有时候你迁移老代码或者复制了经典using的例子,但变量需要比大括号更长的“寿命”。那经典写法就不合适了,using声明就很完美。

但也有细节。比如循环里有两个资源,但其中一个要“活”得比另一个久——那就按需要的顺序声明:


using var resource1 = ...;
for (int i = 0; i < 10; i++)
{
    using var resource2 = ...;
    // resource2只活一轮
    // resource1活整个函数
}

7. 实践

继续完善我们的学习小项目——一个咖啡点单模拟器,你前几天已经改过。现在让它能把点单历史保存到文本文件,启动时还能读出来。

第1步:把订单写入文件


using var writer = new StreamWriter("orders.txt", append: true);
writer.WriteLine("咖啡: 拿铁; 牛奶: 燕麦; 尺寸: 大");

// StreamWriter构造函数的第二个参数append: true表示我们要追加写入,而不是覆盖文件。

第2步:读取订单历史


using var reader = new StreamReader("orders.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
    Console.WriteLine($"订单: {line}");
}

等程序执行完这个方法,文件会自动关掉。

8. using声明的最佳实践

1. 所有实现了IDisposable的对象都要用using
.NET里大多数文件、流、资源相关的类都实现了这个接口。这就是信号:用using来释放我!

2. 注意作用域:别在变量“碍事”的地方声明using-var
如果变量只用几行,就只在需要的地方声明,别提前。

3. 别忘了释放顺序
如果你连着声明多个using变量——Dispose会按相反顺序调用:


using var first = new Resource("第一个");
using var second = new Resource("第二个");
// ... 干活
// 先Dispose second,再Dispose first

有时候很重要,比如写入流要比文件先释放。

4. 不要在方法外用using声明
using声明不能在类级别(比如字段)用。只能在方法、构造函数等里用。

5. 结合异常处理
即使用了using,有些异常还是得自己处理——如果要控制读写错误,建议加try-catch

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION