C# 제네릭 타입에서의 수학적 연산
C# 11은 제네릭 타입(Generic Type)들에 대해서 수학 연산자들을 사용할 수 있도록 하였다. C# 11 이전에는 제네릭 타입 T에 대해 덧셈(+), 뺄셈(-), 증감(++, --) 등등의 수학적 연산자들을 직접적으로 사용할 수 없었다. (이러한 이유로 C# 11 이전에는 보통 제네릭 타입 T를 dynmaic 타입으로 변경한 후 수학적 연산자를 사용하기도 하였다)
예를 들어, 임의의 숫자 타입들에(int, double, decimal 등) 대해 덧셈을 한다고 했을 때, C# 11 이전에는 제네릭 타입 T에 대해 직접 + 연산자를 사용할 수 없었다. 아래 [예제A]에서 타입 T에 대해 + 연산자를 사용하면 CS0019 컴파일 에러가 발생하고, 타입 T에 0을 할당하면 CS0029 컴파일 에러가 발생한다.
이러한 문제점을 해결하기 위해, C# 11 이전에는 아래 [예제B]와 같이 숫자 타입을 dynamic 타입으로 일단 변경한 후 연산을 수행하였는데, 이는 타입 T가 수학적 연산을 수행할 것이라는 가정을 전제로 하는 것으로 만약 이러한 가정이 틀린 경우 런타임 에러를 발생할 여지가 있는 솔루션이다.
예를 들어, 임의의 숫자 타입들에(int, double, decimal 등) 대해 덧셈을 한다고 했을 때, C# 11 이전에는 제네릭 타입 T에 대해 직접 + 연산자를 사용할 수 없었다. 아래 [예제A]에서 타입 T에 대해 + 연산자를 사용하면 CS0019 컴파일 에러가 발생하고, 타입 T에 0을 할당하면 CS0029 컴파일 에러가 발생한다.
이러한 문제점을 해결하기 위해, C# 11 이전에는 아래 [예제B]와 같이 숫자 타입을 dynamic 타입으로 일단 변경한 후 연산을 수행하였는데, 이는 타입 T가 수학적 연산을 수행할 것이라는 가정을 전제로 하는 것으로 만약 이러한 가정이 틀린 경우 런타임 에러를 발생할 여지가 있는 솔루션이다.
예제
// (예제A) 제네릭 메서드 안에서 타입 T에 + 연산자를 사용하거나
// 혹은 0 할당을 하는 경우 에러 발생
T Add<T>(T a, T b)
{
// 에러발생: CS0029: Cannot implicitly convert type 'int' to 'T'
T result = 0;
// 에러발생: CS0019: Operator '+' cannot be applied to operands of type 'T' and 'T'
result = a + b;
return result;
}
var res = Add<int>(1, 2);
Console.WriteLine(res);
// (예제B) 타입 T가 숫자 타입이라는 가정 하에 dynamic 타입을 사용
T Add<T>(T a, T b)
{
dynamic da = a;
dynamic db = b;
return da + db;
}
// 혹은 0 할당을 하는 경우 에러 발생
T Add<T>(T a, T b)
{
// 에러발생: CS0029: Cannot implicitly convert type 'int' to 'T'
T result = 0;
// 에러발생: CS0019: Operator '+' cannot be applied to operands of type 'T' and 'T'
result = a + b;
return result;
}
var res = Add<int>(1, 2);
Console.WriteLine(res);
// (예제B) 타입 T가 숫자 타입이라는 가정 하에 dynamic 타입을 사용
T Add<T>(T a, T b)
{
dynamic da = a;
dynamic db = b;
return da + db;
}
C# 11: Generic Math 지원
C# 11에서는 제네릭 타입들에 대해 수학 연산자들을 직접적으로 사용할 수 있게 되었는데, 이를 위해 여러 가지 언어적인 기능 추가들이 있었다.
먼저 System.Numerics 네임스페이스 안에 INumber 인터페이스가 추가되었는데, 이 인터페이스는 사칙연산, 증감연산, 비교연산 등의 수학적 연산을 지원하는 기능을 가지고 있다.
아래 [예제C]에서 보듯이, 제네릭 타입 T에 대해 INumber<T> 라는 Constraint를 지정하면, 타입 T 파라미터에 대해 사칙연산 등과 같은 수학적 연산이 가능하게 된다.
그런데, INumber는 인터페이스인데, 어디에서 수학적 연산을 처리하게 되는 것일까? 이는 INumber (및 INumberBase 인터페이스)를 자세히 보면, 인터페이스 안에서 Default Method들을 사용하고 있는 것을 알 수 있다. C# 8 버전에서부터 Interface 안에 Default Method 구현체를 넣을 수 있게 되었는데, 이를 활용하여 INumber 및 상위 인터페이스에서 Default Method 구현체를 넣에 Generic Math를 지원할 수 있게 되었다.
먼저 System.Numerics 네임스페이스 안에 INumber 인터페이스가 추가되었는데, 이 인터페이스는 사칙연산, 증감연산, 비교연산 등의 수학적 연산을 지원하는 기능을 가지고 있다.
아래 [예제C]에서 보듯이, 제네릭 타입 T에 대해 INumber<T> 라는 Constraint를 지정하면, 타입 T 파라미터에 대해 사칙연산 등과 같은 수학적 연산이 가능하게 된다.
그런데, INumber는 인터페이스인데, 어디에서 수학적 연산을 처리하게 되는 것일까? 이는 INumber (및 INumberBase 인터페이스)를 자세히 보면, 인터페이스 안에서 Default Method들을 사용하고 있는 것을 알 수 있다. C# 8 버전에서부터 Interface 안에 Default Method 구현체를 넣을 수 있게 되었는데, 이를 활용하여 INumber 및 상위 인터페이스에서 Default Method 구현체를 넣에 Generic Math를 지원할 수 있게 되었다.
예제
// (예제C)
using System.Numerics; // INumber 네임스페이스
T Add<T>(T a, T b) where T : INumber<T> // Generic Constraint 지정
{
T result = T.Zero; // 0 대신 T.Zero 사용
result = a + b; // + 연산자 직접 사용 가능
return result;
}
// Add(int, double) 식으로도 호출 가능
var res = Add(1, 2.5);
using System.Numerics; // INumber 네임스페이스
T Add<T>(T a, T b) where T : INumber<T> // Generic Constraint 지정
{
T result = T.Zero; // 0 대신 T.Zero 사용
result = a + b; // + 연산자 직접 사용 가능
return result;
}
// Add(int, double) 식으로도 호출 가능
var res = Add(1, 2.5);
C# 11: Generic Math 지원을 위한 언어적 기능들
Generic Math를 지원하기 위해 C# 11은 아래와 같은 몇가지 언어적 기능들을 추가하였다.
(1) Interface에서 static abstract, static virtual 지원: Interface 안에 Default Method를 static virtual 혹은 static abstract로 정의할 수 있게 하였다 (예제D).
(2) checked user defined operator 지원: 사용자가 operator를 정의할 때, 특별히 checked 라는 키워드를 사용해서 연산에서 Overflow가 되는 것을 체크하는 버전의 메서드를 정의할 수 있게 하였다 (예제E).
(3) unsigned right-shift operator 지원: unsigned right-shift operator (>>>) 라는 새로운 연산자를 도입하였다. 화살표가 2개인 right-shift operator (>>)는 최상위 비트가 1이면 쉬프트할 때 이동한 만큼의 비트를 왼쪽에서 채울 때 최상위 비트 1을 복사해 넣었다. 이와 달리 새로운 >>> 연산자는 왼쪽에 채우는 값을 항상 0으로 채우게 된다 (예제F).
(4) 쉬프트 연산자의 제약 완화: 쉬프트 연산자의 경우 쉬프트 연산자 뒤의 2번째 Operand는 항상 정수(혹은 정수호환)이어야 했는데, C# 11에서는 이러한 제약을 완화하였다. 이는 Generic Math 지원을 위해 필요하였다.
(1) Interface에서 static abstract, static virtual 지원: Interface 안에 Default Method를 static virtual 혹은 static abstract로 정의할 수 있게 하였다 (예제D).
(2) checked user defined operator 지원: 사용자가 operator를 정의할 때, 특별히 checked 라는 키워드를 사용해서 연산에서 Overflow가 되는 것을 체크하는 버전의 메서드를 정의할 수 있게 하였다 (예제E).
(3) unsigned right-shift operator 지원: unsigned right-shift operator (>>>) 라는 새로운 연산자를 도입하였다. 화살표가 2개인 right-shift operator (>>)는 최상위 비트가 1이면 쉬프트할 때 이동한 만큼의 비트를 왼쪽에서 채울 때 최상위 비트 1을 복사해 넣었다. 이와 달리 새로운 >>> 연산자는 왼쪽에 채우는 값을 항상 0으로 채우게 된다 (예제F).
(4) 쉬프트 연산자의 제약 완화: 쉬프트 연산자의 경우 쉬프트 연산자 뒤의 2번째 Operand는 항상 정수(혹은 정수호환)이어야 했는데, C# 11에서는 이러한 제약을 완화하였다. 이는 Generic Math 지원을 위해 필요하였다.
예제
(예제D) static virtual/static abstract 지원
public interface INumber<TSelf>
: IComparable,
IComparable<TSelf>,
IComparisonOperators<TSelf, TSelf, bool>,
IModulusOperators<TSelf, TSelf, TSelf>,
INumberBase<TSelf>
where TSelf : INumber<TSelf>?
{
// ... 생략 ...
static virtual TSelf Max(TSelf x, TSelf y)
{
// ...구현 내용 생략...
}
}
public interface INumberBase<TSelf> {
// ... 생략 ...
static abstract TSelf Zero { get; }
static abstract bool IsInteger(TSelf value);
}
(예제E) checked user defined operator 지원
// checked + operator (이것은 overflow를 체크함)
static virtual TResult operator checked +(TSelf left, TOther right)
=> left + right;
// unchecked + operator (checked 키워드 없으면 unchecked)
static abstract TResult operator +(TSelf left, TOther right);
(예제F) unsigned right-shift operator 지원
int i = -8; // fffffff8, 11111111111111111111111111111000
// right-shift operator >> (왼쪽 비트가 1로 됨)
int a = i >> 2; // fffffffe, 11111111111111111111111111111110
// unsigned right-shift operator >>> (왼쪽 비트가 0이 됨)
int b = i >>> 2; // 3ffffffe, 00111111111111111111111111111110
Console.WriteLine($"{i:x},{a:x},{b:x}");
public interface INumber<TSelf>
: IComparable,
IComparable<TSelf>,
IComparisonOperators<TSelf, TSelf, bool>,
IModulusOperators<TSelf, TSelf, TSelf>,
INumberBase<TSelf>
where TSelf : INumber<TSelf>?
{
// ... 생략 ...
static virtual TSelf Max(TSelf x, TSelf y)
{
// ...구현 내용 생략...
}
}
public interface INumberBase<TSelf> {
// ... 생략 ...
static abstract TSelf Zero { get; }
static abstract bool IsInteger(TSelf value);
}
(예제E) checked user defined operator 지원
// checked + operator (이것은 overflow를 체크함)
static virtual TResult operator checked +(TSelf left, TOther right)
=> left + right;
// unchecked + operator (checked 키워드 없으면 unchecked)
static abstract TResult operator +(TSelf left, TOther right);
(예제F) unsigned right-shift operator 지원
int i = -8; // fffffff8, 11111111111111111111111111111000
// right-shift operator >> (왼쪽 비트가 1로 됨)
int a = i >> 2; // fffffffe, 11111111111111111111111111111110
// unsigned right-shift operator >>> (왼쪽 비트가 0이 됨)
int b = i >>> 2; // 3ffffffe, 00111111111111111111111111111110
Console.WriteLine($"{i:x},{a:x},{b:x}");