아이템 12: 할당 구문보다 멤버 초기화 구문이 좋다

클래스를 만들다 보면 종종 둘 이상의 생성자를 작성해야 하는 경우가 있다.

그런데 생성자 내에서 멤버 변수들의 값을 초기화하도록 코드를 작성하다 보면 모든 생성자 내에서 멤버 변수들을 초기화해야 함에도 불구하고 자칫 초기화 코드를 누락하는 경우가 있다.

이러한 오류를 범하지 않으려면 생성자의 본문에서 멤버 변수에 값을 할당하기보다 멤버 초기화 구문을 사용하는 것이 좋다.

멤버 변수를 선언할 때 객체를 함께 생성하는 것은 C#에서 매우 자연스러운 구문이므로 멤버 변수를 선언할 때는 항상 초기화 구문을 사용하자..!

멤버 초기화 구문

public class MyClass
{
    // 컬렉션을 선언하는 동시에 초기화
    private List<string> labels = new List<string>();
}

이와 같이 코드를 작성하면 MyClass 타입에 몇 개의 생성자를 추가하든 상관없이 멤버 변수를 올바르게 초기화할 수 있다. (오류 방지)

컴파일러는 모든 생성자의 시작 부분에 멤버 초기화 구문을 포함시키기 때문에 새로운 생성자를 추가하더라도 멤버 초기화 구문이 항상 포함된다.

따라서 생성자의 본문에서 별도로 멤버 변수를 초기화할 필요가 없다.

단지 변수를 선언하는 곳에서 변수를 초기화하도록 코드를 작성하기만 하면 된다.

C# 컴파일러는 생성자를 갖지 않은 타입을 선언한 경우에도 기본 생성자를 자동으로 생성하는데 멤버 초기화 구문을 이용하면 컴파일러가 생성해 주는 기본 생성자 앞쪽에도 초기화 구문이 포함된다.

멤버 초기화 구문은 생성자 본문 내에서 멤버 변수를 올바르게 초기화하는 작업 그 이상의 역할도 한다.

멤버 초기화 구문에 의해 생성된 코드는 생성자 본문의 앞쪽에 덧붙여진다.

이는 생성하려는 타입이 다른 클래스를 상속하고 있는 경우 베이스 클래스의 생성자가 호출되기 전에 멤버에 대한 초기화가 이루어진다는 것을 의미한다.

멤버 변수의 초기화 순서는 변수의 선언 순서대로 수행된다는 점도 알아두면 좋다.

멤버 초기화 구문을 사용하면 자칫 초기화되지 않은 멤버 변수를 사용하는 문제로부터 벗어날 수 있지만 완벽한 것은 아니다.

다음에 알아볼 세 가지 경우에는 멤버 초기화 구문을 사용하지 않는 것이 좋다.

1. 객체를 0이나 null로 초기화하는 경우

첫째로 객체를 0이나 null로 초기화하는 경우이다.

기본 시스템 초기화 루틴은 코드를 실행하기 전에 모든 값을 0으로 설정한다.

이 같은 시스템 초기화 루틴은 저수준에서 직접 CPU 명령을 수행하여 메모리 블록을 0으로 설정하기 때문에 추가적으로 변수의 값을 0이나 null로 설정할 필요가 없다.

C# 컴파일러는 이같이 불필요한 초기화 구문이 있더라도 개의치 않고 코드를 생성할 것이기 때문에 괜한 일을 추가적으로 하는 꼴이 된다.

public struct MyvalType
{

}

MyvalType myVal1; // 0으로 초기화
MyvalType myVal2 = new MyvalType(); // 반복해서 0으로 초기화

이 코드의 두 문장은 모두 변수를 0으로 초기화한다.

첫 번째 문장은 myVal1이 사상된 메모리블록을 모두 0으로 설정한다.

두 번째 문장은 initobj하는 IL 명령을 사용하는데 박싱/언방식된 myVal2 변수 모두에 대해서 0으로 초기화하는 과정이 수행된다.

이 과정으로 인해 약간의 추가 시간이 소요된다.

2. 동일한 객체를 반복해서 초기화하는 경우

둘째는 동일한 객체를 반복해서 초기화하는 경우다.

멤버 초기화 구문은 객체 생성 방법이 모든 생성자에서 동일한 경우에만 사용하는 것이 좋다.(추출)

public class MyClass2
{
    private List<string> labels = new List<string>();

    public MyClass2()
    {
        
    }

    public MyClass2(int size)
    {
        labels = new List<string>(size);
    }
}

이와 같이 List객체를 생성하는 방법이 혼재하는 경우 멤버 초기화 구문을 사용하지 않는 것이 좋다.

MyClass2를 생성할 때 컬렉션의 크기를 지정하게 되면 실제로 2개의 List<> 객체가 생성되며 그중 하나는 즉각 가비지가 된다.

멤버 초기화 구문은 생성자 본문보다 앞서 수행되므로 생성자 본문에서 생성한 객체만 살아남는다.

즉, 실제로 컴파일러가 생성하는 코드는 다음과 같다.

public class MyClass2
{
    private List<string> labels;

    public MyClass2()
    {
        labels = new List<string>();
    }

    public MyClass2(int size)
    {
        labels = new List<string>();
        labels = new List<string>(size);
    }
}

암시적인 속성을 사용하는 경우에도 유사한 상황이 발생할 수 있다.

데이터 요소의 흔히 암시적인 속성을 사용하곤 하는데 이 경우에도 중복 초기화가 발생할 수 있다.(item 14에서 다룰 예정)

3. 예외처리가 반드시 필요한 경우

멤버 초기화 구문은 try로 감쌀 수 없기 때문에 초기화 과정에서 예외가 발생하면 예외가 외부로 전파된다.

따라서 클래스 내부에서 복구를 시도할 수가 없다.

반드시 예외 처리가 필요하다면 멤버 초기화 구문 대신 생성자 내부로 초기화 코드를 옮기고 예외 처리 코드를 적절히 구현해야 한다.

정리

멤버 초기화 구문을 이용하면 타입 내에 생성자가 여러 개일 때도 멤버 변수 초기화를 누락하지 않도록 도와준다.

향후에는 새로운 생성자를 추가하는 경우에도 멤버 변수에 대한 초기화를 누락하는 상황을 미연에 방지할 수 있다.

다만 모든 생성자가 동일한 방법으로 멤버 변수를 초기화하는 경우에 한해서만 이 방법을 사용해야 한다.

또한 멤버 초기화 구문을 사용하면 코드를 읽고 쉽고 유지보수도 용이하다.

댓글남기기