跳转到内容

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() 的方法。这本身 不会 帮助清理资源,因为它只是一个接口,因此开发人员必须正确使用它才能确保资源被释放。两个步骤是

  1. 在完成使用任何实现 IDisposable 的对象后,始终调用 Dispose()。(这可以使用 using 关键字更轻松地实现。)
  2. 使用 finalizer 方法调用 Dispose(),这样如果有人没有关闭您的资源,您的代码将为您完成。

Dispose 模式

[编辑 | 编辑源代码]

通常,您想要清理的内容取决于您的对象是否正在被终结。例如,您不希望在 finalizer 中清理托管资源,因为托管资源可能已经被垃圾收集器回收了。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);
        ...
    }
    
    // The finalizer will call the internal dispose method, telling it not to free managed resources.
    ~MyResource()
    {
        this.Dispose(false);
    }
    
    // The internal dispose method.
    private void Dispose(bool disposing)
    {
        if (disposing)
        {
            // Clean up managed resources
            _someManagedResource.Clear();
        }
        
        // Clean up unmanaged resources
        FreeSomeMemory(_someUnmanagedResource);
    }
    
    // The public dispose method will call the internal dispose method, telling it to free managed resources.
    public void Dispose()
    {
        this.Dispose(true);
        // Tell the garbage collector to not call the finalizer because we have already freed resources.
        GC.SuppressFinalize(this);
    }
}

如果您来自 Visual Basic Classic,您将看到类似这样的代码

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

请注意,oFSOoFile 都没有明确处置。在 Visual Basic Classic 中,这是没有必要的,因为这两个对象都是局部声明的。这意味着引用计数在函数结束时变为零,这会导致对这两个对象的 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
{
    // Code size       39 (0x27)
    .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
        IL_0013:  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
} // end of method Using::Read

请注意,Read 函数的主体已分为三个部分:初始化、try 和 finally。finally 块包含从未在原始 C# 源代码中显式指定的代码,即对 Streamreader 实例的 析构函数 的调用。

请参阅 理解 C# 中的 'using' 语句 由 TiNgZ aBrAhAm

请参阅以下部分以了解此技术的更多应用。

资源获取即初始化

[编辑 | 编辑源代码]

介绍中 using 语句的应用是称为 资源获取即初始化 (RAII) 的惯用法的一个示例。

RAII 是一种在 Visual Basic Classic 和 C++ 等具有确定性终结的语言中自然而然的技术,但在垃圾回收语言(如 C# 和 VB.NET)中编写的程序中通常需要额外的工作才能包含进来。using 语句使它变得同样容易。当然,您可以显式地编写 try..finally 代码,在某些情况下仍然需要这样做。有关 RAII 技术的深入讨论,请参见 HackCraft: The RAII Programming Idiom。维基百科也对此主题进行了简要说明:Resource Acquisition Is Initialization.

正在进行的工作:添加 C# 版本,展示带有和不带有 using 的不正确和正确方法。添加关于 RAII、记忆和缓存的说明(参见 OOP 维基教科书)。

华夏公益教科书