Contents

제네릭

기본 배열 타입의 API 문서를 보면, List 타입이 List<E>로 표기되어 있는 걸 볼 수 있습니다. <…> 표시는 List를 형식 타입 매개변수를 가지는 제네릭 (또는 매개변수화된) 타입으로 지정합니다. 관례상 대부분의 타입 변수는 E, T, S, K, V 같은 single-letter 이름을 가집니다.

제네릭을 왜 사용할까?

보통 타입 세이프티 때문에 제네릭을 사용하지만, 사실 더 많은 기능을 수행합니다:

  • 제네릭 타입을 적절하게 명시한 코드는 더 잘 작성된 코드 입니다.
  • 코드 중복를 줄이기 위해 제네릭을 사용 할 수 있습니다.

리스트가 문자열 값만 가지게 하고 싶다면, List<String>로 리스트를 선언하면 됩니다. 그렇게 함으로써, 동료와 개발 툴이 문자열 이외의 값은 리스트에 추가될 수 없음을 바로 알 수 있습니다:

var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // Error

제네릭을 사용하는 또 다른 이유는 코드 중복을 줄이기 위함입니다. 제네릭은 정적인 분석의 이점을 챙기면서, 많은 타입들이 단일 인터페이스와 구현을 공유할 수 있게 합니다. 예를 들어, 객체를 캐싱하는 인터페이스를 생성한다고 해봅시다:

abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}

해당 인터페이스의 문자열 버전이 필요하다면 다음과 같이 선언하면 됩니다:

abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}

이후에 number 버전이 필요해 졌다면… 어떻게 하는 게 좋을까요?

제네릭 타입은 위처럼 모든 인터페이스를 생성해야하는 문제를 해결해줍니다. 타입 매개변수를 가지는 하나의 단일 인터페이스만을 구현하면 됩니다:

abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
}

위의 코드에서, T는 대체 타입으로 개발자가 추후에 타입을 마음대로 지정할 수 있게 해주는 플레이스 홀더입니다.

컬렉션 리터럴 사용하기

List, set 그리고 map 리터럴은 매개변수화 될 수 있습니다. 매개변수화된 리터럴은 list, set에 <type> 또는 map에 <keyType, valueType>를 시작 괄호에 추가하는 것만 빼면, 일반적으로 사용하는 리터럴과 비슷하게 생겼습니다. 다음은 타입이 있는 리터럴의 예제입니다:

var names = <String>['Seth', 'Kathy', 'Lars'];
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};
var pages = <String, String>{
  'index.html': 'Homepage',
  'robots.txt': 'Hints for web robots',
  'humans.txt': 'We are people, not machines'
};

생성자에 매개변수화된 타입 사용하기

생성자를 사용할 때 하나 혹은 다수의 타입을 특정하고 싶다면, 타입을 클래스 이름 다음의 <...> (angle brackets) 안에 넣으세요:

var nameSet = Set<String>.from(names);

다음 예제에서는 정수 키와 View 타입의 값을 가지는 map을 생성합니다:

var views = Map<int, View>();

제네릭 컬렉션과 제네릭 컬렉션의 타입

Dart 제네릭 타입은 구체화 되어있습니다. 그것은 런타임에 타입들에 대한 정보를 가져온다는 것을 의미합니다. 예를 들어, 콜렉션의 타입을 다음과 같이 테스트 할 수 있습니다:

var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true

매개변수화된 타입 제한하기

제네릭 타입을 구현할 때, 인자로 제공되는 타입을 제한해서 인자가 특정 타입의 서브타입이 되게 해야 할 경우가 발생합니다. extends를 사용하면 가능합니다.

Non-nullalbe인 것을 보장하기 위해, 디폴트인 Object? 대신 Object의 서브타입으로 만들 때 자주 사용됩니다.

class Foo<T extends Object> {
  // Foo에게 제공되는 T 타입은 반드시 non-nullable 입니다.
}

Object 이외의 타입들과 함께 extends를 사용 할 수 있습니다. 다음은 SomeBaseClass를 확장하는 예로, SomeBaseClass의 멤버들은 타입 T의 객체로 볼 수 있습니다:

class Foo<T extends SomeBaseClass> {
  // 클래스 구현 ...
  String toString() => "Instance of 'Foo<$T>'";
}

class Extender extends SomeBaseClass {...}

SomeBaseClass나 이것의 서브타입을 제네릭 인자로 사용하는 것도 가능합니다:

var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();

제네릭 인자를 특정하지 않는 것도 가능합니다:

var foo = Foo();
print(foo); // 'Foo<SomeBaseClass>'의 인스턴스

Non-SomeBaseClass 타입으로 특정하는 것은 에러를 발생시킵니다:

var foo = Foo<Object>();

제네릭 메서드 사용하기

메서드와 함수에도 타입 인자를 사용할 수 있습니다:

T first<T>(List<T> ts) {
  // 초기 작업 또는 에러 확인, 그리고 ...
  T tmp = ts[0];
  // 추가적인 확인 또는 프로세싱 ...
  return tmp;
}

first (<T>)에 있는 제네릭 타입 매개변수로 여러 위치에서 타입 인자인 T를 사용할 수 있습니다.

  • 함수의 반환 타입 (T).
  • 인자의 타입 (List<T>).
  • 지역 변수의 타입 (T tmp).