본 도구에서 사용하는 오픈소스 라이브러리

본 도구의 코드에는 1개의 오픈소스 라이브러리가 포함되어 있습니다.

Dart 치트시트 — 간결한 레퍼런스

Dart 3 문법, 타입 시스템, 비동기, 가장 자주 쓰이는 표준 라이브러리를 모은 치트시트로, 일상 작업의 약 80%를 다룹니다.

D

Dart Dart 3

Dart SDK · 객체 지향 · 제네릭 · 함수형 · 비동기 우선 · 정적(건전한 null 안전성)

추천 학습 경로

먼저 dart create/run과 pubspec 구조를 익히고 → 변수, 타입, 제어 흐름을 익히고 → 함수, 문자열, 컬렉션을 깊이 파고 → 클래스, mixin, 에러 처리를 이해하고 → isolate와 async/await로 동시성을 다루고 → 이후 필요에 따라 네트워크, 시간, 프로세스, 정규식, 빌드/테스트를 배우는 순서입니다. FAQ 챕터는 나중에 함정을 피할 때 다시 보면 좋습니다.

1.Hello World와 실행 환경

최소 프로그램, dart create/run, 그리고 프로젝트 구조.

최소 프로그램

모든 Dart 프로그램은 void main() 진입 함수에서 시작합니다. dart run이 실행하며, print가 표준 출력으로 출력합니다.

1
2
3
4
5
6
7
8
9
void main() {
print('Hello, world!');
}
// 실행 방법:
// dart run hello.dart
// 또는 먼저 컴파일 후 실행:
// dart compile exe hello.dart -o hello
// main은 main이라는 이름이어야 진입점으로 취급됨
// 반환 타입 void는 값을 반환하지 않음을 의미

생성과 실행

dart create가 표준 프로젝트 골격을 만들고, dart run이 진입 파일을 직접 실행합니다. 개발 단계에는 JIT 핫 리로드를, 배포 시에는 AOT 컴파일을 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 콘솔 프로젝트 생성:
// dart create -t console myapp
// Flutter 프로젝트 생성:
// flutter create myapp
// 프로젝트 디렉터리로:
// cd myapp
// 진입 파일 실행:
// dart run
// 파일 지정 실행:
// dart run bin/main.dart
// 실행 파일로 컴파일:
// dart compile exe bin/main.dart
// 정적 분석:
// dart analyze

프로젝트 구조

pubspec.yaml에 패키지명, 의존성, SDK 버전을 선언합니다. 코드는 lib/와 bin/에, 테스트는 test/에 둡니다.

1
2
3
4
5
6
7
8
9
10
11
12
// pubspec.yaml 예시:
// name: myapp
// environment:
// sdk: '>=3.0.0 <4.0.0'
// dependencies:
// http: ^1.1.0
// 디렉터리 관례:
// lib/main.dart 라이브러리 코드
// bin/main.dart 실행 가능 진입점
// test/ 단위 테스트
// 의존성 설치:
// dart pub get

print 출력

print는 stdout으로 출력하고 줄바꿈을 붙입니다. $는 변수를, ${식}은 복잡한 식을 문자열에 삽입합니다.

1
2
3
4
5
6
7
8
9
10
void main() {
print('Hello, Dart!');
final name = 'Rex';
print('Hi, $name'); // 변수 보간
final age = 5;
print('Age is ${age + 1}'); // 표현식 보간
print('Pi = ${3.14159.toStringAsFixed(2)}');
}
// stdout.write는 줄바꿈 없음
// stderr.writeln은 오류 스트림으로 출력

커맨드라인 인수

main은 List<String> args를 받을 수 있습니다. args[0]부터가 커맨드라인의 실제 인수이며, 프로그램 이름은 포함되지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
void main(List<String> args) {
print('参数个数: ${args.length}');
for (final arg in args) {
print('参数: $arg');
}
}
// 실행:
// dart run hello.dart a b c
// args == ['a', 'b', 'c']
// 프로그램 이름 자체는 args에 없음
// 프로그램 경로가 필요하면 Platform.script

비동기 main 함수

main은 Future<void>일 수 있으며, 프로그램은 모든 await가 끝날 때까지 기다린 뒤 종료합니다. 시간이 걸리는 작업에 자주 쓰입니다.

1
2
3
4
5
6
7
8
9
Future<void> main() async {
print('开始');
await Future.delayed(Duration(seconds: 1));
print('一秒后执行');
}
// Future.delayed로 시간이 걸리는 작업 시뮬레이션
// await는 Future가 끝날 때까지 블로킹
// Future<void>를 반환해 런타임이 대기
// 기다리지 않으면 프로그램이 일찍 종료될 수 있음

주석

//는 한 줄 주석, ///는 문서 주석(dart doc에서 사용), /* */는 블록 주석입니다. 주석은 실행에 영향을 주지 않습니다.

1
2
3
4
5
6
7
8
9
// 한 줄 주석
/// 문서 주석:dart doc으로 API 문서 생성
/* 블록 주석:한 줄로 닫으면 됨 */
void main() {
// 줄 끝 주석
print('OK'); // 이 줄이 하는 일 설명
}
// 문서 주석은 선언 앞에 배치
// dart doc으로 HTML 문서 생성

최상위 멤버

Dart는 클래스 밖에 최상위 함수와 변수를 둘 수 있습니다. import로 라이브러리를 가져오고, 최상위 변수는 파일 간 공유됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'dart:math';
// 최상위 변수:
final appName = 'demo';
// 최상위 함수:
int square(int x) => x * x;
void main() {
print(appName);
print(square(4));
print(max(1, 9));
}
// 최상위 멤버는 라이브러리 전체에서 가시
// 변수는 final로 전역 가변을 피할 것
// private 멤버는 밑줄로 시작:_helper
// import는 파일 맨 위에 배치

2.변수와 상수

변수 선언, final/const, null 안전성, 그리고 구조 분해.

var와 final

var는 재할당 가능하지만 타입 추론은 고정됩니다. final은 한 번만 대입할 수 있으며 이후 변경할 수 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
var counter = 0; // int로 추론
counter = 1; // 재대입 가능
final name = 'Rex'; // 한 번만 대입 가능
// name = 'New'; // 오류:final은 변경 불가
final now = DateTime.now(); // 런타임에 확정
print('$counter $name $now');
}
// var는 한 번 타입을 추론하면 고정
// 타입이 다르면 오류:counter = 'x'
// final은 대입 횟수만 제약
// 객체 내부는 여전히 수정 가능

const 상수

const는 컴파일 시점 상수로, 컴파일 시 값이 확정되어야 합니다. const 값은 정형화되어 같은 상수는 한 인스턴스만 존재합니다.

1
2
3
4
5
6
7
8
9
10
11
void main() {
const pi = 3.14159;
const greeting = 'Hello';
const list = [1, 2, 3]; // 컴파일 타임 리스트
const double factor = 0.5;
print('$pi $greeting $list $factor');
}
// const 값은 컴파일 시점에 알려져야 함
// DateTime.now()는 const에 사용 불가
// const 생성자는 컴파일 타임 상수를 생성
// 우선 const를 써서 성능 향상

명시적 타입

변수에 타입을 명시할 수 있습니다. 정적 타입은 컴파일 시 검사에 유용하며, 공개 API에는 명시를 권장합니다.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
int count = 42;
double ratio = 3.14;
String name = 'Dart';
bool enabled = true;
List<int> nums = <int>[1, 2, 3];
print('$count $ratio $name $enabled $nums');
}
// 타입을 변수 이름 앞에 작성
// 명시적 타입으로 의도가 분명해짐
// 지역 변수는 var로 추론 가능
// 인수와 반환값에는 타입 작성 권장

null 허용 변수

건전한 null 안전성에서 기본은 null 불허입니다. 타입 뒤에 ?를 붙여 null 허용을 선언하고, 사용 전 null 체크 또는 ?? 기본값을 사용하세요.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
String? maybe = null; // null 허용
String name = maybe ?? '匿名'; // 널 병합 기본값
if (maybe != null) {
print('有值: $maybe');
}
print(name);
}
// String?은 null 대입 가능
// String은 null 대입 불가(컴파일 오류)
// ??는 왼쪽이 null일 때 오른쪽 사용
// null 체크 후 Dart가 타입 승격 수행

late 지연 초기화

late는 처음 접근할 때까지 초기화를 미룹니다. late final은 한 번만 대입 가능하며 런타임 환경에 의존하는 필드에 적합합니다.

1
2
3
4
5
6
7
8
9
10
11
12
class Config {
late final String apiKey = loadKey();
}
String loadKey() => 'abc-123';
void main() {
final config = Config();
print(config.apiKey); // 첫 접근 시 로드
}
// late는 선언 시 즉시 대입 안 해도 됨
// 첫 사용 전까지 대입 필수, 아니면 오류
// late final은 한 번만 초기화 보장
// 싱글톤, 지연 로딩 설정에 사용

구조 분해 할당

Dart 3는 레코드와 패턴 구조 분해를 지원하여 컬렉션 요소를 한 번에 여러 변수에 대입합니다. 모양이 일치해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
void main() {
final (x, y) = (10, 20);
final (name: n, age: a) = (name: 'Rex', age: 5);
final [first, second, ...] = [1, 2, 3];
print('$x $y $n $a $first $second');
}
// 남는 요소는 _로 무시:
// final [_, second2] = [1, 2];
// rest가 없으면 리스트 패턴 길이 일치 필수
// map 키-값에도 적용 가능
// for (final MapEntry(k: k, v: v) in entries)

스코프와 섀도잉

변수는 선언된 블록에서 보이고, 안쪽에서 바깥쪽의 같은 이름 변수를 가릴 수 있습니다. 최상위 변수는 라이브러리 전체에서 보입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final top = '全局';
void main() {
final inner = '函数内';
if (true) {
final block = '块内';
print(block);
print(inner); // 바깥 스코프 보임
print(top); // 전역 보임
}
// print(block); // 오류:스코프 밖
}
// 블록 스코프는 중괄호와 일치
// 안쪽이 바깥의 같은 이름 변수를 가릴 수 있음
// 같은 스코프에서 중복 선언은 오류
// 깊은 섀도잉은 가독성 해치니 피할 것

dynamic과 Object

dynamic은 정적 검사를 건너뛰고 런타임에 타입이 정해집니다. Object는 모든 클래스의 부모 타입으로 명시적 캐스팅이 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
dynamic d = 42;
d = '变成字符串'; // 런타임에 임의 타입
Object obj = 42;
// obj.length; // 오류:Object에 그 멤버 없음
if (obj is String) {
print(obj.length); // 타입 승격 후 접근 가능
}
print(d);
}
// dynamic은 정적 검사 없음, 신중히 사용
// Object는 모든 클래스의 부모 타입
// is 판정 후 타입 승격 발생
// 명확한 타입이나 제네릭 우선

3.데이터 타입

내장 타입, 컬렉션 타입, 레코드, 그리고 열거형.

숫자 타입

int는 정수, double은 부동소수, num은 둘의 부모 타입입니다. int 리터럴은 double에 대입할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
void main() {
int i = 42;
double d = 3.14;
num n = 42; // num은 정수도 수용
double whole = 42; // int는 바로 double에 대입 가능
print('$i $d $n $whole');
}
// int는 네이티브 플랫폼에서 64비트
// double은 64비트 IEEE 754
// 수치 연산 결과 타입은 피연산자에 따름
// 타입 판정은 is:if (n is int)

문자열 타입

String은 불변의 UTF-16 시퀀스입니다. 작은따옴표와 큰따옴표는 동등하며, 보간과 여러 줄 문자열을 지원합니다.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
String a = '单引号';
String b = "双引号"; // 동등
String multi = '''第一行
第二行''';
print('$a $b');
print(multi);
}
// 문자열은 변경 불가
// 변경 작업은 모두 새 문자열 반환
// 한쪽 따옴표 안에 다른쪽 따옴표를 바로 넣을 수 있음
// 같은 따옴표가 필요하면 백슬래시로 이스케이프

불리언 타입

bool은 true와 false만 가집니다. 조건식은 반드시 bool을 반환하며 숫자의 암묵적 변환은 없습니다.

1
2
3
4
5
6
7
8
9
10
11
void main() {
bool ok = true;
bool done = false;
if (ok) print('成功');
final result = ok && !done;
print(result);
}
// && 논리곱 || 논리합
// ! 논리 부정
// 삼항:ok ? '是' : '否'
// Dart에는 truthy/falsy 개념 없음

리스트 타입

List는 순서가 있는 컬렉션이며 제네릭을 지원합니다. 리터럴 [a, b]로 만들고 기본은 확장 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
List<int> nums = [1, 2, 3];
nums.add(4);
nums.remove(2);
final first = nums.first;
final len = nums.length;
print('$first $len $nums');
}
// List<String> 제네릭이 요소 타입 제한
// 인덱스 접근 nums[0], 범위 밖은 예외
// isEmpty / isNotEmpty로 빈 판정
// first / last로 앞뒤 요소 취득

집합과 맵

Set은 순서 없는 중복 불허 집합이고, Map은 키-값 컬렉션입니다. 둘 다 제네릭과 리터럴 문법을 가집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
Set<String> tags = {'a', 'b', 'a'}; // 자동 중복 제거
tags.add('c');
Map<String, int> ages = {'alice': 30};
ages['bob'] = 25;
print(tags); // {a, b, c}
print(ages['bob']); // 25
print(ages['nobody']); // null
}
// Set 멤버 판정은 contains
// Map은 없는 키에 null 반환
// Map 순회 순서는 삽입 순서
// Set은 중복 제거와 멤버 판정에 적합

레코드 타입

Dart 3의 레코드는 이름 없는 경량 집계 타입입니다. 필드 이름을 가질 수 있고 패턴 구조 분해와 함께 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
var point = (x: 1, y: 2); // 명명 레코드
var pair = ('left', 3); // 위치 레코드
print(point.x);
print(pair.$1); // 위치로 값 취득
final (a, b) = ('first', 2);
print('$a $b');
}
// 레코드는 자동으로 ==와 hashCode 획득
// 레코드 타입은 곧 형태:(int, int)
// 작은 클래스 대신 여러 값을 반환하기에 적합
// 레코드는 불변

열거형

enum은 한정된 이름 있는 상수 집합을 정의합니다. 강화 열거형(Dart 2.17+)은 필드와 메서드를 가질 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum Status { pending, approved, rejected }
enum Color {
red(255), green(0), blue(0);
const Color(this.value);
final int value;
}
void main() {
final s = Status.approved;
print(s.name); // approved
print(Color.red.value);
for (final c in Status.values) {
print(c.name);
}
}
// .name으로 열거명 취득
// .values로 모든 멤버 순회
// switch로 열거를 다루면 기본적으로 완전
// 확장 열거는 데이터와 동작 부착 가능

타입 변환

int.parse / double.parse는 문자열을 숫자로 변환하고, toString은 다시 문자열로 되돌립니다. as는 런타임 타입 변환을 수행합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main() {
int n = int.parse('42');
double d = double.parse('3.14');
String s = 42.toString();
Object obj = 'hello';
if (obj is String) {
final len = obj.length; // 승격 후 안전
print(len);
}
print('$n $d $s');
}
// parse 실패 시 FormatException 발생
// toStringAsFixed(2)는 소수 둘째 자리까지 유지
// as 강제 변환 실패 시 TypeError
// as보다 is + 타입 승격 권장

4.참조와 null 안전성

객체 참조, null 안전성, 깊은 복사, 그리고 네이티브 메모리.

객체 참조

Dart에는 날 포인터가 없으며 변수는 객체 참조를 보관합니다. 대입은 참조만 복사하고, 여러 변수가 같은 객체를 가리킬 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final list1 = [1, 2, 3];
final list2 = list1; // 참조 복사
list2.add(4);
print(list1); // [1, 2, 3, 4]
final copy = List.of(list1); // 실제 복사
copy.add(5);
print(list1); // 영향 없음
}
// 참조 타입 대입은 객체 공유
// List.of / [...list]는 얕은 복사
// 기본 타입은 불변 값 타입
// 가변 객체 공유 시 부작용 주의

null 안전성

건전한 null 안전성은 null이 String? 같은 null 허용 타입에만 나타나도록 합니다. 컴파일러가 비-null을 컴파일 시 보장합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
String name = 'Rex'; // non-null
String? nickname; // null 허용, 기본 null
nickname = '小雷';
if (nickname != null) {
print('$name 的昵称 $nickname');
}
print(nickname);
}
// non-null 타입에 null 대입 시 컴파일 오류
// null 체크 후 변수는 자동으로 non-null 승격
// ??는 기본값을 제공해 null 체크 회피
// ?. 안전 접근:nickname?.length

null 단언 !

null 허용 식 뒤에 !를 붙여 null이 아님을 단언하며, 실패 시 null 체크 예외가 발생합니다. 확실할 때만 사용하세요.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
String? maybe = '有值';
final len = maybe!.length; // non-null 단언
print(len);
String? empty;
final safe = empty?.length; // null
print(safe);
}
// !는 null 안전 검사 우회
// 단언 실패 시 Null check error
// null 체크와 타입 승격 우선
// ?. 안전 접근은 예외 대신 null 반환

null 병합

??는 왼쪽이 null일 때 오른쪽을 반환합니다. ??=는 변수가 null일 때만 대입합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
String? user;
final name = user ?? '匿名'; // 널 병합
user ??= '游客'; // null일 때만 대입
int? count;
count ??= 0;
count++;
print('$name $user $count');
}
// ??는 왼쪽이 null인지만 봄
// ??=는 count = count ?? 0과 동등
// 연결 가능:a ?? b ?? c ?? '默认'
// ??는 컬렉션 기본값에도 사용:list ?? []

late와 const

final은 런타임에 한 번 대입하고, const는 컴파일 시점 상수입니다. late의 지연 초기화는 큰 객체와 순환 의존에 적합합니다.

1
2
3
4
5
6
7
8
9
10
11
void main() {
final now = DateTime.now(); // 런타임
const pi = 3.14159; // 컴파일 타임
late final big = createBig(); // 첫 접근 시
print('$now $pi ${big.length}');
}
List<int> createBig() => List<int>.generate(100, (i) => i);
// final은 생성자나 런타임에 대입 가능
// const 값은 컴파일 타임에 확정 필수
// late는 첫 접근까지 지연
// 최상위와 정적 변수는 기본 지연 초기화

참조와 동등성

==는 기본적으로 참조 비교이며, identical()은 같은 인스턴스인지 판정합니다. 구조적 동등성에는 도우미 함수를 사용하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final a = [1, 2];
final b = [1, 2];
print(a == b); // false:다른 객체
print(identical(a, b)); // false:다른 참조
final c = a;
print(identical(a, c)); // true:같은 참조
print(a.length == b.length); // true
}
// 문자열과 숫자는 값 의미, ==는 내용 비교
// List/Map은 기본 참조 비교
// 구조 비교는 listEquals(package:collection)
// 커스텀 클래스는 ==와 hashCode 오버라이드 가능

깊은 복사와 얕은 복사

얕은 복사는 최상위만 복제하고 깊은 객체는 공유됩니다. 깊은 복사는 모든 계층을 복제하며, 불변 컬렉션은 공유해도 안전합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final nested = [[1], [2]];
final shallow = List.of(nested); // 얕은 복사
shallow[0].add(9);
print(nested[0]); // [1, 9]
final deep = nested.map((e) => List.of(e)).toList();
deep[0].add(8);
print(nested[0]); // [1, 9] 안 변함
}
// 얕은 복사는 중첩 객체 공유
// 깊은 복사는 각 층마다 새 객체 생성
// Map.of / Set.of도 동일
// 불변 컬렉션이 공유 부작용 방지

네이티브 메모리

dart:ffi는 Pointer와 calloc을 제공하여 C 라이브러리와의 상호 운용을 위한 네이티브 메모리에 접근합니다. 포인터 수명은 개발자가 관리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
// dart:ffi는 네이티브 메모리 접근 제공:
// Pointer<Int32>는 C int 포인터를 뜻함
// 할당과 해제:
// final p = calloc<Int32>(1); // package:ffi
// p.value = 42;
// calloc.free(p);
// C 라이브러리 상호 운용:
// final lib = DynamicLibrary.open('libc.so')
// final fn = lib.lookupFunction(...)
// 문자열은 Pointer<Utf8>
// 네이티브 플랫폼만, Web 불가
// 포인터 수명은 개발자가 관리

5.제어 흐름

if, 반복문, switch, 그리고 패턴 매칭.

if / else

if / else if / else는 조건에 따라 분기합니다. 조건은 bool이어야 하며, null 체크는 타입 승격을 일으킵니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main() {
int score = 85;
if (score >= 90) {
print('优秀');
} else if (score >= 60) {
print('及格');
} else {
print('不及格');
}
}
// 조건은 bool을 반환해야 함
// 중괄호는 생략 가능하지만 유지 권장
// null 체크 후 자동 타입 승격:
// if (name != null) print(name.length)
// else가 없으면 조건 불충족 시 스킵

삼항 연산자

조건 ? 참 : 거짓 은 단일 식 분기입니다. ??와 함께 null 기본값 처리에 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final n = 7;
final sign = n > 0 ? '正' : '非正';
String? maybe;
final label = maybe ?? '默认'; // 널 병합
print(sign);
print(label);
}
// 삼항 두 분기 타입은 일치해야 함
// 중첩 삼항은 가독성 낮음, 지양
// ??는 null 기본값 전담
// 복잡한 분기는 if/else가 명확

for 반복문

for의 고전적인 세 부분 형태와, for-in으로 Iterable을 순회합니다. Dart 3에서는 루프 변수의 패턴 구조 분해도 지원합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void main() {
for (var i = 0; i < 3; i++) {
print('i=$i');
}
final names = ['Rex', 'Ada'];
for (final name in names) {
print(name);
}
for (final (i, name) in names.indexed) {
print('$i: $name');
}
}
// i++는 i = i + 1과 동등
// for-in은 Iterable과 Map 순회 가능
// .indexed는 (index, value) 레코드 생성
// 순회 중 컬렉션 수정은 주의

while 반복문

while은 먼저 판정 후 실행, do-while은 최소 한 번 실행 후 판정합니다. 반복 횟수를 모르는 루프에 적합합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void main() {
var n = 3;
while (n > 0) {
print('n=$n');
n--;
}
var count = 0;
do {
print('至少执行一次');
count++;
} while (count < 1);
}
// while은 조건이 false면 한 번도 실행 안 함
// do-while은 최소 1회 실행
// 조건은 bool이어야 함
// 루프 변수 갱신으로 무한 루프 방지

switch 표현식

Dart 3의 switch 표현식은 값을 반환합니다. case 뒤에는 패턴이 오고, =>로 결과를 주며 break가 필요 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final status = 404;
final label = switch (status) {
200 => '成功',
404 => '未找到',
_ => '其他',
};
print(label);
}
// 끝은 완전히 덮거나 _로 폴백
// 표현식 버전 case는 :가 아닌 => 사용
// 패턴 지원:switch (value) { int n => ... }
// if 체인보다 간결, 결과를 대입 가능

switch 문

switch 문은 case에 따라 분기합니다. 빈 case의 fall-through, 가드, 패턴을 지원합니다. 비어있지 않은 분기는 break가 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void main() {
final day = 3;
switch (day) {
case 1:
case 2:
case 3:
print('工作日');
break;
default:
print('周末');
}
switch (day) {
case int n when n > 3:
print('大于 3');
break;
default:
print('其他');
}
}
// 빈 case는 자동으로 아래로 폴스루
// 비어있지 않은 분기는 break나 return 필요
// when으로 추가 조건 가드
// 패턴 매칭에서는 if 체인보다 강력

패턴 매칭

if-case와 switch는 값에 대해 구조 매칭을 수행합니다. 객체 패턴, 레코드 패턴, 가드를 조합해 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Circle {
const Circle(this.radius);
final double radius;
}
void main() {
final shape = Circle(5);
if (shape case Circle(radius: final r)) {
print('半径 $r');
}
final value = (name: 'Rex', age: 5);
final msg = switch (value) {
(name: final n, age: 0) => '$n 刚出生',
(name: final n, age: final a) => '$n $a 岁',
_ => '未知',
};
print(msg);
}
// if-case는 매치 성공 시에만 분기 진입
// 객체 패턴은 필드명으로 분해
// 레코드 패턴은 위치나 이름으로 매치
// 완전한 switch는 컴파일러가 커버 보장

break와 continue

break는 현재 루프를 빠져나오고, continue는 다음 반복으로 이동합니다. 라벨로 중첩 루프의 점프 대상을 제어할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void main() {
for (var i = 0; i < 5; i++) {
if (i == 1) continue; // i=1 건너뜀
if (i == 4) break; // 루프 탈출
print('i=$i'); // 0 2 3
}
outer:
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++) {
if (j == 1) break outer; // 바깥 루프 탈출
print('$i,$j');
}
}
}
// continue는 현재 회차 나머지만 스킵
// break는 루프를 완전히 종료
// 라벨은 루프 앞에 쓰고 라벨명으로 참조
// 라벨은 깊은 중첩 탈출에 적합

6.함수와 람다

함수 정의, 선택적 매개변수, 클로저, 그리고 비동기 함수.

함수 정의

함수는 반환 타입, 이름, 매개변수, 본문으로 구성됩니다. 반환 값이 없는 함수는 void를 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int add(int a, int b) {
return a + b;
}
void greet(String name) {
print('你好,$name');
}
void main() {
print(add(1, 2));
greet('Rex');
}
// 반환 타입은 함수 이름 앞에 작성
// 인수에는 타입 필수
// 반환값 없으면 void
// 함수는 객체, 변수에 대입 가능

애로우 함수

단일 식 본문은 =>로 간결히 쓸 수 있으며 그 식의 값을 반환합니다. 순수 함수와 콜백에 적합합니다.

1
2
3
4
5
6
7
8
9
10
11
12
int square(int x) => x * x;
bool isEven(int n) => n % 2 == 0;
void main() {
print(square(4));
print(isEven(3));
final doubled = [1, 2, 3].map((n) => n * 2).toList();
print(doubled);
}
// => 함수 본문은 단일 표현식이어야 함
// { return expr; }와 동등
// 콜백과 고차 함수에 자주 사용
// void 반환:() => print('x')

선택적 위치 매개변수

대괄호 []는 선택적 위치 매개변수이며 생략 시 기본값 또는 null입니다. 기본값은 컴파일 시점 상수여야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
String join(String a, [String b = '无', int times = 1]) {
return '$a $b 重复 $times 次';
}
void main() {
print(join('你好'));
print(join('你好', '世界'));
print(join('你好', '世界', 3));
}
// [] 안 인수는 위치에 따른 선택 인수
// 기본값은 인수 뒤에 =로 작성
// 기본값이 없으면 타입은 null 허용
// null 허용 선택 인수는 ??로 기본값 지정

이름 있는 매개변수

중괄호 {}는 이름 있는 매개변수이며 호출 시 이름으로 값을 전달합니다. required를 붙이면 반드시 전달해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
String describe(String name,
{int age = 0, String? city, required bool married}) {
return '$name 年龄 $age 城市 $city 已婚 $married';
}
void main() {
final s = describe('Rex',
age: 5, city: '上海', married: false);
print(s);
}
// 명명 인수는 순서 무관
// 안 넘기면 기본값 또는 null
// required는 명시적으로 넘겨야 필수
// 긴 호출은 줄바꿈으로 가독성 향상

고차 함수

함수는 일급 시민으로 인수로 전달하거나 반환값으로 돌려줄 수 있습니다. map / where / fold는 컬렉션의 대표적 고차 함수입니다.

1
2
3
4
5
6
7
8
9
10
11
void main() {
final nums = [1, 2, 3, 4];
final doubled = nums.map((n) => n * 2).toList();
final evens = nums.where((n) => n.isEven).toList();
final sum = nums.fold(0, (acc, n) => acc + n);
print('$doubled $evens $sum');
}
// map은 요소별 변환
// where는 조건 필터
// fold는 단일 결과로 누적
// 결과는 지연 Iterable, toList로 고정

클로저

클로저는 정의 환경의 변수를 캡처합니다. 바깥 함수가 반환된 뒤에도 클로저는 그 변수에 접근하고 수정할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function makeCounter() {
var count = 0;
return () => ++count;
}
void main() {
final counter = makeCounter();
print(counter()); // 1
print(counter()); // 2
final counter2 = makeCounter();
print(counter2()); // 1:독립 상태
}
// 클로저는 환경과 상태 보존
// makeCounter 호출마다 독립 클로저 생성
// 변수가 클로저 간 공유인지 독립인지는 위치에 따름
// 상태 캡슐화 구현에 사용

비동기 함수

async 함수는 Future를 반환하며 내부에서 await로 비동기 결과를 기다립니다. await는 async 함수에서만 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
Future<String> fetchData() async {
await Future.delayed(Duration(milliseconds: 100));
return '数据';
}
Future<void> main() async {
print('开始');
final data = await fetchData();
print(data);
}
// async 함수는 암묵적으로 Future<T> 반환
// await는 Future 완료까지 대기
// 예외는 await 위치로 전달되어 try/catch 가능
// await를 잊으면 Future 객체 자체를 받음

제너레이터

sync*는 지연 Iterable을, async*는 Stream을 생성합니다. yield는 값을 하나씩 내보내고, yield*는 다른 제너레이터에 위임합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Iterable<int> countUp(int max) sync* {
for (var i = 0; i < max; i++) {
yield i;
}
}
Stream<int> streamCount(int max) async* {
for (var i = 0; i < max; i++) {
await Future.delayed(Duration(milliseconds: 10));
yield i;
}
}
void main() {
print(countUp(3).toList());
}
// sync*는 순차 생성, 메모리 비점유
// async*는 await와 결합해 스트림 생성
// yield*는 부분 시퀀스 전개:yield* countUp(2)
// 제너레이터는 필요 시 실행, 첫 접근에 동작

7.문자열

리터럴, 보간, 부분 문자열, 인코딩, 그리고 포매팅.

문자열 리터럴

작은따옴표와 큰따옴표 모두 문자열입니다. 역슬래시로 이스케이프하며, r 접두사는 로 문자열을 뜻합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
String s1 = '单引号';
String s2 = "双引号";
String esc = '换行符\n制表符\t';
String raw = r'不转义\n直接显示';
print('$s1 $s2');
print(esc);
print(raw);
}
// \n 줄바꿈 \t 탭 \\ 백슬래시
// r'...' 원시 문자열은 이스케이프 처리 안 함
// 정규식, Windows 경로에 적합
// 따옴표 안에서 다른 따옴표를 써 이스케이프 회피

문자열 보간

$변수와 ${식}으로 값을 문자열에 삽입합니다. 어떤 객체든 toString이 호출됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final name = 'Rex';
final age = 5;
print('$name 今年 $age 岁');
print('十年后 ${age + 10} 岁');
final list = [1, 2];
print('共 ${list.length} 个元素');
}
// $name으로 변수 직접 삽입
// ${...}로 표현식과 호출 삽입
// 객체는 자동으로 toString 변환
// 문자열 연결보다 명확하고 효율적

여러 줄 문자열

세 개의 작은따옴표 '''는 여러 줄 문자열을 만들며 줄바꿈과 들여쓰기를 그대로 유지합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main() {
String poem = '''
床前明月光,
疑是地上霜。''';
print(poem);
String html = '''
<div>
<p>你好</p>
</div>''';
print(html);
}
// 여러 줄 문자열은 실제 줄바꿈을 유지
// 앞뒤의 줄바꿈도 포함됨
// 보간과 조합해서 사용 가능
// SQL, HTML, 템플릿 텍스트에 적합

자주 쓰는 메서드

contains / startsWith로 포함 여부를 판정하고, replaceAll로 치환, toUpperCase / toLowerCase로 대소문자를 변환합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final text = 'Hello Dart';
print(text.contains('Dart')); // true
print(text.startsWith('Hello')); // true
print(text.toLowerCase()); // hello dart
print(text.replaceAll('Dart', 'Flutter'));
print(text.replaceFirst('l', 'L')); // HeLlo Dart
print(text.length);
}
// 문자열 메서드는 새 문자열을 반환, 원본은 수정 안 함
// trim()은 앞뒤 공백 제거
// isEmpty / isNotEmpty로 빈 판정
// 메서드는 체인 호출 가능

부분 문자열과 검색

substring은 인덱스로 부분 문자열을 잘라내고, indexOf는 위치를 찾으며, split은 구분자로 리스트를 만듭니다.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final text = 'hello world';
print(text.substring(0, 5)); // hello
print(text.substring(6)); // world
print(text.indexOf('world')); // 6
print(text.indexOf('x')); // -1 未找到
print(text.split(' ')); // [hello, world]
}
// substring 인수는 시작(포함)과 끝(미포함)
// indexOf는 못 찾으면 -1 반환
// lastIndexOf는 뒤에서부터 탐색
// split 결과는 구분 문자 자체를 포함하지 않음

문자와 인코딩

Dart 문자열은 UTF-16이며 codeUnits는 코드 단위, runes는 코드 포인트를 반환합니다. 이모지는 runes가 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
void main() {
final s = 'A中😀';
print(s.length); // 4 个 UTF-16 单元
print(s.codeUnits); // 编码单元列表
print(s.runes.toList()); // 码点列表
print(String.fromCharCodes(s.runes));
}
// 중국어와 emoji는 여러 UTF-16 유닛을 차지
// length는 글자 수가 아닌 부호 유닛 수를 반환
// runes는 Unicode 코드포인트 반환
// 실제 글자 수가 필요하면 runes.length 사용

효율적인 연결

반복문 안에서 문자열을 연결하면 임시 객체가 많이 생성됩니다. StringBuffer로 누적한 뒤 한 번에 문자열로 만듭니다.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final buffer = StringBuffer();
for (var i = 0; i < 1000; i++) {
buffer.write('第 $i 行\n');
}
final result = buffer.toString();
print('${result.length} 个字符');
}
// write는 줄바꿈 없이 추가
// writeln은 줄바꿈과 함께 추가
// toString으로 한 번에 문자열 생성
// + 연결보다 훨씬 효율적

포맷과 파싱

toStringAsFixed는 소수 자릿수를 고정하고, padLeft는 자릿수를 채우며, int.parse / double.parse는 숫자 문자열을 파싱합니다.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final pi = 3.14159;
print(pi.toStringAsFixed(2)); // 3.14
print(7.toString().padLeft(3, '0')); // 007
final n = int.parse('42');
final d = double.parse('3.14');
print('$n $d');
}
// toStringAsFixed는 반올림
// padLeft/padRight로 문자 채움
// parse 실패는 FormatException
// 진수 지정:int.parse('ff', radix: 16)

8.컬렉션

리스트, 맵, 집합 연산, 그리고 정렬.

리스트 연산

add는 추가, insert는 삽입, remove는 삭제, sort는 정렬입니다. 기본 List는 가변이며 확장 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final nums = [3, 1, 2];
nums.add(4); // [3, 1, 2, 4]
nums.insert(0, 0); // [0, 3, 1, 2, 4]
nums.remove(3); // 移除元素 3
nums.sort(); // 升序
print(nums);
print(nums.reversed.toList());
}
// remove는 첫 번째로 같은 요소 제거
// removeAt은 인덱스로 제거
// contains는 포함 여부 판정
// indexOf는 처음 일치하는 위치 검색

리스트 고차 함수

map은 변환, where는 필터, reduce / fold는 집계, expand는 평탄화입니다. 반환되는 Iterable은 대개 지연입니다.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final nums = [1, 2, 3, 4];
final doubled = nums.map((n) => n * 2).toList();
final evens = nums.where((n) => n.isEven).toList();
final sum = nums.reduce((a, b) => a + b);
final flat = [[1, 2], [3]].expand((e) => e).toList();
print('$doubled $evens $sum $flat');
}
// map/where는 지연 Iterable 반환
// 지연 시퀀스는 순회 시에만 계산
// reduce는 리스트가 비어 있으면 에러
// fold는 초기값을 줄 수 있어 안전

스프레드 연산자

...는 컬렉션의 요소를 리터럴에 펼칩니다. ...?는 소스가 null일 때 안전하게 건너뜁니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final a = [1, 2];
final b = [3, 4];
final merged = [...a, ...b, 5]; // [1,2,3,4,5]
List<int>? maybe;
final safe = [...a, ...?maybe]; // 空安全
final copy = [...a]; // 浅拷贝
print('$merged $safe $copy');
}
// 스프레드 연산자는 하위 리스트가 아닌 요소를 전개
// ...?는 null 소스를 자동 처리
// Map과 Set 리터럴에도 사용 가능
// 컬렉션 병합·복사에 자주 사용

컬렉션 내 if/for

컬렉션 리터럴 안에서 if로 조건부 포함, for로 일괄 생성이 가능합니다. Dart 3은 패턴 구조 분해도 지원합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void main() {
final includeZero = true;
final nums = [
1,
if (includeZero) 0,
for (var i = 2; i < 4; i++) i,
];
print(nums); // [1, 0, 2, 3]
final pairs = {
for (var i = 0; i < 3; i++) 'k$i': i,
};
print(pairs);
}
// if가 불충족하면 그 요소는 생략
// for 루프는 여러 요소를 생성
// Set과 Map에도 동일하게 사용 가능
// 데이터 기술을 선언적으로 만들 수 있음

맵 연산

Map의 키/값/엔트리를 순회할 수 있습니다. putIfAbsent는 지연 채움, update는 갱신, remove는 삭제입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
final ages = {'alice': 30, 'bob': 25};
ages['carol'] = 22; // 新增
ages.update('bob', (v) => v + 1); // 更新
ages.putIfAbsent('dave', () => 40);
ages.remove('alice');
for (final entry in ages.entries) {
print('${entry.key}: ${entry.value}');
}
}
// 없는 키에 접근하면 null 반환
// putIfAbsent는 없을 때만 계산
// entries는 키-값 쌍 제공
// containsKey는 키 존재 여부 판정

집합 연산

Set은 요소의 유일성을 보장합니다. union / intersection / difference로 집합 연산을 하고, toSet으로 리스트의 중복을 제거합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
final a = {1, 2, 3};
final b = {3, 4, 5};
print(a.union(b)); // {1,2,3,4,5}
print(a.intersection(b)); // {3}
print(a.difference(b)); // {1,2}
final nums = [1, 1, 2, 3];
final unique = nums.toSet();
print(unique);
}
// Set은 순서 없음, 요소는 유일
// contains 멤버 판정이 빠름
// Set으로 중복 제거와 멤버 검사 가능
// add는 이미 있는 요소면 아무것도 안 함

정렬과 검색

sort는 기본 오름차순이며 비교자를 전달할 수 있습니다. indexOf는 선형 탐색, contains는 존재 여부 판정입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void main() {
final names = ['bob', 'alice', 'carol'];
names.sort(); // 字母序
print(names);
final people = [
('bob', 30),
('alice', 20),
];
people.sort((x, y) => x.$2.compareTo(y.$2));
print(people);
print(names.indexOf('bob'));
}
// 기본적으로 Comparable 순서
// 비교 함수는 음/영/양수를 반환
// sort는 리스트를 원위치 수정
// 원래 순서가 필요하면 복사 후 정렬

읽기 전용 컬렉션

List.unmodifiable은 읽기 전용 리스트를 만들고 수정 시 UnsupportedError를 던집니다. List.of는 가변 리스트를 복사합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final frozen = List.unmodifiable([1, 2, 3]);
// frozen.add(4); // 抛 UnsupportedError
final editable = List.of([1, 2, 3]); // 可变副本
editable.add(4);
print(editable);
final constList = const [1, 2, 3]; // 编译期常量
print(frozen.length);
}
// unmodifiable은 런타임 읽기 전용
// const는 컴파일 타임 상수이며 정규화됨
// API에 넘길 때 읽기 전용이 더 안전
// Map.unmodifiable / Set.unmodifiable도 동일

9.메모리와 성능

가비지 컬렉션, const 정규화, 그리고 버퍼 재사용.

가비지 컬렉션

Dart VM이 가비지 컬렉션을 자동으로 수행하므로 메모리를 직접 해제할 필요가 없습니다. 객체는 더 이상 참조되지 않으면 회수됩니다.

1
2
3
4
5
6
7
8
9
10
void main() {
// 참조를 잃은 객체는 수거 후보가 됨:
var data = List<int>.generate(10000, (i) => i);
data = [1, 2, 3]; // 이전 리스트는 더 이상 참조되지 않음
print(data);
}
// 참조 없는 객체는 GC가 자동으로 수거
// 수동 delete는 필요도 없고 불가
// 수명이 짧은 객체가 많아도 걱정 없음
// Flutter는 세대별 GC 사용

const 정형화

const 값은 정형화되어 같은 상수는 한 인스턴스만 존재합니다. 핫 패스에는 const 활용으로 중복 할당을 줄이세요.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
const a = [1, 2, 3];
const b = [1, 2, 3];
print(identical(a, b)); // true:同一实例
final c = [1, 2, 3];
final d = [1, 2, 3];
print(identical(c, d)); // false
}
// const는 같은 값이면 정규화해 공유
// 비 const는 매번 새 객체
// 핫 경로에서는 가능한 한 const 리터럴
// 컴파일 타임 상수는 반복 생성되지 않음

지연 초기화

최상위 변수와 static 변수는 처음 접근할 때 지연 초기화됩니다. late도 동일한 지연 의미를 제공합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
print('开始');
print(cache); // 首次访问触发计算
print(cache); // 复用已有值
}
final cache = buildCache();
String buildCache() {
print('构建缓存中');
return 'heavy-data';
}
// 최상위 변수는 첫 접근 시 초기화
// 이후에는 같은 인스턴스를 재사용
// late 필드도 동일
// 시작 시 불필요한 비용을 피함

리스트 용량

가변 리스트는 필요 시 확장되며 잦은 add는 여러 번 재할당합니다. 크기를 알면 미리 할당하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final nums = <int>[];
for (var i = 0; i < 100; i++) {
nums.add(i); // 自动扩容
}
final sized = List<int>.filled(100, 0);
final growable = List<int>.generate(100, (i) => i);
print('${nums.length} ${sized.length} ${growable.length}');
}
// 기본 리스트는 성장 가능, 확장에 비용
// filled는 고정 길이로 초기화
// generate는 함수로 요소 생성
// 자주 추가한다면 일괄 구성이 효율적

버퍼 재사용

Uint8List 등 typed-data는 바이너리 버퍼에 적합합니다. 버퍼 재사용과 StringBuffer로 잦은 할당을 피하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'dart:typed_data';
void main() {
final bytes = Uint8List(4); // 固定大小
bytes[0] = 255;
final buffer = StringBuffer();
for (var i = 0; i < 10; i++) {
buffer.write(i);
}
print('$bytes ${buffer.toString()}');
}
// Uint8List는 바이트 단위 접근이 빠름
// 큰 바이너리는 List<int>가 아닌 Uint8List
// 버퍼를 재사용해 GC 부담 감소
// 큰 데이터는 통째로 읽지 말고 스트림 처리

약한 연결

Expando는 객체 자체를 수정하지 않고 데이터를 부착하며 키가 약 참조입니다. WeakReference는 회수를 방해하지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final tags = Expando<String>();
final obj = Object();
tags[obj] = '元数据'; // 关联数据
print(tags[obj]); // 元数据
final weak = WeakReference(obj);
print(weak.target == obj); // true
}
// Expando의 키는 약한 참조
// 객체 수거 후 관련 데이터도 함께 사라짐
// 캐시, 디버그, 프레임워크 확장에 적합
// 객체 자체의 필드를 차지하지 않음

객체 수명 주기

객체는 isolate 별 독립 힙에 만들어집니다. isolate 간에는 가변 객체를 공유할 수 없으며 복사나 메시지로 전달합니다.

1
2
3
4
5
6
7
8
9
10
11
void main() {
// 每个 isolate 有独立堆
final local = [1, 2, 3];
// 发送给其它 isolate 的是副本:
// final result = await Isolate.run(() => local);
print(local);
}
// isolate 간에 메모리를 공유하지 않음
// 상태 공유는 메시지 전달이나 포트
// 전역 가변 객체는 위험, 신중히
// 데이터 처리의 독립 작업에 적합

네이티브 메모리

C와 상호 운용할 때 dart:ffi로 네이티브 메모리를 할당합니다. 수동으로 해제해야 하며, 그렇지 않으면 누수가 발생합니다.

1
2
3
4
5
6
7
8
9
10
import 'dart:ffi';
// 포인터 타입 선언:
// typedef NativeBuf = Pointer<Uint8>;
// 네이티브 메모리 할당:
// final p = calloc<Uint8>(1024); // package:ffi
// p[i]로 바이트 읽기·쓰기
// 해제:
// calloc.free(p);
// 해제를 잊으면 누수 발생
// ffi는 네이티브 플랫폼에서만

10.객체 지향

클래스, 상속, mixin, 그리고 인터페이스.

클래스와 생성자

class는 객체 청사진을 정의합니다. 생성자는 클래스명과 같으며, this. 매개변수로 필드에 직접 대입할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Animal {
String name;
int age;
Animal(this.name, this.age); // this 파라미터 대입
void speak() {
print('$name 叫了一声');
}
}
void main() {
final dog = Animal('旺财', 3);
dog.speak();
print('${dog.name} ${dog.age} 岁');
}
// this.name으로 생성자 인수를 필드에 대입
// 필드는 기본적으로 non-null, 초기화 필수
// 메서드는 인스턴스 필드에 접근 가능
// 인스턴스화에 new 키워드 불필요

상속

extends는 부모를 상속하고, @override는 메서드를 재정의하며, super는 부모 구성 요소에 접근합니다. Dart는 단일 상속입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Animal {
Animal(this.name);
final String name;
String sound() => '...';
}
class Dog extends Animal {
Dog(String name) : super(name);
@override
String sound() => '汪汪';
}
void main() {
final dog = Dog('旺财');
print(dog.sound());
}
// 하위 클래스는 부모의 필드와 메서드를 상속
// @override는 오버라이드 명시
// super(name)로 부모 생성자에 인수 전달
// 한 클래스의 부모는 하나뿐

mixin

mixin은 재사용 가능한 동작 조각으로 with로 섞어 사용합니다. 상속보다 유연하며 단일 상속의 제약을 피합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mixin Swimmer {
void swim() => print('游泳');
}
mixin Flyer {
void fly() => print('飞行');
}
class Duck with Swimmer, Flyer {}
void main() {
final duck = Duck();
duck.swim();
duck.fly();
}
// with 키워드로 여러 mixin을 혼입
// mixin은 단독으로 인스턴스화 불가
// 부모 클래스를 한정 가능:mixin X on BaseClass {}
// 깊은 상속보다 동작 합성이 우수

추상 클래스

abstract class는 인스턴스화할 수 없으며 계약만 정의합니다. 서브클래스는 추상 메서드를 구현해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
abstract class Shape {
double area();
}
class Circle extends Shape {
Circle(this.r);
final double r;
@override
double area() => 3.14159 * r * r;
}
void main() {
final c = Circle(2);
print(c.area().toStringAsFixed(2));
}
// 추상 메서드는 본문이 없음
// 추상 클래스는 구체적 구현을 포함 가능
// implements로 인터페이스 구현도 가능
// 타입을 통일하면 부모로 사용 가능

인터페이스

모든 클래스는 암묵적으로 인터페이스이기도 하며 implements로 구현합니다. 모든 멤버를 재정의해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Greeter {
String greet(String name) => '你好,$name';
}
class EnglishGreeter implements Greeter {
@override
String greet(String name) => 'Hello, $name';
}
void main() {
final g = EnglishGreeter();
print(g.greet('Rex'));
}
// implements는 구현을 상속하지 않음
// 인터페이스의 모든 멤버를 오버라이드 필수
// 한 클래스는 여러 인터페이스 구현 가능
// implements로 타입 계약을 표현

접근자

get은 읽기 전용 접근자, set은 쓰기 가능 접근자를 정의합니다. 괄호 없이 호출되며 내부 저장을 숨길 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Rectangle {
Rectangle(this.width, this.height);
double width;
double height;
double get area => width * height;
set scale(double factor) {
width *= factor;
height *= factor;
}
}
void main() {
final r = Rectangle(2, 3);
print(r.area); // 6.0
r.scale = 2;
print(r.area); // 24.0
}
// get은 필드처럼 읽음
// set은 필드처럼 대입
// get/set 안에 검증 로직 작성 가능
// 계산 속성은 get으로 파생

정적 멤버

static 멤버는 인스턴스가 아닌 클래스 자체에 속합니다. static 메서드는 인스턴스 멤버에 접근할 수 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MathUtils {
static const pi = 3.14159;
static int square(int x) => x * x;
static int _counter = 0; // 私有静态
static int get count => _counter;
}
void main() {
print(MathUtils.pi);
print(MathUtils.square(5));
}
// 정적 멤버는 클래스 이름으로 직접 접근
// static은 인스턴스 필드에 접근 불가
// private 멤버는 밑줄로 시작, 라이브러리 내 가시
// 공유 설정에는 static 상수를 자주 사용

sealed 클래스

sealed 클래스는 서브클래스를 같은 라이브러리로 제한하여 switch를 default 없이도 완전 매칭할 수 있게 합니다. Dart 3 기능입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sealed class Shape {}
class Circle extends Shape {}
class Square extends Shape {}
String describe(Shape shape) => switch (shape) {
Circle() => '圆',
Square() => '方',
};
void main() {
print(describe(Circle()));
print(describe(Square()));
}
// sealed 하위 클래스는 같은 라이브러리 내로 한정
// 완전한 switch면 default 불필요
// 하위 클래스를 추가하면 컴파일 경고
// 패턴 매칭과 함께 사용해 안전

11.에러 처리

try/catch, 커스텀 예외, 그리고 비동기 에러.

try / catch

try는 예외 가능 코드를 감싸고, catch가 예외를 잡습니다. catch (e)로 예외 객체를 얻습니다.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
try {
final n = int.parse('abc');
print(n);
} catch (e) {
print('解析失败: $e');
}
}
// catch (e)는 모든 예외를 포착
// 예외가 던져지면 try 내 나머지는 스킵
// 포착되지 않은 예외는 크래시를 유발
// catch (e, st)로 스택 st를 얻을 수 있음

on 절

on 절은 특정 타입의 예외만 잡으며 catch와 함께 쓰입니다. 타입별로 분기 처리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main() {
try {
throw FormatException('格式错误');
} on FormatException catch (e) {
print('格式: ${e.message}');
} on ArgumentError {
print('参数错误');
} catch (e) {
print('其他: $e');
}
}
// on 타입으로 일치하는 예외 포착
// catch가 없는 on 분기는 객체를 받지 않음
// 순서대로 매치, 앞선 것이 우선
// 폴백 catch가 나머지를 포착

finally와 rethrow

finally는 예외 발생 여부와 무관하게 실행되어 자원 정리에 적합합니다. rethrow는 원래 스택을 유지한 채 다시 던집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void main() {
try {
outer();
} catch (e) {
print('最终处理: $e');
}
}
void outer() {
try {
throw Exception('原始错误');
} catch (e) {
print('中间捕获');
rethrow; // 保留堆栈继续抛
} finally {
print('清理资源');
}
}
// finally는 항상 실행됨
// rethrow는 catch 안에 있어야 함
// rethrow는 원래 스택을 유지
// 리소스·연결 정리에 자주 사용

예외 던지기

throw 키워드는 예외 객체를 던집니다. 무엇이든 던질 수 있지만, 관례적으로 Exception이나 Error의 서브클래스를 던집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void main() {
try {
checkAge(-1);
} catch (e) {
print('拒绝: $e');
}
}
void checkAge(int age) {
if (age < 0) {
throw ArgumentError('年龄不能为负');
}
}
// throw는 현재 제어 흐름을 중단
// 예외는 임의의 객체가 될 수 있음
// 관례적으로 Exception이나 Error
// 비즈니스 조건 불충족 시 예외 권장

사용자 정의 예외

Exception을 implements하여 비즈니스 예외를 정의합니다. toString을 재정의해 친절한 메시지를 주고 구조화된 필드를 담을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ValidationError implements Exception {
ValidationError(this.field, this.message);
final String field;
final String message;
@override
String toString() => '字段 $field 无效: $message';
}
void main() {
try {
throw ValidationError('email', '格式错误');
} on ValidationError catch (e) {
print(e.field); // email
print(e);
}
}
// implements Exception으로 예외 명시
// 구조화된 필드를 보유 가능
// on 절로 커스텀 타입을 정확히 포착
// 상위에서 타입별로 나눠 처리하기 쉬움

비동기 에러

async 함수에서 await가 던진 예외는 try/catch로 잡을 수 있습니다. 처리되지 않은 Future 에러는 조용히 사라집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Future<void> load() async {
throw Exception('加载失败');
}
Future<void> main() async {
try {
await load();
} catch (e) {
print('异步捕获: $e');
}
}
// await하지 않는 에러는 포착이 어려움
// catchError로 체인 처리:
// load().catchError(print);
// 처리 안 된 에러는 zone으로 들어갈 수 있음
// 통일된 에러 처리 전략 권장

스트림 에러

Stream을 listen할 때 onError 콜백으로 에러를 처리합니다. StreamController.addError로 스트림에 에러를 주입할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'dart:async';
void main() {
final controller = StreamController<int>();
controller.stream.listen(
(data) => print('数据: $data'),
onError: (e) => print('流错误: $e'),
onDone: () => print('流结束'),
);
controller.add(1);
controller.addError('意外错误');
controller.close();
}
// addError는 에러를 스트림에 주입
// onError 콜백이 스트림 에러 처리
// onDone은 닫힌 후 발동
// 스트림 에러는 다른 데이터를 중단하지 않음

단언

assert는 개발 모드에서 조건을 검사하며 실패 시 AssertionError를 던집니다. 릴리스 빌드에서는 제거됩니다.

1
2
3
4
5
6
7
8
9
void main() {
final score = 85;
assert(score >= 0, '分数不能为负'); // 通过
print('断言通过');
}
// 조건이 false면 AssertionError
// 두 번째 인수는 실패 메시지
// 릴리스 모드 dart compile은 단언 제거
// 불변 조건 검사용, 사용자 입력에는 부적합

12.파일과 I/O

파일 읽기/쓰기, JSON, 그리고 표준 입출력.

파일 읽기

dart:io의 File 클래스로 파일을 읽고 씁니다. readAsString으로 텍스트 파일 전체를 읽으며, import 'dart:io'가 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
import 'dart:io';
Future<void> main() async {
final text = await File('data.txt').readAsString();
print(text);
final exists = await File('data.txt').exists();
print(exists);
}
// readAsString은 한 번에 메모리로 읽음
// 큰 파일은 스트림 읽기
// 경로가 없으면 FileSystemException
// exists는 존재 여부를 불리언으로 반환

파일 쓰기

writeAsString은 텍스트, writeAsBytes는 바이너리를 씁니다. 추가 모드는 mode 매개변수로 지정합니다.

1
2
3
4
5
6
7
8
9
10
11
12
import 'dart:io';
Future<void> main() async {
await File('out.txt').writeAsString('Hello\n');
await File('out.txt').writeAsString('World\n',
mode: FileMode.append); // 追加
final content = await File('out.txt').readAsString();
print(content);
}
// 기본 모드는 원본 파일 덮어쓰기
// FileMode.append는 끝에 추가
// FileMode.writeOnly는 쓰기 전용 덮어쓰기
// 중국어 쓰기는 기본 UTF-8 인코딩

줄 단위 읽기

readAsLines은 파일을 줄 단위 리스트로 나눕니다. 큰 파일은 openRead로 스트림 처리하여 통째로 읽지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'dart:io';
import 'dart:convert';
Future<void> main() async {
final lines = await File('log.txt').readAsLines();
for (final line in lines) {
print(line);
}
final stream = File('big.log').openRead()
.transform(utf8.decoder)
.transform(const LineSplitter());
await for (final line in stream) {
print(line);
}
}
// readAsLines는 작은 파일용
// 큰 파일은 openRead로 스트림 처리
// LineSplitter는 줄 단위로 분할
// 스트림 처리는 메모리를 채우지 않음

JSON 처리

dart:convert의 jsonEncode / jsonDecode로 직렬화와 역직렬화를 합니다. JSON 숫자는 int 또는 double로 파싱됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
import 'dart:convert';
void main() {
final data = {'name': 'Rex', 'age': 5};
final encoded = jsonEncode(data);
print(encoded);
final decoded = jsonDecode(encoded) as Map<String, dynamic>;
print(decoded['name']);
}
// jsonDecode는 dynamic을 반환, 변환 필요
// 숫자는 int나 double로 파싱
// 알 수 없는 필드는 Map에 남음
// 복잡한 객체는 수동 매핑

표준 입출력

stdin.readLineSync으로 한 줄을 동기적으로 읽고, stdout.writeln으로 출력합니다. CLI 대화에 적합합니다.

1
2
3
4
5
6
7
8
9
10
import 'dart:io';
void main() {
stdout.write('请输入姓名: ');
final name = stdin.readLineSync() ?? '';
stdout.writeln('你好,$name');
}
// readLineSync는 한 줄 읽고 String? 반환
// 입력 종료 시 null, 기본값 필요
// stdout.write는 줄바꿈 없음
// stderr.writeln은 오류 스트림으로 출력

디렉터리 연산

Directory.list로 디렉터리 내용을 순회하고, create로 만들고, delete로 지웁니다. 반환은 FileSystemEntity입니다.

1
2
3
4
5
6
7
8
9
10
11
12
import 'dart:io';
Future<void> main() async {
await Directory('data/sub').create(recursive: true);
await for (final entity in Directory('.').list()) {
print('${entity.path} ${entity is Directory ? '目录' : '文件'}');
}
await Directory('data').delete(recursive: true);
}
// create recursive는 다단 디렉터리 생성
// list는 디렉터리 내용을 지연 반복
// delete recursive는 재귀 삭제
// entity로 파일과 디렉터리를 통일 처리

바이너리 바이트

readAsBytes로 바이너리를 읽고 Uint8List로 표현하며 writeAsBytes로 씁니다. 이미지나 오디오에 적합합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import 'dart:io';
import 'dart:typed_data';
Future<void> main() async {
final bytes = await File('image.png').readAsBytes();
print('大小: ${bytes.length} 字节');
final copy = Uint8List.fromList(bytes);
await File('copy.png').writeAsBytes(copy);
print('已复制');
}
// readAsBytes는 Uint8List 반환
// 바이트는 비텍스트 데이터에 적합
// fromList로 바이트 시퀀스 복사
// 큰 파일은 openWrite로 스트림 쓰기

경로 연산

File / Directory의 path 프로퍼티가 전체 경로를 줍니다. absolute는 절대 경로, uri는 file:// 형식으로 변환합니다.

1
2
3
4
5
6
7
8
9
10
11
12
import 'dart:io';
void main() {
final file = File('a/b/data.txt');
print(file.path); // 完整路径
print(file.absolute.path); // 绝对路径
print(file.uri); // file:// URI
print(Platform.pathSeparator);
}
// path 속성은 전체 경로 문자열 반환
// absolute는 절대 경로로 해석
// uri는 file:// URI로 변환
// Windows와 Unix의 구분자가 다름

13.자주 하는 실수

Dart 일상 개발에서 가장 많이 빠지는 함정과 올바른 작성법.

null 단언 남용

!로 null 체크를 우회하면 런타임 크래시가 나기 쉽습니다. null 체크, ??, 타입 승격이 더 안전합니다.

1
2
3
4
5
6
7
8
9
10
11
// BAD:用 ! 掩盖可空设计
String? name = 'Rex';
final len = name!.length; // 依赖运行时断言
print(len);
// GOOD:利用类型提升
String? nick = '小雷';
if (nick != null) {
print(nick.length);
}
// GOOD:?? 提供默认值
print(name?.toUpperCase() ?? '未知');

컬렉션 == 비교

List / Map의 ==는 참조 비교이며 내용이 같아도 동등하지 않습니다. 구조 비교는 도우미 함수를 사용하세요.

1
2
3
4
5
6
7
8
9
// BAD:== 比较的是引用
final a = [1, 2];
final b = [1, 2];
print(a == b); // false
// GOOD:比较内容而非引用
print(a.join(',') == b.join(',')); // true
// GOOD:结构比较推荐
// listEquals(a, b) package:collection
// 字符串与数字直接 == 比较内容

const와 final

const는 컴파일 시점 상수여야 하고, final은 런타임에 정해집니다. 런타임 값을 const로 쓰면 컴파일이 실패합니다.

1
2
3
4
5
6
7
8
9
// BAD:运行时值不能作 const
// const now = DateTime.now(); // 编译错误
// GOOD:运行时值用 final
final now = DateTime.now();
// GOOD:编译期常量用 const
const pi = 3.14159;
// const 值会规范化共享
// final 只是不能重新赋值
// 集合元素也可用 const

지연 Iterable

map / where는 지연 Iterable을 반환하며 순회 시 다시 계산되어 원본 변화가 결과에 반영됩니다. 스냅샷은 toList로 만드세요.

1
2
3
4
5
6
7
8
9
10
11
// BAD:惰性 Iterable 每次遍历重新计算
final nums = [1, 2, 3];
final lazy = nums.where((n) => n.isOdd);
nums.add(5);
print(lazy.toList()); // [1, 3, 5] 含新元素
// GOOD:需要快照用 toList
final snapshot = nums.where((n) => n.isOdd).toList();
nums.add(7);
print(snapshot); // 仍是 [1, 3, 5]
// 底层变化不影响已固化结果
// 流(Stream)只能监听一次

await 빠뜨리기

async 함수를 await하지 않으면 Future가 반환되며 결과가 아닙니다. 병렬 실행은 Future.wait를 사용하세요.

1
2
3
4
5
6
7
8
9
10
11
12
// BAD:await를 잊으면 Future 객체가 됨
Future<String> fetch() async => '数据';
Future<void> main() async {
final result = fetch(); // Future는 문자열이 아님
print(result);
// GOOD:await 해야 결과가 됨
final text = await fetch();
print(text);
// 여러 개를 병렬로 기다림:
final all = await Future.wait([fetch(), fetch()]);
print(all);
}

캐스케이드는 수신자를 반환

.. 캐스케이드는 같은 객체에 연속 호출하며 마지막 식의 값이 아닌 수신자를 반환합니다. 결과 값이 필요하면 캐스케이드를 쓰지 마세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class User {
String name = '';
int age = 0;
}
void main() {
// BAD:캐스케이드가 반환하는 것은 리시버
final result = [1, 2]..add(3);
print(result); // 3이 아닌 [1, 2, 3]
// GOOD:캐스케이드는 제자리 수정 후 원본 객체를 반환
final user = User()
..name = 'Rex'
..age = 5;
print(user.name);
// 함수 결과가 필요하면 일반 호출
print('abc'.toUpperCase());
}

순회 중 수정

순회 중 요소를 추가/삭제하면 ConcurrentModificationError가 발생합니다. removeWhere 또는 수집 후 처리하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// BAD:순회 중 요소 제거는 예외 발생
final items = [1, 2, 3, 4];
// for (final n in items) { items.remove(n); }
// 抛 ConcurrentModificationError
// GOOD:removeWhere로 안전하게 필터
final nums = [1, 2, 3, 4];
nums.removeWhere((n) => n.isEven);
print(nums); // [1, 3]
// GOOD:먼저 모은 뒤 일괄 처리
final keep = <int>[];
for (final n in nums) {
if (n.isOdd) keep.add(n);
}
print(keep);

문자열의 코드 단위

length는 UTF-16 코드 단위 수이므로 한자나 이모지는 글자 수보다 큽니다. 글자 단위 처리는 runes를 순회하세요.

1
2
3
4
5
6
7
8
9
10
11
12
// BAD:length로 중국어 문자를 세기
final s = '你好';
print(s.length); // 2 个单元
// emoji의 경우:
final emoji = '👍';
print(emoji.length); // 2!하나의 기호
// GOOD:runes로 코드포인트를 세기
print(emoji.runes.length); // 1
// GOOD:runes로 글자마다 처리
for (final r in emoji.runes) {
print(String.fromCharCode(r));
}

14.동시성과 비동기

isolate, Future, Stream, 그리고 이벤트 루프.

아이솔레이트 동시성

isolate는 Dart의 동시성 단위로 각각 독립 메모리와 이벤트 루프를 가집니다. Isolate.run 또는 Isolate.spawn으로 시작합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'dart:isolate';
Future<void> main() async {
final result = await Isolate.run(() {
var sum = 0;
for (var i = 0; i < 100000; i++) {
sum += i;
}
return sum;
});
print('结果: $result');
}
// Isolate.run은 Future<결과>를 반환
// 메모리를 분리, 가변 상태를 공유하지 않음
// CPU 집약적 계산에 적합
// 계산 완료 시 자동으로 파괴

spawn과 포트

Isolate.spawn은 진입 함수로 새 isolate를 시작하며, SendPort가 송신, ReceivePort가 수신을 담당합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'dart:isolate';
void entry(SendPort sendPort) {
sendPort.send('来自新 isolate');
}
Future<void> main() async {
final receive = ReceivePort();
await Isolate.spawn(entry, receive.sendPort);
final msg = await receive.first;
print(msg);
receive.close();
}
// spawn의 진입점은 최상위 함수나 정적 메서드 필요
// 메시지는 포트를 통해 전달
// receive.first는 첫 메시지를 기다림
// 사용 후 포트를 close

Future 기초

Future는 미래에 완료되는 결과를 나타냅니다. Future.value로 즉시 성공시키고, await로 완료를 기다립니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Future<int> addLater(int a, int b) async {
await Future.delayed(Duration(milliseconds: 10));
return a + b;
}
Future<void> main() async {
final future = Future.value(42);
print(await future);
final result = await addLater(1, 2);
print(result);
}
// async 함수는 Future를 반환
// await는 Future 완료까지 대기
// Future.delayed는 타이머 지연
// Future.value는 즉시 결과를 감쌈

async / await

async는 비동기 함수를 표시하고, await는 비동기 작업을 기다립니다. await는 async 함수에서만 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Future<String> fetchUser() async {
await Future.delayed(Duration(milliseconds: 50));
return 'Rex';
}
Future<void> main() async {
print('开始加载');
final name = await fetchUser();
print('加载到: $name');
final results = await Future.wait([
fetchUser(),
fetchUser(),
]);
print(results);
}
// await는 흐름을 멈추지만 메인 스레드는 블록하지 않음
// 여러 await는 기본적으로 직렬
// Future.wait는 여러 개를 병렬 대기
// 예외는 await 위치로 전달되어 포착 가능

Stream

Stream은 비동기 이벤트 시퀀스입니다. listen으로 구독하고 await for로 순차 처리합니다. Future의 1회성과 다릅니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'dart:async';
Stream<int> tick(int max) async* {
for (var i = 0; i < max; i++) {
await Future.delayed(Duration(milliseconds: 10));
yield i;
}
}
Future<void> main() async {
await for (final n in tick(3)) {
print('tick $n');
}
tick(2).listen((n) => print('listen $n'));
await Future.delayed(Duration(milliseconds: 100));
}
// await for는 스트림 이벤트를 순서대로 소비
// listen으로 리스너 등록
// 스트림은 한 번만 생성되고 재생 불가
// 각 이벤트는 순서대로 도착

StreamController

StreamController로 수동 제어하며, add는 데이터, addError는 에러, close는 종료를 뜻합니다. broadcast는 다중 구독을 지원합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import 'dart:async';
void main() {
final controller = StreamController<int>();
controller.stream.listen((n) => print('收到 $n'));
controller.add(1);
controller.add(2);
controller.close();
final broadcast = StreamController<int>.broadcast();
broadcast.stream.listen((n) => print('A $n'));
broadcast.stream.listen((n) => print('B $n'));
broadcast.add(5);
broadcast.close();
}
// 기본 스트림은 리스너 하나만 허용
// broadcast는 여러 리스너 허용
// add로 스트림에 데이터 전송
// close를 잊지 말고 리소스 해제

여러 Future 기다리기

Future.wait는 모두 완료를, Future.any는 가장 빠른 완료를 기다립니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Future<int> slow() async {
await Future.delayed(Duration(milliseconds: 30));
return 1;
}
Future<int> fast() async {
await Future.delayed(Duration(milliseconds: 10));
return 2;
}
Future<void> main() async {
final all = await Future.wait([slow(), fast()]);
print(all); // [1, 2]
final first = await Future.any([slow(), fast()]);
print(first); // 2
}
// wait는 모두 성공하면 결과 목록 반환
// 하나라도 실패하면 기본적으로 오류 전파
// any는 가장 먼저 완료된 것을 반환
// eagerError로 즉시 실패를 제어

Completer

Completer로 Future 완료 시점을 수동 제어할 수 있어, 콜백 스타일 API를 async/await 스타일로 감쌀 때 유용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'dart:async';
Future<String> fromCallback() {
final completer = Completer<String>();
Future.delayed(Duration(milliseconds: 20), () {
completer.complete('回调结果');
});
return completer.future;
}
Future<void> main() async {
final result = await fromCallback();
print(result);
}
// complete로 Future를 성공시킴
// completeError로 Future를 실패시킴
// complete는 한 번만 가능
// 서드파티 콜백을 감쌀 때 사용

15.네트워크

HTTP 요청, WebSocket, 그리고 TCP 소켓.

HTTP 클라이언트

dart:io의 HttpClient로 HTTP 요청을 보냅니다. getUrl로 요청 객체를 얻고 close한 뒤 본문을 읽습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'dart:io';
import 'dart:convert';
Future<void> main() async {
final client = HttpClient();
final request = await client.getUrl(Uri.parse('https://example.com'));
final response = await request.close();
final body = await response.transform(utf8.decoder).join();
print('状态码: ${response.statusCode}');
print(body.substring(0, 50));
client.close();
}
// HttpClient는 저수준이며 수동 close 필요
// 응답 본문은 바이트 스트림, utf8.decoder 사용
// statusCode 200은 성공을 의미
// 단순한 용도는 package:http 우선

package:http

package:http는 일반적인 요청을 감쌉니다. http.get / http.post는 Response를 반환하며 body는 그대로 문자열입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<void> main() async {
final response = await http.get(
Uri.parse('https://example.com'),
);
print('状态码: ${response.statusCode}');
print(response.body.substring(0, 50));
final post = await http.post(
Uri.parse('https://example.com/api'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'name': 'Rex'}),
);
print(post.statusCode);
}
// pubspec에 http 의존성 추가 필요
// Response.body는 이미 디코딩된 문자열
// headers에 요청 헤더를 전달
// 상태 코드로 성공 여부 판단

URI 파싱

Uri.parse로 URL을 파싱하고, queryParameters로 쿼리를 읽으며, Uri.http로 요청 URL을 구성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final uri = Uri.parse('https://api.com/users?id=5&page=2');
print(uri.scheme); // https
print(uri.host); // api.com
print(uri.path); // /users
print(uri.queryParameters); // {id: 5, page: 2}
final built = Uri.http('api.com', '/users', {'id': '5'});
print(built);
}
// queryParameters는 자동 디코딩
// Uri.https는 TLS 사용
// queryParametersAll은 중복 키 처리
// replace로 새 URI 파생

JSON API

JSON API를 호출하고 jsonDecode로 응답 본문을 파싱합니다. package:http와 Future를 함께 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<void> main() async {
final response = await http.get(
Uri.parse('https://api.example.com/users'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as List;
for (final item in data) {
final user = item as Map<String, dynamic>;
print(user['name']);
}
}
}
// 상태 코드 확인 후 파싱
// JSON 배열은 List, 객체는 Map으로
// 없는 필드는 null 반환
// 실제 프로젝트는 모델 클래스로 감쌈

WebSocket

WebSocket은 전이중 통신입니다. WebSocket.connect로 연결하고, add로 송신, 스트림으로 수신합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'dart:io';
Future<void> main() async {
final ws = await WebSocket.connect('wss://example.com/ws');
ws.add('你好');
await for (final message in ws) {
print('收到: $message');
if (message == 'bye') {
await ws.close();
}
}
}
// connect로 연결 수립
// add로 메시지 전송
// 스트림으로 응답 메시지 수신
// close로 graceful하게 종료

TCP 소켓

Socket.connect로 TCP 연결을 맺고 요청을 쓰며 응답을 읽습니다. 저수준 프로토콜과 커스텀 서비스에 적합합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'dart:io';
Future<void> main() async {
final socket = await Socket.connect('example.com', 80);
socket.write('GET / HTTP/1.0\r\n\r\n');
await for (final data in socket) {
final text = String.fromCharCodes(data);
if (text.contains('HTTP')) print(text);
}
socket.destroy();
}
// Socket은 바이트 스트림 제공
// write로 요청 내용 전송
// 응답 조각을 스트림으로 읽음
// destroy로 즉시 닫음

HTTP 서버

HttpServer.bind로 로컬 서버를 띄우고 요청을 받아 응답합니다. 개발/디버깅과 도구에 적합합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'dart:io';
Future<void> main() async {
final server = await HttpServer.bind('localhost', 8080);
print('监听 8080 端口');
await for (final request in server) {
request.response.headers.set('Content-Type', 'text/plain; charset=utf-8');
request.response.write('你好,路径 ${request.uri.path}');
await request.response.close();
}
}
// bind로 주소와 포트를 바인딩
// 각 요청은 await for에 들어옴
// response 객체로 내용 반환
// close로 이번 응답 종료

타임아웃과 에러

네트워크 요청은 타임아웃이나 실패가 있을 수 있습니다. .timeout으로 제한하고 try/catch로 예외를 처리하세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'dart:async';
import 'package:http/http.dart' as http;
Future<void> main() async {
try {
final response = await http
.get(Uri.parse('https://example.com'))
.timeout(const Duration(seconds: 5));
print(response.statusCode);
} on TimeoutException {
print('请求超时');
} catch (e) {
print('网络错误: $e');
}
}
// timeout은 초과 시 TimeoutException 발생
// 네트워크 예외는 대부분 SocketException
// 오류 유형별로 나눠 처리
// 운영 환경은 지수 백오프 재시도 추가

16.시간과 날짜

DateTime, Duration, 포매팅, 그리고 타임스탬프.

현재 시각

DateTime.now()로 현재 로컬 시각을 얻습니다. 각 필드에 직접 접근 가능하며 밀리초도 얻을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
void main() {
final now = DateTime.now();
print('${now.year}-${now.month}-${now.day}');
print('${now.hour}:${now.minute}:${now.second}');
print(now.weekday); // 周一=1 周日=7
print(now.millisecondsSinceEpoch);
}
// now는 로컬 타임존 시간
// month/day는 1부터 시작
// weekday는 1-7, 월요일이 1
// 각 필드는 int

시각 생성

DateTime(년, 월, 일, ...)로 지정 시각을 만들고, DateTime.utc로 UTC 시각을 만듭니다.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final birthday = DateTime(2020, 1, 15);
print(birthday);
final launch = DateTime(2026, 8, 2, 10, 30);
print(launch);
final utc = DateTime.utc(2026, 1, 1);
print(utc.isUtc); // true
}
// 전달하지 않은 부분은 기본 0 또는 1
// 잘못된 월일은 자동으로 올림
// UTC 생성은 DateTime.utc
// 비교는 isBefore/isAfter

Duration

Duration은 시간의 길이를 나타내며 시/분/초/마이크로초를 지원합니다. 사칙연산, 비교, 단위 변환이 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final halfHour = Duration(minutes: 30);
final fiveMin = Duration(minutes: 5);
print(halfHour.inSeconds); // 1800
print(halfHour + fiveMin); // 35 分钟
print(halfHour > fiveMin); // true
final d = Duration(days: 1, hours: 2);
print('${d.inHours} 小时'); // 26
}
// inSeconds/inMinutes 등으로 단위 변환
// + - * / 연산 지원
// 크기 비교 가능
// 지연에는 Duration(milliseconds: n)이 정석

포맷 출력

Dart에는 내장 strftime가 없어 padLeft로 0 패딩하거나 intl 패키지의 DateFormat를 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main() {
final now = DateTime.now();
final y = now.year.toString();
final m = now.month.toString().padLeft(2, '0');
final d = now.day.toString().padLeft(2, '0');
print('$y-$m-$d');
print(now.toIso8601String());
// intl 패키지가 날짜 템플릿 제공:
// import 'package:intl/intl.dart';
// print(DateFormat('yyyy-MM-dd').format(now));
}
// padLeft로 자릿수 채움
// toIso8601String은 ISO 형식 출력
// intl은 지역화된 날짜 템플릿 지원
// 단순한 용도는 수동 연결로 충분

시각 파싱

DateTime.parse는 ISO 형식 문자열을 파싱합니다. toIso8601String은 표준 형식을 생성해 파싱과 전송을 쉽게 만듭니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
final parsed = DateTime.parse('2026-08-02 10:30:00');
print(parsed.year); // 2026
final iso = DateTime.parse('2026-08-02T10:30:00Z');
print(iso.isUtc); // true
final parts = '2026-08-02'.split('-');
final d = DateTime(
int.parse(parts[0]), int.parse(parts[1]), int.parse(parts[2]));
print(d.day);
}
// 다양한 ISO 형식 지원
// 타임존 없으면 로컬 시간
// Z를 포함하면 UTC
// 잘못된 형식은 FormatException

시각 연산

add / subtract로 Duration을 가감하고, difference로 두 시각의 차이를 Duration으로 구합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final start = DateTime(2026, 8, 2, 10);
final later = start.add(Duration(hours: 5));
print(later.hour); // 15
final earlier = start.subtract(const Duration(days: 1));
print(earlier.day); // 1
final diff = later.difference(start);
print(diff.inHours); // 5
}
// add/subtract는 새 객체를 반환
// 원본 객체는 불변
// difference는 항상 음이 아님
// 방향 있는 비교는 isBefore 사용

타임스탬프

밀리초/마이크로초 타임스탬프는 저장과 정렬에 편리합니다. fromMillisecondsSinceEpoch으로 시각을 복원합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final now = DateTime.now();
final ms = now.millisecondsSinceEpoch;
print(ms);
final back = DateTime.fromMillisecondsSinceEpoch(ms);
print(back.toIso8601String());
final day = DateTime(now.year, now.month, now.day);
print(day.millisecondsSinceEpoch);
}
// 타임스탬프는 1970-01-01 이후의 밀리초 수
// 저장은 UTC 타임스탬프로 타임존 혼란 방지
// fromMicrosecondsSinceEpoch는 마이크로초 대응
// 같은 타임스탬프로 복원한 시간은 동일

시간대

DateTime은 기본이 로컬 시간대입니다. isUtc로 판정하고 toUtc / toLocal로 변환합니다. 저장은 UTC를 권장합니다.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final now = DateTime.now();
print(now.isUtc); // false
final utc = now.toUtc();
print(utc.isUtc); // true
final local = utc.toLocal();
print(local == now); // 时间一致
}
// toUtc/toLocal 상호 변환은 시각을 바꾸지 않음
// DateTime에 타임존 이름은 내장되지 않음
// IANA 타임존이 필요하면 package:timezone
// 영속화는 UTC 저장 권장

스톱워치

Stopwatch는 코드 실행 시간을 측정합니다. start / stop / reset으로 제어하며 elapsed로 경과 시간을 얻습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
final watch = Stopwatch()..start();
var sum = 0;
for (var i = 0; i < 1000000; i++) {
sum += i;
}
watch.stop();
print('耗时 ${watch.elapsedMilliseconds} ms');
print('微秒 ${watch.elapsedMicroseconds}');
}
// start로 계시 시작
// elapsed는 Duration 반환
// elapsedMilliseconds는 밀리초 수
// reset으로 초기화 후 재시작

17.프로세스와 환경

서브프로세스, 환경 변수, 표준 스트림, 그리고 시그널.

자식 프로세스 실행

Process.run은 외부 명령을 실행하고 종료를 기다립니다. result.stdout / stderr가 출력을 담습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'dart:io';
Future<void> main() async {
final result = await Process.run('dart', ['--version']);
print('退出码: ${result.exitCode}');
print('输出: ${result.stdout}');
final ls = await Process.run(
Platform.isWindows ? 'cmd' : 'ls',
Platform.isWindows ? ['/c', 'dir'] : ['-la'],
);
print(ls.stdout);
}
// run은 프로세스 종료를 기다려 결과 반환
// stdout/stderr는 문자열 또는 바이트
// exitCode 0은 성공
// 대화형이 필요하면 Process.start

스트리밍 프로세스

Process.start는 프로세스를 시작하자마자 반환하며 stdout을 실시간으로 읽고 stdin을 쓸 수 있습니다. 장기 실행 작업에 적합합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'dart:io';
import 'dart:convert';
Future<void> main() async {
final process = await Process.start('dart', ['--version']);
process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) => print('子进程输出: ${line.trim()}'));
final exitCode = await process.exitCode;
print('退出码: $exitCode');
}
// start는 프로세스 종료를 기다리지 않음
// stdout은 스트림으로 실시간 감시
// 대화형이 필요하면 stdin에 작성
// environment로 환경 커스터마이즈

환경 변수

Platform.environment은 환경 변수를 읽기 전용 Map으로 반환합니다. 없는 키는 null입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'dart:io';
void main() {
final env = Platform.environment;
final path = env['PATH'] ?? '未设置';
print('PATH: $path');
print('有 HOME: ${env.containsKey('HOME')}');
for (final entry in env.entries) {
if (entry.key.startsWith('FLUTTER')) {
print('${entry.key}=${entry.value}');
}
}
}
// environment는 읽기 전용 Map
// 없는 키는 null
// containsKey로 존재 판단
// 프로세스 생성 시 environment를 전달해 주입

종료 코드

exitCode로 종료 코드를 설정합니다. 0이 성공, 그 외는 실패 유형이며 셀이 이를 기준으로 판단합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'dart:io';
void main(List<String> args) {
if (args.isEmpty) {
stderr.writeln('用法: app <参数>');
exitCode = 1;
return;
}
print('处理 ${args[0]}');
exitCode = 0; // 成功
}
// exitCode 0은 성공
// 0이 아니면 오류 유형
// exit()는 프로세스를 즉시 종료
// 스크립트는 종료 코드로 결과 판단

인수와 스크립트

args는 커맨드라인 인수, Platform.script는 진입 스크립트 경로, Directory.current는 현재 작업 디렉터리입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import 'dart:io';
void main(List<String> args) {
print('参数个数: ${args.length}');
for (var i = 0; i < args.length; i++) {
print('参数 $i: ${args[i]}');
}
print('脚本路径: ${Platform.script}');
print('当前目录: ${Directory.current.path}');
}
// args에 프로그램 이름은 포함되지 않음
// Platform.script는 진입 파일 경로
// Directory.current는 현재 작업 디렉터리
// 복잡한 인수는 args 패키지로 파싱

표준 스트림

stdout / stderr는 출력, stdin은 입력입니다. flush로 버퍼를 비우고 stderr는 일반 파이프 출력에 포함되지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'dart:io';
void main() {
stdout.write('请输入:');
stdout.flush();
final input = stdin.readLineSync() ?? '';
stdout.writeln('你输入了: $input');
if (input.isEmpty) {
stderr.writeln('警告:输入为空');
}
}
// stdout.write는 줄바꿈 없음
// flush는 버퍼를 즉시 출력
// stderr는 오류 스트림으로
// 파이프 처리 시 stdout이 리다이렉트됨

파일 시스템

File / Directory가 파일 시스템을 다룹니다. create로 생성, delete로 삭제, rename으로 이동, exists로 존재 판정.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'dart:io';
Future<void> main() async {
final dir = Directory('data/sub');
await dir.create(recursive: true);
final file = File('data/note.txt');
await file.writeAsString('内容');
print(await file.exists()); // true
print(file.lengthSync()); // 字节数
await file.rename('data/new.txt');
await Directory('data').delete(recursive: true);
}
// create recursive로 다단 디렉터리 생성
// exists는 존재 여부 판단
// lengthSync는 동기 크기 조회
// rename은 이동 또는 이름 변경

시그널 처리

ProcessSignal로 시스템 시그널(SIGINT / SIGTERM)에 응답합니다. 우아한 종료와 정리용으로 사용됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'dart:io';
Future<void> main() async {
ProcessSignal.sigint.watch().listen((_) {
print('收到中断,清理退出');
exit(0);
});
print('运行中,按 Ctrl+C 退出');
await Future.delayed(Duration(seconds: 30));
}
// sigint는 Ctrl+C
// sigterm은 kill 기본 시그널
// watch는 시그널을 스트림으로 감시
// 처리 후 exit 호출로 종료
// Windows는 일부 시그널만 사용 가능

18.정규 표현식

RegExp의 매칭, 치환, 분할, 그리고 그룹.

생성과 판정

RegExp는 정규 표현식을 나타냅니다. hasMatch로 존재 여부를 판정합니다. r 접두사로 이스케이프를 피합니다.

1
2
3
4
5
6
7
8
9
10
11
void main() {
final digits = RegExp(r'\d+');
print(digits.hasMatch('abc123')); // true
print(digits.hasMatch('abc')); // false
final email = RegExp(r'^[\w.-]+@[\w-]+\.\w+$');
print(email.hasMatch('[email protected]'));
}
// r 접두사는 원시 문자열을 뜻함
// \d 숫자 \w 단어 문자 \s 공백
// ^ 행 시작 $ 행 끝
// hasMatch는 존재 여부만 판단

첫 번째 매칭

firstMatch는 첫 매칭을 반환합니다. group은 그룹, start / end는 위치를 알려주며 매칭이 없으면 null입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
final re = RegExp(r'\d+');
final match = re.firstMatch('编号 42,编号 99');
if (match != null) {
print(match.group(0)); // 42
print(match.start); // 매치 시작 위치
print(match.end); // 매치 끝 위치
print(match.matched); // 42
}
}
// firstMatch는 첫 매치 반환
// 매치 없으면 null
// group(0)은 매치 전체
// group(n)은 n번째 그룹

모든 매칭

allMatches는 모든 매칭의 이터레이터를 반환합니다. 패턴과 결합해 조건에 맞는 내용을 추출할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void main() {
final re = RegExp(r'\d+');
final matches = re.allMatches('a1 b22 c333');
final numbers = <String>[];
for (final m in matches) {
numbers.add(m.group(0)!);
}
print(numbers); // [1, 22, 333]
final list = RegExp(r'\d+')
.allMatches('a1 b22')
.map((m) => m.group(0)!)
.toList();
print(list);
}
// 각 매치 객체를 순회
// group(0)으로 매치 전체 취득
// map과 결합하면 간결
// 지연 반복으로 필요 시 계산

치환

replaceAll은 모두 치환, replaceFirst는 첫 항목만 치환합니다. 콜백으로 동적 치환 문자열을 생성할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
final re = RegExp(r'\d+');
print(re.replaceAll('a1b2', '#')); // a#b#
print(re.replaceFirst('a1b2', '#')); // a#b2
final result = re.replaceAllMapped('a1b2', (m) {
final n = int.parse(m.group(0)!);
return '${n + 1}';
});
print(result); // a2b3
}
// replaceAll은 모두 치환
// replaceFirst는 처음만
// 콜백으로 내용에 따라 계산
// 원본은 불변, 새 문자열 반환

분할

String.split은 RegExp를 받아 패턴으로 분할합니다. 구분자 자체는 결과에 들어가지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
void main() {
final text = 'a, b, c';
final parts = text.split(RegExp(r',\s*'));
print(parts); // [a, b, c]
print('one two three'.split(RegExp(r'\s+')));
print(text.replaceAll(RegExp(r',\s*'), ' | '));
}
// split은 매치 위치에서 분할
// 구분 문자는 결과에 포함되지 않음
// 복잡한 구분은 정규식
// 단순한 구분은 문자열 직접 전달

캡처 그룹

괄호는 캡처 그룹을 정의합니다. group(n)으로 접근하고, (?<name>...)로 이름 있는 그룹을 정의해 namedGroup으로 참조합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void main() {
final re = RegExp(r'(\d{4})-(\d{2})-(\d{2})');
final m = re.firstMatch('日期 2026-08-02');
if (m != null) {
print(m.group(1)); // 2026
print(m.group(2)); // 08
print(m.group(3)); // 02
}
final named = RegExp(r'(?<year>\d{4})');
final nm = named.firstMatch('2026');
print(nm?.namedGroup('year'));
}
// group(1)부터 캡처 그룹
// 비캡처 (?:...)는 번호를 소모하지 않음
// 이름 있는 그룹으로 가독성 향상
// namedGroup으로 이름으로 취득

플래그

RegExp 생성자 두 번째 인수로 플래그를 설정합니다. caseSensitive, multiLine, dotAll, unicode 등이 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final re = RegExp('dart', caseSensitive: false);
print(re.hasMatch('Hello DART')); // true
final multiline = RegExp('^a', multiLine: true);
print(multiline.hasMatch('x\na\nb')); // true
final dotAll = RegExp('a.b', dotAll: true);
print(dotAll.hasMatch('a\nb')); // true
}
// caseSensitive: false로 대소문자 무시
// multiLine은 ^를 모든 행 시작에 매치
// dotAll은 .을 줄바꿈에도 매치
// unicode: true로 Unicode 모드 활성화

자주 쓰는 패턴

이메일, 휴대폰, URL, IP 같은 흔한 검증 패턴을 재사용 가능한 상수로 두면 유지보수가 쉬워집니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
final email = RegExp(r'^[\w.+-]+@[\w-]+\.\w+$');
final phone = RegExp(r'^1[3-9]\d{9}$');
final url = RegExp(r'^https?://\S+$');
final ip = RegExp(r'^\d{1,3}(\.\d{1,3}){3}$');
print(email.hasMatch('[email protected]'));
print(phone.hasMatch('13800138000'));
print(url.hasMatch('https://dart.dev'));
print(ip.hasMatch('192.168.1.1'));
}
// 이메일:일반 검증
// 전화번호:중국 11자리
// URL:http/https
// IP:4세그먼트 점분리 십진수

19.빌드와 디버깅

pub, 정적 검사, 포매팅, 그리고 테스트.

pubspec 설정

pubspec.yaml은 패키지 설정 파일로 이름, 버전, SDK, 의존성을 선언합니다. dart pub get으로 의존성을 해소합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// pubspec.yaml 예시:
// name: myapp
// description: 示例应用
// version: 1.0.0
// environment:
// sdk: '>=3.0.0 <4.0.0'
// dependencies:
// http: ^1.1.0
// dev_dependencies:
// test: ^1.24.0
// 버전 제약:
// ^1.1.0은 >=1.1.0 <2.0.0을 의미
// 의존성 설치:
// dart pub get

pub 명령

dart pub get으로 설치, upgrade로 갱신, outdated로 점검, add / remove로 의존성을 관리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 의존성 설치:
// dart pub get
// 모든 의존성 업그레이드:
// dart pub upgrade
// 만료된 의존성 확인:
// dart pub outdated
// 의존성 추가:
// dart pub add http
// 의존성 제거:
// dart pub remove http
// 패키지 검색:
// dart pub search json
// 배포:
// dart pub publish
// 버전은 pubspec.lock에 고정

정적 분석

dart analyze는 코드를 정적으로 분석해 오류와 경고를 보고합니다. CI에서 실행해 품질을 보장하세요.

1
2
3
4
5
6
7
8
9
10
11
12
// 현재 패키지 분석:
// dart analyze
// 지정 파일만 분석:
// dart analyze lib/main.dart
// 흔한 검사 항목:
// 미사용 import
// 타입 오류
// 비권장 작성법
// lint 수정 적용:
// dart fix --apply
// CI 통합:
// dart analyze && dart test

포맷

dart format은 코드 스타일을 통일합니다. 들여쓰기, 따옴표, 줄바꿈을 자동으로 정리하며 팀 작업에 필수입니다.

1
2
3
4
5
6
7
8
9
10
11
// 현재 디렉터리 서식:
// dart format .
// 지정 파일 서식:
// dart format lib/main.dart
// 변경 없이 검사:
// dart format --output=none --set-exit-if-changed .
// 스타일 관례:
// 2칸 들여쓰기
// 단일 따옴표 우선
// 줄 끝에 공백 금지
// CI에서 서식 일관성 검사

단위 테스트

package:test가 test()와 expect를 제공합니다. dart test로 실행하며 테스트는 test/ 디렉터리에 둡니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// test/calculator_test.dart:
import 'package:test/test.dart';
int add(int a, int b) => a + b;
void main() {
test('加法正确', () {
expect(add(1, 2), 3);
expect(add(-1, 1), 0);
});
group('边界情况', () {
test('大数相加', () {
expect(add(1000000, 1), 1000001);
});
});
}
// 테스트 실행:
// dart test
// 단일 파일만 실행:
// dart test test/calculator_test.dart
// 이름으로 필터:
// dart test -n 加法

컴파일과 배포

dart compile은 실행 파일을 생성합니다. exe는 네이티브, js는 JavaScript, aot-snapshot은 AOT 스냅샷입니다.

1
2
3
4
5
6
7
8
9
10
11
12
// 네이티브 실행 파일로 컴파일:
// dart compile exe bin/main.dart
// 출력 지정:
// dart compile exe bin/main.dart -o myapp
// JS(브라우저)로 컴파일:
// dart compile js lib/main.dart
// AOT 스냅샷:
// dart compile aot-snapshot bin/main.dart
// 컴파일 커널:
// dart compile kernel bin/main.dart
// 컴파일 후 Dart SDK 없이 실행 가능
// 네이티브 exe는 시작이 빠름

lint 규칙

analysis_options.yaml에서 lint 규칙을 설정합니다. lints 패키지가 권장 규칙 집합을 제공합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// analysis_options.yaml 예시:
// include: package:lints/recommended.yaml
// linter:
// rules:
// - prefer_final_locals
// - avoid_print
// analyzer:
// language:
// strict-casts: true
// 권장 규칙 세트:
// package:lints/recommended.yaml
// Flutter 프로젝트는 flutter_lints 사용
// dart analyze가 이 규칙들을 적용

디버깅

print 디버깅이 가장 흔합니다. assert로 불변식을 검증하고 IDE 중단점으로 단계별 실행이 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void main() {
final total = calculate(3, 4);
print('调试: total=$total');
assert(total > 0, '结果应为正');
print(total);
}
int calculate(int a, int b) {
final sum = a + b;
// dart:developer의 log는 레벨 지정 지원:
// log('中间值 $sum', name: 'calc');
return sum;
}
// print는 콘솔로 출력
// dart:developer의 log는 레벨 지정 지원
// 단언은 릴리스 모드에서 제거
// 중단점으로 단계 실행 지원
// Flutter 대량 로그에는 debugPrint 사용

공식 링크

공식 문서와 리소스에 대한 직접 링크.

이 치트시트에 대해

이 페이지는 Dart 3의 자족형 치트시트로, 언어 핵심과 가장 자주 쓰이는 표준 라이브러리가 실제 프로젝트의 약 80% 사용 사례를 다룹니다. 현대적 관용구를 중심으로 정리했습니다 — 건전한 null 안전성, 레코드와 패턴, switch 표현식, sealed 클래스, 캐스케이드 연산자, 그리고 isolate 기반 동시성 모델. Dart는 Google이 설계했고 Flutter의 기반 언어로, JIT의 빠른 개발과 AOT의 효율적 배포를 모두 지원합니다. 권위 있는 레퍼런스는 공식 Dart 언어 투어와 Effective Dart를 참고하세요. 19개 챕터가 각각 한 주제를 다룹니다 — 첫 프로그램부터 isolate, 흔한 함정, 빌드와 테스트까지. 각 챕터는 8개의 짧은 예제(각 5~15줄)로 나뉘며, 총 약 152개의 토픽을 제공합니다. 스니펫은 의도적으로 짧고 자기 설명적으로 작성되었으며, 주석은 중국어 원문 그대로 둡니다. 모든 처리는 브라우저에서 이루어집니다 — 업로드나 추적이 없습니다. 본 페이지는 GuruToolkit의 무료 개발자 도구 모음의 일부이며, 스니펫은 어떠한 보증 없이 자유롭게 사용할 수 있습니다.

버전 2.1.0