티스토리 뷰

다트 공식 문서 번역

다트] 확장 타입

철철박사 2024. 8. 5. 13:51
반응형

확장 타입

확장 타입은 기존 타입을 감싸는 컴파일 타임 추상화로, 기존 타입과 다른 정적 전용 인터페이스를 제공한다.

 

확장 타입은 확장 타입 객체가 사용할 수 있는 연산 또는 인터페이스의 집합(또는 인터페이스)에 대한 규칙을 강제한다. 확장 타입의 인터페이스를 정의할 때, 기존 타입의 일부 멤버를 재사용하거나 생략하거나 대체해서 새로운 기능을 추가할 수 있다.

 

다음 예제는 int 타입을 감싸서 ID 번호에 적합한 연산만 허용하는 IdNumber라는 int의 확장 타입을 생성한다.

extension type IdNumber(int id) {
  // 'int' 타입의 '<' 연산자를 감쌉니다:
  operator <(IdNumber other) => id < other.id;
  
  // ID 번호에는 덧셈이 의미가 없으므로 '+' 연산자를 선언하지 않습니다.
}
void main() {
  // 'int'는 ID 번호를 안전하지 않은 연산에 노출시킵니다:
  int myUnsafeId = 42424242;
  myUnsafeId = myUnsafeId + 10; // 작동하지만, ID에서는 +가 허용되면 안된다.

  var safeId = IdNumber(42424242);
  safeId + 10; // 컴파일 오류: '+' 연산자가 없습니다.
  myUnsafeId = safeId; // 컴파일 오류: 잘못된 타입.
  myUnsafeId = safeId as int; // OK: 런타임에서 표현 타입으로 캐스팅.
  safeId < IdNumber(42424241); // OK: 감싼 '<' 연산자 사용.
}

 

확장 타입은 래퍼 클래스와 같은 목적을 가지고 있지만, 추가 런타임 객체를 생성할 필요가 없으므로 비용이 들지 않는다. 확장 타입은 정적 전용이며 런타임에서 컴파일되기 때문에 본질적으로 비용이 없다.

 

확장 메서드는 확장 타입과 유사한 정적 추상화이다. 그러나 확장 메서드는 기본 타입의 모든 인스턴스에서 직접적으로 기능을 추가한다. 확장 타입은 다르다. 확장 타입의 인터페이스는 해당 확장 타입의 정적 타입을 가진 표현식에만 적용된다. 기본적으로 확장 타입은 기본 타입 인터페이스와는 구별된다.

 

 

 

문법

선언

새 확장 타입을 선언할 때는 확장 타입 이름과 괄호 안에 표현 타입 선언을 한다.

extension type E(int i) {
  // 연산 집합 정의.
}

 

 

표현 타입 선언 (int i)는 확장 타입 E의 기본 타입이 int임을 지정하며, 기존 타입 객체에 대한 참조는 i라는 이름임을 지정한다. 이 선언으로 인해 다음 기능이 추가된다..

  • 표현 타입의 객체에 대한 암시적 게터 : int get i
  • 암시적 생성자 : E(int i) : i = i

표현 타입의 객체에 대한 암시적 게터는 다음과 같이 사용할 수 있다.

  • 확장 타입 본문 내에서 i(또는 생성자에서는 this.i)를 사용하여 접근한다.
  • 외부에서는 e.i처럼 사용하여 접근한다. (여기서 e는 확장 타입을 정적 타입으로 가지는 객체가 된다.)
extension type E(int i) {}

void main() {
  var e = E(10);
  print(e.i);
}

 

확장 타입 선언 클래스나 확장처럼 타입 매개변수도 포함할 수도 있다.

extension type E<T>(List<T> elements) {
  // ...
}

 

 

생성자

선택적으로 확장 타입 본문에 생성자를 선언할 수 있다. 표현 타입 선언 자체는 암시적 생성자이므로 기본적으로 이름없는 생성자의 역할을 한다. 추가로 선언하는 생성자는 초기화 목록이나 형식 매개변수를 사용하여 표현 객체의 인스턴스 변수를 초기화해야 한다.

extension type E(int i) {
  // 형식 매개변수로 표현 객체 인스턴스 변수를 초기화
  E.n(this.i);
  
  // 초기화 목록로 표현 객체 인스턴스 변수를 초기화
  E.m(int j, String foo) : i = j + foo.length;
}

void main() {
  E(4); // 암시적 이름 없는 생성자를 사용
  E.n(3); // 이름 있는 생성자를 사용
  E.m(5, "Hello!"); // 추가 매개변수가 있는 이름 있는 생성자를 사용
}

 

또한, 표현 타입 선언 생성자에 이름을 붙여서 본문에 이름 없는 생성자를 선언할 수도 있다.

extension type const E.origin(int it) {
  E(): this._(42);
  E.otherName(this.it);
}

void main2() {
  E();
  const E.origin(2);
  E.otherName(3);
}

 

 

생성자를 완전히 숨길 수도 있다. 클래스와 동일하게 비공개(private) 생성자 구문인 _를 사용한다. 예를 들어, 기존 타입이 int이지만 문자열로 E를 생성하려는 경우는 다음과 같이 사용할 수 있다.

extension type E._(int i) {
  E.fromString(String foo) : i = int.parse(foo);
}

 

전달 생성자 또는 팩토리 생성자를 선언할 수도 있다.

 

 

멤버

확장 타입의 본문에서 멤버를 선언하는 것은 클래스 멤버를 선언하는 방식과 동일하다. 확장 타입 멤버는 메서드, 게터, 세터 또는 연산자가 될 수 있다. 인스턴스 변수와 추상 멤버는 확장 타입 멤버가 될 수 없다.

extension type NumberE(int value) {
  // 연산자:
  NumberE operator +(NumberE other) =>
      NumberE(value + other.value);
  // Getter:
  NumberE get myNum => this;
  // 메서드:
  bool isValid() => !value.isNegative;
}

 

표현 타입의 인터페이스 멤버는 기본적으로 확장 타입의 인터페이스 멤버가 아니다. 표현 타입의 단일 멤버를 확장 타입에 사용 가능하게 하려면, 확장 타입 정의에서 해당 멤버는 선언해야 한다. 예를 들어, NumberE의 + 연산자가 이에 해당한다. 또한, 표현 타입과 관련없는 새로운 멤버를 정의할 수도 있다. 예를 들어, NumberE의 isValid가 이에 해당한다.

 

 

구현

옵션으로 implements 절을 사용하여, 확장 타입에 서브 타입 관계를 도입하고 표현 객체의 멤버를 확장 타입 인터페이스에 추가할 수 있다.

 

implements 절은 확장 메서드와 

 

확장 타입은 다음을 구현할 수 있다.

  • 표현 타입 자체
  • 표현 타입의 슈퍼 타입
  • 동일한 표현 타입에서 유효한 다른 확장 타입

 

 

표현 타입 자체

표현 타입 자체는 표현 타입의 모든 멤버를 확장 타입에서 사용할 수 있게 한다.

extension type NumberI(int i) implements int {
  // NumberI는 int의 모든 멤버를 호출할 수 있으며,
  // 여기에 선언된 추가 멤버도 사용할 수도 있다.
}

 

 

표현 타입의 슈퍼 타입

표현 타입의 슈퍼 타입은 슈퍼 타입의 멤버를 사용할 수 있지만, 표현 타입의 모든 멤버를 사용할 수 없다.

extension type Sequence<T>(List<T> _) implements Iterable<T> {
  // List의 슈퍼 타입인 Iterable의 모든 멤버를 호출할 수 있다.
}

 

 

동일한 표현 타입에서 유효한 다른 확장 타입

이는 다중 상속과 유사하게 여러 확장 타입에서 연산을 재사용할 수 있게 한다. 

extension type const Opt<T>._(({T value})? _) {
  const factory Opt(T value) = Val<T>;
  const factory Opt.none() = Non<T>;
}
extension type const Val<T>._(({T value}) _) implements Opt<T> {
  const Val(T value) : this._((value: value));
  T get value => _.value;
}
extension type const Non<T>._(Null _) implements Opt<Never> {
  const Non() : this._(null);
}

 

 

@redeclare

확장 타입에서 표현 타입의 멤버를 사용하고 있는데, 동일한 멤버를 선언하는 것은 클래스 간의 오버라이드(재정의) 관계와는 달리 재선언이 된다.

 

컴파일러에게 표현 타입의 멤버와 동일한 이름을 사용하는 것을 알고 있음을 알리기 위해 @redeclare 주석을 사용할 수 있다. @redeclare 주석을 사용하는데, 이름이 잘못 입력된 경우에는 분석기가 경고로 알려주게 된다.

extension type MyString(String _) implements String {
  // 'String.operator[]'를 대체합니다.
  @redeclare
  int operator [](int index) => codeUnitAt(index);
}

 

또한, @redeclare 주석을 사용하지 않고 재선언을 하는 경우, 경고를 받을 수 있도록 lint에 annotate_redeclares를 활성화할 수도 있다.

 

 

 

사용법

확장 타입을 사용하려면 클래스와 마찬가지로 생성자를 호출하여 인스턴스를 생성해야 한다.

extension type NumberE(int value) {
  NumberE operator +(NumberE other) =>
      NumberE(value + other.value);

  NumberE get next => NumberE(value + 1);
  bool isValid() => !value.isNegative;
}

void testE() {
  var num = NumberE(1);
}

 

그런 다음, 클래스 객체처럼 객체에서 멤버를 호출할 수 있다.

 

확장 타입의 두 가지 핵심 사용 사례는 다음과 같다.

  • 기존 타입에 확장된 인터페이스 제공
  • 기존 타입에 다른 인터페이스 제공

 

 

 

타입 고려사항

확장 타입은 컴파일 타임에 래핑되는 구조이다. 런타임에 확장 타입의 흔적이 전혀 없다.

 

이로 인해 확장 타입은 안전하지 않은 추상화가 될 수 있다. 런타임에 표현 타입을 찾아서 기본 객체에 접근할 수 있기 때문이다.

 

동적 타입 테스트(e is T), 캐스팅(e as T), 및 기타 런타임 쿼리(switch (e) ... 또는 if(e case ... ))는 모두 기본 표현 객체를 평가하며, 해당 객체의 런타임 타입에 대해 타입 체크를 수행한다. 이는 정적 타입이 확장 타입이거나 확장 타입에 대해 테스트할 때도 모두 해당된다.

void main() {
  var n = NumberE(1);

  // 'n'의 런타임 타입은 표현 타입 'int'입니다.
  if (n is int) print(n.value); // 1을 출력합니다.

  // 런타임에 'n'에서 'int' 메서드를 사용할 수 있습니다.
  if (n case int x) print(x.toRadixString(10)); // 1을 출력합니다.
  switch (n) {
    case int(:var isEven): print("$n (${isEven ? "even" : "odd"})"); // 1 (홀수)을 출력합니다.
  }
}

 

마찬가지로, 이 예제에서 일치하는 값의 정적 타입은 확장 타입이다.

void main() {
  int i = 2;
  if (i is NumberE) print("It is"); // 'It is'를 출력합니다.
  if (i case NumberE v) print("value: ${v.value}"); // 'value: 2'를 출력합니다.
  switch (i) {
    case NumberE(:var value): print("value: $value"); // 'value: 2'를 출력합니다.
  }
}

 

확장 타입을 사용할 때는 이 특성을 인식하는 것이 중요하다. 확장 타입이 컴파일 타임에 존재하고 중요하지만, 컴파일 중에 지워진다는 점을 항상 염두에 두어야 한다.

 

예를 들어, 정적 타입이 확장 타입 E인 표현식 e를 고려해 보자. E의 표현 타입이 R이라면, e의 런타임 타입은 R의 서브타입이다. 타입 자체도 지워지기 때문에 List<E>는 런타임에 List<R>와 동일하다.

 

다시 말해, 실제 래퍼 클래스는 래핑된 객체를 캡슐화할 수 있지만, 확장 타입은 래핑된 객체에 대한 컴파일 타임 뷰일 뿐이다. 실제 래퍼는 더 안전하지만, 확장 타입은 래퍼 객체를 피할 수 있는 옵션을 제공하며, 이는 일부 시나리오에서 성능을 크게 개선할 수 있다.

반응형

'다트 공식 문서 번역' 카테고리의 다른 글

다트] 클래스 수정자  (0) 2024.08.07
다트] 호출 가능한 객체  (0) 2024.08.07
다트] 확장 메서드  (0) 2024.08.04
다트] 열거형 타입 (enum)  (0) 2024.08.04
다트] 믹스인  (0) 2024.08.04
댓글
공지사항