🔒

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컴파일 타임제네릭 타입 제한

実装ステップ

1

型推論 — varで宣言すると初期値から型が決定、以降別の型は代入不可

2

is/is!と型プロモーション — if (obj is String)ブロック内でStringメソッドが自動利用可能

3

ジェネリクス — List<T>、Map<K,V>で型安全なコレクション、extendsで型制約可能

4

typedef — 複雑な関数型に名前を付与、typedef Compare<T> = int Function(T a, T b)

メリット

  • コンパイル時型チェックでランタイムエラーを事前防止
  • 型プロモーションでis検査後キャストなしに直接使用可能

デメリット

  • dynamicの乱用で型安全性が崩壊

ユースケース

型安全なAPIレスポンスモデルクラス設計