종료자(C# 프로그래밍 가이드)

종료자(이전에는 소멸자라고 함)는 가비지 수집기에서 클래스 인스턴스를 수집할 때 필요한 최종 정리를 수행하는 데 사용됩니다. 대부분의 경우 System.Runtime.InteropServices.SafeHandle 또는 파생 클래스를 사용하여 관리되지 않는 핸들을 래핑하면 종료자를 작성하지 않아도 됩니다.

설명

  • 종료자는 구조체에서 정의할 수 없으며, 클래스에서만 사용됩니다.
  • 클래스에는 종료자가 하나만 있을 수 있습니다.
  • 종료자는 상속하거나 오버로드할 수 없습니다.
  • 종료자를 호출할 수 없습니다. 자동으로 호출됩니다.
  • 종료자는 한정자를 사용하거나 매개 변수를 갖지 않습니다.

예를 들어 다음은 Car 클래스에 대한 종료자의 선언입니다.

class Car
{
    ~Car()  // finalizer
    {
        // cleanup statements...
    }
}

다음 예제와 같이 종료자를 식 본문 정의로 구현할 수도 있습니다.

public class Destroyer
{
   public override string ToString() => GetType().Name;

   ~Destroyer() => Console.WriteLine($"The {ToString()} finalizer is executing.");
}

종료자는 개체의 기본 클래스에서 Finalize를 암시적으로 호출합니다. 따라서 종료자 호출은 다음 코드로 암시적으로 변환됩니다.

protected override void Finalize()
{
    try
    {
        // Cleanup statements...
    }
    finally
    {
        base.Finalize();
    }
}

이 설계에서는 Finalize 메서드가 상속 체인의 모든 인스턴스에 대해 최다 파생에서 최소 파생까지 재귀적으로 호출됩니다.

참고 항목

빈 종료자는 사용할 수 없습니다. 클래스에 종료자가 포함되어 있으면 Finalize 큐에서 항목이 생성됩니다. 이 큐는 가비지 수집기에 의해 처리됩니다. GC는 큐를 처리할 때 각 종료자를 호출합니다. 빈 종료자를 포함하는 불필요한 종료자, 기본 클래스 종료자만 호출하는 종료자 또는 조건부로 내보낸 메서드만 호출하는 종료자는 불필요한 성능 손실을 발생시킵니다.

종료자가 호출되는 시기는 프로그래머가 제어할 수 없고, 가비지 수집기에 의해 결정됩니다. 가비지 수집기는 애플리케이션에서 더 이상 사용되지 않는 개체를 확인합니다. 개체를 종료할 수 있는 것으로 판단되면 종료자(있는 경우)를 호출하고 개체를 저장하는 데 사용된 메모리를 회수합니다. Collect를 호출하여 강제로 가비지를 수집할 수 있지만 대체로 성능 이슈가 발생할 수 있으므로 이 호출을 피해야 합니다.

참고 항목

종료자가 애플리케이션 종료의 일부로 실행되는지 여부는 각 .NET의 구현에 따라 다릅니다. 애플리케이션이 종료될 때, 정리가 억제되지 않은 경우(예: 라이브러리 메서드 GC.SuppressFinalize 호출) .NET Framework는 아직 가비지가 수집되지 않은 개체에 대해 종료자를 호출하기 위해 모든 합리적인 노력을 기울입니다. .NET 5(.NET Core 포함) 이상 버전은 애플리케이션 종료 과정에서 종료자를 호출하지 않습니다. 자세한 내용은 GitHub 이슈 dotnet/csharpstandard #291을 참조하세요.

애플리케이션이 있을 때 안정적으로 정리를 수행해야 하는 경우 System.AppDomain.ProcessExit 이벤트에 대한 처리기를 등록합니다. 해당 처리기는 애플리케이션이 종료되기 전에 정리가 필요한 모든 개체에 대해 IDisposable.Dispose()(또는 IAsyncDisposable.DisposeAsync())이 호출되었는지 확인합니다. Finalize를 직접 호출할 수 없고 종료 전에 가비지 수집기에서 모든 종료자를 호출하도록 보장할 수 없으므로 Dispose 또는 DisposeAsync를 사용하여 리소스가 해제되도록 해야 합니다.

종료자를 사용하여 리소스 해제

일반적으로 C#에서는 개발자 측이 가비지 수집을 사용하는 런타임을 대상으로 하지 않는 언어만큼 많은 메모리 관리를 수행할 필요가 없습니다. 이는 .NET 가비지 수집기에서 개체에 대한 메모리 할당 및 해제를 암시적으로 관리하기 때문입니다. 그러나 애플리케이션에서 창, 파일 및 네트워크 연결 등의 관리되지 않는 리소스를 캡슐화하는 경우 종료자를 사용하여 해당 리소스를 해제해야 합니다. 개체를 종료할 수 있으면 가비지 수집기에서 개체의 Finalize 메서드를 실행합니다.

리소스의 명시적 해제

애플리케이션에서 비용이 많이 드는 외부 리소스를 사용하는 경우 가비지 수집기에서 개체를 해제하기 전에 리소스를 명시적으로 해제하는 방법을 제공하는 것이 좋습니다. 해당 리소스를 해제하려면 IDisposable 인터페이스에서 개체에 필요한 정리를 수행하는 Dispose 메서드를 구현합니다. 이렇게 하면 애플리케이션의 성능을 상당히 향상시킬 수 있습니다. 이렇게 리소스를 명시적으로 제어하는 경우에도 종료자는 Dispose 메서드 호출에 실패할 경우 리소스를 정리하는 안전한 방법이 됩니다.

리소스 정리에 대한 자세한 내용은 다음 문서를 참조하세요.

예시

다음 예제에서는 상속 체인을 구성하는 세 가지 클래스를 만듭니다. First 클래스는 기본 클래스이고, SecondFirst에서 파생되며, ThirdSecond에서 파생됩니다. 세 클래스 모두 종료자가 있습니다. Main에서 최다 파생 클래스의 인스턴스가 만들어집니다. 이 코드의 출력은 애플리케이션이 대상으로 하는 .NET의 구현에 따라 달라집니다.

  • .NET Framework: 애플리케이션이 종료될 때 세 클래스에 대한 종료자가 가장 많이 파생된 것에서 가장 적게 파생된 것의 순서로 자동으로 호출되는 것을 출력에서 보여 줍니다.
  • .NET 5(.NET Core 포함) 이상 버전: 이 .NET 구현은 애플리케이션이 종료될 때 종료자를 호출하지 않으므로 출력이 없습니다.
class First
{
    ~First()
    {
        System.Diagnostics.Trace.WriteLine("First's finalizer is called.");
    }
}

class Second : First
{
    ~Second()
    {
        System.Diagnostics.Trace.WriteLine("Second's finalizer is called.");
    }
}

class Third : Second
{
    ~Third()
    {
        System.Diagnostics.Trace.WriteLine("Third's finalizer is called.");
    }
}

/* 
Test with code like the following:
    Third t = new Third();
    t = null;

When objects are finalized, the output would be:
Third's finalizer is called.
Second's finalizer is called.
First's finalizer is called.
*/

C# 언어 사양

자세한 내용은 C# 언어 사양종료자 섹션을 참조하세요.

참고 항목