Dart Extension Types
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 블록 내부에서 i
를 this.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: 위의 예시에서
int
를num
으로 바꾸어도 동작한다.
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);
}
크게 두 가지를 고려해서 사용할 수 있다.
- 기존 타입을 extend한다는 개념으로 사용
- 기존 타입을 사용하지만 다른 인터페이스를 제공하여 사용
고려할 사항
Extension Type은 컴파일러의 관점에서 보자면 완벽한 캡슐화가 아니다.
이게 무슨말이냐면 결국 런타임에 e is T
와 같은 문법이나 switch
패턴에서 e
는 우리의 코드에선 MyInt
여도 결국 내부 Representation Type인 int
로 해석이 된다는 의미이다.
즉, 우리의 의도와 다르게 e is int
가 true
가 될 수 있다.
반대로 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에 비해 타입적으로 완전히 안전하지는 않지만 특정한 상화에서 성능상 이점을 가져다 준 다는 점에서 유의미하다는 결론이 난다.
Comments