Effective Dart: 사용(Usage)
Effective Dart: 사용(Usage)
유지보수하기 쉬운 코드를 작성하기 위해 언어 기능을 사용하는 지침을 살펴볼게요.
출처: 원문
본문
이 지침들은 Dart 코드 본문에서 매일 사용할 수 있어요. 라이브러리 사용자는 여러분이 여기의 아이디어를 내면화했는지 알 수 없을지 몰라도, 유지보수자는 분명히 알 수 있을 거예요.
라이브러리(Libraries)
이 지침들은 프로그램을 여러 파일로 일관되고 유지보수하기 쉽게 구성하는 데 도움을 줘요. 지침을 간결하게 유지하기 위해, "import"는 import와 export 지시문을 모두 아우르는 말로 사용해요. 지침은 둘 다에 동일하게 적용돼요.
DO: part of 지시문에 문자열을 사용해요
린터 규칙: use_string_in_part_of_directives
많은 Dart 개발자가 part를 아예 사용하지 않아요. 각 라이브러리가 단일 파일일 때 코드에 대해 추론하기 더 쉽다고 생각하거든요. part를 사용해 라이브러리의 일부를 다른 파일로 분리하기로 선택했다면, Dart는 그 다른 파일이 자신이 어떤 라이브러리의 일부인지 차례로 표시하도록 요구해요.
Dart는 part of 지시문이 라이브러리의 이름을 사용하는 것을 허용해요. 라이브러리에 이름을 붙이는 것은 이제 권장되지 않는 레거시 기능이에요. 라이브러리 이름은 part가 어떤 라이브러리에 속하는지 결정할 때 모호함을 도입할 수 있어요.
선호되는 구문은 라이브러리 파일을 직접 가리키는 URI 문자열을 사용하는 것이에요. my_library.dart라는 라이브러리가 다음을 포함한다면:
library my_library;
part 'some/other/file.dart';
그러면 part 파일은 라이브러리 파일의 URI 문자열을 사용해야 해요.
// 좋음
part of '../../my_library.dart';
라이브러리 이름이 아니라요.
// 나쁨
part of my_library;
DON'T: 다른 패키지의 src 디렉터리 안에 있는 라이브러리를 import하지 마세요
린터 규칙: implementation_imports
lib 아래의 src 디렉터리는 패키지 자신의 구현에 비공개인 라이브러리를 담도록 지정되어 있어요. 패키지 유지보수자가 패키지를 버전 관리하는 방식이 이 관례를 고려해요. 그들은 src 아래의 코드를 패키지에 대한 주요 변경 없이 폭넓게 변경할 자유가 있어요.
그것은 다른 패키지의 비공개 라이브러리를 import하면, 그 패키지의 이론상 주요 변경이 아닌 사소한 포인트 릴리스가 여러분의 코드를 깨뜨릴 수 있다는 뜻이에요.
DON'T: import 경로가 lib 안팎으로 침범하지 않게 해요
린터 규칙: avoid_relative_lib_imports
package: import는 패키지가 컴퓨터의 어디에 저장되어 있는지 걱정하지 않고 패키지의 lib 디렉터리 안의 라이브러리에 접근할 수 있게 해줘요. 이것이 작동하려면 lib이 다른 파일들에 대해 디스크의 어떤 위치에 있어야 한다고 요구하는 import를 가질 수 없어요. 다시 말해, lib 안의 파일에 있는 상대 import 경로는 lib 디렉터리 밖의 파일에 도달할 수 없고, lib 밖의 라이브러리는 상대 경로로 lib 디렉터리 안에 도달할 수 없어요. 둘 중 하나를 하면 혼란스러운 오류와 깨진 프로그램이 생겨요.
예를 들어 디렉터리 구조가 다음과 같다고 해 보세요.
my_package/
lib/
api.dart
test/
api_test.dart
그리고 api_test.dart가 api.dart를 두 가지 방식으로 import한다고 해 보세요.
// 나쁨
import 'package:my_package/api.dart';
import '../lib/api.dart';
Dart는 이것들을 완전히 무관한 두 라이브러리의 import로 간주해요. Dart와 여러분 자신을 혼란시키지 않으려면 다음 두 규칙을 따라 주세요.
- import 경로에
/lib/를 사용하지 마세요. lib디렉터리를 빠져나가기 위해../를 사용하지 마세요.
대신 패키지의 lib 디렉터리 안에 도달해야 할 때(같은 패키지의 test 디렉터리나 다른 어떤 최상위 디렉터리에서도) package: import를 사용해 주세요.
// 좋음
import 'package:my_package/api.dart';
패키지는 lib 디렉터리 밖으로 나가 패키지의 다른 곳에서 라이브러리를 import해서는 안 돼요.
PREFER: 상대 import 경로를 선호해요
린터 규칙: prefer_relative_imports
이전 규칙이 적용되지 않을 때마다 이 규칙을 따라 주세요. import가 lib을 가로지르지 않을 때 상대 import를 사용하는 것을 선호해요. 그것들이 더 짧으니까요. 예를 들어 디렉터리 구조가 다음과 같다고 해 보세요.
my_package/
lib/
src/
stuff.dart
utils.dart
api.dart
test/
api_test.dart
test_utils.dart
여기서 다양한 라이브러리가 서로를 이렇게 import해야 해요.
// lib/api.dart
// 좋음
import 'src/stuff.dart';
import 'src/utils.dart';
// lib/src/utils.dart
// 좋음
import '../api.dart';
import 'stuff.dart';
// test/api_test.dart
// 좋음
import 'package:my_package/api.dart'; // Don't reach into 'lib'.
import 'test_utils.dart'; // Relative within 'test' is fine.
Null
DON'T: 변수를 명시적으로 null로 초기화하지 마세요
린터 규칙: avoid_init_to_null
변수가 non-nullable 타입이면, 확실히 초기화되기 전에 그것을 사용하려고 할 때 Dart는 컴파일 오류를 보고해요. 변수가 nullable이면 그것은 암묵적으로 null로 초기화돼요. Dart에는 "초기화되지 않은 메모리"라는 개념이 없고 "안전을 위해" 변수를 명시적으로 null로 초기화할 필요도 없어요.
// 좋음
Item? bestDeal(List<Item> cart) {
Item? bestItem;
for (final item in cart) {
if (bestItem == null || item.price < bestItem.price) {
bestItem = item;
}
}
return bestItem;
}
// 나쁨
Item? bestDeal(List<Item> cart) {
Item? bestItem = null;
for (final item in cart) {
if (bestItem == null || item.price < bestItem.price) {
bestItem = item;
}
}
return bestItem;
}
DON'T: 명시적 기본값 null을 사용하지 마세요
린터 규칙: avoid_init_to_null
nullable 매개변수를 선택적으로 만들되 기본값을 주지 않으면, 언어는 암묵적으로 null을 기본값으로 사용해요. 그러니 그것을 쓸 필요가 없어요.
// 좋음
void error([String? message]) {
stderr.write(message ?? '\n');
}
// 나쁨
void error([String? message = null]) {
stderr.write(message ?? '\n');
}
DON'T: 동등 비교 연산에 true나 false를 사용하지 마세요
동등 연산자를 사용해 non-nullable 불리언 표현식을 불리언 리터럴과 평가하는 것은 중복이에요. 동등 연산자를 제거하고 필요하면 단항 부정 연산자 !를 사용하는 것이 항상 더 단순해요.
// 좋음
if (nonNullableBool) { ... }
if (!nonNullableBool) { ... }
// 나쁨
if (nonNullableBool == true) { ... }
if (nonNullableBool == false) { ... }
nullable 불리언 표현식을 평가하려면 ??나 명시적 != null 검사를 사용해야 해요.
// 좋음
// If you want null to result in false:
if (nullableBool ?? false) { ... }
// If you want null to result in false
// and you want the variable to type promote:
if (nullableBool != null && nullableBool) { ... }
// 나쁨
// Static error if null:
if (nullableBool) { ... }
// If you want null to be false:
if (nullableBool == true) { ... }
nullableBool == true는 유효한 표현식이지만 여러 이유로 사용하면 안 돼요.
- 그것이 코드가 null과 관련된 무언가를 한다는 것을 나타내지 않아요.
- 분명히 null과 관련되어 있지 않기 때문에, 동등 연산자가 중복이라 제거될 수 있는 non-nullable 경우와 쉽게 혼동될 수 있어요. 그것은 왼쪽의 불리언 표현식이 null을 만들어낼 가능성이 없을 때만 맞지만, 만들 가능성이 있을 때는 아니에요.
- 불리언 논리가 혼란스러워요.
nullableBool이 null이면nullableBool == true는 조건이 false로 평가된다는 뜻이에요.
?? 연산자는 null과 관련된 무언가가 일어나고 있음을 명확히 하므로 중복 연산으로 오인되지 않아요. 논리도 훨씬 명확해요. 표현식의 결과가 null인 것이 불리언 리터럴과 같으니까요.
조건 안의 변수에 ?? 같은 null 인식 연산자를 사용해도 변수를 non-nullable 타입으로 승격하지는 않아요. if 문 본문 안에서 변수를 승격시키고 싶다면 ?? 대신 명시적 != null 검사를 사용하는 것이 더 좋아요.
AVOID: 초기화 여부를 확인해야 한다면 late 변수를 사용하지 마세요
Dart는 late 변수가 초기화되었는지 할당되었는지 알려주는 방법을 제공하지 않아요. 접근하면 초기화기는 즉시 실행되거나(있다면) 예외를 throw해요. 때때로 어떤 상태가 지연 초기화되는데 late가 잘 맞을 수 있지만, 초기화가 이미 일어났는지도 알아야 할 수 있어요.
초기화를 검출하기 위해 그 상태를 late 변수에 저장하고 변수가 설정되었는지 추적하는 별도의 불리언 필드를 가질 수도 있지만, Dart가 내부적으로 late 변수의 초기화 상태를 유지하므로 그것은 중복이에요. 대신 변수를 late가 아닌 nullable로 만드는 것이 보통 더 명확해요. 그러면 null을 확인해 변수가 초기화되었는지 볼 수 있어요.
물론 변수에 null이 유효한 초기화 값이라면, 별도의 불리언 필드를 두는 것이 실제로 타당해요.
CONSIDER: nullable 타입을 사용할 때 타입 승격이나 null 검사 패턴을 고려해요
nullable 변수가 null과 같지 않은지 확인하면 변수가 non-nullable 타입으로 승격돼요. 그렇게 하면 변수의 멤버에 접근하고 non-nullable 타입을 기대하는 함수에 전달할 수 있어요.
그러나 타입 승격은 지역 변수, 매개변수, 비공개 final 필드에만 지원돼요. 조작에 열려 있는 값은 타입 승격될 수 없어요.
우리가 일반적으로 권장하는 대로 멤버를 비공개이고 final로 선언하는 것은 이런 제한을 우회하기에 충분한 경우가 많아요. 하지만 항상 그런 것은 아니에요.
타입 승격 제한을 우회하는 한 패턴은 null 검사 패턴을 사용하는 것이에요. 이것은 멤버의 값이 null이 아님을 동시에 확인하고, 그 값을 같은 기본 타입의 새로운 non-nullable 변수에 바인딩해요.
// 좋음
class UploadException {
final Response? response;
UploadException([this.response]);
@override
String toString() {
if (this.response case var response?) {
return 'Could not complete upload to ${response.url} '
'(error code ${response.errorCode}): ${response.reason}.';
}
return 'Could not upload (no response).';
}
}
또 다른 우회 방법은 필드의 값을 지역 변수에 할당하는 것이에요. 그 변수에 대한 null 검사는 승격되므로 non-nullable로 안전하게 취급할 수 있어요.
// 좋음
class UploadException {
final Response? response;
UploadException([this.response]);
@override
String toString() {
final response = this.response;
if (response != null) {
return 'Could not complete upload to ${response.url} '
'(error code ${response.errorCode}): ${response.reason}.';
}
return 'Could not upload (no response).';
}
}
지역 변수를 사용할 때는 조심하세요. 필드에 다시 써야 한다면 지역 변수에 쓰지 않도록 주의하세요. (지역 변수를 final로 만들면 그런 실수를 막을 수 있어요.) 또한 지역 변수가 범위 안에 있는 동안 필드가 바뀔 수 있다면, 지역 변수가 오래된 값을 가질 수 있어요.
때로는 필드에 그냥 !를 사용하는 것이 가장 좋아요. 그러나 어떤 경우에는 값을 non-null로 취급해야 할 때마다 !를 사용하는 것보다 지역 변수나 null 검사 패턴을 사용하는 것이 더 깔끔하고 안전할 수 있어요.
// 나쁨
class UploadException {
final Response? response;
UploadException([this.response]);
@override
String toString() {
if (response != null) {
return 'Could not complete upload to ${response!.url} '
'(error code ${response!.errorCode}): ${response!.reason}.';
}
return 'Could not upload (no response).';
}
}
문자열(Strings)
Dart에서 문자열을 구성할 때 명심해야 할 몇 가지 모범 사례가 있어요.
DO: 인접한 문자열을 사용해 문자열 리터럴을 이어 붙여요
린터 규칙: prefer_adjacent_string_concatenation
두 개의 문자열 리터럴(값이 아니라 실제 인용된 리터럴 형태)이 있다면, 그것들을 연결하기 위해 +를 사용할 필요가 없어요. C와 C++에서처럼, 단순히 서로 옆에 두면 연결돼요. 이것은 한 줄에 들어가지 않는 단일 긴 문자열을 만들기 좋은 방법이에요.
// 좋음
raiseAlarm(
'ERROR: Parts of the spaceship are on fire. Other '
'parts are overrun by martians. Unclear which are which.',
);
// 나쁨
raiseAlarm(
'ERROR: Parts of the spaceship are on fire. Other ' +
'parts are overrun by martians. Unclear which are which.',
);
PREFER: 문자열과 값을 조합할 때 보간(interpolation)을 사용해요
린터 규칙: prefer_interpolation_to_compose_strings
다른 언어에서 왔다면 리터럴과 다른 값들로 문자열을 만들기 위해 긴 + 체인을 사용하는 것에 익숙할 거예요. 그것은 Dart에서도 작동하지만, 보간을 사용하는 것이 거의 항상 더 깔끔하고 짧아요.
// 좋음
'Hello, $name! You are ${year - birth} years old.';
// 나쁨
'Hello, ' + name + '! You are ' + (year - birth).toString() + ' y...';
이 지침은 여러 리터럴과 값을 조합하는 것에 적용된다는 점에 유의해 주세요. 단일 객체만 문자열로 변환할 때 .toString()을 사용하는 것은 괜찮아요.
AVOID: 필요하지 않을 때 보간에서 중괄호를 사용하지 마세요
린터 규칙: unnecessary_brace_in_string_interps
더 많은 영숫자 텍스트가 바로 뒤따르지 않는 단순한 식별자를 보간한다면 {}는 생략해야 해요.
// 좋음
var greeting = 'Hi, $name! I love your ${decade}s costume.';
// 나쁨
var greeting = 'Hi, ${name}! I love your ${decade}s costume.';
컬렉션(Collections)
Dart는 기본적으로 네 가지 컬렉션 타입을 지원해요. List, Map, Queue, Set이에요. 다음 모범 사례는 컬렉션에 적용돼요.
DO: 가능하면 컬렉션 리터럴을 사용해요
린터 규칙: prefer_collection_literals
Dart에는 세 가지 핵심 컬렉션 타입이 있어요. List, Map, Set이에요. Map과 Set 클래스는 대부분의 클래스처럼 이름 없는 생성자를 가져요. 하지만 이 컬렉션들이 너무 자주 사용되기 때문에 Dart는 그것들을 만들기 위한 더 좋은 내장 구문을 가져요.
// 좋음
var points = <Point>[];
var addresses = <String, Address>{};
var counts = <int>{};
// 나쁨
var addresses = Map<String, Address>();
var counts = Set<int>();
이 지침은 그 클래스들의 이름 붙은 생성자에는 적용되지 않는다는 점에 유의해 주세요. List.from(), Map.fromIterable() 등은 각자 용도가 있어요. (List 클래스에도 이름 없는 생성자가 있지만, null-safe Dart에서는 금지되어 있어요.)
컬렉션 리터럴은 Dart에서 특히 강력한데, 다른 컬렉션의 내용을 포함하는 spread 연산자와 내용을 만드는 동안 흐름 제어를 수행하는 if와 for에 접근할 수 있게 해주기 때문이에요.
// 좋음
var arguments = [
...options,
command,
...?modeFlags,
for (var path in filePaths)
if (path.endsWith('.dart')) path.replaceAll('.dart', '.js'),
];
// 나쁨
var arguments = <String>[];
arguments.addAll(options);
arguments.add(command);
if (modeFlags != null) arguments.addAll(modeFlags);
arguments.addAll(
filePaths
.where((path) => path.endsWith('.dart'))
.map((path) => path.replaceAll('.dart', '.js')),
);
DON'T: 컬렉션이 비어 있는지 확인할 때 .length를 사용하지 마세요
린터 규칙: prefer_is_empty, prefer_is_not_empty
Iterable 계약은 컬렉션이 자신의 길이를 알거나 상수 시간에 제공할 것을 요구하지 않아요. 컬렉션에 무언가가 들어있는지 보기 위해 .length를 호출하는 것은 고통스러울 만큼 느릴 수 있어요.
대신 더 빠르고 읽기 쉬운 게터인 .isEmpty와 .isNotEmpty가 있어요. 결과를 부정할 필요가 없는 것을 사용해 주세요.
// 좋음
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');
// 나쁨
if (lunchBox.length == 0) return 'so hungry...';
if (!words.isEmpty) return words.join(' ');
AVOID: 함수 리터럴과 함께 Iterable.forEach()를 사용하지 마세요
린터 규칙: avoid_function_literals_in_foreach_calls
forEach() 함수는 내장 for-in 루프가 보통 원하는 것을 하지 않기 때문에 JavaScript에서 널리 사용돼요. Dart에서 시퀀스를 반복하고 싶다면 관용적인 방법은 루프를 사용하는 것이에요.
// 좋음
for (final person in people) { ... }
// 나쁨
people.forEach((person) { ... });
이 지침이 특히 "함수 리터럴"이라고 말함에 유의해 주세요. 각 요소에 이미 존재하는 어떤 함수를 호출하고 싶다면 forEach()는 괜찮아요.
// 좋음
people.forEach(print);
또한 Map.forEach()를 사용하는 것은 언제나 괜찮다는 점에도 유의해 주세요. Map은 반복 가능(iterable)하지 않으므로 이 지침이 적용되지 않아요.
DON'T: 결과의 타입을 바꾸려는 게 아니라면 List.from()을 사용하지 마세요
Iterable이 주어졌을 때 같은 요소를 포함하는 새 List를 만드는 두 가지 명백한 방법이 있어요.
var copy1 = iterable.toList();
var copy2 = List.from(iterable);
명백한 차이는 첫 번째가 더 짧다는 것이에요. 중요한 차이는 첫 번째가 원본 객체의 타입 인자를 보존한다는 것이에요.
// 좋음
// Creates a List<int>:
var iterable = [1, 2, 3];
// Prints "List<int>":
print(iterable.toList().runtimeType);
// 나쁨
// Creates a List<int>:
var iterable = [1, 2, 3];
// Prints "List<dynamic>":
print(List.from(iterable).runtimeType);
타입을 바꾸고 싶다면 List.from()을 호출하는 것이 유용해요.
// 좋음
var numbers = [1, 2.3, 4]; // List<num>.
numbers.removeAt(1); // Now it only contains integers.
var ints = List<int>.from(numbers);
하지만 목표가 iterable을 복사하고 원래 타입을 보존하는 것뿐이라면, 또는 타입에 신경 쓰지 않는다면 toList()를 사용해 주세요.
DO: 컬렉션을 타입별로 필터링할 때 whereType()을 사용해요
린터 규칙: prefer_iterable_whereType
객체가 섞여 있는 리스트가 있고 그중 정수만 얻고 싶다고 해 보세요. where()를 이렇게 사용할 수 있어요.
// 나쁨
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.where((e) => e is int);
이것은 장황하고, 더 나쁘게는 타입이 아마 원하는 것이 아닌 iterable을 반환해요. 여기 예제에서는 Iterable<int>를 원할 가능성이 높지만(필터링하는 타입이니까) Iterable<Object>를 반환해요.
때로 위 오류를 cast()를 추가해 "수정"하는 코드를 보게 돼요.
// 나쁨
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.where((e) => e is int).cast<int>();
그것은 장황하고 두 개의 래퍼와 두 겹의 간접성, 중복된 런타임 검사가 만들어져요. 다행히 핵심 라이브러리는 정확히 이 사용 사례를 위한 whereType() 메서드를 갖고 있어요.
// 좋음
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.whereType<int>();
whereType()을 사용하면 간결하고 원하는 타입의 Iterable을 만들어내며 불필요한 래핑 수준이 없어요.
DON'T: 가까운 연산으로 처리될 수 있을 때 cast()를 사용하지 마세요
종종 iterable이나 stream을 다룰 때 여러 변환을 수행해요. 마지막에 특정 타입 인자를 가진 객체를 만들어내고 싶어요. cast() 호출을 덧붙이는 대신, 기존 변환 중 하나가 타입을 바꿀 수 있는지 확인해 주세요.
이미 toList()를 호출하고 있다면, 그것을 List<T>.from() 호출(여기서 T는 원하는 결과 리스트의 타입)로 바꿔 주세요.
// 좋음
var stuff = <dynamic>[1, 2];
var ints = List<int>.from(stuff);
// 나쁨
var stuff = <dynamic>[1, 2];
var ints = stuff.toList().cast<int>();
map()을 호출하고 있다면, 원하는 타입의 iterable을 만들어내도록 명시적 타입 인자를 주세요. 타입 추론은 map()에 전달하는 함수를 기반으로 올바른 타입을 자주 골라 주지만, 때로는 명시적으로 해야 해요.
// 좋음
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map<double>((n) => n * 2);
// 나쁨
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map((n) => n * 2).cast<double>();
AVOID: cast()를 사용하지 마세요
이것은 이전 규칙의 더 부드러운 일반화예요. 때로는 어떤 객체의 타입을 고칠 수 있는 가까운 연산이 없어요. 그래도 가능하면 cast()를 사용해 컬렉션의 타입을 "바꾸는" 것을 피해 주세요.
다음 옵션 중 하나를 대신 선호해 주세요.
- 올바른 타입으로 생성하기. 컬렉션이 처음 생성되는 코드를 바꿔 올바른 타입을 갖게 해 주세요.
- 접근할 때 요소를 캐스팅하기. 컬렉션을 즉시 반복한다면, 반복 안에서 각 요소를 캐스팅해 주세요.
List.from()으로 적극적으로 캐스팅하기. 결국 컬렉션의 대부분 요소에 접근할 것이고 객체가 원본 활성 객체에 기반할 필요가 없다면,List.from()으로 변환해 주세요.
cast() 메서드는 지연(lazy) 컬렉션을 반환하며 모든 연산에서 요소 타입을 검사해요. 소수의 요소에 소수의 연산만 수행한다면 그 지연성이 좋을 수 있어요. 그러나 많은 경우 지연 검증과 래핑의 오버헤드가 이점보다 커요.
올바른 타입으로 생성하는 예시:
// 좋음
List<int> singletonList(int value) {
var list = <int>[];
list.add(value);
return list;
}
// 나쁨
List<int> singletonList(int value) {
var list = []; // List<dynamic>.
list.add(value);
return list.cast<int>();
}
접근할 때 각 요소를 캐스팅하는 예시:
// 좋음
void printEvens(List<Object> objects) {
// We happen to know the list only contains ints.
for (final n in objects) {
if ((n as int).isEven) print(n);
}
}
// 나쁨
void printEvens(List<Object> objects) {
// We happen to know the list only contains ints.
for (final n in objects.cast<int>()) {
if (n.isEven) print(n);
}
}
List.from()으로 적극적으로 캐스팅하는 예시:
// 좋음
int median(List<Object> objects) {
// We happen to know the list only contains ints.
var ints = List<int>.from(objects);
ints.sort();
return ints[ints.length ~/ 2];
}
// 나쁨
int median(List<Object> objects) {
// We happen to know the list only contains ints.
var ints = objects.cast<int>();
ints.sort();
return ints[ints.length ~/ 2];
}
물론 이런 대안들이 항상 작동하는 것은 아니며, 때로는 cast()가 올바른 답이에요. 하지만 그 메서드를 약간 위험하고 바람직하지 않은 것으로 생각해 주세요. 빠를 수 있고, 조심하지 않으면 런타임에 실패할 수 있어요.
함수(Functions)
Dart에서는 함수조차도 객체예요. 다음은 함수와 관련된 몇 가지 모범 사례예요.
DO: 함수에 이름을 바인딩할 때 함수 선언을 사용해요
린터 규칙: prefer_function_declarations_over_variables
현대 언어들은 지역 중첩 함수와 클로저가 얼마나 유용한지 깨달았어요. 다른 함수 안에 정의된 함수를 갖는 것은 흔한 일이에요. 많은 경우 이 함수는 즉시 콜백으로 사용되며 이름이 필요 없어요. 그런 경우 함수 표현식이 훌륭해요.
하지만 이름을 정말 주어야 한다면, 람다를 변수에 바인딩하는 대신 함수 선언 문장을 사용해 주세요.
// 좋음
void main() {
void localFunction() { ... }
}
// 나쁨
void main() {
var localFunction = () { ... };
}
DON'T: tear-off로 충분할 때 람다를 만들지 마세요
린터 규칙: unnecessary_lambdas
괄호 없이 함수, 메서드, 이름 붙은 생성자를 참조하면 Dart는 tear-off를 만들어요. 이것은 함수와 같은 매개변수를 받고 호출할 때 기저 함수를 호출하는 클로저예요. 코드가 클로저가 받는 것과 같은 매개변수로 이름 붙은 함수를 호출하는 클로저가 필요하다면, 그 호출을 람다로 감싸지 말고 tear-off를 사용해 주세요.
// 좋음
var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();
// Function:
charCodes.forEach(print);
// Method:
charCodes.forEach(buffer.write);
// Named constructor:
var strings = charCodes.map(String.fromCharCode);
// Unnamed constructor:
var buffers = charCodes.map(StringBuffer.new);
// 나쁨
var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();
// Function:
charCodes.forEach((code) { print(code); });
// Method:
charCodes.forEach((code) { buffer.write(code); });
// Named constructor:
var strings = charCodes.map((code) => String.fromCharCode(code));
// Unnamed constructor:
var buffers = charCodes.map((code) => StringBuffer(code));
변수(Variables)
다음 모범 사례는 Dart에서 변수를 가장 잘 사용하는 방법을 설명해요.
DO: 지역 변수의 var와 final에 일관된 규칙을 따르세요
대부분의 지역 변수는 타입 애너테이션이 없어야 하고 var나 final만 사용해 선언해야 해요. 언제 어느 것을 사용할지에 대해 널리 쓰이는 두 가지 규칙이 있어요.
- 재할당되지 않는 지역 변수에는
final을, 재할당되는 변수에는var을 사용해요. - 재할당되지 않는 변수까지 포함해 모든 지역 변수에
var을 사용해요. 지역 변수에 절대final을 사용하지 마세요. (물론 필드와 최상위 변수에final을 사용하는 것은 여전히 권장돼요.)
어느 규칙이든 허용되지만, 하나를 골라 코드 전체에 일관되게 적용해 주세요. 그러면 독자가 var을 볼 때 그 변수가 함수에서 나중에 할당된다는 뜻인지 알 수 있어요.
AVOID: 계산할 수 있는 것을 저장하지 마세요
클래스를 설계할 때 같은 기저 상태의 여러 뷰를 노출하고 싶은 경우가 많아요. 종종 생성자에서 그 모든 뷰를 계산한 다음 저장하는 코드를 볼 수 있어요.
// 나쁨
class Circle {
double radius;
double area;
double circumference;
Circle(double radius)
: radius = radius,
area = pi * radius * radius,
circumference = pi * 2.0 * radius;
}
이 코드에는 두 가지 잘못된 점이 있어요. 첫째, 메모리를 낭비할 가능성이 높아요. area와 circumference는 엄밀히 말하면 캐시예요. 그것들은 이미 가진 다른 데이터에서 다시 계산할 수 있는 저장된 계산이에요. 증가된 메모리와 감소된 CPU 사용을 맞바꾸는 것이지요. 우리가 그 교환을 정당화하는 성능 문제를 안다고 할 수 있나요?
더 나쁘게, 이 코드는 틀렸어요. 캐시의 문제는 무효화(invalidation)예요. 캐시가 언제 오래되어 다시 계산해야 하는지 어떻게 알 수 있나요? 여기서는 radius가 가변적임에도 그것을 절대 하지 않아요. 다른 값을 할당할 수 있지만 area와 circumference는 이전의, 이제는 틀린 값을 유지해요.
캐시 무효화를 올바르게 처리하려면 이렇게 해야 해요.
// 나쁨
class Circle {
double _radius;
double get radius => _radius;
set radius(double value) {
_radius = value;
_recalculate();
}
double _area = 0.0;
double get area => _area;
double _circumference = 0.0;
double get circumference => _circumference;
Circle(this._radius) {
_recalculate();
}
void _recalculate() {
_area = pi * _radius * _radius;
_circumference = pi * 2.0 * _radius;
}
}
이것은 작성, 유지보수, 디버깅, 읽기에 아주 많은 코드예요. 대신 첫 구현은 다음과 같아야 해요.
// 좋음
class Circle {
double radius;
Circle(this.radius);
double get area => pi * radius * radius;
double get circumference => pi * 2.0 * radius;
}
이 코드는 더 짧고, 메모리를 덜 사용하며, 오류가 덜 발생하기 쉬워요. 원을 나타내는 데 필요한 최소한의 데이터를 저장해요. 하나의 진실 원천만 있기 때문에 동기화되지 않는 필드가 없어요.
어떤 경우에는 느린 계산의 결과를 캐시해야 할 수도 있지만, 성능 문제가 있다는 것을 알고 나서야 하고, 조심스럽게 하며, 최적화를 설명하는 주석을 남겨야 해요.
멤버(Members)
Dart에서 객체는 함수(메서드) 또는 데이터(인스턴스 변수)일 수 있는 멤버를 가져요. 다음 모범 사례는 객체의 멤버에 적용돼요.
DON'T: 필드를 불필요하게 게터와 세터로 감싸지 마세요
린터 규칙: unnecessary_getters_setters
Java와 C#에서는 구현이 필드로만 전달되더라도 모든 필드를 게터와 세터(C#에서는 프로퍼티) 뒤에 숨기는 것이 흔해요. 그렇게 하면 나중에 그 멤버에서 더 많은 작업을 해야 할 때 호출 지점을 건드릴 필요 없이 할 수 있어요. Java에서 게터 메서드를 호출하는 것은 필드에 접근하는 것과 다르고, C#에서 프로퍼티에 접근하는 것은 raw 필드에 접근하는 것과 바이너리 호환이 아니기 때문이에요.
Dart에는 이 제한이 없어요. 필드와 게터/세터는 완전히 구별할 수 없어요. 클래스에 필드를 노출한 다음, 그 필드를 사용하는 어떤 코드도 건드리지 않고 나중에 게터와 세터로 감쌀 수 있어요.
// 좋음
class Box {
Object? contents;
}
// 나쁨
class Box {
Object? _contents;
Object? get contents => _contents;
set contents(Object? value) {
_contents = value;
}
}
PREFER: 읽기 전용 프로퍼티를 만들 때 final 필드를 사용해요
외부 코드가 볼 수는 있지만 할당할 수는 없어야 하는 필드가 있다면, 많은 경우에 작동하는 간단한 해결책은 그냥 final로 표시하는 것이에요.
// 좋음
class Box {
final contents = [];
}
// 나쁨
class Box {
Object? _contents;
Object? get contents => _contents;
}
물론 생성자 밖에서 내부적으로 필드에 할당해야 한다면 "비공개 필드, 공개 게터" 패턴이 필요할 수 있지만, 꼭 필요해지기 전에는 그 패턴을 사용하지 마세요.
CONSIDER: 단순한 멤버에 =>를 사용하는 것을 고려해요
린터 규칙: prefer_expression_function_bodies
함수 표현식에 =>를 사용하는 것 외에도 Dart는 멤버를 그것으로 정의할 수 있게 해요. 그 스타일은 단순히 값을 계산하고 반환하는 단순한 멤버에 잘 맞아요.
// 좋음
double get area => (right - left) * (bottom - top);
String capitalize(String name) => '${name[0].toUpperCase()}${name.substring(1)}';
코드를 쓰는 사람들은 =>를 좋아하는 것 같지만, 남용해서 읽기 어려운 코드로 끝나기 매우 쉬워요. 선언이 몇 줄을 넘거나 깊게 중첩된 표현식(캐스케이드와 조건 연산자가 흔한 범인)을 포함한다면, 여러분 자신과 여러분 코드를 읽어야 하는 모든 사람을 위해 블록 본문과 몇 개의 문장을 사용해 주세요.
// 좋음
Treasure? openChest(Chest chest, Point where) {
if (_opened.containsKey(chest)) return null;
var treasure = Treasure(where);
treasure.addAll(chest.contents);
_opened[chest] = treasure;
return treasure;
}
// 나쁨
Treasure? openChest(Chest chest, Point where) =>
_opened.containsKey(chest) ? null : _opened[chest] =
(Treasure(where)..addAll(chest.contents));
값을 반환하지 않는 멤버에도 =>를 사용할 수 있어요. 세터가 작고 =>를 사용하는 대응 게터가 있을 때 그것은 관용적이에요.
// 좋음
num get x => center.x;
set x(num value) => center = Point(value, center.y);
DON'T: 이름 붙은 생성자로 리다이렉트하거나 그림자(shadowing)를 피할 때를 제외하고 this.를 사용하지 마세요
린터 규칙: unnecessary_this
JavaScript는 현재 실행 중인 메서드가 있는 객체의 멤버를 가리키기 위해 명시적 this.를 요구하지만, Dart(C++, Java, C#처럼)에는 그 제한이 없어요.
this.를 사용해야 하는 경우는 단 두 가지뿐이에요. 하나는 같은 이름의 지역 변수가 접근하려는 멤버를 가릴 때예요.
// 나쁨
class Box {
Object? value;
void clear() {
this.update(null);
}
void update(Object? value) {
this.value = value;
}
}
// 좋음
class Box {
Object? value;
void clear() {
update(null);
}
void update(Object? value) {
this.value = value;
}
}
this.를 사용해야 하는 다른 경우는 이름 붙은 생성자로 리다이렉트할 때예요.
// 나쁨
class ShadeOfGray {
final int brightness;
ShadeOfGray(int val) : brightness = val;
ShadeOfGray.black() : this(0);
// This won't parse or compile!
// ShadeOfGray.alsoBlack() : black();
}
// 좋음
class ShadeOfGray {
final int brightness;
ShadeOfGray(int val) : brightness = val;
ShadeOfGray.black() : this(0);
// But now it will!
ShadeOfGray.alsoBlack() : this.black();
}
생성자 매개변수는 생성자 초기화 리스트에서 필드를 절대 가리지 않는다는 점에 유의해 주세요.
// 좋음
class Box extends BaseBox {
Object? value;
Box(Object? value) : value = value, super(value);
}
이것은 놀랍게 보이지만 원하는 대로 작동해요. 다행히 초기화 형식 인자와 super 초기화 덕분에 이런 코드는 비교적 드물어요.
DO: 가능하면 선언 시점에 필드를 초기화해요
필드가 어떤 생성자 매개변수에도 의존하지 않는다면, 선언 시점에 초기화할 수 있고 그래야 해요. 코드가 더 적고, 클래스에 생성자가 여러 개일 때 중복을 피할 수 있어요.
// 나쁨
class ProfileMark {
final String name;
final DateTime start;
ProfileMark(this.name) : start = DateTime.now();
ProfileMark.unnamed() : name = '', start = DateTime.now();
}
// 좋음
class ProfileMark {
final String name;
final DateTime start = DateTime.now();
ProfileMark(this.name);
ProfileMark.unnamed() : name = '';
}
일부 필드는 this를 참조해야 하기 때문에(예를 들어 다른 필드를 사용하거나 메서드를 호출) 선언 시점에 초기화할 수 없어요. 그러나 필드가 late로 표시되면 초기화기가 this에 접근할 수 있어요.
물론 필드가 생성자 매개변수에 의존하거나, 서로 다른 생성자가 다르게 초기화한다면 이 지침은 적용되지 않아요.
생성자(Constructors)
다음 모범 사례는 클래스의 생성자를 선언하는 것에 적용돼요.
DO: 가능하면 초기화 형식 인자(initializing formals)를 사용해요
린터 규칙: prefer_initializing_formals
많은 필드가 생성자 매개변수에서 직접 초기화돼요. 예를 들어:
// 나쁨
class Point {
double x, y;
Point(double x, double y) : x = x, y = y;
}
필드를 정의하기 위해 여기서 x를 네 번 입력해야 해요. 더 잘할 수 있어요.
// 좋음
class Point {
double x, y;
Point(this.x, this.y);
}
생성자 매개변수 앞의 이 this. 구문을 "초기화 형식 인자"라고 불러요. 항상 그것을 활용할 수 있는 것은 아니에요. 때로는 초기화하는 필드의 이름과 일치하지 않는 이름 붙은 매개변수를 갖고 싶을 수 있어요. 하지만 초기화 형식 인자를 사용할 수 있을 때는 사용해야 해요.
DON'T: 생성자 초기화 리스트로 충분할 때 late를 사용하지 마세요
Dart는 non-nullable 필드를 읽기 전에 초기화하도록 요구해요. 필드는 생성자 본문 안에서 읽을 수 있으므로, 본문이 실행되기 전에 non-nullable 필드를 초기화하지 않으면 오류가 나요.
필드를 late로 표시하면 이 오류를 없앨 수 있어요. 그러면 컴파일 타임 오류가, 초기화되기 전에 필드에 접근하면 발생하는 런타임 오류로 바뀌어요. 어떤 경우에는 그것이 필요한 것이지만, 종종 올바른 수정은 생성자 초기화 리스트에서 필드를 초기화하는 것이에요.
// 좋음
class Point {
double x, y;
Point.polar(double theta, double radius)
: x = cos(theta) * radius,
y = sin(theta) * radius;
}
// 나쁨
class Point {
late double x, y;
Point.polar(double theta, double radius) {
x = cos(theta) * radius;
y = sin(theta) * radius;
}
}
초기화 리스트는 생성자 매개변수에 접근할 수 있게 하고, 필드를 읽기 전에 초기화할 수 있게 해줘요. 그러니 초기화 리스트를 사용할 수 있다면, 필드를 late로 만들어 일부 정적 안전성과 성능을 잃는 것보다 낫다.
DO: 빈 생성자 본문에는 {} 대신 ;을 사용해요
린터 규칙: empty_constructor_bodies
Dart에서 빈 본문을 가진 생성자는 세미콜론만으로 끝낼 수 있어요. (실제로 const 생성자에는 그것이 요구돼요.)
// 좋음
class Point {
double x, y;
Point(this.x, this.y);
}
// 나쁨
class Point {
double x, y;
Point(this.x, this.y) {}
}
PREFER: 간결한 생성자 구문을 사용해요
린터 규칙: unnecessary_type_name_in_constructor
클래스 본문 안에서 생성자를 선언할 때는 클래스 이름을 반복하는 대신 간결한 생성자 구문을 사용해 주세요. factory 생성자에는 factory를, 그 외 모든 생성자에는 new를 사용해 주세요. 간결한 구문 형식의 전체 참조는 간결한 생성자 구문 표를 참고해 주세요.
버전 참고: 이 지침은 간결한 생성자 구문을 사용할 수 있는 언어 버전이 최소 3.13인 라이브러리에만 적용돼요.
// 좋음
class Logger {
final String name;
factory(String name) => _cache[name] ??= Logger._internal(name);
new _internal(this.name);
new fromJson(Map<String, Object?> json) : name = json['name'] as String;
static final Map<String, Logger> _cache = {};
}
// 나쁨
class Logger {
final String name;
factory Logger(String name) => _cache[name] ??= Logger._internal(name);
Logger._internal(this.name);
Logger.fromJson(Map<String, Object?> json) : name = json['name'] as String;
static final Map<String, Logger> _cache = {};
}
클래스 이름을 반복하는 것은 중복이며, 클래스 이름이 바뀌면 리팩터링을 더 어렵게 해요. new와 factory를 사용하면 생성자 선언을 간결하고 일관되게 유지할 수 있어요.
참고: 이 지침은 생성자 선언에만 적용돼요. 생성자 호출에 대해서는 DON'T use
new지침을 참고해 주세요.
DON'T: new를 사용하지 마세요
린터 규칙: unnecessary_new
new 키워드는 생성자를 호출할 때 선택 사항이에요. factory 생성자는 새 호출이 실제로 새 객체를 반환하지 않을 수 있기 때문에 그 의미가 명확하지 않아요.
언어는 여전히 new를 허용하지만, 그것을 deprecated로 간주하고 코드에서 사용하지 마세요.
// 좋음
Widget build(BuildContext context) {
return Row(
children: [
RaisedButton(child: Text('Increment')),
Text('Click!'),
],
);
}
// 나쁨
Widget build(BuildContext context) {
return new Row(
children: [
new RaisedButton(child: new Text('Increment')),
new Text('Click!'),
],
);
}
DON'T: const를 중복해서 사용하지 마세요
린터 규칙: unnecessary_const
표현식이 반드시 상수여야 하는 맥락에서는 const 키워드가 암묵적이라 쓸 필요가 없고 쓰지도 말아야 해요. 그런 맥락은 다음 안의 어떤 표현식이에요.
const컬렉션 리터럴const생성자 호출- 메타데이터 애너테이션
const변수 선언의 초기화기- switch case 표현식 –
case바로 뒤,:앞의 부분 (case 본문은 아님)
(기본값은 이 목록에 포함되지 않아요. 미래 Dart 버전이 비-const 기본값을 지원할 수 있기 때문이에요.)
기본적으로 new 대신 const를 쓰는 것이 오류가 될 어떤 곳에서든, Dart는 const를 생략할 수 있게 해요.
// 좋음
const primaryColors = [
Color('red', [255, 0, 0]),
Color('green', [0, 255, 0]),
Color('blue', [0, 0, 255]),
];
// 나쁨
const primaryColors = const [
const Color('red', const [255, 0, 0]),
const Color('green', const [0, 255, 0]),
const Color('blue', const [0, 0, 255]),
];
오류 처리(Error handling)
Dart는 프로그램에서 오류가 발생할 때 예외를 사용해요. 다음 모범 사례는 예외를 잡고 던지는 것에 적용돼요.
AVOID: on 절 없는 catch를 피하세요
린터 규칙: avoid_catches_without_on_clauses
on 한정자가 없는 catch 절은 try 블록의 코드가 던진 무엇이든 잡아요. Pokémon 예외 처리는 아마 여러분이 원하는 것이 아닐 거예요. 여러분 코드가 StackOverflowError나 OutOfMemoryError를 올바르게 처리하나요? 그 try 블록 안에서 메서드에 잘못된 인자를 전달했다면, 디버거가 실수를 가리키게 하고 싶나요, 아니면 그 도움이 되는 ArgumentError가 삼켜지길 원하나요? 그 코드 안에 있는 어떤 assert() 문장이, 던져진 AssertionError를 잡으므로 사실상 사라지길 원하나요?
대답은 아마 "아니요"일 거예요. 그렇다면 잡는 타입을 필터링해야 해요. 대부분의 경우, 알고 있고 올바르게 처리하는 종류의 런타임 실패로 범위를 제한하는 on 절이 있어야 해요.
드물게 어떤 런타임 오류든 잡고 싶을 수 있어요. 이것은 보통 임의의 애플리케이션 코드가 문제를 일으키는 것을 격리하려는 프레임워크나 저수준 코드에 있어요. 여기서도 모든 타입을 잡는 것보다 Exception을 잡는 것이 보통 더 좋아요. Exception은 모든 런타임 오류의 기본 클래스이며, 코드의 프로그래밍 버그를 나타내는 오류를 제외해요.
DON'T: on 절 없는 catch의 오류를 버리지 마세요
정말 어떤 코드 영역에서 던질 수 있는 모든 것을 잡아야 한다고 느낀다면, 잡은 것으로 무언가를 하세요. 로그를 남기거나, 사용자에게 보여주거나, 다시 던지되(rthrow), 조용히 버리지는 마세요.
DO: 프로그램 오류에만 Error를 구현하는 객체를 throw해요
Error 클래스는 프로그래밍 오류의 기본 클래스예요. 그 타입이나 ArgumentError 같은 하위 인터페이스의 객체가 throw되면, 코드에 버그가 있다는 뜻이에요. API가 호출자에게 잘못 사용되고 있다고 보고하고 싶을 때 Error를 throw하면 그 신호가 명확히 전달돼요.
반대로 예외가 코드의 버그를 나타내지 않는 어떤 종류의 런타임 실패라면, Error를 throw하는 것은 오해를 불러일으켜요. 대신 핵심 Exception 클래스 중 하나나 다른 어떤 타입을 throw해 주세요.
DON'T: Error나 그것을 구현하는 타입을 명시적으로 catch하지 마세요
린터 규칙: avoid_catching_errors
이것은 위의 내용에서 이어져요. Error는 코드의 버그를 나타내므로, 전체 콜스택을 풀고(unwind) 프로그램을 중단하며 스택 추적을 출력해 여러분이 버그를 찾아 고칠 수 있게 해야 해요.
이런 타입의 오류를 잡는 것은 그 과정을 깨뜨리고 버그를 가려요. 나중에 이 예외를 처리하기 위해 오류 처리 코드를 추가하는 대신, 원래 그 예외를 던지게 만든 코드를 돌아가 고쳐 주세요.
DO: 잡은 예외를 다시 던질 때 rethrow를 사용해요
린터 규칙: use_rethrow_when_possible
예외를 다시 던지기로 결정했다면, throw로 같은 예외 객체를 던지는 대신 rethrow 문장을 사용하는 것을 선호해 주세요. rethrow는 예외의 원래 스택 추적을 보존해요. 반면 throw는 스택 추적을 마지막으로 던진 위치로 재설정해요.
// 나쁨
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) throw e;
handle(e);
}
// 좋음
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) rethrow;
handle(e);
}
비동기(Asynchrony)
Dart에는 비동기 프로그래밍을 지원하는 여러 언어 기능이 있어요. 다음 모범 사례는 비동기 코딩에 적용돼요.
PREFER: raw future보다 async/await을 선호해요
비동기 코드는 future 같은 좋은 추상화를 사용해도 읽고 디버깅하기 악명 높게 어려워요. async/await 구문은 가독성을 높이고 비동기 코드 안에서 모든 Dart 흐름 제어 구조를 사용할 수 있게 해줘요.
// 좋음
Future<int> countActivePlayers(String teamName) async {
try {
var team = await downloadTeam(teamName);
if (team == null) return 0;
var players = await team.roster;
return players.where((player) => player.isActive).length;
} on DownloadException catch (e) {
log.error(e);
return 0;
}
}
// 나쁨
Future<int> countActivePlayers(String teamName) {
return downloadTeam(teamName).then((team) {
if (team == null) return Future.value(0);
return team.roster.then((players) {
return players.where((player) => player.isActive).length;
});
}).onError<DownloadException>((e, _) {
log.error(e);
return 0;
});
}
DON'T: 유용한 효과가 없을 때 async를 사용하지 마세요
비동기와 관련된 무언가를 하는 모든 함수에 async를 사용하는 습관이 들기 쉬워요. 하지만 어떤 경우에는 불필요해요. 함수의 동작을 바꾸지 않고 async를 생략할 수 있다면 그렇게 하세요.
// 좋음
Future<int> fastestBranch(Future<int> left, Future<int> right) {
return Future.any([left, right]);
}
// 나쁨
Future<int> fastestBranch(Future<int> left, Future<int> right) async {
return Future.any([left, right]);
}
async가 유용한 경우는 다음과 같아요.
await을 사용할 때. (이것이 당연한 경우예요.)- 오류를 비동기적으로 반환할 때.
throw와 함께async는return Future.error(...)보다 짧아요. - 값을 반환하고 그것이 future에 암묵적으로 감싸지길 원할 때.
async는Future.value(...)보다 짧아요.
// 좋음
Future<void> usesAwait(Future<String> later) async {
print(await later);
}
Future<void> asyncError() async {
throw 'Error!';
}
Future<String> asyncValue() async => 'value';
CONSIDER: 스트림을 변환할 때 고차 메서드를 사용하는 것을 고려해요
이것은 iterable에 대한 위 제안과 평행을 이뤄요. Stream은 같은 메서드들을 많이 지원하고 오류 전송, 닫기 등을 올바르게 처리해요.
AVOID: Completer를 직접 사용하지 마세요
비동기 프로그래밍에 새로 온 많은 사람들이 future를 만들어내는 코드를 쓰고 싶어해요. Future의 생성자들은 그들의 필요에 맞지 않는 것 같아 결국 Completer 클래스를 찾아 사용하게 돼요.
// 나쁨
Future<bool> fileContainsBear(String path) {
var completer = Completer<bool>();
File(path).readAsString().then((contents) {
completer.complete(contents.contains('bear'));
});
return completer.future;
}
Completer는 두 종류의 저수준 코드에 필요해요. 새로운 비동기 프리미티브와 future를 사용하지 않는 비동기 코드와의 연동이에요. 대부분의 다른 코드는 더 명확하고 오류 처리를 쉽게 하기 때문에 async/await이나 Future.then()을 사용해야 해요.
// 좋음
Future<bool> fileContainsBear(String path) {
return File(path).readAsString().then((contents) {
return contents.contains('bear');
});
}
// 좋음
Future<bool> fileContainsBear(String path) async {
var contents = await File(path).readAsString();
return contents.contains('bear');
}
DO: 타입 인자가 Object가 될 수 있는 FutureOr<T>를 구분할 때 Future<T>를 검사해요
FutureOr<T>로 유용한 무언가를 하기 전에, 보통 Future<T>인지 순수한 T인지 is 검사를 해야 해요. 타입 인자가 FutureOr<int> 같은 특정 타입이라면 어떤 검사를 쓰든 상관없어요. is int든 is Future<int>든요. 두 타입이 서로소(disjoint)이기 때문에 둘 다 작동해요.
그러나 값 타입이 Object이거나 Object로 인스턴스화될 수 있는 타입 매개변수라면, 두 분기가 겹쳐요. Future<Object> 자체가 Object를 구현하므로, 객체가 future일 때도 is Object나 Object로 인스턴스화될 수 있는 타입 매개변수인 is T가 true를 반환해요. 대신 명시적으로 Future 경우를 검사해 주세요.
// 좋음
Future<T> logValue<T>(FutureOr<T> value) async {
if (value is Future<T>) {
var result = await value;
print(result);
return result;
} else {
print(value);
return value;
}
}
// 나쁨
Future<T> logValue<T>(FutureOr<T> value) async {
if (value is T) {
print(value);
return value;
} else {
var result = await value;
print(result);
return result;
}
}
나쁜 예제에서 Future<Object>를 전달하면, 그것을 순수하고 동기적인 값처럼 잘못 취급해요.