본문 바로가기

개발

유니티 C# ? ?? ?. ?[]에 관하여 이것저것

유니티 UI Toolkit 예제를 보는데 데이터 타입에 ?가 너무 많이 붙어 나와서 공부를 하게되었다. 이 데이터 타입? 는 nullable 타입이라고 하는데 당연하게도 null연산과 관련되어 있다.

 

요약

더보기

1. Nullable 타입

  • 값 타입이 null 사용을 허용할 수 있도록 함.
  • 값 타입? 변수명( 예 : int? value )

2. ?? 연산자

  • 피연산자 ?? 피연산자( 예 : value ?? 4 )
  • 왼쪽 피연산자의 값이 null인지 아닌지 가장 먼저 판단
  • 왼쪽 피연산자의 값이 null이면 오른쪽 피연산자의 값을 반환, null이 아니면 왼쪽 피연산자의 값을 반환
  • 예)에서 value가 null이면 4를 반환, null이 아니면 value 값을 반환

3. ?. 연산자

  • 피연산자?.멤버( 예 : str?.Length )
  • 왼쪽 피연산자의 값이 null인지 아닌지 가장 먼저 판단
  • 왼쪽 피연산자의 값이 null이면 null값을 반환, null이 아니면 ?.연산자 뒤 표현식을 실행
  • 예)에서 str이 null일 경우 null을, null이 아니면 str.Length를 실행

4. ?[ ] 연산자

  • ?.연산자와 동일, 다만 피연산자가 배열과 같은 인덱스를 이용한 참조에 사용
  • 예 : int? num = nums?[0];
  • 예)에서 int[ ] nums 혹은 nums[0]가 null일 경우 null값 반환, 아니면 nums[0] 값 반환.

※ ??연산자, ?. 연산자, ?[ ] 연산자의 피연산자는 반드시 null을 허용해야한다. 경우에 따라서는 리턴 변수역시 null을 허용해야한다.

 

1. Nullable 타입

코딩을 하다보면 변수를 선언해서 사용해야할 때가 있는데, 아래는 값 타입(원시타입)의 변수를 선언한 것이다.

 

struct type의 변수

값 타입인지 확인하기 위해 int, double 등의 데이터 타입에 마우스를 갖다대면 위 사진과 같이 struct System.~ 인 것을 볼 수 있다. 값 타입은 위의 사진에서 선언한 것 말고도 여러 종류가 있으니 찾아보면 된다.

변수를 선언하고 출력하면 사진과 같이 그 값이 제대로 출력된다.

 

그런데 여기서 값 타입의 변수를 null로 초기화를 하려고하면 어떻게 될까?

 

struct type 변수에 null값으로 초기화하려고 했을 때

바로 컴파일 에러가 난다. 초기화 뿐만이 아니라 null과의 연산 자체가 불가능하다. 

그러나 이러한 값 타입의 변수들을 가지고 여러 이유로 null 값을 활용해야할 때가 있다. 이를 위해 null 연산이 가능한 타입을 지원하고 있으며 구조체 System.Nullable<T>가 정의되어 있다.  

 

값 타입이 null 연산을 하려면 데이터 타입 뒤에 ?를 붙여주면 된다.

[값 타입]? 변수명;

 

위 사진처럼 nullable 타입을 사용하면 컴파일 오류가 나지 않는다.

nullable 타입인 int?는 System.Nullable<int>와 같은 말이다.

nullable에 대한 상세한 내용은 구조체 System.Nullable<T>를 보면 된다.

더보기

구조체 System.Nullable<T>

namespace System
{
     public struct Nullable<T> where T : struct
     {
          public Nullable(T value);

          public bool HasValue { get; }
          public T Value { get; }

          public override bool Equals(object other);
          public override int GetHashCode();
          public T GetValueOrDefault();
          public T GetValueOrDefault(T defaultValue);
          public override string ToString();

          public static implicit operator T?(T value);
          public static explicit operator T(T? value);
     }
}

구조체를 보면 Nullable<T> where T : struct 이므로 T는 struct 타입이어야 한다. 즉, nullable 타입은 값 타입(struct 타입)만 가능하다.

 

 

2. ?? 연산자

구조체 System.Nullable<T>를 보면 GetValueOrDefault(T defaultValue) 함수가 정의되어 있다.

이 함수는 nullable 타입의 변수 값이 null인지를 판단하고, null이면 0을 반환(defaultValue를 지정하지 않는 경우)하거나 혹은 지정한 변수(defaultValue)로 반환하는 함수이다.

 

GetValueOrDefault() 사용한 예

위의 사진처럼 GetValueOrDefault()를 사용하여 변수의 값이 null일 때 원하는 값으로 초기화 할 수 있다.

이를 더 간단하게 할 수 있는데, 바로 ?? 연산자를 사용하는 것이다.

피연산자 ?? 피연산자;

?? 연산자는 왼쪽 피연산자의 값이 null인지 아닌지 확인하고 null이면 오른쪽 피연산자의 값을 반환하고, null이 아니라면 왼쪽 피연산자의 값을 반환한다.

 

GetValueOrDefault() 대신 ?? 연산자 사용

 

사진을 보면 num이 null 값이기 때문에 4가 반환되어 출력되었다.

위를 예로 설명하자면, 다음과 같다.

 

int returnValue = num ?? 4
// 먼저 num의 값이 null인지 판단하고, null이라면 4를 returnValue에 대입
// num의 값이 null이 아니라면, num의 값을 그대로 returnValue에 대입, ?? 연산자 뒤는 무시

 

또한 [num = num ?? 4;]와 같이 자기 자신에게 대입할 수 있는데, 다른 수식 연산자 처럼 [num ??= 4;]로 줄여쓸 수 있다.

 

?? 연산자는 피연산자의 타입이 null을 허용하면 사용 가능하다. 따라서 class 타입, nullable 타입 등에서도 사용할 수 있다. 아래의 예시에서는 class 타입인 string과 ?? 연산자를 사용했다.

 

string 타입에서 ?? 연산자 활용

string tmp = str ?? GetString();
// str 값이 null인지 판단하고, str의 값이 null이면 GetString() 실행, 함수의 반환 값을 tmp에 대입. 
// str의 값이 null이 아니면 str 값을 tmp에 대입. ?? 연산자 뒤는 무시.
// GetString()은 반환 값이 존재하는 함수여야 함.

 

당연한 말이지만, 바로 위의 예제에서 tmp의 데이터 타입과 str 데이터 타입, 그리고 GetString() 함수의 반환 값의 데이터 타입은 동일해야 한다. 그래야 값이 null 인지에 따라 tmp에 누구든 들어갈 수 있기 때문이다.

 

 

 

3. ?. 연산자

?. 연산자는 피연산자가 null인지 아닌지 판단하고 null이라면 null값을 리턴하고, null이 아니라면 .뒤에 지정된 멤버(연산자 오른쪽의 표현식)를 리턴한다.

# 클래스 멤버

더보기

#클래스 멤버

멤버는 클래스에서 데이터와 동작을 나타냄.

멤버는 생성자, 소멸자, 상수, 필드(Field), 메서드(Method), 속성(Property, 프로퍼티), 이벤트(Event), 인덱서, 연산자, 클래스 등으로 구성.

멤버 설명 예시
 
생성자 클래스 초기화 시 실행 Example(...)
소멸자 클래스 종료 시 실행 ~Example(...)
상수 상수 const float pi = 3.14f;
필드 변수, public 메소드 int filed;
메소드 메소드 void Method(){...};
속성 클래스 밖에서는 변수처럼, 클래스 내에서는 메소드처럼 사용 int Property{get; set;}
이벤트 속성처럼 사용할 수 있는 메소드 event EventHandler Event;
인덱서 속성처럼 사용할 수 있는 배열 int this[int i]{...}
연산자 오버로드 된 연산자 static Example operator+(Example x, Example y){...}
클래스 클래스 내의 클래스 class Example2{...}

 

 

https://wikidocs.net/124994

피연산자?.멤버

 

?. 연산자의 사용

예제를 보면, 피연산자의 값이 null이 아닐 경우 멤버의 반환 값을 리턴했지만 null일 경우 null값을 리턴하고 ?. 연산자 뒤는무시하는 것을 알 수 있다.

 

string str1 = "Test"; 
print(str1?.Length);
// str1이 null이 아니므로 str1.Length의 리턴 값인 4를 출력.

string str2 = null;
print(str2?.Length);
// str2가 null이므로 null을 출력. ?. 연산자 뒤는 무시.

 

?. 연산자를 사용하려면 리턴 변수 항상 null 값을 허용하는 타입이어야 한다. 피연산자의 값이 null일 때, 리턴 변수에 null 값을 대입해야하기 때문이다. 만약 리턴 변수가 null 값을 허용하지 않는 타입이라면 어떻게 해야할까? 바로 ?? 연산자를 활용해서 null 반환을 막으면 된다.

 

?.연산자와 ?? 연산자를 같이 활용

string str2 = null;
int num2 = str2?.Length ?? -1;
// str2의 값이 null이므로 str2.Length는 실행되지 않는다.
// str2의 값이 null이므로 ??연산자를 통해 -1값이 num2에 대입된다.

 

피연산자의 값이 null일 경우 ?? 연산자의 오른쪽 피연산자 값이 리턴 변수로 반환되기 때문에 리턴변수는 꼭 null 을 허용하는 타입일 필요가 없게 된다.

 

※ 참고로 피연산자가 UnityEngine.Object인 경우에 null 값을 허용하는 타입이지만 ?. , ?? 연산자를 사용하면 에러가 난다. C#에서는 지원하는 연산자이지만 UnityEngine에서는 지원하지 않기 때문이다. 따라서 유니티에서 사용할 때는 주의가 필요하다.

예를 들어, Rigidbody rb; 이고, rb?.MovePosition(Vector3.one); 인 코드가 있을 때 rb가 null이면 ?.연산자 뒤가 실행되지 않아야 하지만, 게임을 실행하면 오작동 및 에러가 일어난다.

 

4. ?[ ] 연산자

?.연산자와 동일한 기능을 수행하나, 멤버의 접근이 아닌 배열과 같은 인덱스를 이용한 참조에 사용된다.

 

public class Test : MonoBehaviour
{
     int[] nums;
     void Start()
     {
          // nums에 아무 값도 할당되지 않았으므로 에러가 난다.
          int num1 = nums[0]; 
          print(num1);
          
          // nums가 null인지 아닌지 판단하고, null이면 null값을 num2에 대입한다.
          // null이 아니라면 nums[0]의 값을 num2에 대입한다.
          int? num2 = nums?[0];
          print(num2);
     }
}

 

??, ?. 연산자도 그렇지만 ?[ ]연산자 역시 가장먼저 피연산자가 null 인지 아닌지 판단한다. 그리고 그 후에 연산자 뒤의 내용을 실행 할지말지를 결정한다.

?[ ]연산자도 ?? 연산자와 함께 사용하면 코드를 예쁘게 짤 수 있다.

 

참조

더보기

 

'개발' 카테고리의 다른 글

[v7.3.0] GoogleAdmob 보상형 광고 이것저것  (0) 2023.03.15
[v10.14] Gpgs 이것저것 메모  (0) 2023.03.14