Dart 치트시트

Dart 치트시트

Dart는 다른 언어에서 넘어온 개발자들이 배우기 쉽게 설계됐지만, 몇 가지 독특한 기능을 갖고 있어요. 이 튜토리얼에서는 그중 가장 중요한 언어 기능들을 차근차근 살펴보면서, 직접 코드를 완성해 보는 방식으로 익혀볼게요.

출처: Dart cheatsheet

본문

이 튜토리얼에 포함된 편집기에는 부분적으로 완성된 코드 조각이 들어 있어요. 이 편집기들을 사용해서 코드를 완성하고 Run 버튼을 눌러 자신의 지식을 테스트해 볼 수 있어요. 편집기에는 철저한 테스트 코드도 담겨 있으니, 테스트 코드는 수정하지 마세요. 다만 공부 삼아 살펴보는 것은 자유예요.

도움이 필요하면, 각 DartPad 아래의 Solution for... 드롭다운을 펼쳐 설명과 답을 확인하세요.

이 페이지는 실행 가능한 예제를 보여주기 위해 내장 DartPad를 사용해요. DartPad 대신 빈 상자만 보인다면, DartPad 문제 해결 페이지(DartPad troubleshooting page)를 방문해 보세요.

문자열 보간(String interpolation)

문자열 안에 표현식의 값을 넣으려면 ${expression}을 사용하세요. 표현식이 식별자라면 {}를 생략할 수 있어요.

문자열 보간을 사용하는 몇 가지 예시를 볼게요.

'${3 + 2}''5'   '${\"word\".toUpperCase()}''WORD'   '$myObject'myObject.toString()의 값

연습문제

다음 함수는 두 개의 정수를 매개변수로 받아요. 이 함수가 두 정수를 공백으로 구분해서 담은 문자열을 반환하도록 만드세요. 예를 들어, stringify(2, 3)'2 3'을 반환해야 해요.

String stringify(int x, int y) {
  TODO('Return a formatted string here');
}
// Tests your solution (Don't edit!):
void main() {
  assert(stringify(2, 3) == '2 3',
      "Your stringify method returned '${stringify(2, 3)}' instead of '2 3'");
  print('Success!');
}

xy는 둘 다 단순한 값이라서, Dart의 문자열 보간이 이들을 문자열 표현으로 변환해 줄 거예요. 여러분이 할 일은 작은따옴표 안에서 $ 연산자를 사용해 참조하고, 그 사이에 공백을 넣는 것뿐이에요.

String stringify(int x, int y) {
  return '$x $y';
}

Nullable 변수

Dart는 sound null safety를 강제해요. 이는 값이 명시되지 않는 한 null이 될 수 없다는 뜻이에요. 다시 말해, 타입은 기본적으로 non-nullable이에요.

예를 들어, 다음 코드를 생각해 볼게요. null safety 하에서 이 코드는 오류를 반환해요. int 타입의 변수는 null 값을 가질 수 없으니까요.

int a = null; // INVALID.

변수를 만들 때 타입에 ?를 붙이면 그 변수가 null이 될 수 있다는 뜻이 돼요.

int? a = null; // Valid.

Dart의 모든 버전에서 초기화되지 않은 변수의 기본값은 null이기 때문에, 그 코드를 조금 더 단순화할 수 있어요.

int? a; // The initial value of a is null.

Dart의 null safety에 대해 더 배우고 싶다면, sound null safety 가이드(sound null safety guide)를 읽어보세요.

연습문제

이 DartPad에서 두 변수를 선언하세요.

  • 값이 'Jane'인 nullable String name
  • 값이 null인 nullable String address

DartPad의 초기 오류는 모두 무시하세요.

// TODO: Declare the two variables here
// Tests your solution (Don't edit!):
void main() {
  try {
    if (name == 'Jane' && address == null) {
      // Verify that "name" is nullable.
      name = null;
      print('Success!');
    } else {
      print('Not quite right, try again!');
    }
  } catch (e) {
    print('Exception: ${e.runtimeType}');
  }
}

두 변수를 String 뒤에 ?를 붙여 선언하세요. 그리고 name에는 'Jane'을 할당하고, address는 초기화하지 않은 채로 두세요.

String? name = 'Jane';
String? address;

Null 인지 연산자(Null-aware operators)

Dart는 null일 수 있는 값을 다루기 위한 편리한 연산자를 제공해요. 그중 하나가 ??= 할당 연산자인데, 이 연산자는 변수가 현재 null일 때만 값을 할당해요.

int? a; // = null
a ??= 3;
print(a); // <-- Prints 3.

a ??= 5;
print(a); // <-- Still prints 3.

또 다른 null 인지 연산자는 ??인데, 이 연산자는 왼쪽의 표현식 값이 null이 아니면 그 값을 반환하고, null이라면 오른쪽 표현식을 평가해 반환해요.

print(1 ?? 3); // <-- Prints 1.
print(null ?? 12); // <-- Prints 12.

연습문제

다음 코드 조각에서 설명된 동작이 일어나도록 ??=?? 연산자를 대입해서 구현해 보세요.

DartPad의 초기 오류는 모두 무시하세요.

String? foo = 'a string';
String? bar; // = null
// Substitute an operator that makes 'a string' be assigned to baz.
String? baz = foo /* TODO */ bar;
void updateSomeVars() {
  // Substitute an operator that makes 'a string' be assigned to bar.
  bar /* TODO */ 'a string';
}
// Tests your solution (Don't edit!):
void main() {
  try {
    updateSomeVars();
    if (foo != 'a string') {
      print('Looks like foo somehow ended up with the wrong value.');
    } else if (bar != 'a string') {
      print('Looks like bar ended up with the wrong value.');
    } else if (baz != 'a string') {
      print('Looks like baz ended up with the wrong value.');
    } else {
      print('Success!');
    }
  } catch (e) {
    print('Exception: ${e.runtimeType}.');
  }
}

이 연습문제에서 할 일은 TODO 주석을 ?? 또는 ??=로 바꾸는 것뿐이에요. 위의 설명을 잘 읽고 두 연산자를 이해한 뒤 직접 시도해 보세요.

// Substitute an operator that makes 'a string' be assigned to baz.
String? baz = foo ?? bar;

void updateSomeVars() {
  // Substitute an operator that makes 'a string' be assigned to bar.
  bar ??= 'a string';
}

조건부 프로퍼티 접근(Conditional property access)

null일 수 있는 객체의 프로퍼티나 메서드에 접근할 때는 점(.) 앞에 물음표(?)를 붙여서 보호할 수 있어요.

myObject?.someProperty

위 코드는 다음 코드와 동일해요.

(myObject != null) ? myObject.someProperty : null

하나의 표현식 안에서 ?.를 여러 번 연결할 수도 있어요.

myObject?.someProperty?.someMethod()

위 코드는 myObjectmyObject.somePropertynull이면 null을 반환하고(그리고 someMethod()는 절대 호출하지 않아요).

연습문제

다음 함수는 nullable 문자열을 매개변수로 받아요. 조건부 프로퍼티 접근을 사용해 str의 대문자 버전을 반환하고, strnull이면 null을 반환하도록 만들어 보세요.

String? upperCaseIt(String? str) {
  // TODO: Try conditionally accessing the `toUpperCase` method here.
}
// Tests your solution (Don't edit!):
void main() {
  try {
    String? one = upperCaseIt(null);
    if (one != null) {
      print('Looks like you\'re not returning null for null inputs.');
    } else {
      print('Success when str is null!');
    }
  } catch (e) {
    print('Tried calling upperCaseIt(null) and got an exception: \n ${e.runtimeType}.');
  }
  try {
    String? two = upperCaseIt('a string');
    if (two == null) {
      print('Looks like you\'re returning null even when str has a value.');
    } else if (two != 'A STRING') {
      print('Tried upperCaseIt(\'a string\'), but didn\'t get \'A STRING\' in response.');
    } else {
      print('Success when str is not null!');
    }
  } catch (e) {
    print('Tried calling upperCaseIt(\'a string\') and got an exception: \n ${e.runtimeType}.');
  }
}

이 연습문제가 문자열을 조건부로 소문자로 만들라고 요구했다면, str?.toLowerCase()처럼 할 수 있었을 거예요. 대문자로 만드는 데도 동일한 방법을 사용하세요!

String? upperCaseIt(String? str) {
  return str?.toUpperCase();
}

컬렉션 리터럴(Collection literals)

Dart는 리스트, 맵, 셋을 기본적으로 지원해요. 리터럴을 사용해 만들 수 있죠.

final aListOfStrings = ['one', 'two', 'three'];
final aSetOfStrings = {'one', 'two', 'three'};
final aMapOfStringsToInts = {'one': 1, 'two': 2, 'three': 3};

Dart의 타입 추론이 이 변수들의 타입을 대신 정해줄 수 있어요. 이 경우 추론된 타입은 List<String>, Set<String>, Map<String, int>예요.

또는 직접 타입을 지정할 수도 있어요.

final aListOfInts = <int>[];
final aSetOfInts = <int>{};
final aMapOfIntToDouble = <int, double>{};

서브타입의 내용으로 리스트를 초기화하면서도 리스트 자체는 List<BaseType>로 유지하고 싶을 때 타입을 지정하면 편리해요.

final aListOfBaseType = <BaseType>[SubType(), SubType()];

연습문제

다음 변수들을 지시된 값으로 설정해 보세요. 기존의 null 값들을 대체하세요.

// Assign this a list containing 'a', 'b', and 'c' in that order:
final aListOfStrings = null;
// Assign this a set containing 3, 4, and 5:
final aSetOfInts = null;
// Assign this a map of String to int so that aMapOfStringsToInts['myKey'] returns 12:
final aMapOfStringsToInts = null;
// Assign this an empty List<double>:
final anEmptyListOfDouble = null;
// Assign this an empty Set<String>:
final anEmptySetOfString = null;
// Assign this an empty Map of double to int:
final anEmptyMapOfDoublesToInts = null;
// Tests your solution (Don't edit!):
void main() {
  final errs = <String>[];
  if (aListOfStrings is! List<String>) {
    errs.add('aListOfStrings should have the type List<String>.');
  } else if (aListOfStrings.length != 3) {
    errs.add('aListOfStrings has ${aListOfStrings.length} items in it, \n rather than the expected 3.');
  } else if (aListOfStrings[0] != 'a' || aListOfStrings[1] != 'b' || aListOfStrings[2] != 'c') {
    errs.add('aListOfStrings doesn\'t contain the correct values (\'a\', \'b\', \'c\').');
  }
  if (aSetOfInts is! Set<int>) {
    errs.add('aSetOfInts should have the type Set<int>.');
  } else if (aSetOfInts.length != 3) {
    errs.add('aSetOfInts has ${aSetOfInts.length} items in it, \n rather than the expected 3.');
  } else if (!aSetOfInts.contains(3) || !aSetOfInts.contains(4) || !aSetOfInts.contains(5)) {
    errs.add('aSetOfInts doesn\'t contain the correct values (3, 4, 5).');
  }
  if (aMapOfStringsToInts is! Map<String, int>) {
    errs.add('aMapOfStringsToInts should have the type Map<String, int>.');
  } else if (aMapOfStringsToInts['myKey'] != 12) {
    errs.add('aMapOfStringsToInts doesn\'t contain the correct values (\'myKey\': 12).');
  }
  if (anEmptyListOfDouble is! List<double>) {
    errs.add('anEmptyListOfDouble should have the type List<double>.');
  } else if (anEmptyListOfDouble.isNotEmpty) {
    errs.add('anEmptyListOfDouble should be empty.');
  }
  if (anEmptySetOfString is! Set<String>) {
    errs.add('anEmptySetOfString should have the type Set<String>.');
  } else if (anEmptySetOfString.isNotEmpty) {
    errs.add('anEmptySetOfString should be empty.');
  }
  if (anEmptyMapOfDoublesToInts is! Map<double, int>) {
    errs.add('anEmptyMapOfDoublesToInts should have the type Map<double, int>.');
  } else if (anEmptyMapOfDoublesToInts.isNotEmpty) {
    errs.add('anEmptyMapOfDoublesToInts should be empty.');
  }
  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
  // ignore_for_file: unnecessary_type_check
}

각 등호(=) 뒤에 리스트, 셋, 또는 맵 리터럴을 추가하세요. 빈 선언들은 추론할 수 없으므로 타입을 반드시 지정해야 한다는 점을 잊지 마세요.

// Assign this a list containing 'a', 'b', and 'c' in that order:
final aListOfStrings = ['a', 'b', 'c'];

// Assign this a set containing 3, 4, and 5:
final aSetOfInts = {3, 4, 5};

// Assign this a map of String to int so that aMapOfStringsToInts['myKey'] returns 12:
final aMapOfStringsToInts = {'myKey': 12};

// Assign this an empty List<double>:
final anEmptyListOfDouble = <double>[];

// Assign this an empty Set<String>:
final anEmptySetOfString = <String>{};

// Assign this an empty Map of double to int:
final anEmptyMapOfDoublesToInts = <double, int>{};

화살표 문법(Arrow syntax)

Dart 코드에서 => 기호를 본 적이 있을 거예요. 이 화살표 문법은 오른쪽의 표현식을 실행하고 그 값을 반환하는 함수를 정의하는 방법이에요.

예를 들어, List 클래스의 any() 메서드에 대한 이 호출을 생각해 볼게요.

bool hasEmpty = aListOfStrings.any((s) {
  return s.isEmpty;
});

그 코드를 더 간단하게 쓴 방식이에요.

bool hasEmpty = aListOfStrings.any((s) => s.isEmpty);

연습문제

화살표 문법을 사용하는 다음 문장들을 완성해 보세요.

class MyClass {
  int value1 = 2;
  int value2 = 3;
  int value3 = 5;
  // Returns the product of the above values:
  int get product => TODO();
  // Adds 1 to value1:
  void incrementValue1() => TODO();
  // Returns a string containing each item in the
  // list, separated by commas (e.g. 'a,b,c'):
  String joinWithCommas(List<String> strings) => TODO();
}
// Tests your solution (Don't edit!):
void main() {
  final obj = MyClass();
  final errs = <String>[];
  try {
    final product = obj.product;
    if (product != 30) {
      errs.add('The product property returned $product \n instead of the expected value (30).');
    }
  } catch (e) {
    print('Tried to use MyClass.product, but encountered an exception: \n ${e.runtimeType}.');
    return;
  }
  try {
    obj.incrementValue1();
    if (obj.value1 != 3) {
      errs.add('After calling incrementValue, value1 was ${obj.value1} \n instead of the expected value (3).');
    }
  } catch (e) {
    print('Tried to use MyClass.incrementValue1, but encountered an exception: \n ${e.runtimeType}.');
    return;
  }
  try {
    final joined = obj.joinWithCommas(['one', 'two', 'three']);
    if (joined != 'one,two,three') {
      errs.add('Tried calling joinWithCommas([\'one\', \'two\', \'three\']) \n and received $joined instead of the expected value (\'one,two,three\').');
    }
  } catch (e) {
    print('Tried to use MyClass.joinWithCommas, but encountered an exception: \n ${e.runtimeType}.');
    return;
  }
  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}

product*를 사용해 세 값을 곱하면 돼요. incrementValue1은 증가 연산자(++)를 사용하면 되고요. joinWithCommasList 클래스의 join 메서드를 사용하세요.

class MyClass {
  int value1 = 2;
  int value2 = 3;
  int value3 = 5;

  // Returns the product of the above values:
  int get product => value1 * value2 * value3;

  // Adds 1 to value1:
  void incrementValue1() => value1++;

  // Returns a string containing each item in the
  // list, separated by commas (e.g. 'a,b,c'):
  String joinWithCommas(List<String> strings) => strings.join(',');
}

캐스케이드(Cascades)

같은 객체에 대해 일련의 연산을 수행하려면 캐스케이드(..)를 사용하세요. 우리 모두 이런 표현식을 본 적이 있을 거예요.

myObject.someMethod()

이것은 myObject에서 someMethod()를 호출하고, 표현식의 결과는 someMethod()의 반환값이에요.

캐스케이드를 사용한 같은 표현식이에요.

myObject..someMethod()

여전히 myObject에서 someMethod()를 호출하지만, 표현식의 결과는 반환값이 아니라 myObject에 대한 참조예요!

캐스케이드를 사용하면 별도의 문장이 필요했던 연산들을 하나로 연결할 수 있어요. 예를 들어, 조건부 멤버 접근 연산자(?.)를 사용해 buttonnull이 아닐 때 button의 프로퍼티를 읽는 다음 코드를 생각해 볼게요.

final button = web.document.querySelector('#confirm');
button?.textContent = 'Confirm';
button?.classList.add('important');
button?.onClick.listen((e) => web.window.alert('Confirmed!'));
button?.scrollIntoView();

대신 캐스케이드를 사용하려면, null-shorting 캐스케이드(?..)로 시작할 수 있는데, 이는 null인 객체에 대해 어떤 캐스케이드 연산도 시도하지 않도록 보장해 줘요. 캐스케이드를 사용하면 코드가 짧아지고 button 변수도 필요 없어져요.

web.document.querySelector('#confirm')
  ?..textContent = 'Confirm'
  ..classList.add('important')
  ..onClick.listen((e) => web.window.alert('Confirmed!'))
  ..scrollIntoView();

연습문제

캐스케이드를 사용해 BigObjectanInt, aString, aList 프로퍼티를 각각 1, 'String!', [3.0]으로 설정한 다음 allDone()을 호출하는 단일 문장을 만들어 보세요.

class BigObject {
  int anInt = 0;
  String aString = '';
  List<double> aList = [];
  bool _done = false;
  void allDone() {
    _done = true;
  }
}
BigObject fillBigObject(BigObject obj) {
  // Create a single statement that will update and return obj:
  return TODO('obj..');
}
// Tests your solution (Don't edit!):
void main() {
  BigObject obj;
  try {
    obj = fillBigObject(BigObject());
  } catch (e) {
    print('Caught an exception of type ${e.runtimeType} \n while running fillBigObject');
    return;
  }
  final errs = <String>[];
  if (obj.anInt != 1) {
    errs.add(
        'The value of anInt was ${obj.anInt} \n rather than the expected (1).');
  }
  if (obj.aString != 'String!') {
    errs.add(
        'The value of aString was \'${obj.aString}\' \n rather than the expected (\'String!\').');
  }
  if (obj.aList.length != 1) {
    errs.add(
        'The length of aList was ${obj.aList.length} \n rather than the expected value (1).');
  } else {
    if (obj.aList[0] != 3.0) {
      errs.add(
          'The value found in aList was ${obj.aList[0]} \n rather than the expected (3.0).');
    }
  }
  if (!obj._done) {
    errs.add('It looks like allDone() wasn\'t called.');
  }
  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}

이 연습문제의 가장 좋은 해법은 obj..로 시작해서 네 개의 할당 연산을 연결하는 거예요. return obj..anInt = 1로 시작한 뒤, 또 다른 캐스케이드(..)를 추가하고 다음 할당을 시작하세요.

BigObject fillBigObject(BigObject obj) {
  return obj
    ..anInt = 1
    ..aString = 'String!'
    ..aList.add(3)
    ..allDone();
}

Getter와 Setter

단순한 필드가 허락하는 것보다 프로퍼티를 더 많이 제어해야 할 때, getter와 setter를 정의할 수 있어요.

예를 들어, 프로퍼티의 값이 유효한지 확인할 수 있어요.

class MyClass {
  int _aProperty = 0;

  int get aProperty => _aProperty;

  set aProperty(int value) {
    if (value >= 0) {
      _aProperty = value;
    }
  }
}

getter를 사용해 계산된 프로퍼티(computed property)를 정의할 수도 있어요.

class MyClass {
  final List<int> _values = [];

  void addValue(int value) {
    _values.add(value);
  }

  // A computed property.
  int get count {
    return _values.length;
  }
}

연습문제

가격이 담긴 private List<double>를 유지하는 쇼핑 카트 클래스가 있다고 상상해 보세요. 다음을 추가해 보세요.

  • 가격의 합을 반환하는 total이라는 getter
  • 새 리스트에 음수 가격이 포함되어 있지 않다면(포함되어 있다면 setter가 InvalidPriceException을 던져야 해요) 그 리스트로 기존 리스트를 대체하는 setter

DartPad의 초기 오류는 모두 무시하세요.

class InvalidPriceException {}
class ShoppingCart {
  List<double> _prices = [];
  // TODO: Add a "total" getter here:
  // TODO: Add a "prices" setter here:
}
// Tests your solution (Don't edit!):
void main() {
  var foundException = false;
  try {
    final cart = ShoppingCart();
    cart.prices = [12.0, 12.0, -23.0];
  } on InvalidPriceException {
    foundException = true;
  } catch (e) {
    print('Tried setting a negative price and received a ${e.runtimeType} \n instead of an InvalidPriceException.');
    return;
  }
  if (!foundException) {
    print('Tried setting a negative price \n and didn\'t get an InvalidPriceException.');
    return;
  }
  final secondCart = ShoppingCart();
  try {
    secondCart.prices = [1.0, 2.0, 3.0];
  } catch(e) {
    print('Tried setting prices with a valid list, \n but received an exception: ${e.runtimeType}.');
    return;
  }
  if (secondCart._prices.length != 3) {
    print('Tried setting prices with a list of three values, \n but _prices ended up having length ${secondCart._prices.length}.');
    return;
  }
  if (secondCart._prices[0] != 1.0 || secondCart._prices[1] != 2.0 || secondCart._prices[2] != 3.0) {
    final vals = secondCart._prices.map((p) => p.toString()).join(', ');
    print('Tried setting prices with a list of three values (1, 2, 3), \n but incorrect ones ended up in the price list ($vals) .');
    return;
  }
  var sum = 0.0;
  try {
    sum = secondCart.total;
  } catch (e) {
    print('Tried to get total, but received an exception: ${e.runtimeType}.');
    return;
  }
  if (sum != 6.0) {
    print('After setting prices to (1, 2, 3), total returned $sum instead of 6.');
    return;
  }
  print('Success!');
}

이 연습문제에 유용한 함수가 두 가지 있어요. 하나는 fold로, 리스트를 단일 값으로 줄일 수 있어요(total을 계산하는 데 사용). 다른 하나는 any로, 여러분이 준 함수로 리스트의 각 항목을 검사할 수 있어요(prices setter에 음수 가격이 있는지 확인하는 데 사용).

/// The total price of the shopping cart.
double get total => _prices.fold(0, (e, t) => e + t);

/// Set [prices] to the [value] list of item prices.
set prices(List<double> value) {
  if (value.any((p) => p < 0)) {
    throw InvalidPriceException();
  }

  _prices = value;
}

선택적 위치 매개변수(Optional positional parameters)

Dart에는 두 종류의 함수 매개변수가 있어요: 위치(positional)와 이름(named)이에요. 위치 매개변수는 여러분이 이미 잘 알고 있는 종류일 거예요.

int sumUp(int a, int b, int c) {
  return a + b + c;
}
  // ···
  int total = sumUp(1, 2, 3);

Dart에서는 이 위치 매개변수들을 대괄호로 감싸 선택적으로 만들 수 있어요.

int sumUpToFive(int a, [int? b, int? c, int? d, int? e]) {
  int sum = a;
  if (b != null) sum += b;
  if (c != null) sum += c;
  if (d != null) sum += d;
  if (e != null) sum += e;
  return sum;
}
  // ···
  int total = sumUpToFive(1, 2);
  int otherTotal = sumUpToFive(1, 2, 3, 4, 5);

선택적 위치 매개변수는 항상 함수의 매개변수 목록에서 마지막에 위치해요. 다른 기본값을 제공하지 않는 한 기본값은 null이에요.

int sumUpToFive(int a, [int b = 2, int c = 3, int d = 4, int e = 5]) {
  // ···
}

void main() {
  int newTotal = sumUpToFive(1);
  print(newTotal); // <-- prints 15
}

연습문제

하나에서 다섯 개의 정수를 받아, 그 숫자들을 쉼표로 구분한 문자열을 반환하는 joinWithCommas() 함수를 구현하세요. 함수 호출과 반환값의 몇 가지 예시는 다음과 같아요.

joinWithCommas(1)'1'   joinWithCommas(1, 2, 3)'1,2,3'   joinWithCommas(1, 1, 1, 1, 1)'1,1,1,1,1'

String joinWithCommas(int a, [int? b, int? c, int? d, int? e]) {
  return TODO();
}
// Tests your solution (Don't edit!):
void main() {
  final errs = <String>[];
  try {
    final value = joinWithCommas(1);
    if (value != '1') {
      errs.add('Tried calling joinWithCommas(1) \n and got $value instead of the expected (\'1\').');
    }
  } on UnimplementedError {
    print('Tried to call joinWithCommas but failed. \n Did you implement the method?');
    return;
  } catch (e) {
    print('Tried calling joinWithCommas(1), \n but encountered an exception: ${e.runtimeType}.');
    return;
  }
  try {
    final value = joinWithCommas(1, 2, 3);
    if (value != '1,2,3') {
      errs.add('Tried calling joinWithCommas(1, 2, 3) \n and got $value instead of the expected (\'1,2,3\').');
    }
  } on UnimplementedError {
    print('Tried to call joinWithCommas but failed. \n Did you implement the method?');
    return;
  } catch (e) {
    print('Tried calling joinWithCommas(1, 2 ,3), \n but encountered an exception: ${e.runtimeType}.');
    return;
  }
  try {
    final value = joinWithCommas(1, 2, 3, 4, 5);
    if (value != '1,2,3,4,5') {
      errs.add('Tried calling joinWithCommas(1, 2, 3, 4, 5) \n and got $value instead of the expected (\'1,2,3,4,5\').');
    }
  } on UnimplementedError {
    print('Tried to call joinWithCommas but failed. \n Did you implement the method?');
    return;
  } catch (e) {
    print('Tried calling stringify(1, 2, 3, 4 ,5), \n but encountered an exception: ${e.runtimeType}.');
    return;
  }
  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}

b, c, d, e 매개변수는 호출자가 제공하지 않으면 null이에요. 따라서 핵심은 이 인자들을 최종 문자열에 추가하기 전에 null인지 확인하는 거예요.

String joinWithCommas(int a, [int? b, int? c, int? d, int? e]) {
  var total = '$a';
  if (b != null) total = '$total,$b';
  if (c != null) total = '$total,$c';
  if (d != null) total = '$total,$d';
  if (e != null) total = '$total,$e';
  return total;
}

이름 매개변수(Named parameters)

매개변수 목록 끝에 중괄호 문법을 사용하면 이름을 가진 매개변수를 정의할 수 있어요.

이름 매개변수는 명시적으로 required로 표시하지 않는 한 선택적이에요.

void printName(String firstName, String lastName, {String? middleName}) {
  print('$firstName ${middleName ?? ''} $lastName');
}

void main() {
  printName('Dash', 'Dartisan');
  printName('John', 'Smith', middleName: 'Who');
  // Named arguments can be placed anywhere in the argument list.
  printName('John', middleName: 'Who', 'Smith');
}

예상대로, nullable 이름 매개변수의 기본값은 null이지만, 사용자 정의 기본값을 제공할 수도 있어요.

매개변수의 타입이 non-nullable이라면, 기본값을 제공하거나(다음 코드 참고) 매개변수를 required로 표시해야 해요(생성자 섹션constructor section 참고).

void printName(String firstName, String lastName, {String middleName = ''}) {
  print('$firstName $middleName $lastName');
}

함수는 선택적 위치 매개변수와 이름 매개변수를 동시에 가질 수 없어요.

연습문제

MyDataObject 클래스에 copyWith() 인스턴스 메서드를 추가하세요. 이 메서드는 세 개의 이름, nullable 매개변수를 받아야 해요.

  • int? newInt
  • String? newString
  • double? newDouble

copyWith() 메서드는 현재 인스턴스를 기반으로, 앞선 매개변수의 데이터(있다면)를 객체의 프로퍼티에 복사한 새 MyDataObject를 반환해야 해요. 예를 들어, newInt가 non-null이면 그 값을 anInt에 복사하세요.

DartPad의 초기 오류는 모두 무시하세요.

class MyDataObject {
  final int anInt;
  final String aString;
  final double aDouble;
  MyDataObject({
     this.anInt = 1,
     this.aString = 'Old!',
     this.aDouble = 2.0,
  });
  // TODO: Add your copyWith method here:
}
// Tests your solution (Don't edit!):
void main() {
  final source = MyDataObject();
  final errs = <String>[];
  try {
    final copy = source.copyWith(newInt: 12, newString: 'New!', newDouble: 3.0);
    if (copy.anInt != 12) {
      errs.add('Called copyWith(newInt: 12, newString: \'New!\', newDouble: 3.0), \n and the new object\'s anInt was ${copy.anInt} rather than the expected value (12).');
    }
    if (copy.aString != 'New!') {
      errs.add('Called copyWith(newInt: 12, newString: \'New!\', newDouble: 3.0), \n and the new object\'s aString was ${copy.aString} rather than the expected value (\'New!\').');
    }
    if (copy.aDouble != 3) {
      errs.add('Called copyWith(newInt: 12, newString: \'New!\', newDouble: 3.0), \n and the new object\'s aDouble was ${copy.aDouble} rather than the expected value (3).');
    }
  } catch (e) {
    print('Called copyWith(newInt: 12, newString: \'New!\', newDouble: 3.0) \n and got an exception: ${e.runtimeType}');
  }
  try {
    final copy = source.copyWith();
    if (copy.anInt != 1) {
      errs.add('Called copyWith(), and the new object\'s anInt was ${copy.anInt} \n rather than the expected value (1).');
    }
    if (copy.aString != 'Old!') {
      errs.add('Called copyWith(), and the new object\'s aString was ${copy.aString} \n rather than the expected value (\'Old!\').');
    }
    if (copy.aDouble != 2) {
      errs.add('Called copyWith(), and the new object\'s aDouble was ${copy.aDouble} \n rather than the expected value (2).');
    }
  } catch (e) {
    print('Called copyWith() and got an exception: ${e.runtimeType}');
  }
  try {
    final sourceWithoutDefaults = MyDataObject(
      anInt: 520,
      aString: 'Custom!',
      aDouble: 20.25,
    );
    final copy = sourceWithoutDefaults.copyWith();
    if (copy.anInt == 1) {
      errs.add('Called `copyWith()` on an object with a non-default `anInt` value (${sourceWithoutDefaults.anInt}), but the new object\'s `anInt` was the default value of ${copy.anInt}.');
    }
    if (copy.aString == 'Old!') {
      errs.add('Called `copyWith()` on an object with a non-default `aString` value (\'${sourceWithoutDefaults.aString}\'), but the new object\'s `aString` was the default value of \'${copy.aString}\'.');
    }
    if (copy.aDouble == 2.0) {
      errs.add('Called copyWith() on an object with a non-default `aDouble` value (${sourceWithoutDefaults.aDouble}), but the new object\'s `aDouble` was the default value of ${copy.aDouble}.');
    }
  } catch (e) {
    print('Called copyWith() and got an exception: ${e.runtimeType}');
  }
  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}

copyWith 메서드는 많은 클래스와 라이브러리에서 등장해요. 여러분의 메서드는 몇 가지를 해야 해요: 선택적 이름 매개변수를 사용하고, MyDataObject의 새 인스턴스를 만들고, 매개변수의 데이터로 채우기(매개변수가 null이면 현재 인스턴스의 데이터 사용). ?? 연산자를 더 연습할 좋은 기회예요!

MyDataObject copyWith({int? newInt, String? newString, double? newDouble}) {
  return MyDataObject(
    anInt: newInt ?? this.anInt,
    aString: newString ?? this.aString,
    aDouble: newDouble ?? this.aDouble,
  );
}

예외(Exceptions)

Dart 코드는 예외를 던지고 잡을 수 있어요. Java와 달리, Dart의 모든 예외는 unchecked예요. 메서드는 어떤 예외를 던질 수 있는지 선언하지 않으며, 예외를 잡을 의무도 없어요.

Dart는 ExceptionError 타입을 제공하지만, null이 아닌 어떤 객체든 던질 수 있어요.

throw Exception('Something bad happened.');
throw 'Waaaaaaah!';

예외를 처리할 때는 try, on, catch 키워드를 사용하세요.

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  // A specific exception
  buyMoreLlamas();
} on Exception catch (e) {
  // Anything else that is an exception
  print('Unknown exception: $e');
} catch (e) {
  // No specified type, handles all
  print('Something really unknown: $e');
}

try 키워드는 대부분의 다른 언어에서처럼 동작해요. on 키워드로 타입을 기준으로 특정 예외를 필터링하고, catch 키워드로 예외 객체에 대한 참조를 얻어요.

예외를 완전히 처리할 수 없다면, rethrow 키워드를 사용해 예외를 전파하세요.

try {
  breedMoreLlamas();
} catch (e) {
  print('I was just trying to breed llamas!');
  rethrow;
}

예외가 던져지든 말든 코드를 실행하려면 finally를 사용하세요.

try {
  breedMoreLlamas();
} catch (e) {
  // ... handle exception ...
} finally {
  // Always clean up, even if an exception is thrown.
  cleanLlamaStalls();
}

연습문제

아래 tryFunction()을 구현하세요. 신뢰할 수 없는 메서드를 실행한 뒤 다음을 수행해야 해요.

  • untrustworthy()ExceptionWithMessage를 던지면, 예외 타입과 메시지로 logger.logException을 호출하세요(oncatch 사용을 시도해 보세요).
  • untrustworthy()Exception을 던지면, 예외 타입으로 logger.logException을 호출하세요(이번에는 on 사용을 시도해 보세요).
  • untrustworthy()가 다른 어떤 객체를 던지면, 그 예외는 잡지 마세요.
  • 모든 것을 잡고 처리한 뒤에는 logger.doneLogging을 호출하세요(finally 사용을 시도해 보세요).
typedef VoidFunction = void Function();
class ExceptionWithMessage {
  final String message;
  const ExceptionWithMessage(this.message);
}
// Call logException to log an exception, and doneLogging when finished.
abstract class Logger {
  void logException(Type t, [String? msg]);
  void doneLogging();
}
void tryFunction(VoidFunction untrustworthy, Logger logger) {
  try {
    untrustworthy();
  } // Write your logic here
}
// Tests your solution (Don't edit!):
class MyLogger extends Logger {
  Type? lastType;
  String lastMessage = '';
  bool done = false;
  void logException(Type t, [String? message]) {
    lastType = t;
    lastMessage = message ?? lastMessage;
  }
  void doneLogging() => done = true;
}
void main() {
  final errs = <String>[];
  var logger = MyLogger();
  try {
    tryFunction(() => throw Exception(), logger);
    if ('${logger.lastType}' != 'Exception' && '${logger.lastType}' != '_Exception') {
      errs.add('Untrustworthy threw an Exception, but a different type was logged: \n ${logger.lastType}.');
    }
    if (logger.lastMessage != '') {
      errs.add('Untrustworthy threw an Exception with no message, but a message \n was logged anyway: \'${logger.lastMessage}\'.');
    }
    if (!logger.done) {
      errs.add('Untrustworthy threw an Exception, \n and doneLogging() wasn\'t called afterward.');
    }
  } catch (e) {
    print('Untrustworthy threw an exception, and an exception of type \n ${e.runtimeType} was unhandled by tryFunction.');
  }
  logger = MyLogger();
  try {
    tryFunction(() => throw ExceptionWithMessage('Hey!'), logger);
    if (logger.lastType != ExceptionWithMessage) {
      errs.add('Untrustworthy threw an ExceptionWithMessage(\'Hey!\'), but a \n different type was logged: ${logger.lastType}.');
    }
    if (logger.lastMessage != 'Hey!') {
      errs.add('Untrustworthy threw an ExceptionWithMessage(\'Hey!\'), but a \n different message was logged: \'${logger.lastMessage}\'.');
    }
    if (!logger.done) {
      errs.add('Untrustworthy threw an ExceptionWithMessage(\'Hey!\'), \n and doneLogging() wasn\'t called afterward.');
    }
  } catch (e) {
    print('Untrustworthy threw an ExceptionWithMessage(\'Hey!\'), \n and an exception of type ${e.runtimeType} was unhandled by tryFunction.');
  }
  logger = MyLogger();
  bool caughtStringException = false;
  try {
    tryFunction(() => throw 'A String', logger);
  } on String {
    caughtStringException = true;
  }
  if (!caughtStringException) {
    errs.add('Untrustworthy threw a string, and it was incorrectly handled inside tryFunction().');
  }
  logger = MyLogger();
  try {
    tryFunction(() {}, logger);
    if (logger.lastType != null) {
      errs.add('Untrustworthy didn\'t throw an Exception, \n but one was logged anyway: ${logger.lastType}.');
    }
    if (logger.lastMessage != '') {
      errs.add('Untrustworthy didn\'t throw an Exception with no message, \n but a message was logged anyway: \'${logger.lastMessage}\'.');
    }
    if (!logger.done) {
      errs.add('Untrustworthy didn\'t throw an Exception, \n but doneLogging() wasn\'t called afterward.');
    }
  } catch (e) {
    print('Untrustworthy didn\'t throw an exception, \n but an exception of type ${e.runtimeType} was unhandled by tryFunction anyway.');
  }
  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}

이 연습문제는 까다로워 보이지만, 사실 하나의 큰 try 문이에요. try 안에서 untrustworthy를 호출하고, on, catch, finally를 사용해 예외를 잡고 logger의 메서드를 호출하세요.

void tryFunction(VoidFunction untrustworthy, Logger logger) {
  try {
    untrustworthy();
  } on ExceptionWithMessage catch (e) {
    logger.logException(e.runtimeType, e.message);
  } on Exception {
    logger.logException(Exception);
  } finally {
    logger.doneLogging();
  }
}

생성자에서 this 사용하기

Dart는 생성자에서 프로퍼티에 값을 할당하는 편리한 지름길을 제공해요. 생성자를 선언할 때 this.propertyName을 사용하면 돼요.

class MyColor {
  int red;
  int green;
  int blue;

  MyColor(this.red, this.green, this.blue);
}

final color = MyColor(80, 80, 128);

이 기법은 이름 매개변수에도 동일하게 적용돼요. 프로퍼티 이름이 매개변수의 이름이 돼요.

class MyColor {
  // ...

  MyColor({required this.red, required this.green, required this.blue});
}

final color = MyColor(red: 80, green: 80, blue: 80);

위 코드에서 red, green, blue는 이 int 값들이 null이 될 수 없기 때문에 required로 표시돼요. 기본값을 추가하면 required를 생략할 수 있어요.

MyColor([this.red = 0, this.green = 0, this.blue = 0]);
// or
MyColor({this.red = 0, this.green = 0, this.blue = 0});

연습문제

this. 문법을 사용해 클래스의 세 프로퍼티 모두에 대한 값을 받아 할당하는 한 줄짜리 생성자를 MyClass에 추가하세요.

DartPad의 초기 오류는 모두 무시하세요.

class MyClass {
  final int anInt;
  final String aString;
  final double aDouble;
  // TODO: Create the constructor here.
}
// Tests your solution (Don't edit!):
void main() {
  final errs = <String>[];
  try {
    final obj = MyClass(1, 'two', 3);
    if (obj.anInt != 1) {
      errs.add('Called MyClass(1, \'two\', 3) and got an object with anInt of ${obj.anInt} \n instead of the expected value (1).');
    }
    if (obj.anInt != 1) {
      errs.add('Called MyClass(1, \'two\', 3) and got an object with aString of \'${obj.aString}\' \n instead of the expected value (\'two\').');
    }
    if (obj.anInt != 1) {
      errs.add('Called MyClass(1, \'two\', 3) and got an object with aDouble of ${obj.aDouble} \n instead of the expected value (3).');
    }
  } catch (e) {
    print('Called MyClass(1, \'two\', 3) and got an exception \n of type ${e.runtimeType}.');
  }
  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}

이 연습문제는 한 줄짜리 해법이 있어요. this.anInt, this.aString, this.aDouble을 그 순서대로 매개변수로 갖는 생성자를 선언하세요.

MyClass(this.anInt, this.aString, this.aDouble);

초기화 리스트(Initializer lists)

생성자를 구현할 때 생성자 본문이 실행되기 전에 몇 가지 설정을 해야 할 때가 있어요. 예를 들어, final 필드는 생성자 본문이 실행되기 전에 값을 가져야 해요. 이 작업을 생성자의 시그니처와 본문 사이에 위치하는 초기화 리스트에서 수행하세요.

Point.fromJson(Map<String, double> json) : x = json['x']!, y = json['y']! {
  print('In Point.fromJson(): ($x, $y)');
}

초기화 리스트는 개발 중에만 실행되는 assert를 넣기에도 편리한 곳이에요.

NonNegativePoint(this.x, this.y) : assert(x >= 0), assert(y >= 0) {
  print('I just made a NonNegativePoint: ($x, $y)');
}

연습문제

아래 FirstTwoLetters 생성자를 완성하세요. 초기화 리스트를 사용해 word의 첫 두 문자를 letterOneletterTwo 프로퍼티에 할당하세요. 보너스로, 두 글자보다 짧은 단어를 잡아내는 assert를 추가해 보세요.

DartPad의 초기 오류는 모두 무시하세요.

class FirstTwoLetters {
  final String letterOne;
  final String letterTwo;
  // TODO: Create a constructor with an initializer list here:
  FirstTwoLetters(String word)
}
// Tests your solution (Don't edit!):
void main() {
  final errs = <String>[];
  try {
    final result = FirstTwoLetters('My String');
    if (result.letterOne != 'M') {
      errs.add('Called FirstTwoLetters(\'My String\') and got an object with \n letterOne equal to \'${result.letterOne}\' instead of the expected value (\'M\').');
    }
    if (result.letterTwo != 'y') {
      errs.add('Called FirstTwoLetters(\'My String\') and got an object with \n letterTwo equal to \'${result.letterTwo}\' instead of the expected value (\'y\').');
    }
  } catch (e) {
    errs.add('Called FirstTwoLetters(\'My String\') and got an exception \n of type ${e.runtimeType}.');
  }
  bool caughtException = false;
  try {
    FirstTwoLetters('');
  } catch (e) {
    caughtException = true;
  }
  if (!caughtException) {
    errs.add('Called FirstTwoLetters(\'\') and didn\'t get an exception \n from the failed assertion.');
  }
  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}

두 개의 할당이 일어나야 해요: letterOneword[0]로, letterTwoword[1]로 할당돼야 해요.

  FirstTwoLetters(String word)
      : assert(word.length >= 2),
        letterOne = word[0],
        letterTwo = word[1];

이름 있는 생성자(Named constructors)

클래스가 여러 개의 생성자를 가질 수 있도록, Dart는 이름 있는 생성자를 지원해요.

class Point {
  double x, y;

  Point(this.x, this.y);

  Point.origin() : x = 0, y = 0;
}

이름 있는 생성자를 사용하려면 전체 이름으로 호출하세요.

final myPoint = Point.origin();

연습문제

Color 클래스에 세 프로퍼티를 모두 0으로 설정하는 Color.black이라는 이름의 생성자를 추가하세요.

DartPad의 초기 오류는 모두 무시하세요.

class Color {
  int red;
  int green;
  int blue;
  Color(this.red, this.green, this.blue);
  // TODO: Create a named constructor called "Color.black" here:
}
// Tests your solution (Don't edit!):
void main() {
  final errs = <String>[];
  try {
    final result = Color.black();
    if (result.red != 0) {
      errs.add('Called Color.black() and got a Color with red equal to \n ${result.red} instead of the expected value (0).');
    }
    if (result.green != 0) {
      errs.add('Called Color.black() and got a Color with green equal to \n ${result.green} instead of the expected value (0).');
    }
    if (result.blue != 0) {
  errs.add('Called Color.black() and got a Color with blue equal to \n ${result.blue} instead of the expected value (0).');
    }
  } catch (e) {
    print('Called Color.black() and got an exception of type \n ${e.runtimeType}.');
    return;
  }
  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}

생성자의 선언은 Color.black():으로 시작해야 해요. 초기화 리스트(콜론 뒤)에서 red, green, blue0으로 설정하세요.

Color.black() : red = 0, green = 0, blue = 0;

팩토리 생성자(Factory constructors)

Dart는 서브타입이나 심지어 null을 반환할 수 있는 팩토리 생성자를 지원해요. 팩토리 생성자를 만들려면 factory 키워드를 사용하세요.

class Square extends Shape {}

class Circle extends Shape {}

class Shape {
  Shape();

  factory Shape.fromTypeName(String typeName) {
    if (typeName == 'square') return Square();
    if (typeName == 'circle') return Circle();

    throw ArgumentError('Unrecognized $typeName');
  }
}

연습문제

IntegerHolder.fromList라는 팩토리 생성자 안의 TODO(); 줄을 다음을 반환하도록 교체하세요.

  • 리스트에 값이 하나 있으면, 그 값을 사용해 IntegerSingle 인스턴스를 만들어요.
  • 리스트에 값이 두 개 있으면, 그 값들을 순서대로 사용해 IntegerDouble 인스턴스를 만들어요.
  • 리스트에 값이 세 개 있으면, 그 값들을 순서대로 사용해 IntegerTriple 인스턴스를 만들어요.
  • 그 외에는 Error를 던져요.

성공하면 콘솔에 Success!가 표시돼야 해요.

class IntegerHolder {
  IntegerHolder();
  // Implement this factory constructor.
  factory IntegerHolder.fromList(List<int> list) {
    TODO();
  }
}
class IntegerSingle extends IntegerHolder {
  final int a;
  IntegerSingle(this.a);
}
class IntegerDouble extends IntegerHolder {
  final int a;
  final int b;
  IntegerDouble(this.a, this.b);
}
class IntegerTriple extends IntegerHolder {
  final int a;
  final int b;
  final int c;
  IntegerTriple(this.a, this.b, this.c);
}
// Tests your solution (Don't edit from this point to end of file):
void main() {
  final errs = <String>[];
  // Run 5 tests to see which values have valid integer holders.
  for (var tests = 0; tests < 5; tests++) {
    if (!testNumberOfArgs(errs, tests)) return;
  }
  // The goal is no errors with values 1 to 3,
  // but have errors with values 0 and 4.
  // The testNumberOfArgs method adds to the errs array if
  // the values 1 to 3 have an error and
  // the values 0 and 4 don't have an error.
  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}
bool testNumberOfArgs(List<String> errs, int count) {
  bool _threw = false;
  final ex = List.generate(count, (index) => index + 1);
  final callTxt = "IntegerHolder.fromList(${ex})";
  try {
    final obj = IntegerHolder.fromList(ex);
    final String vals = count == 1 ? "value" : "values";
    // Uncomment the next line if you want to see the results realtime
    // print("Testing with ${count} ${vals} using ${obj.runtimeType}.");
    testValues(errs, ex, obj, callTxt);
  } on Error {
    _threw = true;
  } catch (e) {
    switch (count) {
      case (< 1 && > 3):
        if (!_threw) {
          errs.add('Called ${callTxt} and it didn\'t throw an Error.');
        }
      default:
        errs.add('Called $callTxt and received an Error.');
    }
  }
  return true;
}
void testValues(List<String> errs, List<int> expectedValues, IntegerHolder obj,
    String callText) {
  for (var i = 0; i < expectedValues.length; i++) {
    int found;
    if (obj is IntegerSingle) {
      found = obj.a;
    } else if (obj is IntegerDouble) {
      found = i == 0 ? obj.a : obj.b;
    } else if (obj is IntegerTriple) {
      found = i == 0
          ? obj.a
          : i == 1
              ? obj.b
              : obj.c;
    } else {
      throw ArgumentError(
          "This IntegerHolder type (${obj.runtimeType}) is unsupported.");
    }
    if (found != expectedValues[i]) {
      errs.add(
          "Called $callText and got a ${obj.runtimeType} " +
          "with a property at index $i value of $found " +
          "instead of the expected (${expectedValues[i]}).");
    }
  }
}

팩토리 생성자 안에서 리스트의 길이를 확인한 다음, 적절하게 IntegerSingle, IntegerDouble, 또는 IntegerTriple을 만들어 반환하세요.

TODO();를 다음 코드 블록으로 교체하세요.

  switch (list.length) {
    case 1:
      return IntegerSingle(list[0]);
    case 2:
      return IntegerDouble(list[0], list[1]);
    case 3:
      return IntegerTriple(list[0], list[1], list[2]);
    default:
      throw ArgumentError("List must between 1 and 3 items. This list was ${list.length} items.");
  }

리다이렉팅 생성자(Redirecting constructors)

때때로 생성자의 유일한 목적이 같은 클래스의 다른 생성자로 리다이렉트하는 것일 수 있어요. 리다이렉팅 생성자의 본문은 비어 있고, 생성자 호출이 콜론(:) 뒤에 나타나요.

class Automobile {
  String make;
  String model;
  int mpg;

  // The main constructor for this class.
  Automobile(this.make, this.model, this.mpg);

  // Delegates to the main constructor.
  Automobile.hybrid(String make, String model) : this(make, model, 60);

  // Delegates to a named constructor
  Automobile.fancyHybrid() : this.hybrid('Futurecar', 'Mark 2');
}

연습문제

앞서 본 Color 클래스를 기억하시나요? black이라는 이름 있는 생성자를 만들되, 프로퍼티를 수동으로 할당하는 대신 인자로 0을 넘겨 기본 생성자로 리다이렉트하세요.

DartPad의 초기 오류는 모두 무시하세요.

class Color {
  int red;
  int green;
  int blue;
  Color(this.red, this.green, this.blue);
  // TODO: Create a named constructor called "black" here
  // and redirect it to call the existing constructor
}
// Tests your solution (Don't edit!):
void main() {
  final errs = <String>[];
  try {
    final result = Color.black();
    if (result.red != 0) {
      errs.add('Called Color.black() and got a Color with red equal to \n ${result.red} instead of the expected value (0).');
    }
    if (result.green != 0) {
      errs.add('Called Color.black() and got a Color with green equal to \n ${result.green} instead of the expected value (0).');
    }
    if (result.blue != 0) {
  errs.add('Called Color.black() and got a Color with blue equal to \n ${result.blue} instead of the expected value (0).');
    }
  } catch (e) {
    print('Called Color.black() and got an exception of type ${e.runtimeType}.');
    return;
  }
  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}

생성자는 this(0, 0, 0)으로 리다이렉트해야 해요.

Color.black() : this(0, 0, 0);

Const 생성자

여러분의 클래스가 절대 변하지 않는 객체를 만들어낸다면, 이 객체들을 컴파일 타임 상수로 만들 수 있어요. 그러려면 const 생성자를 정의하고 모든 인스턴스 변수가 final인지 확인하세요.

class ImmutablePoint {
  static const ImmutablePoint origin = ImmutablePoint(0, 0);

  final int x;
  final int y;

  const ImmutablePoint(this.x, this.y);
}

연습문제

Recipe 클래스의 인스턴스가 상수가 될 수 있도록 수정하고, 다음을 수행하는 상수 생성자를 만드세요.

  • ingredients, calories, milligramsOfSodium 세 개의 매개변수를 가져요(그 순서대로).
  • this. 문법을 사용해 매개변수 값을 같은 이름의 객체 프로퍼티에 자동으로 할당해요.
  • 생성자 선언에서 Recipe 바로 앞에 const 키워드를 붙여 상수예요.

DartPad의 초기 오류는 모두 무시하세요.

class Recipe {
  List<String> ingredients;
  int calories;
  double milligramsOfSodium;
  // TODO: Create a const constructor here.
}
// Tests your solution (Don't edit!):
void main() {
  final errs = <String>[];
  try {
    const obj = Recipe(['1 egg', 'Pat of butter', 'Pinch salt'], 120, 200);
    if (obj.ingredients.length != 3) {
      errs.add('Called Recipe([\'1 egg\', \'Pat of butter\', \'Pinch salt\'], 120, 200) \n and got an object with ingredient list of length ${obj.ingredients.length} rather than the expected length (3).');
    }
    if (obj.calories != 120) {
      errs.add('Called Recipe([\'1 egg\', \'Pat of butter\', \'Pinch salt\'], 120, 200) \n and got an object with a calorie value of ${obj.calories} rather than the expected value (120).');
    }
    if (obj.milligramsOfSodium != 200) {
      errs.add('Called Recipe([\'1 egg\', \'Pat of butter\', \'Pinch salt\'], 120, 200) \n and got an object with a milligramsOfSodium value of ${obj.milligramsOfSodium} rather than the expected value (200).');
    }
    try {
      obj.ingredients.add('Sugar to taste');
      errs.add('Tried adding an item to the \'ingredients\' list of a const Recipe and didn\'t get an error due to it being unmodifiable.');
    } on UnsupportedError catch (_) {
      // We expect an `UnsupportedError` due to
      // `ingredients` being a const, unmodifiable list.
    }
  } catch (e) {
    print('Tried calling Recipe([\'1 egg\', \'Pat of butter\', \'Pinch salt\'], 120, 200) \n and received a null.');
  }
  if (errs.isEmpty) {
    print('Success!');
  } else {
    errs.forEach(print);
  }
}

생성자를 const로 만들려면 모든 프로퍼티를 final로 만들어야 해요.

class Recipe {
  final List<String> ingredients;
  final int calories;
  final double milligramsOfSodium;

  const Recipe(this.ingredients, this.calories, this.milligramsOfSodium);
}

다음 단계

Dart 언어의 가장 흥미로운 기능 몇 가지를 배우거나 자신의 지식을 테스트하기 위해 이 튜토리얼을 즐겁게 사용하셨길 바라요.

다음으로 시도해 볼 수 있는 것들은 다음과 같아요.

별도로 명시하지 않는 한, 이 사이트의 문서는 Dart 3.13.3을 기준으로 해요. 페이지 마지막 업데이트: 2026-02-02. 소스 보기(View source) 또는 이슈 보고하기.

더 알아보기