해당 글은 다트 공식 문서의 Null safety를 읽으며 정리한 내용입니다.
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
}
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].
}
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.
}
다음은 변수나 표현식이 null인 경우에도 안전하게 동작하도록 도와주는 연산자들이다.
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;
}
// nullableString이 null인 경우 'alternate' 문자열 출력
print(nullableString ?? 'alternate');
print(nullableString != null ? nullableString : 'alternate');
// 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();
}
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);
}
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
}
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
}
가끔 변수는 non-nullable해야 하지만 즉시 값을 할당할 수 없는 경우가 있다. 이러한 경우 late 키워드를 사용한다.
즉, late는 현재는 값을 설정할 수 없지만 null이 되지 않게 나중에 설정하겠다는 것을 flutter에게 알려주는 키워드.
late 키워드는 값의 초기화를 뒤로 미루지만, 개발자가 null을 실수로 사용하는것을 막아준다.
해당 변수의 값은 위젯이 초기화될 때인 initState 에서 값 설정하는 것이 바람직하다.
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!
}
한 번 값을 설정하면 그 값은 읽기 전용으로 유지된다.
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!
}
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();
print('In _computeValue...');
값이 출력된다.print('Getting value...');
print('The value is ${provider.value}!');
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();
print('Getting value...');
print('The value is ${provider.value}!');
In _computeValue...
출력The value is 3!
출력결과적으로 두 코드 모두 _computeValue() 함수를 호출하지만 호출되는 시점이 다르기 때문에 다른 결과값을 띄는 것.