Ssssong += Dev

[C#] [Effective C#] 생성자 내에서는 절대로 가상함수를 호출하지 말라 본문

개발/공부

[C#] [Effective C#] 생성자 내에서는 절대로 가상함수를 호출하지 말라

ssong_dev 2022. 11. 16. 11:51

객체가 완전히 생성되기 이전에 가상 함수를 호출하면 이상 동작을 일으킨다.

어떤 타입이든 생성자가 수행을 완료할 때 까지는 객체가 완전히 생성되었다고 할 수 없기 때문이다.

 

class B
{
	protected B()
    {
    	VFunc();
    }
    
    protected virtual void VFunc()
    {
    	Console.WriteLine("VFunc in B");
    }
}

class Derived : B
{
	private readonly string msg = "Set by initializer";
    
    public Derived(string msg)
    {
    	this.msg = msg;
    }
    
    protected override void VFunc()
    {
    	Console.Write(msg);
    }
    
    public static void Main()
    {
    	var d = new Derived("Constructed in main");
    }
}

 

 

이 코드가 실행되었을 때, 부모 기본 생성자가 먼저 실행되므로 "VFunc in B" 가 출력되는 것처럼 보일 수 있다.

하지만 d가 런타임에 Derived 타입이기 때문에 VFunc은 오버라이드 된 상태로 호출된다.

 

따라서 출력되는 건 "Set by initializer"이다.

자식 클래스의 생성자가 완료되지 않았지만 자식클래스 함수가 호출된 것이다.

 

C#의 정의에 따르면 생성자의 본문으로 진입하는 순간 해당 객체는 이미 초기화가 완료된 것으로 간주한다.

대부분의 경우 모든 멤버 변수가 이 시점에 유효한 값을 갖도록 초기화되었다고 단정하기 어렵다.

단지 멤버 변수에 대한 초기화 구문을 완료했을 뿐이며 실상 파생 클래스의 생성자 본문은 아직 수행조차 되지 않았기 때문이다.

 

이 때문에 객체를 생성하는 동안 가상 함수를 호출하면 일관성 문제가 발생할 수 있다.

 

생성 중인 객체는 Derived 타입의 객체다. 따라서 Derived 객체에서 재정의한 가상 함수를 호출해야 한다.

 

C++ 의 경우 각 클래스의 생성자가 실행되면 객체의 런타임 타입이 변경된다.

 

C# 에서는 현재 타입이 추상 클래스인 경우 가상 메서드가 null 메서드 포인터가 될 가능성을 원칙적으로 배제하려 했다.

== 런타임을 고려하여 함수를 호출한다.

 

부모 생성자에서 추상 함수를 호출한다면 C++은 런타임 오류가 발생하지만

C#은 자식의 오버라이딩 함수가 호출될 것이다.

 

abstract class B
{
	protected B()
    {
    	VFunc();
    }
    
    protected abstract void VFunc();
}

class Derived : B
{
	private readonly string msg = "Set by initializer";
    
    public Derived(string msg)
    {
    	this.msg = msg;
    }
    
    protected override void VFunc()
    {
    	Console.WriteLine(msg);
    }
    
    public static void Main()
    {
    	var d = new Derived("Constructed in main");
    }
}

 

이 코드를 C++에서 동작할 시에는 크래시가 나지만

C#에서 실행하면 런타임에 타입을 고려하여 오버라이드된 VFunc()함수를 호출한다.

 

 

베이스 클래스의 생성자 내에서 가상 함수를 호출하면

파생 클래스가 가상 함수를 어떻게 구현했는지에 따라 매우 민감하게 동작하게 된다.

파생 클래스가 어떻게 작성될 지 예상할 수 없는 노릇이므로 베이스 클래스의 생성자 내에서 가상 함수를 호출하면

구조가 매우 취약한 코드가 되어버린다.

 

파생 클래스에서 멤버 초기화 구문을 통해서 모든 변수를 초기화하면 될 것 같지만

대부분의 경우 생성자로 전달된 매개변수를 이용하여 객체를 초기화할 것이기 때문에

이 같은 방법은 제약이 너무 많다.

 

생성자에서 가상 함수를 호출해도 되는 유일한 경우는 파생 클래스가 기본 생성자만을 정의하고 있고,

다른 어떤 생성자도 가지고 있지 않은 경우뿐이다. 하지만 이는 파생 클래스 정의에 심각한 제약이 된다.

'개발 > 공부' 카테고리의 다른 글

[유니티] IL2CPP  (0) 2022.12.08
[유니티] memory profiler  (0) 2022.12.08
[C#/유니티] (최적화) Dictionary와 Enum  (0) 2022.11.14
Message Queue  (0) 2022.11.03
[C#] ReferenceEqual  (0) 2022.11.03