출시·고도화 중
Dart 안내서 · 3/6
Dart의 타입 시스템은 건전한(sound) 정적 타입 시스템입니다. 컴파일러가 타입이 맞는다고 판단한 코드는 실행 중에도 그 타입을 지킨다는 뜻입니다. 여기에 null 안전성이 더해져, null이 들어갈 수 있는 곳과 없는 곳을 타입에서 구분합니다. 이 장에서는 null 안전성, 컬렉션, 클래스와 생성자, 레코드와 패턴, 열거형을 살펴봅니다.
자주 쓰는 기본 타입은 int, double, num(둘의 상위 타입), String, bool입니다. 모든 값은 객체이며, 숫자도 메서드를 가집니다. 모든 타입의 맨 위에는 Object?가 있고, dynamic을 쓰면 정적 타입 검사를 끌 수 있지만 꼭 필요할 때만 씁니다.
Dart 3부터 null 안전성은 항상 켜져 있습니다. 타입 이름 뒤에 ?를 붙여야만 null을 담을 수 있습니다.
int count = 0; // null 을 담을 수 없음
int? maybe; // null 을 담을 수 있음(기본값 null)
int lengthOf(String? text) {
if (text == null) return 0;
return text.length; // 위에서 null 검사를 했으므로 String 으로 승격
}
void main() {
print(lengthOf(null)); // 0
print(lengthOf('dart')); // 4
String? input = 'abc';
print(input!.toUpperCase()); // ! 는 "null 이 아님"을 단언(틀리면 실행 오류)
maybe ??= 10;
print(count + maybe!);
}컴파일러는 if (text == null) return 같은 검사를 따라가며 변수의 타입을 좁혀 줍니다. 이것을 타입 승격(promotion)이라고 합니다. ! 연산자는 컴파일러보다 개발자가 더 확실히 알 때만 쓰고, 가능하면 ??나 검사로 처리하는 편이 안전합니다.
| 타입 | 특징 | 리터럴 |
|---|---|---|
List<E> | 순서가 있고 중복을 허용합니다 | [1, 2, 3] |
Set<E> | 중복이 없는 집합입니다 | {'a', 'b'} |
Map<K, V> | 키로 값을 찾습니다 | {'a': 1} |
컬렉션은 제네릭 타입이라 원소 타입이 검사됩니다. map, where, fold 같은 메서드는 지연 계산되는 Iterable을 돌려주므로, 리스트가 필요하면 toList()를 붙입니다.
void main() {
final numbers = <int>[5, 3, 8, 1];
numbers.add(10);
numbers.sort();
final evens = numbers.where((n) => n.isEven).toList();
final doubled = numbers.map((n) => n * 2).toList();
final total = numbers.fold(0, (sum, n) => sum + n);
print('$evens $doubled $total');
final visited = <String>{'서울'};
print(visited.add('서울')); // false: 이미 있음
final stock = <String, int>{'사과': 3, '배': 0};
stock['귤'] = 7;
stock.update('사과', (v) => v + 1);
for (final MapEntry(:key, :value) in stock.entries) {
print('$key: $value');
}
print(stock['없는 키']); // null: 맵 조회 결과는 V? 입니다
}const [1, 2]처럼 만든 컬렉션이나 List.unmodifiable로 만든 컬렉션은 바꿀 수 없으며, 바꾸려 하면 실행 중 오류가 납니다.
Dart는 클래스 기반 객체 지향 언어입니다. 생성자에서 this.필드로 매개변수를 바로 필드에 넣을 수 있고, 이름 있는 생성자, 초기화 목록, const 생성자, factory 생성자를 지원합니다. 이름이 _로 시작하는 멤버는 라이브러리 밖에서 보이지 않습니다.
class Point {
final double x;
final double y;
const Point(this.x, this.y); // const 생성자
const Point.origin() : x = 0, y = 0; // 이름 있는 생성자 + 초기화 목록
factory Point.fromJson(Map<String, dynamic> json) {
return Point(json['x'] as double, json['y'] as double);
}
double get distanceSquared => x * x + y * y; // 게터
Point operator +(Point other) => Point(x + other.x, y + other.y);
@override
String toString() => 'Point($x, $y)';
}
void main() {
const a = Point(1, 2);
final b = Point.fromJson({'x': 3.0, 'y': 4.0});
print(a + b); // Point(4.0, 6.0)
print(b.distanceSquared); // 25.0
}Dart의 모든 클래스는 암묵적으로 인터페이스이기도 해서 implements로 다른 클래스의 인터페이스를 구현할 수 있습니다. 추상 클래스는 abstract class로 선언합니다. Dart 3에는 sealed, final, base, interface 같은 클래스 수식어가 추가되어, 라이브러리 밖에서 상속이나 구현을 허용할지 정할 수 있습니다.
레코드는 여러 값을 하나로 묶는 익명의 불변 타입입니다. 클래스를 따로 만들지 않고 함수에서 값 여러 개를 돌려줄 때 편리합니다. 위치 필드는 $1, $2로, 이름 있는 필드는 이름으로 읽습니다. 레코드는 필드 값이 같으면 ==로 같다고 판단합니다.
패턴은 값의 모양을 검사하고 구조를 분해하는 문법입니다. 변수 선언, switch, if-case, for 등에서 쓸 수 있습니다.
(double, double) minMax(List<double> values) {
var lo = values.first, hi = values.first;
for (final v in values) {
if (v < lo) lo = v;
if (v > hi) hi = v;
}
return (lo, hi);
}
({String name, int age}) loadUser() => (name: '지우', age: 27);
void main() {
final range = minMax([3, 9, 1]);
print('${range.$1} ~ ${range.$2}'); // 1.0 ~ 9.0
final (lo, hi) = minMax([4, 2]); // 구조 분해
final (:name, :age) = loadUser(); // 이름 있는 필드 분해
print('$lo $hi $name $age');
final Object value = [1, 2];
final text = switch (value) {
int n when n < 0 => '음수',
int() => '정수',
[var first, _] => '두 원소 리스트, 첫 값 $first',
{'type': String type} => '맵 타입 $type',
_ => '기타',
};
print(text);
final json = <String, Object?>{'id': 7};
if (json case {'id': int id}) {
print('id = $id');
}
}sealed 클래스는 같은 라이브러리 안에서만 하위 타입을 만들 수 있으므로, 컴파일러가 모든 하위 타입을 압니다. 그래서 switch가 모든 경우를 다뤘는지(완전성) 검사해 줍니다.
열거형은 enum으로 선언합니다. 향상된 열거형(enhanced enum)은 필드, 생성자, 메서드를 가질 수 있습니다.
sealed class Shape {}
class Circle extends Shape { Circle(this.r); final double r; }
class Square extends Shape { Square(this.side); final double side; }
double area(Shape s) => switch (s) {
Circle(:final r) => 3.14159 * r * r,
Square(:final side) => side * side,
}; // 하위 타입을 하나 빠뜨리면 컴파일 오류
enum Level {
low(1),
medium(5),
high(10);
const Level(this.weight);
final int weight;
bool get isUrgent => this == Level.high;
}
void main() {
print(area(Circle(1)));
for (final level in Level.values) {
print('${level.name} ${level.weight} ${level.isUrgent}');
}
}Dart는 건전한 정적 타입 시스템과 null 안전성으로 많은 오류를 컴파일 단계에서 막습니다. List, Set, Map은 제네릭 컬렉션이고, 클래스는 this.필드 생성자, 이름 있는 생성자, const와 factory 생성자를 지원합니다. 레코드로 여러 값을 가볍게 묶고, 패턴으로 구조를 분해하며, sealed 클래스와 열거형을 switch와 함께 쓰면 빠뜨린 경우를 컴파일러가 알려 줍니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.