Null 안전성 (Null Safety)

ssh·2023년 12월 5일
0

dart

목록 보기
5/22

개요

  • null safety는 개발자가 null 값을 참조할 수 없도록 하는 것이다.
  • String뒤에 ?를 붙여줌으로서 name이 String 또는 null이 될 수 있다고 명시해준 것이다.
  • 기본적으로 모든 변수는 non-nullable(null이 될 수 없음)이다.
  • nullable변수와 일반 변수는 대입이 되지 않는다.
  • 다트에서 ?와 ?가 없는 변수는 다른 타입이다.
  • ?
    • nullable
    • 이 변수는 아직 Null 값일 수 있다고 선언한다.
  • !
    • non-nullable
    • 이 변수는 이제 Null 값일 리 없다고 사용한다.
  • 2.12.0 버전부터 지원
    • 다트는 변수에 값을 초기화하여 타입을 추론 결정하는 방식을 권장한다.
    • 하지만 필요에 따라 변수에 값을 초기화하지 않고 선언 먼저 하고 변수를 나중에 사용해야 하는 경우가 있다.
    • 이 때 실수로 초기화하지 않은 변수를 사용하는 논리적인 오류를 최소화하기 위하여 컴파일러 수준에서 에러를 발생시킨다.
    • 기본값 초기화 없이 선언하는 변수는 프로그래머가 다음 사항을 인지하고 프로그램에 알려줘야 한다.

nullable 연산자

  • null 여부 확인을 위해 사용한다.

? 연산자 (Null-aware 연산자)

  • Dart에서 null이 될 수 있는 변수 또는 표현식을 다룰 때 사용된다.
  • ?.은 null이 아닌 경우에만 속성이나 메서드에 접근합니다. 만약 변수가 null이라면 연산이 중단되고 결과는 null이 된다.
    • 코드
      String? name;
      int length = name?.length ?? 0;
      위 코드에서 namenull이라면 length는 0이 된다.

??

  • ?? 연산자는 null이 아닌 경우에는 왼쪽 피연산자를 반환하고, null인 경우에는 오른쪽 피연산자를 반환한다.

    • 코드
      String? name;
      String displayName = name ?? 'Guest';
      위 코드에서 namenull이라면 displayName은 'Guest'가 된다.
  • ?? 연산자를 사용할 때, 왼쪽 피연산자가 null이 아닌 경우에만 오른쪽 피연산자는 계산되지 않는다.

    • 즉, 오른쪽 피연산자는 lazy evaluation이 적용된다.
  • ?? 연산자를 이용하면 왼쪽 값이 null인지 체크해서 null이 아니면 왼쪽 값을 리턴하고 null이면 오른쪽 값을 리턴한다.

  • ??(QQ)의 뜻은 만약 왼쪽의 있는 값이 Null이라면 오른쪽 값을 return 시킨다는 것이다.

  • 쉽게 말해서 위 코드를 보면 name.toUpperCase()를 리턴할 것이다.

  • 그런데 만약 name이 null이여서 toUpperCase()를 실행할 수 없다면 "NONE"을 리턴하는 것이다.

  • name에 ?(옵셔널)을 붙인 이유는 name이 null일 수도 있기 때문이다.

  • ?? 연산자는 flutter에서 자주 사용된다.

    • 코드
      void main() {
        // null
        double? number = 4.0;
      
        print(number);
      
        number = 2.0;
      
        print(number);
      
        number = null;
      
        print(number);
      
        number ?? = 3.0; // number가 Null이라면 오른쪽 값으로 바꿔라, Null이 아니라면 바꾸지 않는다
      
        print(number);
      }
    • 좌, 우 피연산자 중에서 Null이 아닌 것을 선택한다.
      • 코드
          const x = null;
          const y = 1;
          const z = 2;
        
          print(x ?? y); // 1, 우측 y값 선택
          print(y ?? x); // 1, 좌측 y값 선택
          print(y ?? z); // 1, 둘다 null이 아니지만 좌측 y값 선택
        
  • 플러터에서 이용 사례

    _image ?? const Text('No Image')

??=

  • ??= 연산자를 이용하면 변수 안에 값이 null일 때를 체크해서 값을 할당해줄 수 있다.
    • 코드
        const x = null;
        const y = 1;
      
        var t;
      
        t ??= x;  // t가 null이면 x를 대입하는데 x도 null이다.
        print(t);  // null
      
        t ??= y;  // t가 null이면 y를 대입한다.
        print(t);  // 1
      
        t ??= x;  // t가 null이면 x를 대입하는데 null이 아니므로 x를 대입하지 않는다.
        print(t);  // 1
      

?.

x객체가 null이 아닐 때에만 속성과 메소드에 접근한다.

x?.foo();
x?.bar

...?

const a = [
    ...[1, 2],
    null,
  ];

위 결과는 [1, 2, null]이며 끝에 null이 아닌 [1, 2]가 필요하다면 아래와 같이 작성한다.

var list = [
  ...[1, 2],
  ...?newList,
];

하지만 아래 경우에는 null이 아닌 원소만 추가할 때는 if 구문으로 작성할 수 밖에 없다.

var list = [
  1,
  2,
  if (elem() != null) elem(),
];

! non-null 단언 연산자(non-null assertion operator)

  • 해당 표현식이 런타임 시점에서 null일 가능성이 없다고 확신하는 경우 사용한다.
  • !를 남발하면서 사용하는 것은 프로그램의 안정성을 저해할 수 있다.
  • 항상 상황에 맞게 사용하고, 특히 사용자 입력과 같이 예측하기 어려운 상황에서는 예외 처리를 신중하게 고려해야 한다.
  • 사용자가 예상치 못한 입력을 할 경우, 이러한 단언 연산자를 사용하면 프로그램이 예기치 않게 종료될 수 있다.

Null 해결 방법

1. 변수는 무조건 초기화하여 선언한다.

  • 변수는 무조건 초기화하여 사용하는 것이다.
  • 하지만 생성자, 함수 매개변수나 클래스 인스턴스 변수 중에는 어쩔 수 없이 초기화 없이 선언해야 하는 경우가 발생한다.

2. Null 값이 들어 갈 수 있다고 변수를 선언한다.

  • 변수를 선언할 때 초기화하지 않아도 컴파일 에러가 발생하지 않도록 Null 허용 변수를 선언하는 방법이 있다.
  • 일반 타입 뒤에 명시적으로 ?를 붙여 주면 해당 변수는 해당 타입 또는 null이 될 수 있다.
    String? name;
    위와 같이 선언하면 초기화하지 않아도 에러가 발생하지 않는다.

3. 절대 Null 값일 수가 없다고 알려주며 변수를 사용한다.

  • 프로그래머는 ?를 붙여 name 변수에 이제 Null 값을 넣어도 좋다고 컴파일러한테 허락을 받았다.
  • 하지만 매번 프로그래머는 Null 여부 확인하는 코드를 작성해서 Null 값일 리가 없다고 직접 컴파일러에게 알려줘야 한다.
  • 아래 예시는 변수 뒤에 !를 붙여 컴파일러에게 Null이 아님을 주장하고 변수를 사용했다.
    int? age = 1;
    age = age! + 1;
  • 코드에서 변수 뒤에 !를 붙여 명시적으로 해당 변수는 Null이 아님을 표시한다.
  • 프로그래머가 매번 해당 변수가 사용할 때마다 Null이 아님을 확인하고 증명할 수 있도록 다트 언어에서 의도적인 불편함을 강제하는 것이다.

required 키워드

  • 클래스의 변수를 선언할 때 초기화하지 않으면서 ?를 붙이지 않고 선언하는 방법이 있다.
  • 생성자의 매개변수에 required를 붙여서 인스턴스 생성할 때 매개변수로 값을 넘겨 받는다고 약속해주는 것이다.
    • 코드
      class Person {
        String name; // ? 없이 초기화 하지 않고 변수 선언
        int age;
        String? address;
      
        Person({required this.name, required this.age, this.address});
      }
      

late 키워드

  • 초기 데이터 없이 먼저 변수를 생성하고 추후에 데이터를 넣을 때 주로 사용한다.
  • required 외에 추가로 late 키워드가 있다.
  • 클래스에 속성이 반드시 필요하지만 당장은 초기화하지 않고 나중에 값을 할당을 약속한다는 의미로 붙여준다.
  • 오히려 이 약속으로 인해 이 late 변수는 널 확인 검사를 할 수 없다.
  • flutter로 data fecthing을 할 때 유용하다.
  • late 변수를 만들고, API에 요청을 보낸 뒤에 API에서 값을 보내주면 그 응답값을 late변수에 넣어 사용할 수 있다.
    • 코드1
      void main() {
        
        late final String name;
        // do something, go to api
        name = 'shin';
        print(name);
        // data fetching할때 유용
      
      }
    • 코드2
      class Person {
        late String name;
        late int age;
      
        
        void initState() {
          super.initState();
          name = 'pincoin';  // late 변수는 반드시 `initState()` 메소드 안에서 초기화해준다.
          age = 40;  // 초기화
        }
      }
      
  • late 변수는 initState() 메소드에서 초기화하고 사용할 때는 null 확인 검사를 하지 않는다.
  • 프로그래머가 나중에 직접 초기화하기로 약속했기 때문에 컴파일러는 오히려 late 변수를 널인지 확인한다고 하면 오류를 낸다.
  • 프로그래머는 언제나 late 변수는 절대 null이 아님을 논리적인 코드를 통해 보장해야 한다.
  • 단순 널 허용(nullable) 변수이면서 널 여부 확인이 필요하다면 late String name;이 아니라 
    String? name;으로 선언한다.

주의점

  • ! < late < ? 순으로 사용
    • 가급적 사용하지 않는 편이 좋다

0개의 댓글

관련 채용 정보