using
语句在 C# 中有很多种用法,比如引入命名空间,为类型起别名,或者释放资源等。这篇文章我们主要讨论 using
语句在释放资源时的陷阱。
using 以前的使用方式
¶
在很久很久以前,我们如果想要读取一个外部文本文件的内容,可能会这样写(不考虑更简洁易用的 File.ReadAllText()
等方法):
1
2
3
4
5
6
7
8
| using (var stream = new FileStream(filename, FileMode.Open))
{
using (var reader = new StreamReader(stream))
{
var content = reader.ReadToEnd();
Console.WriteLine(content);
}
}
|
其实上面的代码,是可以减少一层缩进的,并且这也是各种 IDE 推荐的写法,形如:
1
2
3
4
5
6
| using var stream = new FileStream(filename, FileMode.Open)
using var reader = new StreamReader(stream)
{
var content = reader.ReadToEnd();
Console.WriteLine(content);
}
|
这个其实很有意思,因为一般我们都认为,即便外层的语句省略了花括号,内层的语句依旧会保持缩进,就比如多层 if
语句:
1
2
3
4
5
| if (condition1)
if (condition2)
{
// do something
}
|
但是上面展示的 using
的省略外层花括号的新语法,内层的语句并不会额外添加缩进,而是会与外层保持同一层级。不信的话,可以使用任意一个格式化工具,比如 Visual Studio 的 Ctrl+K, Ctrl+D
,格式化一下上面的代码,看看会是什么样子。
using 的新语法
¶
C# 8.0 为我们带来了一个新的 using
语句的用法,可以减少一层缩进,让代码看起来更简洁。同样是上面的代码,现在可以写成:
1
2
3
4
5
6
| string filename = "test.txt";
using var stream = new FileStream(filename, FileMode.Open);
using var reader = new StreamReader(stream);
var content = reader.ReadToEnd();
Console.WriteLine(content);
|
上面的代码实际上会被编译为这样的 low-level C# 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| string path = "test.txt";
FileStream fileStream = new FileStream(path, FileMode.Open);
try
{
StreamReader streamReader = new StreamReader(fileStream);
try
{
string value = streamReader.ReadToEnd();
Console.WriteLine(value);
}
finally
{
if (streamReader != null)
{
((IDisposable)streamReader).Dispose();
}
}
}
finally
{
if (fileStream != null)
{
((IDisposable)fileStream).Dispose();
}
}
|
不难看出,using
语句用于资源释放时,其实是通过 try-finally
语句来实现的。当存在多层的 using
语句时,每一层都会对应一个 try-finally
语句,也就变成了上面的样子。
新的语法会将 using
语句下面的内容(准确地说,是当前作用域中剩下的代码)包装在 try-finally
语句中,从而保证代码在离开作用域前,会释放资源。
Info
仔细观察还可以发现,Dispose
的顺序是从内到外的,或者说先被 using
的对象会后被释放。
新语法的陷阱
¶
学了这个新语法之后,相信很多人都打算全面替代掉旧方法,毕竟少写了花括号,而且减少了缩进,效果还一模一样。但实际上,这种新语法并不是适用于所有情况的。也就是说,效果未必一模一样。比如之前我就踩了一个坑。
当时的情况是,我在使用 System.IO.Compression
命名空间下的 GZipStream
来压缩一个文本,并输出压缩后的内容。我使用的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
| using System.IO.Compression;
using System.Text;
string input = "text to be compressed.";
using var outputStream = new MemoryStream();
using var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(input));
using var compressor = new GZipStream(outputStream, CompressionLevel.Optimal);
inputStream.CopyTo(compressor);
var compressed = outputStream.ToArray();
Console.WriteLine(compressed.Length);
|
运行后,输出了压缩后的内容长度为 10
。
但是当我修改了 input
的字符串内容后,发现输出的长度依旧是 10
,这显然是不可能的。我检查了一下代码,发现问题出在了 using
语句上。只要把上面的代码修改成这样,就能得到正确的结果:
1
2
3
4
5
6
7
8
| using var outputStream = new MemoryStream();
using (var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(input)))
using (var compressor = new GZipStream(outputStream, CompressionLevel.Optimal))
{
inputStream.CopyTo(compressor);
}
var compressed = outputStream.ToArray();
Console.WriteLine(compressed.Length);
|
造成这一现象的原因是,如果想要得到正确的压缩后的内容,需要保证 GZipStream
已经被释放。但是如果我们不加声明 GZipStream
这一行的花括号,会导致它直到离开作用域时才被释放,而不是在 inputStream.CopyTo(compressor)
之后立即释放。
所以,大家在使用新的 using
语句时,一定要根据实际情况来判断是否适用,不要无脑替换掉以前的旧写法。