[C#] 생성자(Constructor)와 종료자(Finalizer)

Running boy·2023년 8월 6일
0

컴퓨터 공학

목록 보기
3/36

생성자(Constructor)

줄여서 'ctor'라고도 한다.

사용자가 명시적으로 new를 통해 객체를 할당하는 시점에 호출된다.

부모 클래스가 존재하는 경우 부모 클래스의 생성자가 우선 호출된다.

class Animal
{
    public Animal()
    {
        Console.WriteLine("Animal Created!");
    }
}

class Dog : Animal
{
    public Dog()
    {
        Console.WriteLine("Dog Created!");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Dog dog = new Dog();
    }
}

// 출력:
// Animal Created!
// Dog Created!

생성자는 반드시 정의해야 될까?

C#의 클래스는 생성자를 정의하지 않아도 기본 생성자를 지원한다.

class Dog
{
    
}

class Program
{
    static void Main(string[] args)
    {
        Dog dog = new Dog(); // 컴파일 에러가 발생하지 않는다.
    }
}

하지만 생성자를 하나라도 정의했다면 기본 생성자를 지원하지 않는다.

class Dog
{
    public Dog(string s)
    {

    }
}

class Program
{
    static void Main(string[] args)
    {
        Dog dog = new Dog(); // 컴파일 에러가 발생한다.
    }
}

이 경우 기본 생성자를 추가로 정의해줘야 한다.


부모 클래스의 생성자에서 가상 함수를 호출한다면?

솔직히 이런 기형적인 행동을 왜 하는지 모르겠지만 면접에서 해당 질문을 받은 적이 있다.

그래도 궁금하니 코드를 작성해서 실행해봤다.

class Animal
{
    public virtual void AnimalTest()
    {
        Console.WriteLine("Animal Test");
    }

    public Animal()
    {
        AnimalTest();
        Console.WriteLine("Animal Created!");
    }
}

class Dog : Animal
{
    public override void AnimalTest()
    {
        Console.WriteLine("Dog Test");
    }

    public Dog()
    {
        AnimalTest();
        Console.WriteLine("Dog Created!");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Dog dog = new Dog();
    }
}

// 출력:
// Dog Test
// Animal Created!
// Dog Test
// Dog Created!

아직 자식 클래스가 생성되지 않은 시점에 가상 함수를 호출했음에도 자식 클래스에서 오버라이드된 함수가 정상적으로 호출되었다.

중요한 것은 컴파일 에러는 물론 런타임 에러도 발생하지 않는다는 것이다.

C++의 경우 런타임 에러가 발생한다고 한다.

비록 위 사례에서는 아무 문제 없이 실행되었다고 하나 이와 같은 형식의 코드는 좋지 않으므로 가급적이면 생성자 내에서 가상 함수의 호출은 피해야 한다.


정적 생성자(Static Constructor)

줄여서 'cctor'라고도 한다.

클래스의 정적 필드(Static Field)를 초기화하는 생성자이다.

클래스의 어떤 멤버든 최초로 접근하는 시점에 단 한 번만 실행된다.

기본 생성자와 구분되는 특징
1. 반드시 한개만 정의할 수 있다.
2. 접근 제한자를 허용하지 않는다.
3. 매개변수를 갖지 않는다.

class Animal
{
    public static int zero = 10;
    public static int ten = 10;

    static Animal()
    {
        zero = 0;
        Console.WriteLine("Static Animal Created!");
    }

    public Animal()
    {
        Console.WriteLine("Animal Created!");
    }
}

class Dog : Animal
{
    static Dog()
    {
        Console.WriteLine("Static Dog Created!");
    }

    public Dog()
    {
        Console.WriteLine("Dog Created!");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Dog dog = new Dog();
        Console.WriteLine($"zero: {Dog.zero}, ten: {Dog.ten}");
    }
}

// 출력:
// Static Dog Created!
// Static Animal Created!
// Animal Created!
// Dog Created!
// zero: 0, ten: 10

Dog 클래스의 인스턴스를 할당하면서 기본 생성자에 접근했기 때문에 Dog 클래스의 정적 생성자가 우선 호출되었다.

이후 Dog 클래스의 기본 생성자를 통해 Animal의 기본 생성자에 접근하므로 Animal 클래스의 정적 생성자가 호출된다.

만약

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine($"zero: {Dog.zero}, ten: {Dog.ten}");
        Dog dog = new Dog();
    }
}

와 같이 Main 메서드를 수정한다면?

Dog.zero를 통해 Dog 클래스에 접근한 것처럼 보이지만 zero는 Animal 클래스의 정적 필드이다. 따라서 Animal 클래스의 정적 생성자가 우선 호출된다.

이후 Dog 클래스의 인스턴스를 할당하면서 이전과 같이 진행되지만 Animal 클래스의 정적 생성자는 이미 호출됐으므로 다시 호출되지 않는다.


종료자(Finalizer) / 소멸자(Destructor)

줄여서 'dtor'라고도 한다.

원래 소멸자라고 썼으나 MS에서 공식적으로 종료자라고 명칭했다. 하지만 아직 일부 언어에서는 소멸자라고 쓰는 듯하다.

GC는 메모리를 정리하는 과정에서 객체의 종료자를 호출한다. 하지만 GC가 메모리를 정리하는 시점은 명확하지가 않다. 따라서 종료자가 호출되는 시점을 예측할 수 없다.

부모 클래스가 존재하는 경우 자식 클래스의 종료자가 우선 호출된 이후에 부모 클래스의 종료자가 호출된다.

class Animal
{
    ~Animal()
    {
        Console.WriteLine("Animal Destroyed!");
    }
}

class Dog : Animal
{
    ~Dog()
    {
        Console.WriteLine("Dog Destroyed!");
    }
}

class Program
{
    static void Test()
    {
        Dog dog = new Dog();
    }

    static void Main(string[] args)
    {
        Test();
        GC.Collect();
        Console.ReadLine();
    }
}

// 출력:
// Dog Destroyed!
// Animal Destroyed!

GC 입장에서 일반 참조 객체와는 달리 종료자가 정의된 클래스의 객체를 관리하려면 더 복잡한 과정을 거쳐야 하기 때문에 성능에 문제가 생길 수 있어 종료자를 정의하는 것은 신중히 판단해야 한다.

따라서 닷넷이 관리하지 않는 시스템 자원을 얻은 경우에만 종료자를 정의하는 것이 바람직하다. (ex. 네이티브 프로그램과의 협업)


참고 자료
시작하세요! C# 10 프로그래밍 - 정성태

profile
Runner's high를 목표로

0개의 댓글