Null safety 뿌시기

소밍·2023년 8월 22일
0
post-thumbnail

해당 글은 다트 공식 문서의 Null safety를 읽으며 정리한 내용입니다.

1. Nullable and non-nullable types

1-1. Nullable하게 하고 싶으면 (?)

  • 일반적인 변수 선언은 non-nullable, 즉 null 값을 가질 수 없는 변수로 간주한다.
  • Flutter가 사용하는 Dart언어의 경우 null-safety를 지원하기 때문에
    변수를 명시적으로 nullable하게 만들 수 있다.
  • null값을 허용하고 싶다면 변수 선언시 타입에 ? 를 추가한다.
void main() {
  int a;
  a = null; 
  print('a is $a.'); // error
}

int값만 가지는 a 변수를 선언했으나 a에 null값이 할당되었으므로 아래와 같은 에러가 발생한다.

Error: The value 'null' can't be assigned to a variable of type 'int' because 'int' is not nullable.

a변수가 nullable하도록 수정한다.

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

1-2. 제네릭 타입 매개변수를 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.');
}

위 코드 실행시 발생하는 에러 메세지는 아래와 같다.

Error: The value 'null' can't be assigned to a variable of type 'String' because 'String' is not nullable.
  List<String> aListOfNullableStrings = ['one', null, 'three'];
                                                ^
lib/main.dart:7:37:
Error: Non-nullable variable 'aNullableListOfStrings' must be assigned before it can be used.
  print('aNullableListOfStrings is $aNullableListOfStrings.');
                                    ^^^^^^^^^^^^^^^^^^^^^^

String 타입을 갖는 List라고 선언했지만 현재 aNullableListOfStrings은 값을 할당하지 않은 null 타입, 그러므로 List 자체가 nullable할 수 있도록 수정한다.
aListOfNullableStrings의 경우 배열의 값으로 String과 null을 갖기 때문에 배열의 값이 nullanble할 수 있도록 수정한다.

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

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

2. null 단언 연산자 (!)

  • null 값이 들어오지 않는다고 확신할 경우 사용한다.
  • 즉, 해당 변수 또는 표현식이 null이 아님을 개발자가 단언하는 것
int? couldReturnNullButDoesnt() => -3;

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

  int a = couldBeNullButIsnt;
  int b = listThatCouldHoldNulls.first;
  int c = couldReturnNullButDoesnt().abs(); 

  print('a is $a.');
  print('b is $b.');
  print('c is $c.');
}

위 코드 실행시 발생하는 에러 메세지는 아래와 같다.

Error: A value of type 'int?' can't be assigned to a variable of type 'int' because 'int?' is nullable and 'int' isn't.
  int b = listThatCouldHoldNulls.first; // first item in the list
                                 ^
Error: Method 'abs' cannot be called on 'int?' because it is potentially null.
  int c = couldReturnNullButDoesnt().abs(); // absolute value
                                     ^^^

listThatCouldHoldNulls 리스트의 첫번째 아이템은 null이 아닌 int값이다.
couldReturnNullButDoesnt는 null이 아닌 int값인 -3을 리턴한다.
때문에 다음과 같이 코드를 수정한다.

int? couldReturnNullButDoesnt() => -3;

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

  int a = couldBeNullButIsnt;
  int b = listThatCouldHoldNulls.first!; 
  int c = couldReturnNullButDoesnt()!.abs(); 

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

3. Null-aware 연산자

다음은 변수나 표현식이 null인 경우에도 안전하게 동작하도록 도와주는 연산자들이다.

3-1. 조건부 프로퍼티 액세스

- conditional member access operator (?.)

int? stringLength(String? nullableString) {
  return nullableString.length;
}

위 코드 실행시 발생하는 에러 메세지는 아래와 같다.

Error: Property 'length' cannot be accessed on 'String?' because it is potentially null.
  return nullableString.length;
                        ^^^^^^

stringLength 함수의 인자로 들어오는 nullableString의 경우 nullable하고, 리턴값 역시 int? 로 nullable 하다.
nullableString이 null인 경우 null을 반환하고, null이 아닌 경우에만 length를 접근할 수 있도록 코드를 아래와 같이 수정한다.

int? stringLength(String? nullableString) {
  return nullableString?.length;
}

3-2. Null 결합 연산자

- null-coalescing operator (??)

// nullableString이 null인 경우 'alternate' 문자열 출력
print(nullableString ?? 'alternate'); 
print(nullableString != null ? nullableString : 'alternate');

- null-coalescing assignment operator (??=)

// nullableString이 값을 가지고 있다면 기존 값 그대로, null인 경우에만 'alternate' 할당
nullableString ??= 'alternate';
nullableString = nullableString != null ? nullableString : 'alternate';
abstract class Store {
  int? storedNullableValue;

  /// [storedNullableValue]가 현재 'null'인 경우,
  /// [calculateValue] 결과로 설정합니다
  /// 또는 [calculateValue]가 'null'을 반환하는 경우 '0'입니다.
  void updateStoredValue() {
    TODO('Implement following documentation comment');
  }

  /// 사용할 값을 계산합니다,
  /// 잠재적으로 'null'입니다.
  int? calculateValue();
}
abstract class Store {
  int? storedNullableValue;

  void updateStoredValue() {
    storedNullableValue ??= calculateValue() ?? 0;
  }

  int? calculateValue();
}

4. 타입 프로모션

4-1. 타입 확정해서 할당하기

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); 
}

4-2. Null 체크

int getLength(String? str) {
  // if문으로 null 체크, str이 null이면 0 반환
  if(str == null){
    return 0;
  }
  return str.length;
}

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

4-3. 예외를 던지는 null 체크

int getLength(String? str) {
  // 'str'이 null인 경우 예외 던지기
  if (str == null) {
    throw ArgumentError('Input string cannot be null');
  }
  return str.length;
}

void main() {
  print(getLength(null)); 
  // Uncaught Error: Invalid argument(s): Input string cannot be null
}

5. late 키워드

가끔 변수는 non-nullable해야 하지만 즉시 값을 할당할 수 없는 경우가 있다. 이러한 경우 late 키워드를 사용한다.
즉, late는 현재는 값을 설정할 수 없지만 null이 되지 않게 나중에 설정하겠다는 것을 flutter에게 알려주는 키워드.
late 키워드는 값의 초기화를 뒤로 미루지만, 개발자가 null을 실수로 사용하는것을 막아준다.
해당 변수의 값은 위젯이 초기화될 때인 initState 에서 값 설정하는 것이 바람직하다.

late 키워드를 사용한 변수

  • 개발자가 아직 이 변수에 값을 할당하고 싶지 않다는 것.
  • 변수의 값을 나중에 받을 것.
  • 변수가 사용되기 전에 값이 할당될 것.
  • late 변수를 선언하고 값이 할당되기 전 해당 변수를 읽으면 에러 발생.

5-1. late

class Meal {
  String _description;

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

  String get description => _description;
}

void main() {
  final myMeal = Meal();
  myMeal.description = 'Feijoada!';
  print(myMeal.description);
}

위 코드 실행시 발생하는 에러 메세지는 아래와 같다.

Error: Field '_description' should be initialized because its type 'String' doesn't allow null.
  String _description;
         ^^^^^^^^^^^^

non-nullable한 String으로 선언된 _description 필드가 초기화되지 않아 발생한 에러. 위 코드에서 _description는 선언 시점에 초기화되지 않고 set 메서드를 통해 값을 할당할 때 초기화 되고 있음. 이 때문에 _description 필드가 초기화되지 않았다는 에러 발생. 이를 해결하기 위해 값 할당 전 필드를 초기화 해야한다. 하지만 late 키워드를 사용하면 필드를 선언할 때 초기화하지 않고, 값을 할당할 때까지 초기화를 늦출 수 있으므로 코드를 아래와 같이 수정한다.

class Meal {
  late String _description;

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

  String get description => _description;
}

void main() {
  final myMeal = Meal();
  myMeal.description = 'Feijoada!';
  print(myMeal.description); // Meal description: Feijoada!
}

5-2. late와 순환참조

late final 변수

한 번 값을 설정하면 그 값은 읽기 전용으로 유지된다.

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!');
}

위 코드 실행시 발생하는 에러 메세지는 아래와 같다.

Error: The setter 'coach' isn't defined for the class 'Team'.
 - 'Team' is from 'package:dartpad_sample/main.dart' ('lib/main.dart').
  myTeam.coach = myCoach;
         ^^^^^
Error: The setter 'team' isn't defined for the class 'Coach'.
 - 'Coach' is from 'package:dartpad_sample/main.dart' ('lib/main.dart').
  myCoach.team = myTeam;
          ^^^^
Error: Final field 'coach' is not initialized.
  final Coach coach;
              ^^^^^
Error: Final field 'team' is not initialized.
  final Team team;
             ^^^^

main 함수 내에서 Team, Coach 클래스의 인스턴스인 myTeam, myCoach 생성.
myTeam 인스턴스의 coach 필드에 myCoach 인스턴스를 할당하고
myCoach 인스턴스의 team 필드에 myTeam 인스턴스를 할당하고 있는 순환참조 관계 설정.

하지만,
Team 클래스의 coach 필드는 final로 선언되어 있어 값 변경 불가.
Coach 클래스의 team 필드는 final로 선언되어 있어 값 변경 불가.
Team 클래스의 coach 필드를 final로 선언했지만 초기화하지 않았다는 오류.
Coach 클래스의 team 필드를 final로 선언했지만 초기화하지 않았다는 오류.

즉, 순환 참조 관계를 설정하려면 클래스의 필드 값을 선언시 초기화해주는 것이 아니라 생성자에서 초기화해야하는데
현재 두 클래스 모두 필드를 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!
}

5-3. late와 지연 초기화

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

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

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

위 코드는 _cache이 final로 선언되어 있어 선언 시점에 _computeValue() 함수가 호출되고 초기화된다.
(_cache에 값 3이 할당)

  • print('Calling constructor...');
  • var provider = CachedValueProvider();
    - CachedValueProvider 클래스의 인스턴스 provider를 생성할 때 해당 클래스 내부에 있던
    print('In _computeValue...'); 값이 출력된다.
  • print('Getting value...');
  • print('The value is ${provider.value}!');
    - 이후 provider.value를 호출할 때 초기화된 값인 3을 출력한다.
int _computeValue() {
  print('In _computeValue...');
  return 3;
}

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

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

두 번째 코드에서 _cache는 late final로 선언되어 있으므로 초기화가 지연된다.
때문에 CachedValueProvider 클래스의 인스턴스 provider를 생성할 때 _computeValue() 함수가 호출되지 않고, _cache에 처음 접근될 때 호출된다.

  • print('Calling constructor...');
  • var provider = CachedValueProvider();
    CachedValueProvider 클래스의 인스턴스 provider를 생성할 때 _computeValue() 함수 호출되지 않음.
  • print('Getting value...');
  • print('The value is ${provider.value}!');
    - provider.value를 호출할 때 _cache에 처음으로 접근, 이 때 _computeValue()함수가 호출,
    In _computeValue... 출력
    계산된 결과값이 _cache에 저장되며 The value is 3!출력

결과적으로 두 코드 모두 _computeValue() 함수를 호출하지만 호출되는 시점이 다르기 때문에 다른 결과값을 띄는 것.



참고자료

Dart 공식문서 - Null safety
Flutter 공식 유튜브 - Null safety

profile
생각이 길면 용기는 사라진다.

0개의 댓글

관련 채용 정보