Microsoft MVP성태의 닷넷 이야기
.NET Framework: 2031. C# 11 - 사용자 정의 checked 연산자 [링크 복사], [링크+제목 복사]
조회: 1856
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 16개 있습니다.)

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

Language Feature Status
; https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 11/30/2022]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13237정성태1/30/202361.NET Framework: 2091. C# - 웹 사이트가 어떤 버전의 TLS/SSL을 지원하는지 확인하는 방법
13236정성태1/29/202398개발 환경 구성: 663. openssl을 이용해 인트라넷 IIS 사이트의 SSL 인증서 생성
13235정성태1/29/202387개발 환경 구성: 662. openssl - 윈도우 환경의 명령행에서 SAN 적용하는 방법
13234정성태1/28/202385개발 환경 구성: 661. dnSpy를 이용해 소스 코드가 없는 .NET 어셈블리의 코드를 변경하는 방법
13233정성태1/28/202382오류 유형: 840. C# - WebClient로 https 호출 시 "The request was aborted: Could not create SSL/TLS secure channel" 예외 발생
13232정성태1/27/202372스크립트: 43. uwsgi의 --processes와 --threads 옵션
13231정성태1/27/202375오류 유형: 839. python - TypeError: '...' object is not callable
13230정성태1/26/2023110개발 환경 구성: 660. WSL 2 내부로부터 호스트 측의 네트워크로 UDP 데이터가 1개의 패킷으로만 제한되는 문제
13229정성태1/25/2023162.NET Framework: 2090. C# - UDP Datagram의 최대 크기
13228정성태1/24/2023155.NET Framework: 2089. C# - WMI 논리 디스크가 속한 물리 디스크의 정보를 얻는 방법 [2]파일 다운로드1
13227정성태1/23/2023118개발 환경 구성: 659. Windows - IP MTU 값을 바꿀 수 있을까요?
13226정성태1/23/2023113.NET Framework: 2088. .NET 5부터 지원하는 GetRawSocketOption 사용 시 주의할 점
13225정성태1/21/2023140개발 환경 구성: 658. Windows에서 실행 중인 소켓 서버를 다른 PC 또는 WSL에서 접속할 수 없는 경우
13224정성태1/21/2023158Windows: 221. Windows - Private/Public/Domain이 아닌 네트워크 어댑터 단위로 방화벽을 on/off하는 방법
13223정성태1/20/2023118오류 유형: 838. RDP 연결 오류 - The two computers couldn't connect in the amount of time allotted
13222정성태1/20/2023112개발 환경 구성: 657. WSL - DockerDesktop.vhdx 파일 위치를 옮기는 방법
13221정성태1/19/2023158Linux: 57. C# - 리눅스 프로세스 메모리 정보파일 다운로드1
13220정성태1/19/2023114오류 유형: 837. NETSDK1045 The current .NET SDK does not support targeting .NET ...
13219정성태1/18/2023191Windows: 220. 네트워크의 인터넷 접속 가능 여부에 대한 판단 기준
13218정성태1/17/2023175VS.NET IDE: 178. Visual Studio 17.5 (Preview 2) - 포트 터널링을 이용한 웹 응용 프로그램의 외부 접근 허용
13217정성태1/13/2023198디버깅 기술: 185. windbg - 64비트 운영체제에서 작업 관리자로 뜬 32비트 프로세스의 덤프를 sos로 디버깅하는 방법
13216정성태1/12/2023154디버깅 기술: 184. windbg - 32비트 프로세스의 메모리 덤프인 경우 !peb 명령어로 나타나지 않는 환경 변수
13215정성태1/11/2023152Linux: 56. 리눅스 - /proc/pid/stat 정보를 이용해 프로세스의 CPU 사용량 구하는 방법
13214정성태1/10/2023270.NET Framework: 2087. .NET 6부터 SourceGenerator와 통합된 System.Text.Json [1]파일 다운로드1
13213정성태1/9/2023208오류 유형: 836. docker 이미지 빌드 시 "RUN apt install ..." 명령어가 실패하는 이유
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...