Dart Extension
既存クラスにメソッド/プロパティを追加
Extensionは元のクラスを修正や継承せずにメソッド、プロパティ、演算子を追加できる機能です。Stringにcapitalize、intにisPrime、Listにdistinctのようなユーティリティを型に直接接続でき、コード可読性が大幅に向上します。ジェネリクスもサポートし、Flutterでのウィジェット拡張に非常に有用です。
Extension 기본 문법
Extension은 기존 클래스의 소스 코드를 수정하거나 상속하지 않고도 새로운 메서드, getter, setter, 연산자를 추가할 수 있는 기능입니다.
extension <ExtensionName> on <TargetType> {
// methods, getters, setters, operators
}
String 확장 예제
첫 글자 대문자 변환, 이메일 검증, 반복, 타이틀 케이스 등 문자열 유틸리티를 추가합니다.
extension StringExtension on String {
String get capitalize =>
isNotEmpty ? '${this[0].toUpperCase()}${substring(1)}' : '';
bool get isValidEmail =>
RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(this);
String repeat(int n) => List.filled(n, this).join();
String toTitleCase() {
return split(' ')
.map((word) => word.isNotEmpty
? '${word[0].toUpperCase()}${word.substring(1)}'
: '')
.join(' ');
}
}
// 사용 예시
String name = 'john doe';
print(name.capitalize); // John doe
print(name.toTitleCase()); // John Doe
print('hello'.repeat(3)); // hellohellohello
print('test@example.com'.isValidEmail); // true
int 확장 예제
초 단위를 시:분:초로 변환, 소수 판별, 팩토리얼 계산 등 정수에 수학 연산을 추가합니다.
extension IntExtension on int {
String toTimeString() {
int h = this ~/ 3600;
int m = (this % 3600) ~/ 60;
int s = this % 60;
return '${h.toString().padLeft(2, '0')}:'
'${m.toString().padLeft(2, '0')}:'
'${s.toString().padLeft(2, '0')}';
}
bool get isPrime {
if (this <= 1) return false;
if (this <= 3) return true;
if (this % 2 == 0 || this % 3 == 0) return false;
int i = 5;
while (i * i <= this) {
if (this % i == 0 || this % (i + 2) == 0) return false;
i += 6;
}
return true;
}
int get factorial {
if (this < 0) throw ArgumentError('음수의 팩토리얼은 정의되지 않습니다.');
if (this <= 1) return 1;
return this * (this - 1).factorial;
}
}
// 사용 예시
print(3665.toTimeString()); // 01:01:05
print(7.isPrime); // true
print(5.factorial); // 120
List<T> 제네릭 확장 예제
제네릭 타입 파라미터를 사용하여 모든 리스트 타입에 적용 가능한 유틸리티를 만듭니다.
extension ListExtension<T> on List<T> {
T? get firstOrNull => isEmpty ? null : first;
T? get lastOrNull => isEmpty ? null : last;
List<T> get distinct => toSet().toList();
List<List<T>> chunk(int size) {
return List.generate(
(length / size).ceil(),
(i) => sublist(
i * size,
(i + 1) * size > length ? length : (i + 1) * size,
),
);
}
}
// 사용 예시
List<int> numbers = [1, 2, 3, 4, 5, 1, 2];
print(numbers.distinct); // [1, 2, 3, 4, 5]
List<String> fruits = ['사과', '바나나', '오렌지', '딸기', '포도'];
print(fruits.chunk(2)); // [[사과, 바나나], [오렌지, 딸기], [포도]]
List<int> empty = [];
print(empty.firstOrNull); // null
Getter/Setter — 타입 파싱 확장
String에 안전한 타입 변환 getter를 추가하여 int, double, bool로 파싱합니다.
extension NumberParsing on String {
int? get asIntOrNull => int.tryParse(this);
double? get asDoubleOrNull => double.tryParse(this);
bool get asBool {
final lower = toLowerCase();
return lower == 'true' || lower == '1'
|| lower == 'yes' || lower == 'y';
}
}
print('42'.asIntOrNull); // 42
print('3.14'.asDoubleOrNull); // 3.14
print('abc'.asIntOrNull); // null
print('YES'.asBool); // true
정적 멤버 (Static Members)
Extension에 static 메서드를 정의할 수 있습니다. 단, 호출 시 Extension 이름을 명시해야 합니다.
extension DateTimeExtension on DateTime {
String get formattedDate =>
'$year-${month.toString().padLeft(2, '0')}-'
'${day.toString().padLeft(2, '0')}';
static DateTime fromFormattedString(String s) {
final parts = s.split('-');
if (parts.length != 3) {
throw FormatException('잘못된 날짜 형식: $s');
}
return DateTime(
int.parse(parts[0]),
int.parse(parts[1]),
int.parse(parts[2]),
);
}
static DateTime get tomorrow =>
DateTime.now().add(Duration(days: 1));
}
// 인스턴스 메서드는 직접 호출
print(DateTime.now().formattedDate); // 2023-11-15
// static 메서드는 Extension 이름으로 호출
final date = DateTimeExtension.fromFormattedString('2023-11-15');
print(DateTimeExtension.tomorrow);
제네릭 Extension — Nullable 타입 확장
T? (nullable) 타입에 Extension을 적용하면 null 안전 유틸리티를 만들 수 있습니다.
extension OptionalExtension<T> on T? {
T orDefault(T defaultValue) => this ?? defaultValue;
R? mapIf<R>(R Function(T) mapper) =>
this != null ? mapper(this as T) : null;
void ifPresent(void Function(T) action) {
if (this != null) action(this as T);
}
}
String? nullableString = null;
print(nullableString.orDefault('기본값')); // 기본값
int? nullableNumber = 42;
print(nullableNumber.orDefault(0)); // 42
String? name = '홍길동';
int? length = name.mapIf((n) => n.length); // 3
name.ifPresent((n) => print('안녕하세요, $n님!')); // 안녕하세요, 홍길동님!
이름 충돌 해결
동일 타입에 같은 이름의 메서드를 가진 Extension이 여러 개 있으면 컴파일 에러가 발생합니다. 이때 Extension 이름을 명시하여 해결합니다.
extension NumberParsing on String {
int parseInt() => int.parse(this);
}
extension StringParsing on String {
int parseInt() => int.parse(this) * 2;
}
void main() {
// '42'.parseInt(); // 컴파일 에러! 어떤 Extension인지 모호함
// Extension 이름을 명시하여 해결
print(NumberParsing('42').parseInt()); // 42
print(StringParsing('42').parseInt()); // 84
}
핵심 정리
- ✓Extension은 원본 클래스를 수정하지 않고 기능을 추가하는 강력한 도구
- ✓getter, setter, 메서드, 연산자 모두 추가 가능
- ✓제네릭 Extension으로 다양한 타입에 범용 유틸리티 구현
- ✓static 멤버는 Extension 이름으로만 호출 가능
- ⚠dynamic 타입에서는 Extension 메서드 호출 불가
- ⚠이름 충돌 시 Extension 이름을 명시하거나 import show/hide로 제어
実装ステップ
String拡張 — capitalize、isValidEmail、repeat等ユーティリティメソッド追加
int拡張 — toTimeString()、isPrime、factorial等数学演算
List<T>拡張 — ジェネリクスでfirstOrNull、distinct、chunk(n)等安全なコレクションユーティリティ
名前衝突解決 — extension MyStringExt on Stringのように明示的命名、importでshow/hideで制御
メリット
- ✓ 元のクラスを修正せず機能拡張可能
- ✓ ユーティリティ関数を型に直接接続してコード可読性向上
デメリット
- ✗ Extensionメソッドはdynamic型では呼び出し不可