본문 바로가기
C#

[C#] 널(NULL) 관련 형식[Nullable<T>] 및 연산자[??, ?.]

by RucA 2024. 4. 24.
728x90
반응형

개인적인 경험으로는 프로그램을 할 때 오타를 제외한 대부분의 오류는 null 체크를 제대로 하지 않는 경우에 발생하는 것 같다. C#에서는 null을 다루는 형식과 연산자가 따로 있다고 해, null의 개념과 관련 내용을 정리해보겠다.

 

 

널(null) 값


null은 의미 있는 값이 없음을 의미한다.

단, 아무것도 없는 "Empty"의 개념보다 알 수 없는 "undefined"의 개념에 더 가깝다.

 

  • null을 주로 접하게 되는 경우
    • 참조형 변수를 선언한 후 초기화하지 않은 경우
    • 알려지지 않는 값을 가리키고 있어 의미 없는 경우
    • 변수 또는 개체가 생성된 후 아무런 참조를 하지 않은 경우
    • 가리키고(참조하고) 있던 데이터가 소실되어 가리키는 데이터가 의미 없어진 경우 

즉, 주로 무엇인가를 참조하는 상황에서 참조하는 것이 없거나 의도치 않는 참조인 경우 null 값을 가진다고 볼 수 있다.

 

 

Nullable<T> 형식 : null 가능 형식


기본적으로 원래 참조 형식인 string과 object를 제외한 값 형식인 int, float 등은 null값을 저장할 수 없다. 그러나 이 형식을 이용한다면 null값을 가지도록 할 수 있다.

 

  • bool : true, false
  • Nullable<bool> : true, false, null

 

Nullable<T> 형식을 줄여서 표현하는 방법이 존재한다. 아래의 두 표현은 같은 표현이다.

//Nullable<T> 줄임 표현 : <타입>?
Nullable<int> nullInt1;
int? nullInt2;

 

 

Nullable<T> 형식이 가지는 속성은 다음과 같다.

 

  • HasValue : 값이 있으면 true, 없으면(null이면) false
  • Value : 값을 반환
  • GetValueOrDefault : 값 또는 기본값을 반환 
class NullablePractice
{
    static void Main()
    {
        //Nullable<bool> 형식 사용하는 2가지 방법
        Nullable<bool> bin1 = null;
        bool? bin2 = false;
        
        //bin1은 null이므로 false
        Console.WriteLine(bin1.HasValue);
        //bin2는 값(true)이 있으므로 true
        Console.WriteLine(bin2.HasValue);
    }
}
//출력 : false true

 

 

?? 연산자 : null 병합 연산자(null coalescing operator)


기존에 ?를 쓰는 연산자인 삼항 연산자와 구조가 몹시 유사하다. 값이 null이면 값 대신 설정한 값을 전달해준다.

 

  • ??연산자 형식 : (타겟 변수) = (저장할 변수) ?? (null일 시 저장할 값);
class NullablePractice
{
    static void Main()
    {
        string nullStr = null;
        
        //??연산자를 사용
        string msg = nullStr ?? "null이면 이 문장을 저장";
        
        //nullStr이 null이므로 오른쪽 문장을 저장해 출력
        Console.WriteLine(msg);
    }
}
//출력 : null이면 이 문장을 저장

 

  • ?연산자(삼항 연산자) 또는 if문을 활용해 비슷하게 구현할 수 있다.
class NullablePractice
{
    static void Main()
    {
        string nullStr = null;
        
        //??연산자를 사용
        string msg1 = nullStr ?? "null이면 이 문장을 저장";
        
        //? 연산자를 사용해 ??연산자 구현
        string msg2 = (nullStr == null) ? "?연산자로 구현" : nullStr;
        
        //if문을 통해 ??연산자 구현
        string msg3 = nullStr;
        if (nullStr == null)
        {
            msg3 = "if문으로 구현";
        }
        
        //nullStr이 null이므로 대체값을 출력
        Console.WriteLine(msg1);
        Console.WriteLine(msg2);
        Console.WriteLine(msg3);
    }
}
//출력 : null이면 이 문장을 저장  ?연산자로 구현  if문으로 구현

 

??연산자를 활용해 null인 값을 원하는 값으로 대체할 수 있음을 확인했다. 하지만 변수가 많아진다면 대체값을 하나씩 설정하는 것이 번거로워진다. 이때 default라는 키워드를 사용하는 것으로 작업을 단순화할 수 있다.

 

  • ??연산자 + default 키워드
class NullablePractice
{
    static void Main()
    {
        int? nullInt = null;
        //default(T)는 자동적으로 저장되는 변수의 타입에 맞춰진다.
        int i1 = nullInt ?? default(int);
        int i2 = nullInt ?? default;

        //int의 기본값은 0
        Console.WriteLine(i1);
        Console.WriteLine(i2);
    }
}
//출력 : 0 0

 

  • ??연산자 + Nullable<T> 형식
    • Nullable<T> 형식과 ??연산자를 활용해 알 수 없는 값에 대해서도 제어를 할 수 있다.
class NullablePractice
{
    static void Main()
    {
        bool? unknown = null;
        
        //true와 null만 통과하도록 if문 조건 설정
        if (unknown ?? true)
        {
            Console.WriteLine("통과");
        }
    }
}
//출력 : 통과

 

 

?.연산자 : null 조건부 연산자(null conditional operator)


Nullabe<T> 형식의 변수가 null인지 확인해서 null이면 null을, 아니면 메서드를 실행한다. 따로 명시적으로 선언을 하지 않더라도 string, object와 같이 null이 가능한 변수나 개체는 사용 가능하다.

 

  • ?.연산자 형식 : (Nullable<T> 형식의 변수)?.(메서드);
class NullablePractice
{
    static void Main()
    {
        double? d1 = null;
        double? d2 = 3.141592;
        
        //변수가 null이면 null 반환, 아니면 뒤의 메서드 정상 실행
        Console.WriteLine(d1?.ToString());
        Console.WriteLine(d2?.ToString());
    }
}
//출력 : null(화면에는 표시되지 않음) 3.141592

 

  • ?.연산자는 명시적으로 선언하지 않은 변수(string)에서도 사용 가능
class NullablePractice
{
    static void Main()
    {
        string diary1 = null;
        string diary2 = "HappyHappy";
        
        Console.WriteLine(diary1?.Length);
        Console.WriteLine(diary2?.Length);
    }
}
//출력 : null(화면에는 표시되지 않음) 10

 

  • ?.연산자는 ?[ ]형태로도 사용 가능
class NullablePractice
{
    static void Main()
    {
        int?[] arr = {111, null, 999};

        Console.WriteLine(arr?[0]);
        Console.WriteLine(arr?[1]);
        Console.WriteLine(arr?[2]);
    }
}
//출력 : 111 null(화면에는 표시되지 않음) 999

 

  • ?.연산자 + 컬렉션(제네럴) 클래스
using System.Collections.Generic;

class NullablePractice
{
    static void Main()
    {
        List<string> list = null;

        //list가 null이므로 null 반환
        Console.WriteLine(list?.Count);

        list = new List<string>();
        list.Add("item1");
        list.Add("item2");
        list.Add("item3");
        
        //list가 null이 아니므로 Count 실행
        Console.WriteLine(list?.Count); 
    }
}
//출력 : null(화면에는 표시되지 않음) 3

 

  • ?.연산자 + ??연산자
    • 두 연산자를 함께 사용하는 것으로 좀 더 범용적이고 안전한 코드가 완성되었다.
using System.Collections.Generic;

class NullablePractice
{
    static void Main()
    {
        List<string> list = null;

        //list가 null이므로 null 반환 -> ?? 연산자에 의해 0으로 대체
        Console.WriteLine(list?.Count ?? 0);

        list = new List<string>();
        list.Add("item1");
        list.Add("item2");
        list.Add("item3");
        
        //list가 null이 아니므로 Count 실행
        Console.WriteLine(list?.Count ?? 0); 
    }
}
//출력 : 0 3

 

null 값은 제대로 체크하지 않으면 오류가 많이 발생하고, 나중에 찾으려하면 찾기도 어려우니 평소에 null 관련 형식과 연산자를 이용해 이중 체크, 안전 장치를 설치해야겠다.

728x90
반응형