Dart - Null safety & Dart Codelab Exercise

2022. 6. 21. 23:17DEV/Dart

반응형

Null safety

  • 명시적으로 null 값을 사용하겠다고 설정하지 않는 한 변수에 null 값을 대입할 수 없는 non-nullable로 간주한다.
  • 기본적으로 변수에 null 값이 지정되지 않도록 해주어 그로인해 발생하는 오류 방지한다.
  • version : Dart 2.12 and Flutter 2
// Without null safety:
bool isEmpty(String string) => string.length == 0;

main() {
  isEmpty(null);
}

위 코드를 Null safety가 적용되지 않은 상태에서 실행하면 .length를 호출할 때 NoSuchMethodError가 발생하게 된다. 이는 null 값을 가진 string 변수의 타입이 Null 클래스이므로 length getter가 없기 때문이다. Null safety 적용으로 오류를 빠르게 발견하고 수정할 수 있게 된다.

 Nullability & Null safety

Null은 모든 다른 타입의 하위 타입(subtype)으로 취급된다. List 타입을 예로 들면, .add()를 호출할 수 있는데, null 값의 경우 method가 정의되어 있지 않으므로 오류가 발생하게 된다.
Null safety는 Null을 다른 타입의 하위 타입이 아닌 별도로 타입으로 분리시켰다.

Nullable & Non-Nullable

  • Nullable : null 값 사용 가능
  • Non-Nullable : null 값 사용 불가능

Non-Nullable

기본적으로 null 값을 대입할 수 없다.

  // # Non-Nullable
  String name = 'Dart';
  print(name);  // => Dart
  name = null; // => 🚫 A value of type 'Null' can't be assigned to a variable of type 'String'.

Nullable

변수 타입 뒤에 ?(물음표)를 붙여서 null 값을 사용할 수 있다.

eg. String?String|null을 의미한다.

  // # Nullable
  // 변수 타입 뒤에 ?를 붙여서 설정
  String? name2 = 'Dart';
  print(name2); // => Dart
  name2 = null;
  print(name2); // => null

Null assertion operator

null 값이 아니라는 것을 확실하게 표기하기 위해 !를 뒤에 붙여준다.

  // null assertion operator
  List<int?> list = [1, null, 3];
  int n = list.first!; // null 값이 아니라는 것이 확실한 경우 !를 붙여줌
  print(n); => 1
💡 Fun Tip
연산자 중 ? 의 재미난 규칙이 있다.

int? num;
num ??= 3;
print(num); // => 3
num ??= 1;
print(num); // => 3

??= 은 변수의 값이 null인 경우 우항의 값으로 지정한다는 뜻이다.
즉, num ??= 3;if(num == null) num = 3; 과 동일한 결과를 만든다.
? 를 nullable 관련해서 아주 재미지게 사용하는 듯하다~

자, 이제 최종적으로 타입의 계층도를 따져보면 아래와 같아진다.

Dart Codelab 문제 풀이

Dart Codelab

Nullable and non-nullable types

null safety를 선택하면 기본적으로 모든 타입이 Non-nullalbe이다. 예를 들어 String 타입의 변수가 있는 경우 항상 문자열이 포함된다.
When you opt in to null safety, all types are non-nullable by default. For example, if you have a variable of type String, it will always contain a string.

String 변수에 문자열나 null 값을 대입하려고 할 때 변수를 nullable 타입으로 설정하기 위해 변수명 뒤에 ?(물음표)를 붙이면 된다. 예를 들면 String?은 문자열 또는 null 값을 가질 수 있다.
If you want a variable of type String to accept any string or the value null, give the variable a nullable type by adding a question mark (?) after the type name. For example, a variable of type String? can contain a string, or it can be null.

Non-nullable types

null 값이 아니게 값을 대입해라.

void main() {
  int a;
  a = null;
  print('a is $a.');
}
=>
A value of type 'Null' can't be assigned to a variable of type 'int'.

void main() {
  int a;
  a = 3;
  print('a is $a.');
}
=>
a is 3.

Nullable types

Nullable 변수로 설정해라.

void main() {
  int a;
  a = null;
  print('a is $a.');
}
=>
A value of type 'Null' can't be assigned to a variable of type 'int'.

void main() {
  int? a;
  a = null;
  print('a is $a.');
}
=> 
a is null.

Nullable type parameters for generics

?(물음표)를 사용해서 Nullable 변수로 설정해라.

void main() {
  List<String> aListOfStrings = ['one', 'two', 'three'];
  List<String> aNullableListOfStrings;
  List<String> aListOfNullableStrings = ['one', null, 'three'];

  print('aListOfStrings is $aListOfStrings.');
  print('aNullableListOfStrings is $aNullableListOfStrings.');
  print('aListOfNullableStrings is $aListOfNullableStrings.');
}
=>
The element type 'Null' can't be assigned to the list type 'String'.
The non-nullable local variable 'aNullableListOfStrings' must be assigned before it can be used.

void main() {
  List<String> aListOfStrings = ['one', 'two', 'three'];
  List<String>? aNullableListOfStrings;
  List<String?> aListOfNullableStrings = ['one', null, 'three'];

  print('aListOfStrings is $aListOfStrings.');
  print('aNullableListOfStrings is $aNullableListOfStrings.');
  print('aListOfNullableStrings is $aListOfNullableStrings.');
}
=>
aListOfStrings is [one, two, three].
aNullableListOfStrings is null.
aListOfNullableStrings is [one, null, three].

The null assertion operator (!)

Nullable 타입이 null 값이 아닌 것이 확실하다면 null assersion operator (!)를 사용해서 다트가 Non-nullable 처럼 취급하게 한다. expression 뒤에 !(느낌표)를 붙이기만 하면 값이 절대 null이 아니며 Non-nullable 변수에 대입해도 안전한다는 것을 다트에게 알리게 된다.
If you’re sure that an expression with a nullable type isn’t null, you can use a null assertion operator (!) to make Dart treat it as non-nullable. By adding ! just after the expression, you tell Dart that the value won’t be null, and that it’s safe to assign it to a non-nullable variable.

Null assertion

오류가 발생한 부분에 !를 사용해서 해결해라.

int? couldReturnNullButDoesnt() => -3;

void main() {
  int? couldBeNullButIsnt = 1;
  List<int?> listThatCouldHoldNulls = [2, null, 4];

  int a = couldBeNullButIsnt;
  int b = listThatCouldHoldNulls.first; // first item in the list
  int c = couldReturnNullButDoesnt().abs(); // absolute value

  print('a is $a.');
  print('b is $b.');
  print('c is $c.');
}
=>
A value of type 'int?' can't be assigned to a variable of type 'int'.
The method 'abs' can't be unconditionally invoked because the receiver can be 'null'.

int? couldReturnNullButDoesnt() => -3;

void main() {
  int? couldBeNullButIsnt = 1;
  List<int?> listThatCouldHoldNulls = [2, null, 4];

  int a = couldBeNullButIsnt;
  int b = listThatCouldHoldNulls.first!; // first item in the list
  int c = couldReturnNullButDoesnt()!.abs(); // absolute value

  print('a is $a.');
  print('b is $b.');
  print('c is $c.');
}
=>
a is 1.
b is 2.
c is 3.

Type promotion

Definite assignment

다트의 타입 시스템은 변수가 어디에서 대입되고 사용되는 지 추적할 수 있고, Non-nullable 변수가 코드에서 사용되기 전에 주어진 값을 확인할 수 있다. 이 프로세스를 Definite assignment라 부른다.
Dart’s type system can track where variables are assigned and read, and can verify that non-nullable variables are given values before any code tries to read from them. This process is called definite assignment.

void main() {
  String text;

  //if (DateTime.now().hour < 12) {
  //  text = "It's morning! Let's make aloo paratha!";
  //} else {
  //  text = "It's afternoon! Let's make biryani!";
  //}

  print(text);
  print(text.length);
}
=>
The non-nullable local variable 'text' must be assigned before it can be used.
The non-nullable local variable 'text' must be assigned before it can be used.

void main() {
  String text;

  if (DateTime.now().hour < 12) {
   text = "It's morning! Let's make aloo paratha!";
  } else {
   text = "It's afternoon! Let's make biryani!";
  }

  print(text);
  print(text.length);
}
=>
It's afternoon! Let's make biryani!
35

Null checking

getLength 함수의 도입부에 if문을 사용해 null 값이면 0(zero)를 반환해라.

int getLength(String? str) {
  // Add null check here

  return str.length;
}

void main() {
  print(getLength('This is a string!'));
}
=>
The property 'length' can't be unconditionally accessed because the receiver can be 'null'.

int getLength([String? str]) {
  // Add null check here
  if(str == null) return 0;

  return str.length;
}

void main() {
  print(getLength('This is a string!'));
  print(getLength());
}
=>
17
0

Promotion with exceptions

null 값인 경우 exceoption을 발생시켜라.

int getLength(String? str) {
  // Try throwing an exception here if `str` is null.

  return str.length;
}

void main() {
  print(getLength(null));
}
=>
The property 'length' can't be unconditionally accessed because the receiver can be 'null'.

int getLength(String? str) {
  // Try throwing an exception here if `str` is null.
  if(str == null) throw Exception('The value is null');
  return str.length;
}

void main() {
  print(getLength(null));
}
=>
Uncaught Error: Exception: The value is null

The late keyword

변수 필드가 class이거나 상위 레벨의 변수일 때가 있다. 물론 Non-nullable이어야 하고. 하지만 때로는 그 변수 값을 즉시 대입할 수 없을 수 있다. 이와 같은 경우에 late keyword를 사용한다.
Sometimes variables—fields in a class, or top-level variables—should be non-nullable, but they can’t be assigned a value immediately. For cases like that, use the late keyword.

변수 선언 앞에 late를 표기하면 다트에게 아래 내용을 전달하게 된다.
When you put late in front of a variable declaration, that tells Dart the following:

  • 변수 값을 아직 대입하지 말아라. Don’t assign that variable a value yet.
  • 값은 나중에 대입할 예정이다. You will assign it a value later.
  • 변수가 사용되기 전에 확실하게 값을 대입하겠다. You’ll make sure that the variable has a value before the variable is used.
  • 변수 값이 대입되기 전에 사용되면 오류를 발생시켜라. If you declare a variable late and the variable is read before it’s assigned a value, an error is thrown.

Using late

late keyword를 사용해서 코드를 수정해라. 재미로 description 코멘트를 달아봐라.

class Meal {
  String _description;

  set description(String desc) {
    _description = 'Meal description: $desc';
  }

  String get description => _description;
}

void main() {
  final myMeal = Meal();
  print(myMeal.description); // => description 값을 대입하기 전에 사용하려고 하면 오류 발생 : Uncaught Error: LateInitializationError: Field '_description' has not been initialized.
  myMeal.description = 'Feijoada!';
  print(myMeal.description);
}
=>
Non-nullable instance field '_description' must be initialized.

class Meal {
  late String _description;

  set description(String desc) {
    _description = 'Meal description: $desc';
  }

  String get description => _description;
}

void main() {
  final myMeal = Meal();
  print(myMeal.description); // => 값 대입 전에 사용하면 오류 발생 : Uncaught Error: LateInitializationError: Field '_description' has not been initialized.
  myMeal.description = '그냥 맛있게 먹자!';
  print(myMeal.description);
}
=>
Meal description: 그냥 맛있게 먹자!

Late circular references

late keyword는 순환 참조와 같은 까다로운 패턴에 유용하다. 다음 코드에는 서로 non-nullable인 참조를 유지해야 하는 두 개의 객체가 있다. late keyword를 사용해서 코드를 수정해라.
The late keyword is helpful for tricky patterns like circular references. The following code has two objects that need to maintain non-nullable references to each other. Try using the late keyword to fix this code.

final을 제거할 필요가 없다. late final 변수를 만들 수 있다: 값을 한 번 설정하면 그 후에는 읽기 전용이 된다.
Note that you don’t need to remove final. You can create late final variables: you set their values once, and after that they’re read-only.

class Team {
  final Coach coach;
}

class Coach {
  final Team team;
}

void main() {
  final myTeam = Team();
  final myCoach = Coach();
  myTeam.coach = myCoach;
  myCoach.team = myTeam;

  print('All done!');
}
=>
The final variable 'coach' must be initialized.
The final variable 'team' must be initialized.
'coach' can't be used as a setter because it's final.
'team' can't be used as a setter because it's final.

class Team {
  late final Coach coach;
}

class Coach {
  late final Team team;
}

void main() {
  final myTeam = Team();
  final myCoach = Coach();
  myTeam.coach = myCoach;
  myCoach.team = myTeam;

  print('All done!');
}
=>
All done!

Late and lazy

late가 도울 수 있는 다른 패턴, 비용이 많이 드는 Non-nullable 필드를 위한 lazy initialization이 있다. (아마도 리소스를 많이 사용하는 로직 등에 유용하다는 의미로 생각된다.)
Here’s another pattern that late can help with: lazy initialization for expensive non-nullable fields. Try this:

  1. 코드를 수정하지 말고 실행해서 결과를 메모해둬라. Run this code without changing it, and note the output.
  2. _cache를 late 필드로 바꾸면 어떻게 될 지 생각해봐라. Think: What will change if you make _cache a late field?
  3. _cache 필드를 late 필드로 바꾼 후 실행해라. 예상이 맞았나? Make _cache a late field, and run the code. Was your prediction correct?

※ 출력 내용이 복잡해서 나는 코드 순서대로 라벨을 달았다.

int _computeValue() {
  print('A. In _computeValue...');
  return 3;
}

class CachedValueProvider {
  final _cache = _computeValue();
  int get value => _cache;
}

void main() {
  print('B. Calling constructor...');
  var provider = CachedValueProvider();
  print('C. Getting value...');
  print('D. The value is ${provider.value}!');
}
=>
B. Calling constructor...  // main의 첫줄이니 당연히 가장 먼저
A. In _computeValue...     // CachedValueProvider() -> _computeValue() 순서로 실행
C. Getting value...        // var provider 생성 완료 후
D. The value is 3!         // 마지막 출력 실행

int _computeValue() {
  print('A. In _computeValue...');
  return 3;
}

class CachedValueProvider {
  late final _cache = _computeValue();
  int get value => _cache;
}

void main() {
  print('B. Calling constructor...');
  var provider = CachedValueProvider();
  print('C. Getting value...');
  print('D. The value is ${provider.value}!');
}
=>
B. Calling constructor...  // main의 첫줄이니 역시 가장 먼저
C. Getting value...        // _cache가 late이니 생성 시점에서 _computeValue()를 실행하지 않으므로 바로 'C'를 실행
A. In _computeValue...     // 'D'에서 provider.value 값을 지정하기 위해 _computeValue()가 실행됨
D. The value is 3!         // _cache 값이 정해진 후 'D' 실행
💡 late and lazy
리소스를 많이 사용하는 함수나 클래스가 있는 경우, 생성 시점(build time)에서 실행시키지 않고 사용하는 시점(run time)에서 실행시켜 리소스를 아낄 수 있다.
eg. 만약 위 코드에서 _computeValue() 함수에서 리소스를 많이 사용하는 경우 late final _cache로 설정하여 실제 사용하는 시점에 실행시키게 할 수 있다

 

🎙 Fun fact
_cache를 late 필드로 설정한 후 _computeValue 함수를 CachedValueProvider 클래스 내부로 이동시켜도 코드는 문제 없이 작동한다. late 필드를 위한 초기화 구문을 초기화 함수에서 인스턴스 함수로 사용할 수 있다.
After you add late to the declaration of _cache, if you move the _computeValue function into the CachedValueProvider class the code still works! Initialization expressions for late fields can use instance methods in their initializers.
class CachedValueProvider {
  late final _cache = _computeValue();
  int get value => _cache;

  int _computeValue() {
  print('A. In _computeValue...');
  return 3;
}
}

void main() {
  print('B. Calling constructor...');
  var provider = CachedValueProvider();
  print('C. Getting value...');
  print('D. The value is ${provider.value}!');
}
=>
B. Calling constructor...
C. Getting value...
A. In _computeValue...
D. The value is 3!

// ⚠️ final을 제거하면 오류 발생
Error compiling to JavaScript:
Info: Compiling with sound null safety
lib/main.dart:2:18:
Error: Can't access 'this' in a field initializer to read '_computeValue'.
  final _cache = _computeValue();
                 ^^^^^^^^^^^^^
Error: Compilation failed.

REFERENCE
Sound null safety
Understanding null safety

반응형

'DEV > Dart' 카테고리의 다른 글

Dart Async programming - Stream  (0) 2022.06.24
Dart Async programing - Future, await  (0) 2022.06.23
Dart의 형변환 - List, Map, Set  (0) 2022.06.22
Dart의 함수를 알아보자  (0) 2022.06.22
Dart 시작하기 - 구조 및 변수  (0) 2022.06.21