Dart型システム
静的型、型推論、ジェネリクス、typedef
Dartは静的型言語でありながら型推論もサポートします。is/asで型チェックとキャストを行い、is検査後は自動的に型プロモーションが適用されます。ジェネリクスで型安全なコレクションとクラスを作れ、typedefで複雑な関数型に名前を付けられます。
기본 제공 타입
Dart는 정적 타입 언어로, 컴파일 시간에 타입 검사를 수행합니다. 아래는 Dart가 기본으로 제공하는 타입들입니다.
int integer = 42;
double decimal = 3.14;
num number = 10; // int와 double의 상위 타입
String text = '안녕하세요';
bool flag = true;
List<int> numbers = [1, 2, 3];
Map<String, dynamic> person = {'name': '홍길동', 'age': 30};
Set<String> uniqueNames = {'홍길동', '김철수', '이영희'};
Symbol symbol = #symbolName;
특수 타입: void, dynamic, Object
void printMessage() {
print('메시지 출력');
}
dynamic dynamicValue = '문자열';
dynamicValue = 42; // 다른 타입 재할당 가능
Object objectValue = 'Hello';
dynamic vs Object
dynamic은 타입 검사를 완전히 건너뜁니다. Object는 모든 Dart 객체의 최상위 타입이지만 메서드 호출 시 타입 검사가 이루어집니다.
타입 추론 (Type Inference)
var로 선언하면 초기값에서 타입이 자동 결정되며, 이후 다른 타입으로 변경할 수 없습니다.
var name = '홍길동'; // String으로 추론
var age = 30; // int로 추론
var height = 175.5; // double로 추론
var active = true; // bool로 추론
var items = [1, 2, 3]; // List<int>로 추론
var getName = () {
return '홍길동'; // 반환 타입도 추론
};
var people = [ // List<Map<String, Object>>로 추론
{'name': '홍길동', 'age': 30},
{'name': '김철수', 'age': 25},
];
타입 체크 & 캐스팅
is / is! 연산자로 타입을 확인하면, 해당 블록 안에서 자동으로 타입 프로모션이 적용됩니다.
Object value = '문자열';
if (value is String) {
// 이 블록 안에서 자동으로 String으로 캐스팅됨
print('문자열 길이: ${value.length}');
}
if (value is! int) {
print('정수가 아닙니다');
}
as 연산자는 명시적 캐스팅이며, 실패 시 런타임 에러가 발생합니다.
Object value = '문자열';
String text = value as String;
print(text.toUpperCase());
// int number = value as int; // 런타임 에러!
타입 프로모션 (Type Promotion)
is 검사 이후 해당 블록 안에서 자동으로 타입이 승격됩니다. 블록 바깥에서는 원래 타입으로 돌아갑니다.
Object value = '안녕하세요';
if (value is String) {
// 자동으로 String으로 승격
print('대문자: ${value.toUpperCase()}');
print('길이: ${value.length}');
}
// print(value.length); // 에러: Object에는 length가 없음
제네릭 클래스 (Generic Classes)
타입을 매개변수로 받아 재사용 가능한 클래스를 작성할 수 있습니다.
class Box<T> {
T value;
Box(this.value);
T getValue() {
return value;
}
void setValue(T newValue) {
value = newValue;
}
}
void main() {
var stringBox = Box<String>('안녕하세요');
print(stringBox.getValue()); // '안녕하세요'
var intBox = Box<int>(42);
print(intBox.getValue()); // 42
var doubleBox = Box(3.14); // Box<double>로 추론
}
제네릭 함수 (Generic Functions)
T first<T>(List<T> items) {
return items.first;
}
void main() {
var names = ['홍길동', '김철수', '이영희'];
var firstString = first<String>(names);
print(firstString); // '홍길동'
var numbers = [1, 2, 3, 4, 5];
var firstInt = first(numbers); // T가 int로 추론
print(firstInt); // 1
}
제네릭 타입 제한 (Type Constraints)
extends를 사용하여 제네릭 타입에 제한을 걸 수 있습니다.
class NumberBox<T extends num> {
T value;
NumberBox(this.value);
void square() {
print(value * value);
}
}
void main() {
var intBox = NumberBox<int>(10);
intBox.square(); // 100
var doubleBox = NumberBox<double>(2.5);
doubleBox.square(); // 6.25
// var stringBox = NumberBox<String>('오류'); // 컴파일 에러!
}
다중 제네릭 매개변수 & 제네릭 상속
class Pair<K, V> {
K first;
V second;
Pair(this.first, this.second);
}
// 제네릭 클래스를 상속하여 타입 고정
class IntBox extends Box<int> {
IntBox(int value) : super(value);
void increment() {
setValue(getValue() + 1);
}
}
// 타입 별칭
typedef StringList = List<String>;
typedef KeyValueMap<K, V> = Map<K, V>;
컬렉션과 제네릭
List, Map, Set은 모두 제네릭을 활용하여 타입 안전하게 사용할 수 있습니다.
// List
List<String> names = ['홍길동', '김철수', '이영희'];
var fruits = <String>['사과', '바나나', '오렌지'];
var numbers = List<int>.filled(5, 0); // [0, 0, 0, 0, 0]
var evens = List<int>.generate(5, (i) => i * 2); // [0, 2, 4, 6, 8]
var filteredNames = names.where((name) => name.length > 2).toList();
var mappedScores = [90, 85, 95].map((score) => score * 1.1).toList();
// Map
Map<String, int> ages = {
'홍길동': 30,
'김철수': 25,
'이영희': 28,
};
var config = Map<String, dynamic>();
config['debug'] = true;
config['timeout'] = 30;
// Set
Set<String> uniqueNames = {'홍길동', '김철수', '이영희'};
var colors = <String>{'빨강', '파랑', '녹색'};
var nums = Set<int>.from([1, 2, 3, 3, 4]); // {1, 2, 3, 4}
typedef — 타입 별칭 & 함수 타입
복잡한 함수 타입이나 타입에 이름을 부여하여 코드 가독성을 높일 수 있습니다.
// 함수 타입 별칭
typedef IntOperation = int Function(int a, int b);
int add(int a, int b) => a + b;
int subtract(int a, int b) => a - b;
void calculate(IntOperation operation, int x, int y) {
print('결과: ${operation(x, y)}');
}
void main() {
calculate(add, 10, 5); // 결과: 15
calculate(subtract, 10, 5); // 결과: 5
}
// Dart 2.13+ 일반 타입 별칭
typedef StringList = List<String>;
typedef UserInfo = Map<String, dynamic>;
void printNames(StringList names) {
for (var name in names) {
print(name);
}
}
void displayUserInfo(UserInfo user) {
print('이름: ${user[\'name\']}, 나이: ${user[\'age\']}');
}
Dart 3 패턴 매칭과 타입
Dart 3에서는 switch 표현식에서 타입 기반 패턴 매칭이 가능합니다.
Object value = '문자열';
switch (value) {
case String():
print('문자열: $value');
case int():
print('정수: $value');
default:
print('기타 타입: $value');
}
실전 예제: 제네릭 캐시 클래스
class Cache<T> {
final Map<String, T> _cache = {};
T? get(String key) => _cache[key];
void set(String key, T value) {
_cache[key] = value;
}
bool has(String key) => _cache.containsKey(key);
void remove(String key) => _cache.remove(key);
void clear() => _cache.clear();
}
void main() {
var stringCache = Cache<String>();
stringCache.set('greeting', '안녕하세요');
print(stringCache.get('greeting')); // '안녕하세요'
var userCache = Cache<Map<String, dynamic>>();
userCache.set('user1', {'name': '홍길동', 'age': 30});
var user = userCache.get('user1');
print('사용자: ${user?[\'name\']}, 나이: ${user?[\'age\']}');
}
실전 예제: Result 타입 패턴
제네릭을 활용하여 성공/실패를 타입 안전하게 처리하는 Result 패턴입니다.
abstract class Result<S, E> {
factory Result.success(S value) = Success<S, E>;
factory Result.failure(E error) = Failure<S, E>;
bool get isSuccess;
bool get isFailure;
S? get value;
E? get error;
void when({
required void Function(S value) success,
required void Function(E error) failure,
});
}
class Success<S, E> extends Result<S, E> {
final S _value;
Success(this._value);
@override bool get isSuccess => true;
@override bool get isFailure => false;
@override S get value => _value;
@override E? get error => null;
@override
void when({
required void Function(S value) success,
required void Function(E error) failure,
}) => success(_value);
}
class Failure<S, E> extends Result<S, E> {
final E _error;
Failure(this._error);
@override bool get isSuccess => false;
@override bool get isFailure => true;
@override S? get value => null;
@override E get error => _error;
@override
void when({
required void Function(S value) success,
required void Function(E error) failure,
}) => failure(_error);
}
사용법:
Result<String, Exception> fetchData() {
try {
return Result.success('데이터');
} catch (e) {
return Result.failure(Exception('오류: $e'));
}
}
void main() {
var result = fetchData();
result.when(
success: (data) => print('성공: $data'),
failure: (error) => print('실패: $error'),
);
}
참고: fpdart 패키지
더 풍부한 함수형 프로그래밍 기능이 필요하다면 fpdart 패키지를 참고하세요. Either, Option 등 강력한 타입을 제공합니다.
타입 시스템 비교 표
| 키워드 | 타입 검사 | 용도 |
|---|---|---|
| var | 컴파일 타임 추론 | 타입 추론 (변경 불가) |
| dynamic | 없음 (런타임) | 아무 타입이나 할당 가능 |
| Object | 컴파일 타임 | 모든 타입의 상위 타입 |
| is / is! | 런타임 체크 | 타입 확인 + 프로모션 |
| as | 런타임 캐스트 | 명시적 타입 캐스팅 |
| T extends X | 컴파일 타임 | 제네릭 타입 제한 |
実装ステップ
型推論 — varで宣言すると初期値から型が決定、以降別の型は代入不可
is/is!と型プロモーション — if (obj is String)ブロック内でStringメソッドが自動利用可能
ジェネリクス — List<T>、Map<K,V>で型安全なコレクション、extendsで型制約可能
typedef — 複雑な関数型に名前を付与、typedef Compare<T> = int Function(T a, T b)
メリット
- ✓ コンパイル時型チェックでランタイムエラーを事前防止
- ✓ 型プロモーションでis検査後キャストなしに直接使用可能
デメリット
- ✗ dynamicの乱用で型安全性が崩壊