[C#] 55. 값의 초기화 및 기본 데이터 값(default)를 설정하는 방법 그리고 원시 데이터의 null 처리, ?와 ??의 사용법


Study/C#  2021. 10. 21. 17:26

안녕하세요. 명월입니다.


이 글은 C#의 값의 초기화 및 기본 데이터 값(default)를 설정하는 방법 그리고 원시 데이터의 null 처리, ?와 ??의 사용법에 대한 글입니다.


우리가 프로그램을 작성하다 보면 변수의 초기값을 설정해야 하는 경우가 있습니다.

예를 들면 int 타입의 맴버 변수를 생성하면 초기 데이터를 -1로 할 것인가? 0으로 할 것인가에 대한 고민을 할 경우가 있습니다. 물론 -1로 설정할 지, 0으로 설정할 지에 대해서는 사양에 따라 설정을 해야하는 것이 기본입니다만 그래서 int a = 0;식의 설정보다는 조금 더 프로그램 같은 설정이 없을까?

using System;
namespace Example
{
  // 예제 클래스
  class Node
  {
    // 맴버 변수의 data에 int 타입의 기본 데이터
    private int data = default(int);
    // 프로퍼티
    public int Data
    {
      get
      {
        // 맴버 변수 data를 리턴
        return this.data;
      }
    }
  }
  // Program 클래스
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 인스턴스 생성
      var node = new Node();
      // node의 맴버 변수 data를 콘솔 출력
      Console.WriteLine(node.Data);

      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

위 예제를 보면 int형의 data 맴버 변수에 default 키워드를 사용해서 int의 초기 값을 선언했습니다. 콘솔에 출력을 하니 0의 값이 나오네요.

즉, default는 해당 데이터 타입(자료형)의 초기 데이터의 값을 리턴하는 키워드입니다.

원시 데이터의 경우는 초기 int의 경우 0을, float의 경우는 0.0을 입력하게 됩니다.


그럼 여기서 맴버 변수의 값에 default(int)를 빼고 출력하면 어떠한 값이 나올까요?

using System;
namespace Example
{
  // 예제 클래스
  class Node
  {
    // 맴버 변수
    private int data;
    // 프로퍼티
    public int Data
    {
      get
      {
        // 맴버 변수 data를 리턴
        return this.data;
      }
    }
  }
  // Program 클래스 분할
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 인스턴스 생성
      var node = new Node();
      // node의 맴버 변수 data를 콘솔 출력
      Console.WriteLine(node.Data);

      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

똑같이 0이 나옵니다. 그럼 default를 설정할 필요가 없는 것인가? 정답은 필요없습니다.

기본적으로 맴버 변수는 기본 초기 값이 설정이 됩니다. 즉, default(int)를 넣지 않아도 default(int)값이 넣는 것처럼 표시가 됩니다. 그런데 사양에 따라서 int값에 null를 허용하고 싶을 수도 있습니다.

즉, 기본 값이 0이 아닌 null을 말이죠.

using System;
namespace Example
{
  // 예제 클래스
  class Node
  {
    // 원시 데이터에 ?를 넣으면 null을 허용한다.
    private int? data;
    // 프로퍼티
    public int? Data
    {
      get
      {
        // 맴버 변수 data를 리턴
        return this.data;
      }
    }
  }
  // Program 클래스
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 인스턴스 생성
      var node = new Node();
      // node의 맴버 변수 data가 null이라면
      if(node.Data == null)
      {
        // 콘솔 출력
        Console.WriteLine("node.Data is null!");
      }
      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

그렇습니다. 자료형에다가 ?를 넣으면 원시 데이터에 null를 허용하는 것입니다. 그리고 default(int?)는 null입니다. 기본 초기값이 null이 들어갑니다.

원시 데이터가 아닌 클래스의 경우는 어떨까요?

using System;
namespace Example
{
  // 예제 클래스
  class Node
  {
    // string은 원시 데이터가 아니고 클래스이다.
    private string data;
    // 프로퍼티
    public string Data
    {
      get
      {
        // 맴버 변수 data를 리턴
        return this.data;
      }
    }
  }
  // Program 클래스
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 인스턴스 생성
      var node = new Node();
      // node의 맴버 변수 data가 null이라면
      if(node.Data == null)
      {
        // 콘솔 출력
        Console.WriteLine("node.Data is null!");
      }
      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

그렇습니다. class는 기본적으로 null를 허용하기 때문에 ?를 넣지 않아도 기본 default(string)은 null의 값입니다.


그럼 원시 데이터와 class의 차이는 무엇인가 했을때, 기본적으로 C#에서는 원시 데이터를 구조체(struct)로 인식하고 있습니다.

즉, struct는 null를 허용하지 않습니다.

제가 struct으로 구조체를 생성하면 사용법은 기본적으로 class와 비슷하지만 null이 허용되지 않습니다.

struct에서 null를 허용하게 사용하려면 ?를 사용해야 합니다.


여기서 또 궁금한 점이 맴버 변수는 default를 사용하지 않아도 클래스는 null이 입력이 되고 구조체(Struct)는 기본 값이 설정되는 것을 알았는데 default라는 키워드는 의미가 없지 않을까하고 생각되네요.

그러나 default 키워드는 제네릭(Generic)에서 사용됩니다.

link - [C#] 30. 제네릭(Generic) 사용법

제네릭이라는 것은 클래스 내부에서 객체를 설정하는 것이 아니고 외부에서 설정하는 것이라고 했습니다.

using System;

namespace Example
{
  // 제네릭 클래스
  class Node<T>
  {
    // 기본 default 값을 리턴하는 함수
    public T GetDefault()
    {
      return default(T);
    }
  }
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // Node 클래스에 제네릭 데이터를 int를 넣는다.
      var node1 = new Node<int>();
      // 값이 null 아니면
      if (node1.GetDefault() != null)
      {
        // 콘솔 출력
        Console.WriteLine(node1.GetDefault());
      }
      else
      {
        // 콘솔 출력
        Console.WriteLine("node1 is null!");
      }
      // Node 클래스에 제네릭 데이터를 string를 넣는다.
      var node2 = new Node<string>();
      // 값이 null 아니면
      if (node2.GetDefault() != null)
      {
        // 콘솔 출력
        Console.WriteLine(node2.GetDefault());
      }
      else
      {
        // 콘솔 출력
        Console.WriteLine("node2 is null");
      }
      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

즉, 제네릭에서는 클래스가 설정될 지 구조체가 설정될 지, 외부에서 설정하기 때문에 내부의 제네릭(Generic)값은 default로 값을 설정해야 합니다.


추가로 위에서 구조체를 클래스처럼 null를 허용하게 하려면 ?를 사용한다고 설명했습니다.

추가적으로 ?의 용법이 하나 더 있는데, 바로 삼항 다항식의 null 체크입니다.

using System;

namespace Example
{
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // string 타입에 null를 넣는다.
      string node = null;
      // node의 값에 공백을 제거한다.
      Console.WriteLine(node.Trim());
      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

당연히 위처럼 작성하면 100% 에러가 발생합니다. 왜냐하면 node의 변수는 null인데 인스턴스의 함수를 실행하라고 요청을 했습니다.

link - [C#] 10. 인스턴스 생성(new)과 메모리 할당(Stack 메모리와 Heap 메모리) 그리고 null


그렇기 때문에 보통은 if(node != null) { }의 null 체크 분기식을 소스에 넣습니다.

그런데 ?를 사용하면 위의 if(node != null) { }의 의미의 분기식이 포함됩니다.

using System;

namespace Example
{
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // string 타입에 null를 넣는다.
      string node = null;
      // node의 값이 null이 아니면 에 공백을 제거한다.
      Console.WriteLine(node?.Trim());
      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

결과는 node?.Trim()을 하면 사실 공백 없는 string의 값이 아니고 계속 null이 리턴됩니다. Console.WriteLine에서는 null값이 들어오면 공백없는 개행이 추가되네요.

?의 의미는 null이 아니면 실행이라는 것을 알겠는데 null이라면 다른 처리를 하고 싶습니다.

using System;

namespace Example
{
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // string 타입에 null를 넣는다.
      string node = null;
      // node의 값이 null이 아니면 에 공백을 제거한다. null이라면 다른 string 값을 출력하라
      Console.WriteLine(node?.Trim()??"This is null!");
      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

위에서 node?.Trim()은 node가 null이기 때문에 null이라고 이야기했습니다. ??는 node?.Trim()가 null이기 때문에 다른 값으로 치환하게 됩니다. 즉, 만약 node에 값이 있다면 그 값이 출력이 되는 것입니다.

using System;

namespace Example
{
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // string 타입에 문자열을 넣는다.
      string node = "Hello world      ";
      // node의 값이 null이 아니면 에 공백을 제거한다.
      Console.WriteLine(node?.Trim()??"This is null!");
      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

여기까지 C#의 값의 초기화 및 기본 데이터 값(default)를 설정하는 방법 그리고 원시 데이터의 null 처리, ?와 ??의 사용법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.