생성자는 new로 객체를 만들 때 실행되는 코드입니다. 클래스와 이름이 같고, 반환 타입이 없으며, 누군가 객체를 쓰기 전에 새 객체를 올바른 상태로 만드는 것이 역할입니다.
출력:
Mug: 8.50
Rejected: price
생성자가 음수 가격을 거부하므로 가격이 음수인 Product는 존재할 수 없습니다. 이 보장이 밖에서 필드를 설정하는 대신 생성자를 작성하는 주된 이유입니다.
기본 생성자와 그것이 사라지는 때
클래스가 생성자를 하나도 선언하지 않으면, 컴파일러가 필드 이니셜라이저를 실행하는 것 말고는 아무것도 하지 않는 public 매개변수 없는 생성자를 제공합니다. 필드만 있는 클래스에서 new BankAccount()가 동작하는 이유입니다.
어떤 생성자든 선언하는 순간 암시적 생성자는 더 이상 생성되지 않습니다:
class Product
{
public string Name;
public Product(string name) { Name = name; }
}
var p = new Product(); // error CS7036: There is no argument given that corresponds to the required parameter 'name'
직렬 변환기, ORM, 제네릭 new() 제약 조건이 매개변수 없는 생성자를 필요로 할 때 이 점에 사람들이 놀랍니다. 둘 다 원한다면 매개변수 없는 생성자를 직접 선언하세요:
public Product() { } // or
public Product() : this("Unnamed") { } // delegate to the other one (next section)
생성자는 public, internal, protected, private일 수 있습니다. 접근 수준이 누가 그 생성자로 객체를 만들 수 있는지 정합니다.
오버로딩과 this(...)로 연결하기
클래스는 매개변수 목록이 다른 생성자를 여러 개 가질 수 있습니다. 같은 대입문을 각 생성자에 복사하지 않으려면 연결하세요. : this(...)는 현재 본문이 실행되기 전에 같은 클래스의 다른 생성자를 호출합니다.
출력:
new Pizza():
full constructor: medium, classic, 0
size-only constructor body
parameterless constructor body
Result: medium classic with 0 toppings
연결은 가장 완전한 생성자를 먼저 실행하고 호출한 생성자 쪽으로 되돌아옵니다. 실제 작업(검증, 대입)은 한 생성자에 두고, 나머지는 기본값만 제공하게 하세요.
선택적 매개변수도 대안입니다. public Pizza(string size = "medium", string crust = "classic", int toppings = 0)은 세 가지 호출을 모두 처리하는 생성자 하나를 줍니다. 짧은 생성자가 무언가를 계산해야 하거나, 기본값을 바꿔도 호출하는 쪽을 다시 컴파일하지 않아야 할 때는(선택적 매개변수의 기본값은 컴파일 시점에 호출 코드로 복사됩니다) 여전히 연결이 더 잘 맞습니다.
base(...)로 기본 클래스 생성자 호출하기
파생 클래스는 생성자를 상속하지 않습니다. 파생 클래스의 모든 생성자는 먼저 기본 클래스의 생성자를 실행해야 합니다. 아무것도 쓰지 않으면 컴파일러가 기본 클래스의 매개변수 없는 생성자 호출을 넣고, 기본 클래스에 그런 생성자가 없으면 : base(...)로 하나를 골라야 합니다.
모든 것이 실행되는 순서는 한 번 볼 만합니다:
출력:
Truck field initializer
Vehicle field initializer
Vehicle constructor body
Truck constructor body
KL-204, 3 axles
필드 이니셜라이저가 먼저, 파생 클래스가 기본 클래스보다 먼저 실행되고, 그다음 생성자 본문이 기본 클래스부터 아래로 실행됩니다. 그래서 Truck의 본문이 실행될 때는 Plate가 이미 설정되어 있습니다. 이 순서의 유일한 함정은 이것입니다. 기본 생성자가 Truck이 재정의한 virtual 메서드를 호출하면, 재정의가 Truck의 생성자 본문보다 먼저 실행되어 Axles가 여전히 0인 상태를 봅니다. 생성자에서 가상 메서드를 호출하지 마세요.
정적 생성자
정적 생성자는 객체가 아니라 타입을 초기화합니다. 매개변수도 접근 한정자도 없으며, 런타임이 타입을 처음 쓰기 직전에 정확히 한 번 호출합니다.
출력:
Program started
Loading tax table (runs once)
123.00
119.00
런타임은 정적 생성자를 스레드로부터 안전하게 만듭니다. 여러 스레드가 동시에 타입을 건드려도 한 번만 실행됩니다. 예외를 던지면 그 뒤로 프로세스가 끝날 때까지 타입을 쓸 때마다 TypeInitializationException이 발생하므로, 단순하게 유지하고 실패할 수 있는 I/O를 넣지 마세요.
생성자와 개체 이니셜라이저
개체 이니셜라이저 new Pizza("large") { Toppings = 3 }은 두 번째 생성자가 아닙니다. 컴파일러는 이를 "생성자를 실행한 뒤 이 멤버들을 대입한다"로 바꿉니다. 각각 잘하는 일에 쓰세요:
- 객체가 그 값 없이는 올바를 수 없는 값에는 생성자 매개변수. 모든 호출하는 쪽이 넘겨야 하고 생성자가 검사할 수 있습니다.
- 합리적인 기본값이 있는 선택적 설정에는 이니셜라이저.
var order = new Order(customerId: 42) { Note = "Leave at the door", GiftWrap = true };
C# 11부터는 required로 표시한 속성을 이니셜라이저에서 반드시 설정해야 하므로, 이니셜라이저 문법도 생성자의 보장을 일부 얻습니다. 속성 페이지에서 다룹니다.
private 생성자
private 생성자는 클래스 자신만 인스턴스를 만들 수 있다는 뜻입니다. 이를 쓰는 패턴이 두 가지 있습니다. 클래스가 생성을 통제하며 캐시된 객체나 null을 반환할 수 있는 정적 팩터리 메서드, 그리고 싱글턴입니다:
출력:
21
100
클래스 밖의 new Temperature(5m)에 대한 컴파일러 오류는 CS0122로, 접근 한정자가 모든 private 멤버에 대해 내는 것과 같습니다. 이름 붙은 팩터리 메서드는 실제 한계를 해결합니다. 두 생성자가 모두 decimal 하나를 받을 수는 없지만 FromCelsius와 FromFahrenheit는 가능합니다. 정적 멤버만 있는 클래스(유틸리티 클래스)는 생성자를 숨기는 대신 static으로 선언하세요.
식 본문 생성자
본문이 문장 하나인 생성자는 =>를 쓸 수 있습니다:
public Product(string name) => Name = name;
튜플을 쓰면 한 줄에 여러 필드를 대입할 수도 있습니다: public Point(int x, int y) => (X, Y) = (x, y);.
기본 생성자(C# 12)
C# 12에서는 클래스나 구조체가 타입 자체에 생성자 매개변수를 선언할 수 있습니다. 매개변수는 모든 멤버에서 범위 안에 있습니다:
public class Customer(string name, int id)
{
public string Name { get; } = name; // copy into a property
public string Label => $"#{id} {Name}"; // or read a parameter directly
public Customer(string name) : this(name, 0) { } // other constructors must chain to it
}
레코드와의 두 가지 차이가 사람들을 놀라게 합니다. 클래스의 기본 생성자 매개변수는 public 속성이 되지 않으므로 직접 노출해야 합니다. 그리고 멤버 안에서 쓴 매개변수는 변경 가능한 숨겨진 필드로 캡처되므로, 메서드 안의 name = "x";가 컴파일됩니다. 기본 생성자는 매개변수가 클래스가 호출만 하는 서비스인 의존성 주입에서 가장 자연스럽습니다:
public class OrderService(IOrderRepository repo, ILogger<OrderService> log)
{
public Order Get(int id) => repo.Find(id);
}
흔한 실수
- 매개변수 없는 생성자를 잃기. 매개변수 있는 생성자를 추가하면 암시적 생성자가 사라집니다(모든
new X()에서 CS7036). - 기본 클래스에 매개변수 없는 생성자가 없는데
: base(...)를 잊기. 컴파일러가 암시적 호출을 넣을 수 없어서, 기본 생성자의 필수 매개변수에 대한 인수가 없다고 보고합니다. - 생성자에서 가상 메서드 호출하기. 재정의가 파생 생성자의 본문보다 먼저 실행됩니다.
- 생성자에서 무거운 작업 하기. 네트워크 호출, 파일 읽기 등 느린 작업은 객체를 비싸게 만들고 예외를 다루기 어렵게 합니다. 그런 작업에는 정적 팩터리 메서드나 비동기 초기화 메서드를 쓰세요.
- 반환 타입 쓰기.
public void Product()는Product라는 이름의 일반 메서드이며, 멤버는 자신을 둘러싼 타입과 같은 이름을 가질 수 없으므로 컴파일러가 거부합니다(CS0542).
자주 묻는 질문
C#에서 생성자란 무엇인가요?
생성자는 new로 객체를 만들 때 실행되는 특별한 메서드입니다. 클래스와 이름이 같고 반환 타입이 없습니다: public Product(string name) { Name = name; }. 역할은 새 객체를 올바른 상태로 만드는 것이며, 보통 매개변수로 필드를 대입하고 잘못된 입력을 거부합니다.
C#은 기본 생성자를 자동으로 만들어 주나요?
클래스가 생성자를 하나도 선언하지 않을 때만 그렇습니다. 그러면 컴파일러가 모든 필드를 이니셜라이저 값이나 기본값으로 두는 public 매개변수 없는 생성자를 추가합니다. 이름을 받는 생성자 같은 것을 하나라도 작성하는 순간 암시적 생성자는 사라지고 new Product()는 컴파일되지 않습니다(CS7036 오류). 여전히 필요하다면 public Product() { }를 직접 선언하세요.
C#에서 한 생성자에서 다른 생성자를 호출하려면 어떻게 하나요?
생성자의 매개변수 목록 뒤에 : this(...)를 씁니다: public Product(string name) : this(name, 0m) { }. 대상 생성자가 먼저 실행되고, 그다음 호출한 생성자의 본문이 실행됩니다. 기본 클래스의 생성자를 호출하려면 같은 방식으로 : base(...)를 쓰세요.
C#의 정적 생성자란 무엇인가요?
static으로 표시하고 매개변수와 접근 한정자가 없는 생성자로, 첫 인스턴스가 만들어지거나 첫 정적 멤버가 쓰이기 전에 타입마다 한 번 실행됩니다. 한 줄 이니셜라이저로는 부족한 정적 필드를 초기화하는 데 씁니다. 직접 호출할 수 없고, 예외를 던지면 그 타입은 프로그램이 끝날 때까지 쓸 수 없게 됩니다(TypeInitializationException).
생성자와 개체 이니셜라이저 중 무엇을 써야 하나요?
객체가 그 값 없이는 올바를 수 없는 값에는 생성자 매개변수를 쓰세요. 컴파일러가 모든 호출하는 쪽에 값을 넘기도록 강제하고 생성자가 이를 검사할 수 있습니다. 선택적 설정에는 개체 이니셜라이저(new Product("Mug") { Color = "blue" })를 쓰세요. 이니셜라이저는 생성자가 끝난 뒤 실행됩니다.
C# 12의 기본 생성자(primary constructor)란 무엇인가요?
클래스 선언 자체에 쓰는 매개변수로, class Customer(string name, int id) { ... }처럼 쓰며 클래스의 모든 멤버에서 쓸 수 있습니다. 레코드의 위치 매개변수와 달리 public 속성이 되지 않으므로, public string Name => name;이나 public string Name { get; } = name;으로 직접 노출하세요.