기본 생성자

기본 생성자 (Primary constructors)

클래스 헤더에서 파라미터와 필드를 선언해서 클래스를 간결하게 만들 수 있어요. 이 기본 생성자(primary constructor) 문법이 어떻게 동작하는지 살펴볼게요.

출처: Dart 공식 문서

본문

개요 (Overview)

기본 생성자는 클래스의 필드와 주요 생성자를 한 줄로 선언하는 간결한 방법을 제공해요. 필드 선언, 파라미터 전달, 그리고 생성자 본문에서의 할당이라는 보일러플레이트(boilerplate)를 줄여 주죠. 이 축약 문법은 선언을 쓰는 방식을 바꾸지만, 런타임 동작은 바꾸지 않아요.

전후 비교 (Before and after)

필드 두 개와 생성자를 가진 전통적인 클래스를 생각해 볼게요.

// Current syntax.
class Point {
  int x;
  int y;

  Point(this.x, this.y);
}

기본 생성자를 쓰면 같은 클래스가 훨씬 간결해져요.

// Using a primary constructor.
class Point(var int x, var int y);

기본 생성자에서 파라미터를 varfinal로 선언하면, 그 파라미터에 대해 인스턴스 변수가 암묵적으로 생성돼요.

클래스 본문 안에 선언된 다른 모든 생성자는 **본문 생성자(in-body constructors)**라고 불러요.

기본 생성자가 모든 새 인스턴스에서 실행되도록 보장하기 위해, 기본 생성자가 있는 클래스·mixin 클래스·enum은 다른 non-redirecting generative 본문 생성자를 가질 수 없어요.

파라미터의 필드 선언 (Field declarations in parameters)

varfinal 수정자가 붙은 기본 생성자 파라미터를 **선언 파라미터(declaring parameters)**라고 해요. 이들은 필드를 암묵적으로 생성해요.

수정자를 생략하면 그 파라미터는 필드를 만들지 않아요. 전통적인 생성자의 파라미터처럼 동작하죠.

// Declares both fields x and y.
class Point(var int x, var int y);

// Doesn't declare a field.
class User(String name);

확장 타입(extension types)의 경우 기본 생성자는 정확히 하나의 파라미터를 가져야 해요. 이 파라미터는 수정자를 생략해도 항상 선언 파라미터예요.

Mixin 클래스는 파라미터·본문·initializer 리스트가 없는 기본 생성자만 가질 수 있어요.

기본 생성자 유효 범위 (Primary constructor scopes)

기본 생성자를 사용하면 클래스 헤더에 선언한 파라미터들이 클래스 선언의 여러 부분에서 사용할 수 있어요. Dart는 두 가지 별개의 유효 범위(scope)로 이들의 가시성을 관리해요.

  • 기본 initializer 유효 범위(Primary initializer scope): 클래스 본문의 non-late 필드 initializer와 기본 생성자의 initializer 리스트(this : 뒤)에 적용돼요. 이 유효 범위에서 x 같은 파라미터 이름은 생성자 파라미터를 직접 가리켜요.
  • 기본 파라미터 유효 범위(Primary parameter scope): 기본 생성자의 본문 블록({ ... } 안)에 적용돼요. 이 유효 범위에서 선언 파라미터의 이름은 생성된 인스턴스 변수(필드)를 가리키고, 선언이 아닌 파라미터의 이름은 여전히 생성자 파라미터를 가리켜요.

기본 initializer 유효 범위 덕분에 헤더 파라미터를 non-late 필드를 초기화하는 데 직접 사용할 수 있어요. 그래서 별도의 initializer 리스트가 필요 없어져요. 전통적인 생성자의 initializer 리스트에서 변수를 초기화하는 것과 똑같이 동작해요.

class DeltaPoint(final int x, int delta) {
  // Accesses 'x' and 'delta' parameters directly!
  final int y = x + delta;
}

기본 파라미터 유효 범위는 인스턴스 변수에 대한 업데이트가 생성자 본문에 올바르게 반영되도록 보장해요. 반면 initializer는 여전히 원래 파라미터에 접근할 수 있죠. 아래 예시는 같은 이름 x가 각각의 유효 범위에서 다르게 해석되는 모습을 보여줘요.

class ScopingDemo(var String x, String suffix) {
  // In a non-late field initializer, 'x' refers to the parameter 'x'.
  final String fieldAtDeclaration = x;
  final String fieldInInitializer;

  // In the initializer list, 'x' refers to the parameter 'x'.
  this : fieldInInitializer = x {
    // Inside the body, 'x' refers to the induced instance variable,
    // so assigning to it updates the field.
    x = x.toUpperCase();
    // 'suffix' induces no field, so it still refers to the parameter.
    print('$x$suffix');
  }
}

이 일관된 동작 덕분에 전통적인 생성자와 기본 생성자 사이의 리팩터링이 더 단순하고 안전해져요.

생성자 본문 추가 (Add constructor bodies)

입력을 검증하거나 복잡한 초기화를 수행하려면, 클래스 정의 안에서 기본 생성자에 본문을 추가할 수 있어요. 이 본문은 this 키워드 뒤에 블록을 붙이는 형태예요.

class Point(var int x, var int y) {
  this : assert(x >= 0 && y >= 0) {
    print('Point initialized at ($x, $y)');
  }
}

블록은 this 뒤에 initializer 리스트와/또는 함수 본문을 지정할 수 있어요. initializer 리스트만 제공하려면 세미콜론으로 끝내면 되는데, 예를 들어 this : assert(x >= 0);처럼요. 블록에 @metadata this;처럼 메타데이터를 붙일 수도 있어요.

비공개 필드 초기화 (Initialize private fields)

이름 있는 파라미터로 비공개(private) 필드를 초기화하려면, 전통적인 생성자에서는 수동 할당 보일러플레이트를 작성해야 해요.

// Variant not using a private named parameter.
class User({required String name}) {
  String _name = name;
}

기본 생성자와 비공개 이름 있는 파라미터(private named parameters) 기능을 쓰면, 비공개 필드를 생성자 헤더에 직접 선언할 수 있어요. 이름 있는 파라미터에 비공개 이름(앞에 밑줄이 붙은)을 사용하면, 컴파일러가 밑줄을 제거해서 호출자에게는 파라미터 이름을 자동으로 공개(public)하게 만들어요.

// Variant using a private named parameter.
class User({required var String _name});

두 경우 모두 호출자는 호출 지점에서 User(name: 'John Doe')처럼 공개 이름 name을 사용해요.

빈 본문 (Empty bodies)

클래스, mixin 클래스, 확장(extension), 확장 타입의 빈 본문({})은 세미콜론(;)으로 바꿀 수 있어요. 일반적으로 이 선언들에 대해 성립하지만, 기본 생성자를 쓸 때 전체 선언을 한 줄로 유지하는 데 특히 유용해요.

class Point(var int x, var int y);

상수 기본 생성자 (Constant primary constructors)

전통적인 생성자처럼, 기본 생성자도 클래스와 그 필드가 허용한다면 상수(constant)가 될 수 있어요. 상수 기본 생성자를 선언하려면 클래스 헤더에서 클래스 이름 앞에 const 수정자를 붙이면 돼요.

class const ConstPoint(final int x, final int y) {
  final int z;
  // A constant primary constructor can have an initializer list,
  // but can't have a body block.
  this : z = x + y;
}

상수 기본 생성자에는 중요한 제약들이 있어요.

  • 본문 블록 금지: { ... } 본문을 가진 것은 비어 있더라도 컴파일 타임 오류예요. 상수 기본 생성자는 세미콜론이 뒤따르는 initializer 리스트만 사용할 수 있어요.
  • 확실히 초기화된 final 필드: generative const 생성자가 있는 모든 클래스처럼, 모든 인스턴스 변수는 final이어야 하고 late가 될 수 없으며, 선언 파라미터·필드 initializer·기본 생성자의 initializer 리스트에 의해 확실히 초기화되어야 해요.
  • 잠재적으로 상수(Potentially constant) initializer: 각 인스턴스 변수의 초기화 표현식은 잠재적으로 상수여야 해요. 여기에는 필드 initializer와 기본 생성자 initializer 리스트의 표현식이 포함돼요.

이름 있는 기본 생성자 (Named primary constructors)

클래스 헤더에서 클래스 이름 뒤에 점(.)과 이름을 붙여 기본 생성자를 **이름 있는 생성자(named constructor)**로도 선언할 수 있어요.

// A named primary constructor.
class Point.custom(var int x, var int y);

흔한 패턴 중 하나는 직접 인스턴스화를 제한하고, 호출자가 팩토리 메서드나 다른 생성자를 쓰도록 강제하는 비공개 기본 생성자(예: Point._)를 정의하는 거예요.

// A private named primary constructor.
class Point._(var int x, var int y);

수퍼 파라미터 (Super parameters)

수퍼 파라미터(super parameters)는 전통적인 생성자에서처럼 동작해요. 파라미터를 수퍼클래스 생성자로 전달할 수 있게 해 주죠.

class Person(final String name, final int age);

class Employee(super.name, super.age, final String role) extends Person;

이렇게 하면 계층 구조의 클래스에서 보일러플레이트를 줄여 주고, initializer 리스트를 수동으로 작성하거나 파라미터 선언을 중복할 필요가 없어져요.

enum 기본 생성자 (Enum primary constructors)

기본 생성자를 사용하면 향상된 enum(enhanced enums)을 훨씬 간결하게 선언할 수 있어요.

기본 생성자를 쓰면 enum의 필드와 생성자를 한 줄로 정의할 수 있어요. 필드 선언·파라미터·초기화라는 보통의 보일러플레이트가 없어지죠.

enum Color(final String hex) {
  red('#FF0000'),
  green('#00FF00'),
  blue('#0000FF');
}

enum의 기본 생성자는 암묵적으로 상수예요. enum 이름 앞에 const 수정자를 선택적으로 쓸 수는 있지만(예: enum const Color), 이는 중복이라 생략할 수 있어요.

제약과 변경 사항 (Constraints and breaking changes)

기본 생성자를 쓸 때 다음 제약과 잠재적 오류를 기억해 두세요.

  • 선언 파라미터는 lateexternal이 될 수 없어요: 기본 생성자 헤더의 파라미터에는 lateexternal 수정자가 허용되지 않아요. 이 수정자를 쓰려면 클래스 본문에 필드를 평소처럼 선언하면 돼요.
  • 이름 충돌(Name collisions): 기본 생성자에서 클래스 본문의 메서드나 다른 필드와 같은 이름으로 파라미터를 선언하면 컴파일 타임 오류가 나요.
  • 기본 생성자 파라미터에 할당 금지: 기본 생성자 파라미터는 기본 initializer 유효 범위 안에서 읽기 전용이에요. 필드 initializer나 기본 생성자의 initializer 리스트에서 여기에 할당하는 것(x = 5x++ 같은)은 컴파일 타임 오류예요.
  • 이중 초기화(Double initialization): 인스턴스 변수를 그 선언에서와 기본 생성자의 initializer 리스트(또는 initializing formal 파라미터)에서 둘 다 초기화할 수 없어요. 필드가 mutable이어도 마찬가지예요. 이렇게 하면 컴파일 타임 오류가 나요.
  • 본문 부분 제약(Body part constraints):
    • 기본 생성자 본문 부분(this 블록)은 async, async*, sync* 수정자를 쓸 수 없고, 표현식 본문 화살표(=>) 문법도 쓸 수 없어요.
    • 클래스 헤더가 기본 생성자를 선언하지 않으면 this 블록을 쓸 수 없어요.
    • 클래스는 기본 생성자 본문 부분을 최대 하나만 가질 수 있어요.
  • mixin 클래스 기본 생성자는 단순해야 해요: mixin 클래스는 파라미터·initializer 리스트·본문이 없을 때만 기본 생성자를 선언할 수 있어요.
  • 공변 파라미터(Covariant parameters): covariant 수정자는 기본 생성자 파라미터가 mutable 선언 파라미터(var 수정자 사용)일 때만 쓸 수 있어요.
  • factory 메서드의 엣지 케이스: 반환 타입이 없는 factory라는 이름의 메서드(예: factory() {})가 있으면, 컴파일러가 이를 간결한 팩토리 생성자로 해석해요. 이런 충돌을 피하려면 그런 메서드에 명시적 반환 타입(예: void factory() {})을 붙여야 해요.

파라미터 수정자 제한 (Parameter modifier restrictions)

finalvar 파라미터 수정자는 기본 생성자의 선언 파라미터를 위해서만 예약되어 있어요. 다른 선언(최상위 함수, 메서드, 클로저, 본문 생성자 등)의 형식 파라미터(formal parameters)에 final이나 var를 쓰면 컴파일 타임 오류(extraneous_modifier)가 나요.

// Valid: Primary constructor declaring parameter.
class Point(final int x, final int y);

// Compile-time error (extraneous_modifier):
void printValue(final int x) => print(x);

이 오류를 고치려면 수정자를 제거하거나 dart fix를 실행하면 돼요. 불변 파라미터를 스타일 선택으로 강제하고 싶다면 parameter_assignments lint 규칙을 대신 써요.

language version이 3.12 이하라면, avoid_final_parametersvar_with_no_type_annotation lint 규칙을 켜서 Dart 3.13으로 업그레이드하기 전에 이런 수정자를 미리 찾아 제거할 수 있어요.

더 알아보기 (Learn more)