c#编程
介绍
所有的计算机程序都会消耗内存,无论是内存中的变量、打开的文件,还是与数据库的连接。问题是,运行时环境如何回收不再使用的内存呢?这个问题有三个答案:
- 如果你使用的是托管资源,它会被垃圾回收器自动释放。
- 如果你使用的是非托管资源,你必须使用
IDisposable
接口来帮助清理。 - 如果你直接调用垃圾回收器,可以使用
System.GC.Collect()
方法,强制立即清理资源。
在讨论托管资源和非托管资源之前,了解垃圾回收器的实际工作原理会很有趣。
垃圾回收器
垃圾回收器是一个在程序中后台运行的进程。它始终存在于所有 .NET 应用程序中。它的工作是查找那些不再被程序使用的对象(即引用类型)。如果对象被赋值为 null
,或对象超出了作用域,垃圾回收器会将该对象标记为待清理,可能并不立即释放其资源!
为什么这么做?因为垃圾回收器难以跟上程序运行的速度,特别是在每次释放内存时。因此,它只有在资源变得紧张时才会运行。垃圾回收器有三个“代”:
- 第0代:最近创建的对象。
- 第1代:生命周期中的中期对象。
- 第2代:长期存在的对象。
所有引用类型的对象都将存在于这三代之一。它们首先被分配到第0代,然后根据其生命周期被移动到第1代和第2代。垃圾回收器的工作方式是只移除需要的对象,因此它通常仅扫描第0代进行快速修复。这是因为大多数局部变量都会被放置在这一代中。
如需更深入的了解,可以访问MSDN文章以获取更详细的解释。
现在你已经了解了垃圾回收器,我们来讨论它所管理的资源。
托管资源
托管资源是完全在 .NET 框架内运行的对象。所有的内存都会被自动回收,所有资源都会被关闭,通常情况下,当应用程序关闭或垃圾回收器运行时,你可以保证所有内存会被释放。
你无需手动关闭连接或做其他操作,它是一个自动清理的对象。
非托管资源
有些情况下,.NET 框架无法释放资源。这可能是因为对象引用了 .NET 框架之外的资源,例如操作系统,或引用了另一个非托管组件,或者资源访问了使用 COM、COM+ 或 DCOM 的组件。
无论什么原因,如果你使用的对象实现了 IDisposable
接口,那么你也需要实现 IDisposable
接口。
public interface IDisposable
{
void Dispose();
}
此接口暴露了一个名为 Dispose()
的方法。单独使用它并不能帮助清理资源,因为它只是一个接口,因此开发者必须正确使用它以确保资源被释放。需要遵循两个步骤:
- 在使用完任何实现了
IDisposable
的对象后,始终调用Dispose()
方法。(这可以通过using
关键字使其更加简便) - 使用终结器方法调用
Dispose()
,这样即使有人没有关闭你的资源,你的代码也会为他们处理。
Dispose 模式
通常情况下,根据对象是否正在被终结,清理的内容会有所不同。例如,你不想在终结器中清理托管资源,因为这些托管资源可能已经被垃圾回收器回收。Dispose
模式可以帮助你在这种情况下正确实施资源管理:
public class MyResource : IDisposable
{
private IntPtr _someUnmanagedResource;
private List<long> _someManagedResource = new List<long>();
public MyResource()
{
_someUnmanagedResource = AllocateSomeMemory();
for (long i = 0; i < 10000000; i++)
_someManagedResource.Add(i);
}
// 终结器会调用内部的 Dispose 方法,并指示它不释放托管资源。
~MyResource()
{
this.Dispose(false);
}
// 内部的 Dispose 方法。
private void Dispose(bool disposing)
{
if (disposing)
{
// 清理托管资源
_someManagedResource.Clear();
}
// 清理非托管资源
FreeSomeMemory(_someUnmanagedResource);
}
// 公共的 Dispose 方法会调用内部的 Dispose 方法,指示它释放托管资源。
public void Dispose()
{
this.Dispose(true);
// 告诉垃圾回收器不要调用终结器,因为我们已经释放了资源。
GC.SuppressFinalize(this);
}
}
应用
如果你是从经典的 Visual Basic 迁移到 C#,你可能会见到类似这样的代码:
Public Function Read(ByRef FileName) As String
Dim oFSO As FileSystemObject
Set oFSO = New FileSystemObject
Dim oFile As TextStream
Set oFile = oFSO.OpenTextFile(FileName, ForReading, False)
Read = oFile.ReadLine
End Function
注意,在 Visual Basic Classic 中,oFSO
和 oFile
并没有显式地调用 Dispose
。这是因为在经典的 Visual Basic 中,这些对象是局部声明的。这意味着当函数结束时,引用计数会降为零,从而触发这两个对象的 Terminate
事件处理程序,这些事件处理程序关闭文件并释放相关资源。
而在 C# 中并不会发生这种情况,因为这些对象没有引用计数。终结器直到垃圾回收器决定销毁这些对象时才会被调用。如果程序使用的内存很少,这可能需要很长时间。
这会造成一个问题,因为文件会保持打开状态,这可能阻止其他进程访问它。
在许多语言中,解决方法是显式地关闭文件并释放对象资源,许多 C# 程序员就是这样做的。然而,更好的方法是使用 using
语句:
public read(string fileName)
{
using (TextReader textReader = new StreamReader(filename))
{
return textReader.ReadLine();
}
}
在幕后,编译器会将 using
语句转换成 try...finally
,并生成以下中间语言(IL)代码:
.method public hidebysig static string Read(string FileName) cil managed
{
.maxstack 5
.locals init (class [mscorlib]System.IO.TextReader V_0, string V_1)
IL_0000: ldarg.0
IL_0001: newobj instance void [mscorlib]System.IO.StreamReader::.ctor(string)
IL_0006: stloc.0
.try
{
IL_0007: ldloc.0
IL_0008: callvirt instance string [mscorlib]System.IO.TextReader::ReadLine()
IL_000d: stloc.1
IL_000e: leave IL_0025
} // end .try
finally
{
IL_0018: ldloc.0
IL_0019: brfalse IL_0024
IL_001e: ldloc.0
IL_001f: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0024: endfinally
} // end handler
IL_0025: ldloc.1
IL_0026: ret
}
注意,Read
函数的主体已经分为三个部分:初始化、try
和 finally
。finally
块包括了原始 C# 代码中未显式指定的代码,特别是调用 StreamReader
实例的析构函数。
资源获取即初始化 (RAII)
在引言中使用 using
语句的示例就是所谓的 资源获取即初始化(RAII) 的一种惯用法。
RAII 是像经典的 Visual Basic 和 C++ 这样的语言中的一种自然技术,这些语言具有确定性的终结,但通常需要额外的工作才能在像 C# 和 VB.NET 这样的垃圾回收语言中应用。using
语句使得这一点变得简单。当然,你也可以显式地编写 try..finally
代码,在某些情况下这仍然是必要的。有关 RAII 技术的详细讨论,请参考《HackCraft: The RAII Programming Idiom》。Wikipedia 上也有简短的说明:Resource Acquisition Is Initialization。