checked 및 unchecked 문 — 오버플로 검사
checked 및 unchecked 문 — 오버플로 검사 (C# 참조)
정수 타입의 산술 연산이나 형 변환을 하다 보면 계산 결과가 타입이 담을 수 있는 범위를 넘치는 상황이 생기죠. 이때 어떤 결과를 받아들일지, 즉 **오버플로 검사 컨텍스트(overflow-checking context)**를 지정하는 게 바로 checked와 unchecked 문이에요. 기본값은 unchecked인데요, 정수 산술 오버플로가 발생하면 이 컨텍스트가 그 뒤의 동작을 완전히 갈라놓아요.
출처: Microsoft Learn
본문
checked 컨텍스트에서는 오버플로가 나는 순간 xref:System.OverflowException?displayProperty=nameWithType 예외가 던져져요. 특히 그 계산이 상수식(constant expression) 안에서 벌어지면 실행도 되기 전에 컴파일 타임 오류가 나죠. 반대로 unchecked 컨텍스트에서는 결과가 잘려나가요. 목적 타입에 들어가지 못하는 높은 자리의 비트(high-order bits)를 버리는데, 더하기 연산이 대표적인 예로 최댓값에서 다시 최솟값으로 감싸서(wrap) 돌아오게 되죠. 다음 예시는 완전히 같은 연산을 checked와 unchecked 컨텍스트에서 각각 수행하는 모습이에요.
uint a = uint.MaxValue;
unchecked
{
Console.WriteLine(a + 3); // output: 2
}
try
{
checked
{
Console.WriteLine(a + 3);
}
}
catch (OverflowException e)
{
Console.WriteLine(e.Message); // output: Arithmetic operation resulted in an overflow.
}
uint.MaxValue + 3은 unchecked에서는 2로 감싸서 돌아오고, checked에서는 예외가 되죠.
[!NOTE] 사용자 정의(user-defined) 연산자와 변환의 오버플로 동작은 위에서 설명한 동작과 다를 수 있어요. 특히 사용자 정의 checked 연산자는
checked컨텍스트에서도 예외를 던지지 않을 수 있으니 주의하셔야 해요.
이 주제에 대한 자세한 내용은 산술 연산자 문서의 산술 오버플로와 0으로 나누기, 사용자 정의 checked 연산자 섹션에서 더 볼 수 있어요.
[!INCLUDEcsharp-version-note]
문이 아니라 식 하나에만 오버플로 검사 컨텍스트를 걸고 싶을 때도 있어요. 그럴 땐 checked와 unchecked 문 대신 연산자(operator) 형태로 쓸 수 있는데, 다음 예시가 그 모습이에요.
double a = double.MaxValue;
int b = unchecked((int)a);
Console.WriteLine(b); // output: -2147483648
try
{
b = checked((int)a);
}
catch (OverflowException e)
{
Console.WriteLine(e.Message); // output: Arithmetic operation resulted in an overflow.
}
이제 스코프(영역) 이야기를 해볼게요. checked와 unchecked 문·연산자는 자기 영향이 텍스트상 그 블록이나 괄호 안에 들어 있는 연산에만 미칩니다. 다음 예시를 한번 볼까요.
int Multiply(int a, int b) => a * b;
int factor = 2;
try
{
checked
{
Console.WriteLine(Multiply(factor, int.MaxValue)); // output: -2
}
}
catch (OverflowException e)
{
Console.WriteLine(e.Message);
}
try
{
checked
{
Console.WriteLine(Multiply(factor, factor * int.MaxValue));
}
}
catch (OverflowException e)
{
Console.WriteLine(e.Message); // output: Arithmetic operation resulted in an overflow.
}
위 예시에서 Multiply 로컬 함수의 첫 번째 호출을 보면 예외가 발생하지 않아요. 이게 바로 checked 문이 Multiply 함수 안쪽의 오버플로 검사 컨텍스트까지는 건드리지 않는다는 증거죠. 반면 두 번째 호출에서는 두 번째 인자로 들어가는 factor * int.MaxValue라는 식이 checked 문 블록의 텍스트 안쪽에 있기 때문에 checked 컨텍스트에서 평가되면서 예외가 나요.
한 가지 더 기억하셔야 할 점이 있어요. checked·unchecked의 동작은 타입과 연산에 따라 달라진다는 거죠. 정수라 해도 unchecked(x / 0)처럼 의미 있는 동작이 없는 연산은 언제나 예외를 던져요. 그러니 실제 코드에 적용하기 전에 해당 타입과 연산의 동작을 확인해 보시는 게 좋아요.
숫자 타입과 오버플로 검사 컨텍스트
checked와 unchecked 키워드는 주로 의미 있는 오버플로 동작이 있는 정수 타입에 적용돼요. T.MaxValue + 1이 T.MinValue가 되는 감싸기(wraparound) 동작은 2의 보수(two's complement) 체계에서 의미가 있거든요. 다만 그 값이 타입의 저장 공간에 들어갈 수 없기 때문에 표현된 값이 정확하지는 않아요. 다시 말하면 그 비트들은 전체 결과의 하위 n비트를 나타낼 뿐이에요.
반면 decimal, float, double, Half처럼 더 복잡한 값이나 1의 보수 형태를 나타내는 타입에서는 감싸기가 의미가 없어요. 감싸기로 더 크고 정확한 결과를 계산할 수 있는 게 아니니 unchecked가 딱히 유리할 게 없죠.
float, double, Half는 PositiveInfinity(양의 무한대)와 NegativeInfinity(음의 무한대)라는 의미 있는 **포화값(saturating value)**이 있어서, unchecked 컨텍스트에서도 오버플로를 감지할 수 있어요. 그런데 decimal에는 그런 한계가 없어서 MaxValue에서 포화시키면 오히려 오류나 혼란으로 이어질 수 있어요. decimal 연산은 checked와 unchecked 양쪽 모두에서 예외를 던지게 되죠.
오버플로 검사 컨텍스트의 영향을 받는 연산
오버플로 검사 컨텍스트는 다음 연산들에 영향을 줘요.
-
다음 기본 제공 산술 연산자 — 단항
++,--,-와 이항+,-,*,/—가 피연산자로 정수 타입(즉 정수 숫자 또는 char 타입)이나 enum 타입을 쓸 때요. -
정수 타입 사이의 명시적 숫자 변환, 또는
float나double에서 정수 타입으로의 명시적 변환.[!NOTE]
decimal값을 정수 타입으로 변환할 때 결과가 목적 타입의 범위를 벗어나면, 오버플로 검사 컨텍스트와 무관하게 항상 xref:System.OverflowException이 던져져요. -
사용자 정의 checked 연산자와 변환을 사용할 수도 있어요. 자세한 내용은 산술 연산자 문서의 사용자 정의 checked 연산자 섹션을 참고하세요.
기본 오버플로 검사 컨텍스트
오버플로 검사 컨텍스트를 따로 지정하지 않으면, 비상수식의 기본 컨텍스트는 CheckForOverflowUnderflow 컴파일러 옵션 값이 정해줘요. 기본적으로 이 옵션은 설정되어 있지 않아서, 정수 타입의 산술 연산과 변환은 unchecked 컨텍스트에서 실행돼요.
그런데 상수식은 기본적으로 checked 컨텍스트에서 평가되고, 오버플로가 나면 컴파일 타임 오류가 발생해요. 상수식에 대해 unchecked 컨텍스트를 명시적으로 지정하고 싶다면 unchecked 문이나 연산자를 쓰면 돼요.
C# 언어 사양
자세한 내용은 C# 언어 사양의 다음 섹션을 참고하세요.