널일 수 있는 유형을 제네릭 인자로 표현하게 되면 T?
이 될 것 같은데요. T가 참조 유형이면 그 동작이 예상이 되지만 만약 T가 값 유형일 경우 어떻게 동작할까요?
먼저 다음 처럼은 쓸 수는 없습니다.
interface IAddable<T?> // CS1003: 구문 오류
{
T Add(T a, T b);
}
다음처럼 사용할 수 있습니다.
interface IAddable<T>
{
T? Add(T? a, T? b);
}
이제 IAddable<T>
를 구현해서 값과 참조 유형이 어떻게 처리되는지를 살펴봅시다.
class TestClass : IAddable<int>, IAddable<int?>, IAddable<string>
{
public int Add(int a, int b)
{
return a + b;
}
public int? Add(int? a, int? b)
{
if (a is null || b is null)
return null;
return a + b;
}
public string? Add(string? a, string? b)
{
if (a is null || b is null)
return null;
return a + b;
}
}
흥미롭게도(?) int 유형의 경우 T? Add(T? a, T? b)
임에도 불구하고 int Add(int a, int b)
가 되었습니다.
결국에 'T?'의 동작은 [AllowNull] T
의 동작과 동일하며, 제네릭 인자 T가 널인지의 확인은 참조형에만 적용된다는 것을 알 수 있습니다.
- int와 int?는 int와
Nullable<int>
로 해석하므로 다른 유형 - string과 string?은 string과
[AllowNull] string
으로 동일한 유형. 단, 컴파일시 특성을 이용해 경고 처리
| 테스트 코드
using System.Runtime.CompilerServices;
var i = new TestClass();
Print(i.Add(5, 4));
Print(i.Add(5, null));
Print(i.Add("A", "B"));
void Print(object? value, [CallerArgumentExpression("value")] string? argumentExpression = null)
{
System.Console.WriteLine($"{argumentExpression}: {value}");
}
interface IAddable<T>
{
T? Add(T? a, T? b);
}
class TestClass : IAddable<int>, IAddable<int?>, IAddable<string>
{
public int Add(int a, int b)
{
return a + b;
}
public int? Add(int? a, int? b)
{
if (a is null || b is null)
return null;
return a + b;
}
public string? Add(string? a, string? b)
{
if (a is null || b is null)
return null;
return a + b;
}
}
//interface IAddable2<T>
//{
// T Add(T a, T b);
//}
//class TestClass2 : IAddable2<string>, IAddable2<string?> // CS8645: 인터페이스 중복
//{
// public string Add(string a, string b) // CS8767: 구현된 멤버와 일치하지 않음
// {
// throw new NotImplementedException();
// }
// public string? Add(string? a, string? b) // CS0111: 동일한 'Add' 멤버
// {
// throw new NotImplementedException();
// }
//}
| 출력
i.Add(5, 4): 9
i.Add(5, null):
i.Add("A", "B"): AB
참고로 제네릭 인자 유형의 제약 조건은 아래를 참조하세요.