Docs의 내용을 살펴보자.

개발 배경

자료형에 대한 Wrapper class를 실제로 만드는 것은 비용이 든다. Actual Wrapper는 실제로 코드상에 존재해야 하며 런타임에도 더 오버헤드가 생길 수 있다.

Extension Types는 Dart 3.3에 등장한 새로운 문법으로 암시적으로 어떤 Representation Type에 대한 Wrapping을 해주는 역할이다.

과연 이것이 그렇게 큰 성능적 역할을 할까? 에 대해서는 사실 잘 모르겠다. 어떤 Wrapper class가 실제로 매우 많이 생성이 되는 경우가 아니라면 그렇게 큰 역할을 하지 않을 것 같다.

하지만 꼭 성능적인 목적뿐만이 아닌 어떤 Wrapper를 암시적으로 정의해줌으로써 객체지향적으로도 이점을 챙길 수 있는데, 그냥 Extension Method를 정의한다면 그 Method가 사용될 맥락이 아님에도 사용을 할 수 있게 되지만, 제한적인 Wrapper를 만들고 그것을 타입으로 사용하면 그럴일이 없이 때문이다.

가령 다음과 같다.

extension IntDateExtension on int {
  DateTime get date => DateTime.fromMillisecondsSinceEpoch(this);
}

extension type MyDateTime(int raw) {
  DateTime get date => DateTime.fromMillisecondsSinceEpoch(raw);
}

void main() {
  int a = 1709168525000;
  MyDateTime b = MyDateTime(1709168525000);

  print(a.date);
  print(b.date);
}

만약 int 가 다른 곳에서 date time을 나타낼 의도로 쓰이지 않는데 .date 라는 getter가 추천이 되기 때문에 완전한 캡슐화가 되었다고 보기 어렵지만, MyDateTime을 사용하면 int 형에서 date getter를 사용할 수 없기 때문에 안전하다.

Cross Platform Interop

Type Extension이 꼭 성능상 이점뿐 아니라 다른 언어(특히 JavaScript)와의 Interop에 대하여 편의성을 제공하기 위해 개발된 목적도 있다.

이번에 dart:js_interop 도 새롭게 발표되면서 JavaScript와의 Interop에 대해 발전이 되었다.

문법

Declaration

다음과 같이 선언한다. 꼭 인자 한개를 Representation Type으로 넣어주어야 한다.

extension type E(int i) {
  // Define set of operations.
}

이 extension type 블록 내부에서 ithis.i 나 그냥 i로 참조할 수 있다. 외부에선 다음과 같이 참조할 수 있다.

var a = E(1);
print(a.i) // 1

제네릭(Type parameters)도 사용할 수 있다.

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

Constructors

이런 저런 생성자를 만들 수 있고, 생성자를 만들지 않으면 자동으로 Representation Type인자를 하나 받는 생성자가 존재하게 된다.

Non-redirecting Generative Constructor들은 항상 Representation Type을 초기화시켜주어야 한다.

예시는 다음과 같다.

extension type E(int i) {
  E.n(this.i);
  E.m(int j, String foo) : i = j + foo.length;
}

void main() {
  E(4); // Implicit unnamed constructor.
  E.n(3); // Named constructor.
  E.m(5, "Hello!"); // Named constructor with additional parameters.
}

기본 생성자를 변형하는 방법은 다음과 같다.

extension type const E._(int it) {
  E(String t): this._(int.parse(t));
}

void main() {
  var a = E('1');
}

생성자를 외부에 감추는 방식은 싱글톤 객체를 만드는 패턴과 유사한데, _ 생성자를 만들어주면 된다.

{1 2}

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

Members

당연히 getter, setter가 아닌 멤버 변수는 가질 수 없다.

단, external로 선언된 변수는 제외

그 외의 것들은 대부분 허용된다고 볼 수 있다.

중요한 점은 Representation Type의 Interface Member들이 자동으로 Extension Type에서 참조할 수 있게 되지 않는다는 점이다.

extension type const MyInt(int raw) {}

void main() {
  var a = MyInt(1);
  var b = MyInt(2);

  print(a + b); // error
}

MyInt 엔 operator +overload 되어있지 않기 때문에 에러가 난다.

Implements

위의 예시에서 int의 모든 기능을 가져오지못해서 아쉬웠다면 implements를 사용하면 가능하다.

extension type const MyInt(int raw) implements int {}

void main() {
  var a = MyInt(1);
  var b = MyInt(2);

  print(a + b);
}

implements 뒤에 올 수 있는 타입종류는 몇 가지가 있다.

  • Representation Type 그 자체: 위의 예시처럼 int 로 동일하다.
  • Representation Type 의 Super Type: 위의 예시에서 intnum으로 바꾸어도 동작한다.
extension type const MyInt(int raw) implements int {}

void main() {
  var a = MyInt(1);
  var b = MyInt(2);

  print(a + b);
}
  • 다른 Extension Type

@redeclare

만약 super type을 implements 했는데 동일한 이름의 getter나 메서드를 선언하는 것은 상속에서의 override 개념보다는 완전한 replace의 개념으로 생각된다.

따라서 이를 선언하기 위해 @redeclare annotation을 붙일 수 있고, annotate_redeclares linter rule을 켜 이것이 지켜지지 않았을 시 warn을 띄울 수 있다.

extension type MyString(String _) implements String {
  // Replaces 'String.operator[]'
  @redeclare
  int operator [](int index) => codeUnitAt(index);
}

사용처

사실상 Class랑 크게 다르지 않게 선언 및 사용이 가능하다.

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);
}

크게 두 가지를 고려해서 사용할 수 있다.

  1. 기존 타입을 extend한다는 개념으로 사용
  2. 기존 타입을 사용하지만 다른 인터페이스를 제공하여 사용

고려할 사항

Extension Type은 컴파일러의 관점에서 보자면 완벽한 캡슐화가 아니다.

이게 무슨말이냐면 결국 런타임에 e is T 와 같은 문법이나 switch 패턴에서 e 는 우리의 코드에선 MyInt 여도 결국 내부 Representation Type인 int로 해석이 된다는 의미이다.

즉, 우리의 의도와 다르게 e is inttrue가 될 수 있다.

반대로 int 자체가 int 를 Representation Type으로 가지는 타입이 맞냐 물었을 때 런타임에선 “그렇다” 라고 결론지어진다.

다음과 같다.

void main() {
  int i = 2;
  if (i is NumberE) print("It is"); // Prints 'It is'.
  if (i case NumberE v) print("value: ${v.value}"); // Prints 'value: 2'.
  switch (i) {
    case NumberE(:var value): print("value: $value"); // Prints 'value: 2'.
  }
}

Type Extension은 항상 컴파일 시에 타입이 지워져 그것의 Representation Type으로 변환된다는 사실만 기억하면 된다.

결국 Type Extension은 Actual Wrapper Class에 비해 타입적으로 완전히 안전하지는 않지만 특정한 상화에서 성능상 이점을 가져다 준 다는 점에서 유의미하다는 결론이 난다.

Categories:

Updated:

Comments