본문 바로가기
프로그래밍/Effective C#

Effective C# Item 12 : 할당 구문보다 멤버 초기화 구문이 좋다

by bantomak 2023. 10. 20.

할당 구문보다 멤버 초기화 구문이 좋다

클래스를 만들다 보면 종종 둘 이상의 생성자를 작성해야 하는 경우가 있다. 그런데 생성자 내에서 멤버 변수들의 값을 초기화하도록 코드를 작성하다 보면 모든 생성자 내에서 멤버 변수들을 초기화해야 함에도 불구하고 자칫 초기화 코드를 누락하는 경우가 생긴다. 이러한 오류를 범하지 않으려면 생성자의 본문에서 멤버 변수에 값을 할당하기보다 멤버 초기화 구문(Member Initializer)을 사용하는 것이 좋다. 정적 변수와 인스턴스 변수 어느 쪽이라도 가능한 한 멤버 초기화 구문을 사용하는 것이 좋다. 멤버 변수를 선언할 때 객체를 함께 생성하는 것은 C#에서 매우 자연스러운 구문이므로 멤버 변수를 선언할 때는 항상 초기화 구문을 사용하자.

 

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

 

이와 같은 코드를 작성하면 MyClass 타입에 몇 개의 생성자를 추가하든 상관없이 멤버 변수를 올바르게 초기화할 수 있다. 컴파일러는 모든 생성자의 시작 부분에 멤버 초기화 구문을 포함시키기 때문에 새로운 생성자를 추가하더라도 멤버 초기화 구문이 항상 포함된다. 따라서 생성자의 본문에서 별도로 멤버 변수를 초기화할 필요가 없다. 단지 변수를 선언하는 곳에서 변수를 초기화하도록 코드를 작성하기만 하면 된다. C# 컴파일러는 생성자를 갖지 않는 타입을 선언한 경우에도 기본 생성자를 자동으로 생성하는데 멤버 초기화 구문을 이용하면 컴파일러가 생성해 주는 기본 생성자의 앞쪽에도 초기화 구문이 포함된다.

 

멤버 초기화 구문은 생성자 본문 내에서 멤버 변수를 올바르게 초기화하는 작업 그 이상의 역할도 한다. 멤버 초기화 구문에 의해 생성된 코드는 생성자 본문의 앞쪽에 덧붙여진다. 이는 생성하려 나는 타입이 다른 클래스를 상송하고 있는 경우 베이스 클래스의 생성자가 호출되기 전에 멤버에 대한 초기화가 이루어진다는 것을 의미한다. 세부적인 특성이긴 하지만 멤버 변수의 초기화 순서는 변수의 선언 순서대로 수행되는 점도 알아두면 좋다.

 

멤버 초기화 구문을 사용하면 자칫 초기화되지 않은 멤버 변수를 사용하는 문제로부터 벗어날 수 있지만 완벽한 것은 아니다. 다음에 알아볼 세 가지 경우에는 멤버 초기화 구문을 사용하지 않는 것이 좋다. 

 

첫째로 객체를 0이나 null로 초기화하는 경우다. 기본 시스템 초기화 루틴은 코드를 실행하기 전에 모든 값을 0으로 설정한다. 이 같은 시스템 초기화 루틴은 저수준에서 직접 CPU 명령을 수행하여 메모리 블록을 0으로 설정하기 때문에 추가적으로 변수의 값을 0이나 null로 설정할 필요가 없다. C# 컴파일러는 이같이 불필요한 초기화 구문이 있더라도 개의치 않고 코드를 생성할 것이기 때문에 괜한 일을 추가적으로 하는 꼴이 된다.

 

public struct MyValType
{
   // 생략
   MyValType myVal1;
   MyValType myVal2 = new MyValType(); // 반복해서 0으로 초기화
}

 

이 코드의 두 문장은 모두 변수를 0으로 초기화한다. 첫 번째 문장은 myVal1이 사상된 메모리 블록을 모두 0으로 설정한다. 두 번째 문장은 initobj라는 IL 명령을 사용하는데 박싱/언박싱된 myVal2 변수 모두에 대해서 0으로 초기화하는 과정이 수행된다. 이 과정으로 인해 약간의 추가 시간이 소요된다.(아이템 9: 박싱과 언박싱을 최소화하라. 참조)

 

둘째는 동일한 객체를 반복해서 초기화하는 경우다. 멤버 초기화 구문은 객체 생성 방법이 모든 생성자에서 동일한 경우에만 사용하는 것이 좋다. 만약 아래의 MyClass2와 같이 List 객체를 생성하는 방식이 다양하게 혼재할 경우 멤버 초기화 구문을 사용하지 않는 것이 좋다.

 

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

 

MyClass2를 생성할 때 컬렉션의 크기를 지정하게 되면 실제로 2개의 List<> 객체가 생성되며 그중 하나는 즉각 가비지가 된다. 멤버 초기화 구문은 생성자의 본문보다 앞서 수행되므로 생성자 본문에서 생성한 객체만 살아남는다. 컴파일러가 실제로 생성하는 코드는 다음과 유사하다. 우리가 직접 작성하지는 않았지만 불필요한 코드가 포함된 꼴이다.

 

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

 

멤버 초기화 구문 대신 생성자 본문에 코드를 두는 것이 좋은 세번째 경우는 예외 처리가 반드시 필요한 경우다. 멤버 초기화 구문은 try로 감쌀 수 없기 때문에 초기화 과정에서 예외가 발생하면 예외가 외부로 전파된다. 따라서 클래스 내부에서 복구를 시도할 수가 없다. 반드시 예외처리가 필요하다면 멤버 초기화 구문 대신 생성자 내부로 초기화 코드를 옮기고 예외처리 코드를 적절히 구현해야 한다.

 

멤버 초기화 구문을 이용하면 타입 내에 생성자가 여러 개일 때도 멤버 변수 초기화를 누락하지 않도록 도와준다. 향후에 새로운 생성자를 추가하는 경우에도 멤버 변수에 대한 초기화를 누락하는 상황을 미연에 방지할 수 있다. 다만 모든 생성자가 동일한 방법으로 멤버 변수를 초기화하는 경우에 한해서만 이 방법을 사용해야 ㅇ한다. 멤버 초기화 구문을 사용하면 코드를 읽기도 쉽고 유지보수도 용이하다.

 

출처

 

이펙티브 C# - 예스24

더 나은 C# 코드를 작성하는 새로운 방법 50가지 C#은 전통적인 .NET 기반 개발에서 유니티 게임 엔진으로 개발 영역을 확대하면서 더욱 주목받고 있다. 또한 자마린으로 다양한 모바일 플랫폼에

댓글