Skip to main content

Dart의 확장된 메소드

Dart2.19.6About 1 min

원문 : https://dart.dev/language/extension-methodsopen in new window

확장된 메소드은 기존의 라이브러리에 기능요소를 추가하는 것 입니다.
아마 자세히 알지 못한 상태에서 확장된 메소드를 사용하고 있을 것 입니다.
예로 들어, IDE에서 코드완성을 사용할 때 일반 메소드말고 확장된 메소드를 제안할 것 입니다.

이 비디오를 시청하시면 확장된 메소드에 대한 개요를 배울 수 있습니다.

개요

몇몇 API를 사용하거나 널리사용되는 라이브러리를 구현할 때 API를 바꾸는 것이 비실용적이거나 불가능할 때가 많습니다.
그러나 일부 기능을 추가하고 싶을 때가 있습니다.

예로 들어, 문자열을 정수로 파싱하는 아래 코드를 고려해보겠습니다.

int.parse('42')

대신 문자열에 이 기능을 가지고 있다면, 툴에서 좀더 짧고 쉽게 사용할 수 있을 것 입니다.

'42'.parseInt()

위 코드가 가능하려면 String 클래스의 확장을 포함한 라이브러리를 가져와야 합니다.

import 'string_apis.dart';
// ···
print('42'.parseInt()); // 확장된 함수 사용하기

확장은 메소드 뿐만아니라 getter, setter와 연산자 등 다른 멤버도 가능합니다.
또한, 확장은 API 충돌이 발생할때 도움이 되도록 명명할 수 있습니다.
아래에서 문자열의 연산자인 확장(NumberParsing)을 사용하여 확장된 메소드 parseInt()를 어떻게 구현하는지 보여줍니다.

extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }
  // ···
}

다음 절에서는 확장된 메소드를 어떻게 사용하는지 보겠습니다.
그 후 확장된 메소드를 구현하는 것을 살펴보겠습니다.

확장된 메소드 사용하기

모든 Dart 코드 처럼, 확장된 메소드는 라이브러리에 있습니다.
이미 확장된 메소드를 어떻게 사용하는지 봤을 것 입니다.
라이브러리 자체를 가져와서 평범한 메소드처럼 사용하면 됩니다.

// String에 확장을 가지고 있는 라이브러리를 가져옵니다.
import 'string_apis.dart';
// ···
print('42'.padLeft(5)); // String 메소드를 사용
print('42'.parseInt()); // 확장된 메소드를 사용

확장된 메소드를 사용하기 위해 알아야될 전부 입니다.
코드를 작성할 때 확장된 메소드를 정적 타입(dynamic과 반대)에 의존시키고 API 충돌을 해결해야하는 방법이 필요할 수 있습니다.

정적 타입과 dynamic

변수의 타입이 dynamic일 경우 확장된 메소드를 실행할 수 없습니다.
예로 들어, 아래 코드의 결과는 런타임 예외가 발생합니다.

dynamic d = '2';
print(d.parseInt()); // 런타임 예외: NoSuchMethodError

확장된 메소드는 Dart의 타입추론에도 동작합니다.
아래 코드는 변수 vString 타입을 가지도록 추론되어 정상동작을 합니다.

var v = '2';
print(v.parseInt()); // 출력 : 2

dynamic이 동작하지 않은 이유는 확장된 메소드가 정적 타입의 수신자에 대해 처리되기 때문입니다.
확장된 메소드가 정적으로 처리되기 때문에 정적 함수를 호출하는 것처럼 빠릅니다.

정적 타입과 dynamic에 대한 자세한 내용은 Dart 타입 시스템을 참고하세요.

API 충돌

확장된 멤버가 인터페이스나 다른 확장된 멤버와 충돌할 경우 몇가지 선택지가 있습니다.

한가지는 showhide를 사용하여 노출되는 API를 제한하는 충돌된 확장을 가져오는 것으로 변경합니다.

// String 확장된 메소드 parseInt()를 정의
import 'string_apis.dart';

// 동일하게 parseInt()를 정의했지만 NumberParsing2를 숨겨서 확장된 메소드를 숨김
import 'string_apis_2.dart' hide NumberParsing2;

// ···
// 'string_apis.dart'에 정의된 parseInt()
print('42'.parseInt());

다른방법으로는 확장이 래퍼클래스처럼 보이도록 명시적 확장을 사용하는 것 입니다.

// 두 라이브러리 모두 다른 이름을 가진 확장으로 문자열의 확장인 parseInt()를 정의합니다.
import 'string_apis.dart'; // NumberParsing 확장을 포함
import 'string_apis_2.dart'; // NumberParsing2 확장을 포함

// ···
// print('42'.parseInt()); // 동작하지 않음
print(NumberParsing('42').parseInt());
print(NumberParsing2('42').parseInt());

만약 확장의 이름이 동일하다면 접두어를 사용하여 가져와야됩니다.

// 두 라이브러리 모두 다른 이름을 가진 확장으로 문자열의 확장인 parseInt()를 정의합니다.
// 또한 'string_apis_3.dart'의 NumberParsing 확장은 parseNum()을 정의합니다.
import 'string_apis.dart';
import 'string_apis_3.dart' as rad;

// ···
// print('42'.parseInt()); // 동작하지 않음

// string_apis.dart의 ParseNumbers 확장을 사용
print(NumberParsing('42').parseInt());

// string_apis_3.dart의 ParseNumbers 확장을 사용
print(rad.NumberParsing('42').parseInt());

// string_apis_3.dart만 parseNum()를 가짐
print('42'.parseNum());

예제가 보여주듯 접두어를 사용하여 가져오더라도 확장된 메소드를 암묵적으로 실행할 수 있습니다.
확장을 명시적으로 실행할 때 이름이 충돌되는 것을 피하고자 할때만 접두어를 사용하시면 됩니다.

확장된 메소드 구현하기

아래의 문법이 확장을 생성하는 것 입니다.

extension <extension name>? on <type> {
  (<member definition>)*
}

예로 들어, String 클래스에 확장을 구현하는 것 입니다.

extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }

  double parseDouble() {
    return double.parse(this);
  }
}

확장의 멤버는 메소드, getter, setter 또는 연산자가 될 수 있습니다.
또한 확장은 정적 필드와 정적 도우미 메소드를 가질 수 있습니다.

이름없는 확장

확장을 선언할 때 이름을 생략할 수 있습니다.
이름없는 확장은 선언된 라이브러리 내에서만 보입니다.
이름을 가지지 않기 때문에, API 충돌을 해결하기 위해 명시적으로 적용할 수 없습니다.

extension on String {
  bool get isBlank => trim().isEmpty;
}

Note

이름없는 확장의 정적 멤버는 확장 선언 내에서만 실행할 수 있습니다.

제너릭 확장 구현하기

확장은 제너릭 타입 매개변수를 가질 수 있습니다.
예로 들어, 내장형 List<T> 타입에 getter, 연산자, 메소드를 확장하는 코드입니다.

extension MyFancyList<T> on List<T> {
  int get doubleLength => length * 2;
  List<T> operator -() => reversed.toList();
  List<List<T>> split(int at) => [sublist(0, at), sublist(at)];
}

T 타입은 메소드가 호출되는 list의 정적 타입에 기반으로 할당됩니다.

자료

확장된 메소드에 더 자세한 정보는 아래를 참고하세요.