C# 11 - 사용자 정의 checked 연산자
(Visual Studio 2022 17.3 이후 버전에서 테스트할 수 있습니다.)
현재 overflow 체크는 다음의 동작에서 영향을 주는데요,
- 정수형 타입에 대한 ++, --, 음수 부호 -, 4칙 연산자(+, -, *, /)
- 정수 형식끼리의 형변환, 또는 float, double 타입을 정수형 타입에 형변환
단, 이미 만들어진 타입에 한해서만 동작을 할 뿐 사용자 정의 타입에서는 overflow 제어를 위한 checked/unchecked 구문에 참여할 수 없습니다.
예를 들어 볼까요?
checked {
sbyte a = 127;
a++; // Unhandled exception. System.OverflowException: Arithmetic operation resulted in an overflow.
}
위의 코드는 (컴파일 시점이 아닌) 실행 시에 System.OverflowException 예외가 발생합니다. 반면, checked 예약어를 제거하면 아무런 오류 없이 a의 값은 -128로 바뀝니다.
즉, C#은 기본적으로 overflow를 체크하지 않는 코드를 생성합니다. 이러한 기본값은 csproj의
CheckForOverflowUnderflow 옵션을 이용하면 변경할 수 있습니다.
<Project Sdk="Microsoft.NET.Sdk">
...[생략]...
<LangVersion>preview</LangVersion>
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
...[생략]...
</Project>
그래서 위와 같이 설정한 후부터는 다음의 코드도 런타임 시에 System.OverflowException이 발생합니다.
sbyte a = 127;
a++; // CheckForOverflowUnderflow 옵션이 true인 경우 System.OverflowException 예외 발생
그런데, 이 상황에서도 예외가 발생하지 않도록 하는 방법이 있습니다. 바로 unchecked를 설정하는 것입니다.
unchecked {
sbyte a = 127;
a++; // CheckForOverflowUnderflow 옵션이 true여도, 예외 발생하지 않고 a == -128
}
대충 CheckForOverflowUnderflow, checked, unchecked 관계를 아시겠죠? ^^
그런데 위와 같은 overflow 처리 정책을 우리가 만드는 C# 클래스에서는 따를 수가 없습니다. 가령 3 바이트 정수형 타입을 정의한다고 했을 때,
public struct Int3
{
int value;
public Int3(int value)
{
this.value = value;
}
public static implicit operator Int3(int value) => new Int3(value);
public static Int3 operator ++(Int3 lhs)
{
return new Int3(lhs.value + 1);
}
public override string ToString()
{
return $"{value}";
}
}
이렇게 사용한다고 해서 overflow 예외가 발생할 수는 없습니다.
checked
{
Int3 n = 0x007F_FFFF;
n++; // n == 8388608
}
당연한 결과인데요, 이때 개발자는 checked/unchecked 문맥과 상관없이 그냥 무조건 overflow 예외를 발생하는 정도로 구현할 수는 있습니다.
public static Int3 operator ++(Int3 lhs)
{
if (lhs.value + 1 > 8388607)
{
throw new OverflowException((lhs.value + 1).ToString());
}
return new Int3(lhs.value + 1);
}
그래서 설령 unchecked여도,
unchecked
{
Int3 n = 0x007F_FFFF;
n++; // 또는 CheckForOverflowUnderflow 옵션이 false여도 무조건 OverflowException 예외 발생
}
overflow 처리가 되는 것입니다.
이 문제를 해결하려면, 근본적으로 C# 컴파일러와의 연동이 필요한데 애당초 C# 컴파일러는 현재 코드 영역이 checked/unchecked 인지 파악할 수 있기 때문입니다. 그렇다면 이제 필요한 것은, C# 컴파일러가 overflow 문맥에 따라 생성할 코드도 함께 checked 또는 unchecked 유형에 맞는 것으로 고를 수 있게 해주면 됩니다. 이를 위해 C# 11부터 추가된 것이 바로 "checked" 유형의 연산자 재정의입니다. 그리하여, 위에서 정의한 Int3 구조체에 다음과 같이 2가지 overflow 문맥에 따른 연산자를 제공할 수 있고,
public static Int3 operator checked ++(Int3 lhs)
{
if (lhs.value + 1 > 8388607)
{
throw new OverflowException((lhs.value + 1).ToString());
}
return new Int3(lhs.value + 1);
}
public static Int3 operator ++(Int3 lhs) => new Int3(lhs.value + 1);
결국 C# 컴파일러는 대상 코드 영역의 overflow 문맥에 따라 "operator checked ++" 또는 "operator ++" 호출 코드를 적절하게 선택해 빌드를 하게 됩니다.
checked 연산자 재정의가 된 경우 내부 메서드 이름에는 "Checked"가 붙습니다. 가령 위에서 정의한 ++ 연산자의 경우 원래 이름은 "op_Increment"지만 checked 버전의 경우 "op_CheckedIncrement"라는 이름으로 정의됩니다.
또한, 새롭게 추가한 연산자를 "checked operator"라고 하고 기존의 연산자를 "regular operator"라고 부릅니다. 이러한 연산자는 1개 또는 2개를 제공할 수 있지만 "checked operator" 하나만을 제공할 수는 없습니다. (이것은 C# 컴파일러에서의 제약이고, 다른 언어라면 설계에 따라 그것이 가능할 수도 있습니다. 즉, IL 언어 수준에서의 제약은 아닙니다.)
public struct Int3
{
// checked와 regular 연산자 모두 제공하면 OK (checked/unchecked 문맥에 따라 다른 메서드가 선택됨)
public static Int3 operator checked +(Int3 lhs, Int3 rhs);
public static Int3 operator +(Int3 lhs, Int3 rhs);
// 혹은, regular 연산자 하나만 제공해도 OK (기존처럼 동작)
public static Int3 operator -(Int3 lhs, Int3 rhs);
// 하지만 checked 연산자 하나만 제공하는 경우 컴파일 에러
// error CS9025: The operator 'Int3.operator checked ++(Int3)' requires a matching non-checked version of the operator to also be defined
public static Int3 operator checked *(Int3 lhs, Int3 rhs);
}
아래의 코드는 부호 연산자 -, 4칙 연산자 *, 증가 연산자 ++를 재정의한 Int3 타입의 예를 보여줍니다.
public struct Int3
{
public static Int3 MaxValue = new Int3(8388607);
public static Int3 MinValue = new Int3(-8388608);
int value;
public Int3(int value) : this(value, false) { }
public Int3(int value, bool checkOverflow)
{
int checkValue = value;
if (value > 8388607)
{
checkValue = -(value & 0x00FF_FFFF);
}
else if (value < -8388608)
{
checkValue = value & 0x007F_FFFF;
}
if (checkOverflow == true)
{
if (value != checkValue)
{
throw new OverflowException($"{checkValue}");
}
}
this.value = checkValue;
}
public static implicit operator Int3(int value) => new Int3(value);
public static Int3 operator checked ++(Int3 lhs) => new Int3(lhs.value + 1, true);
public static Int3 operator ++(Int3 lhs) => new Int3(lhs.value + 1);
public static Int3 operator checked *(Int3 lhs, Int3 rhs) => new Int3(lhs.value * rhs.value, true);
public static Int3 operator *(Int3 lhs, Int3 rhs) => new Int3(lhs.value * rhs.value);
public static Int3 operator checked -(Int3 lhs) => new Int3(-lhs.value, true);
public static Int3 operator -(Int3 lhs) => new Int3(-lhs.value);
public override string ToString()
{
return $"{value}";
}
}
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
구현 가능한 checked 버전의 연산자는 ++, --, 음수 부호 연산자(-), 사칙 연산자(+, -, *, /)입니다.
마지막으로, 위의 코드를 구현하면서 "implicit operator checked" 형변환 연산자를 재정의할 수 있다면 "public Int3(int value, bool checkOverflow) {...}" 생성자를 굳이 구현할 필요가 없어 편리하겠다는 생각을 했는데요, 이에 대해 C# 개발자들은 다음과 같이 답변하고 있습니다.
질문: (Resolved) Should we support implicit checked conversion operators?
답변: In general, implicit conversion operators are not supposed to throw.
Proposal: No.
Resolution: Approved - https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-07.md#checked-implicit-conversions
또한 위의 링크에 더 자세한 답변이 나옵니다. ^^
Checked implicit conversions?
We clarified why conversions cannot be implicit and checked. Generally, implicit conversions should not fail, throw or be lossy. That's already the case with (most) existing conversions and .NET guidelines. There's an exception: integer to floating point conversion in C# does allow loss.
Conclusion: Stick with proposed restriction (conversions cannot be implicit and checked).
C# 11 - 인터페이스 내에 정적 추상 메서드 정의 가능 (공식 문서, Static Abstract Members In Interfaces C# 10 Preview)
; https://www.sysnet.pe.kr/2/0/12814
C# 11 - 제네릭 타입의 특성 적용 (공식 문서, Generic attributes)
; https://www.sysnet.pe.kr/2/0/12839
C# 11 - 사용자 정의 checked 연산자 (공식 문서, Checked user-defined operators)
; https://www.sysnet.pe.kr/2/0/13099
C# 11 - shift 연산자 재정의에 대한 제약 완화 (공식 문서, Relaxing Shift Operator)
; https://www.sysnet.pe.kr/2/0/13100
C# 11 - IntPtr/UIntPtr과 nint/unint의 통합 (공식 문서, Numeric IntPtr)
; https://www.sysnet.pe.kr/2/0/13111
C# 11 - 새로운 연산자 ">>>" (Unsigned Right Shift) (공식 문서, Unsigned right shift operator)
; https://www.sysnet.pe.kr/2/0/13110
C# 11 - 원시 문자열 리터럴 (공식 문서, raw string literals)
; https://www.sysnet.pe.kr/2/0/13085
C# 11 - 문자열 보간 개선 2가지 (공식 문서, Allow new-lines in all interpolations)
; https://www.sysnet.pe.kr/2/0/13086
C# 11 - 목록 패턴 (공식 문서, List patterns)
; https://www.sysnet.pe.kr/2/0/13112
C# 11 - Span 타입에 대한 패턴 매칭 (공식 문서, Pattern matching on ReadOnlySpan<char>)
; https://www.sysnet.pe.kr/2/0/13113
C# 11 - Utf8 문자열 리터럴 지원 (공식 문서, Utf8 Strings Literals)
; https://www.sysnet.pe.kr/2/0/13096
C# 11 - ref struct에 ref 필드를 허용 (공식 문서, ref fields)
; https://www.sysnet.pe.kr/2/0/13015
C# 11 - 파일 범위 내에서 유효한 타입 정의 (공식 문서, File-local types)
; https://www.sysnet.pe.kr/2/0/13117
C# 11 - 메서드 매개 변수에 대한 nameof 지원 (공식 문서, nameof(parameter))
; https://www.sysnet.pe.kr/2/0/13122
C# 11 - 멤버(속성/필드)에 지정할 수 있는 required 예약어 추가 (공식 문서, Required members)
; https://www.sysnet.pe.kr/2/0/13123
C# 11 - 구조체 필드의 자동 초기화 (공식 문서, auto-default structs)
; https://www.sysnet.pe.kr/2/0/13125
C# 11 - 정적 메서드에 대한 delegate 처리 시 cache 적용 (공식 문서, Cache delegates for static method group)
; https://www.sysnet.pe.kr/2/0/13126
Language Feature Status
; https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]