변수

변수 (Variables)

Dart에서 변수를 만들고 초기화하는 가장 기본적인 방법부터, 예상치 못한 문제를 막아 주는 null safety와 late, final/const까지 살펴볼게요.

출처: Dart 공식 문서

본문

변수를 만들고 초기화하는 예시부터 볼게요.

var name = 'Bob';

변수는 참조(reference)를 저장해요. name이라는 변수는 "Bob"이라는 값을 가진 String 객체에 대한 참조를 담고 있어요.

name 변수의 타입은 String으로 추론(infer)되지만, 타입을 직접 지정해서 바꿀 수도 있어요. 객체가 단일 타입으로 제한되지 않는다면 Object 타입을 지정하면 되고, (필요하다면 dynamic도 쓸 수 있어요.)

Object name = 'Bob';

또 다른 방법은 추론될 타입을 명시적으로 선언하는 거예요.

String name = 'Bob';

Null safety

Dart 언어는 사운드 null safety(sound null safety)를 강제해요.

null safety는 null로 설정된 변수를 의도치 않게 접근할 때 생기는 오류를 막아 줘요. 그 오류를 null 역참조 오류(null dereference error)라고 해요. null 역참조 오류는 null로 평가되는 표현식에서 프로퍼티에 접근하거나 메서드를 호출할 때 발생해요. 단, null이 그 프로퍼티나 메서드를 지원하는 경우, 예를 들어 toString()이나 hashCode 같은 경우는 예외예요. null safety를 쓰면 Dart 컴파일러가 이런 잠재적 오류를 컴파일 타임에 감지해요.

예를 들어 int 변수 i의 절댓값을 구하고 싶다고 해 볼게요. inull이라면 i.abs()를 호출할 때 null 역참조 오류가 나요. 다른 언어에서는 이 시도가 런타임 오류로 이어질 수 있는데, Dart 컴파일러는 이런 동작을 금지함으로써 오류를 막아 줘요.

null safety는 세 가지 핵심 변화를 가져와요.

  1. 변수, 매개변수, 또는 다른 관련 구성 요소의 타입을 지정할 때, 그 타입이 null을 허용하는지 제어할 수 있어요. nullable하게 만들려면 타입 선언 끝에 ?를 붙이면 돼요.
String? name  // Nullable type. Can be `null` or string.

String name   // Non-nullable type. Cannot be `null` but can be string.
  1. 변수를 사용하기 전에 반드시 초기화해야 해요. Nullable 변수는 기본값이 null이라 기본적으로 초기화가 이루어져요. Dart는 non-nullable 타입에 초기값을 넣어 주지 않아요. 초기값을 직접 설정하도록 강제하죠. Dart는 초기화되지 않은 변수를 관찰하게 두지 않아요. 이렇게 해서 수신자(receiver) 타입이 null일 수 있는데 null이 그 메서드나 프로퍼티를 지원하지 않는 상황에서 프로퍼티에 접근하거나 메서드를 호출하는 걸 막아 줘요.

  2. nullable 타입을 가진 표현식에서는 프로퍼티에 접근하거나 메서드를 호출할 수 없어요. 단 null이 지원하는 프로퍼티나 메서드, 이를테면 hashCodetoString()은 같은 예외가 적용돼요.

사운드 null safety는 잠재적인 런타임 오류편집 시간(edit-time) 분석 오류로 바꿔 줘요. null safety는 다음 중 하나에 해당하는 non-null 변수에 플래그를 표시해요.

  • non-null 값으로 초기화되지 않음
  • null 값을 할당받음

이 확인 덕분에 앱을 배포하기 전에 오류를 고칠 수 있어요.

기본값 (Default value)

nullable 타입을 가진 초기화되지 않은 변수는 초기값이 null이에요. 숫자 타입의 변수도 처음에는 null이에요. 왜냐하면 숫자도 Dart의 다른 모든 것처럼 객체이기 때문이에요.

int? lineCount;
assert(lineCount == null);

null safety를 쓰면 non-nullable 변수의 값을 사용하기 전에 반드시 초기화해야 해요.

int lineCount = 0;

로컬 변수는 선언한 곳에서 반드시 초기화할 필요는 없지만, 사용하기 전에 값을 할당해야 해요. 예를 들어 아래 코드는 유효해요. lineCountprint()에 전달될 때쯤에는 non-null이라는 것을 Dart가 감지할 수 있기 때문이에요.

int lineCount;

if (weLikeToCount) {
  lineCount = countLines();
} else {
  lineCount = 0;
}

print(lineCount);

최상위(top-level) 변수와 클래스 변수는 지연(lazily) 초기화돼요. 초기화 코드는 변수가 처음 사용되는 시점에 실행돼요.

Late 변수

late 수정자는 두 가지 용도로 쓰여요.

  • 선언 이후에 초기화되는 non-nullable 변수를 선언할 때
  • 변수를 지연 초기화할 때

보통 Dart의 제어 흐름 분석이 non-nullable 변수가 사용되기 전에 non-null 값으로 설정되는 것을 감지할 수 있지만, 가끔 분석이 실패하기도 해요. 흔한 두 경우가 최상위 변수와 인스턴스 변수예요. Dart는 이들이 설정되었는지 종종 판단할 수 없어서 시도조차 하지 않아요.

변수가 사용되기 전에 설정된다는 걸 확신하는데 Dart가 동의하지 않는다면, 그 변수를 late로 표시해 오류를 고칠 수 있어요.

late String description;

void main() {
  description = 'Feijoada!';
  print(description);
}

변수를 late로 표시하면서 선언할 때 초기화하면, 그 initializer는 변수가 처음 사용될 때 실행돼요. 이 지연 초기화는 몇 가지 경우에 유용해요.

  • 변수가 필요하지 않을 수도 있는데 초기화 비용이 클 때
  • 인스턴스 변수를 초기화하는데, 그 initializer가 this에 접근해야 할 때

아래 예시에서 temperature 변수가 전혀 사용되지 않는다면, 비용이 큰 readThermometer() 함수는 호출되지 않아요.

// This is the program's only call to readThermometer().
late String temperature = readThermometer(); // Lazily initialized.

Final과 Const

변수를 바꿀 의도가 없다면 var 대신, 또는 타입에 더해 final이나 const를 쓰면 돼요. final 변수는 단 한 번만 설정할 수 있고, const 변수는 컴파일 타임 상수예요. (const 변수는 암묵적으로 final이에요.)

final 변수를 만들고 설정하는 예시예요.

final name = 'Bob'; // Without a type annotation
final String nickname = 'Bobby';

final 변수의 값은 바꿀 수 없어요.

name = 'Alice'; // Error: a final variable can only be set once.

컴파일 타임 상수로 만들고 싶은 변수에는 const를 쓰면 돼요. const 변수가 클래스 레벨이라면 static const로 표시해요. 변수를 선언한 곳에서 값은 컴파일 타임 상수로 설정해야 하는데, 숫자나 문자열 리터럴, const 변수, 또는 상수 숫자들의 산술 연산 결과 같은 것이 여기에 해당해요.

const bar = 1000000; // Unit of pressure (dynes/cm2)
const double atm = 1.01325 * bar; // Standard atmosphere

const 키워드는 상수 변수를 선언하는 데만 쓰이는 게 아니에요. 상수 (constant values)을 만드는 데도 쓸 수 있고, 상수 값을 생성하는 생성자를 선언하는 데도 쓸 수 있어요. 어떤 변수든 상수 값을 가질 수 있어요.

var foo = const [];
final bar = const [];
const baz = []; // Equivalent to `const []`

const 선언에서는 위 baz처럼 초기화 표현식에서 const를 생략할 수 있어요. 자세한 내용은 DON'T use const redundantly를 참고해요.

non-final, non-const 변수는 이전에 const 값을 가졌더라도 참조를 바꿀 수 있어요.

foo = [1, 2, 3]; // Was const []

const 변수의 값은 바꿀 수 없어요.

baz = [42]; // Error: Constant variables can't be assigned a value.

타입 검사와 캐스트(isas), 컬렉션 if, 스프레드 연산자(......?)를 사용하는 상수도 정의할 수 있어요.

const Object i = 3; // Where i is a const Object with an int value...
const list = [i as int]; // Use a typecast.
const map = {if (i is int) i: 'int'}; // Use is and collection if.
const set = {if (list is List<int>) ...list}; // ...and a spread.

const를 사용해 상수 값을 만드는 방법에 대한 자세한 내용은 Lists, Maps, 그리고 Classes를 참고해요.

와일드카드 변수 (Wildcard variables)

_라는 이름의 와일드카드 변수는 바인딩되지 않는(non-binding) 로컬 변수나 매개변수를 선언해요. 본질적으로 자리 표시자(placeholder)죠. initializer가 있다면 여전히 실행되지만 값에는 접근할 수 없어요. 같은 네임스페이스에 _라는 이름의 선언이 여러 개 있어도 충돌 오류가 나지 않아요.

라이브러리 프라이버시에 영향을 줄 수 있는 최상위 선언이나 멤버에는 와일드카드 변수를 유효하게 쓸 수 없어요. 블록 유효 범위에 국한된 선언, 예를 들어 아래와 같은 경우에는 와일드카드를 선언할 수 있어요.

로컬 변수 선언

main() {
  var _ = 1;
  int _ = 2;
}

for 루프 변수 선언

for (var _ in list) {}

catch 절 매개변수

try {
  throw '!';
} catch (_) {
  print('oops');
}

제네릭 타입과 함수 타입 매개변수

class T<_> {}
void genericFunction<_>() {}

takeGenericCallback(<_>() => true);

함수 매개변수

Foo(_, this._, super._, void _()) {}

list.where((_) => true);

void f(void g(int _, bool _)) {}

typedef T = void Function(String _, String _);

더 알아보기 (Learn more)