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(声明) | |
|---|---|---|
| 样子 | |
|
| 作用域 | 在块的大括号里 | 到当前块(方法、循环等)结束 |
| 简洁度 | 稍微啰嗦点 | 简洁,缩进少 |
| 从哪个版本开始 | 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。
GO TO FULL VERSION