기본 제공 숫자 형식 변환

기본 제공 숫자 형식 변환 (C# 참조)

C#에서 숫자 형식끼리의 변환은 크게 암시적 변환(implicit)과 명시적 변환(explicit) 두 갈래로 나뉘어요. 정수 형식과 부동 소수점 형식 사이에 값이 어떻게 옮겨지는지, 언제 컴파일러가 알아서 넓혀 주고 언제 우리가 직접 캐스트를 써야 하는지를 이 문서에서 정리해 볼게요.

출처: Microsoft Learn

본문

C#은 여러 정수 형식부동 소수점 형식을 제공해요. 그리고 임의의 두 숫자 형식 사이에는 암시적 변환이나 명시적 변환이 반드시 하나는 존재합니다. 명시적 변환을 수행할 때는 캐스트 식을 사용하죠.

즉, "어떤 숫자 형식이든 다른 숫자 형식으로 바꿀 방법은 있다"가 기본 전제예요. 다만 그 변환이 컴파일러가 알아서 해 주는(암시적) 건지, 아니면 우리가 손으로 직접 지시해야 하는(명시적) 건지가 형식에 따라 달라집니다.

암시적 숫자 변환

먼저 암시적 변환입니다. 다음 표는 기본 제공 숫자 형식 사이에서 미리 정의된 암시적 변환을 보여줘요. 이 표에 나온 변환은 데이터가 안전하다고 보장되기 때문에 캐스트 없이 그냥 대입해도 컴파일러가 허용합니다.

From To
sbyte short, int, long, float, double, decimal, or nint
byte short, ushort, int, uint, long, ulong, float, double, decimal, nint, or nuint
short int, long, float, double, decimal, or nint
ushort int, uint, long, ulong, float, double, decimal, nint, or nuint
int long, float, double, decimal, or nint
uint long, ulong, float, double, decimal, or nuint
long float, double, or decimal
ulong float, double, or decimal
float double
nint long, float, double, or decimal
nuint ulong, float, double, or decimal

[!NOTE] The implicit conversions from int, uint, long, ulong, nint, or nuint to float and from long, ulong, nint, or nuint to double can cause a loss of precision, but never a loss of an order of magnitude. The other implicit numeric conversions never lose any information.

이 표만 봐서는 왜 어떤 건 암시적이고 어떤 건 아닌지 감이 안 올 수 있어요. 핵심 규칙을 몇 가지로 정리하면 이렇습니다.

  • 어떤 정수 형식이든 부동 소수점 형식으로는 암시적으로 변환할 수 있어요. 정수는 항상 소수점을 담는 더 "큰" 형식으로 넓혀진다고 보면 돼요.

  • 반대로 bytesbyte로 가는 암시적 변환은 존재하지 않아요. 또 double이나 decimal에서 출발하는 암시적 변환도 없습니다.

  • decimal 형식과 float/double 형식 사이의 암시적 변환은 없어요. 소수점 표현 방식이 서로 달라서 안전하게 넓혀지지 않기 때문이에요.

  • int 형식의 상수 식(예: 정수 리터럴로 표현한 값)은 대상 형식의 범위 안에 들어온다면 sbyte, byte, short, ushort, uint, ulong, nint, nuint로 암시적 변환이 가능합니다.

    byte a = 13;
    byte b = 300;  // CS0031: Constant value '300' cannot be converted to a 'byte'
    

즉 리터럴 값이 대상 형식의 담을 수 있는 범위를 벗어나면 컴파일 오류 CS0031이 발생해요. 위 코드에서 300byte의 최댓값인 255를 넘어가니 컴파일러가 바로 잡아 주는 거죠.

명시적 숫자 변환

이제 명시적 변환 차례입니다. 다음 표는 암시적 변환이 정의되지 않은, 기본 제공 숫자 형식 사이의 명시적 변환을 보여줘요. 여기 나오는 변환은 값이 손실될 수 있기 때문에 반드시 캐스트 식으로 직접 지시해야 합니다.

From To
sbyte byte, ushort, uint, ulong, or nuint
byte sbyte
short sbyte, byte, ushort, uint, ulong, or nuint
ushort sbyte, byte, or short
int sbyte, byte, short, ushort, uint, ulong, or nuint
uint sbyte, byte, short, ushort, int, or nint
long sbyte, byte, short, ushort, int, uint, ulong, nint, or nuint
ulong sbyte, byte, short, ushort, int, uint, long, nint, or nuint
float sbyte, byte, short, ushort, int, uint, long, ulong, decimal, nint, or nuint
double sbyte, byte, short, ushort, int, uint, long, ulong, float, decimal, nint, or nuint
decimal sbyte, byte, short, ushort, int, uint, long, ulong, float, double, nint, or nuint
nint sbyte, byte, short, ushort, int, uint, ulong, or nuint
nuint sbyte, byte, short, ushort, int, uint, long, or nint

[!NOTE] An explicit numeric conversion might result in data loss or throw an exception, typically an xref:System.OverflowException.

명시적 변환은 데이터가 손실되거나 xref:System.OverflowException 같은 예외가 발생할 수 있어요. 어떤 상황에서 값이 어떻게 바뀌는지, 유형별로 정리해 보면 이렇습니다.

  • 정수 형식을 다른 정수 형식으로 바꿀 때 결과는 오버플로 검사 컨텍스트에 따라 달라져요.

    • checked 컨텍스트라면, 원본 값이 대상 형식의 범위 안에 있으면 변환이 성공하고, 벗어나면 xref:System.OverflowException이 발생합니다.
    • unchecked 컨텍스트라면 변환은 항상 성공하고, 다음 규칙을 따릅니다.
      • 원본 형식이 대상 형식보다 크다면, 상위 비트(가장 큰 자릿수 비트)를 버리고 남은 값이 대상 형식의 값으로 취급돼요. 이걸 절단(truncation)이라고 합니다.
      • 원본 형식이 대상 형식보다 작다면, 원본 값이 부호 확장(sign-extension) 또는 0 확장(zero-extension)되어 대상 형식과 같은 크기로 맞춰집니다. 원본 형식이 부호 있는 형식이면 부호 확장을, 부호 없는 형식이면 0 확장을 사용해요.
      • 원본 형식과 대상 형식의 크기가 같다면, 원본 값을 그대로 대상 형식의 값으로 취급합니다.
  • decimal 값을 정수 형식으로 변환하면, 그 값은 0 방향으로 가장 가까운 정수로 반올림돼요. 만약 결과 정수 값이 대상 형식의 범위를 벗어나면 xref:System.OverflowException이 발생합니다.

  • double이나 float 값을 정수 형식으로 변환할 때도 0 방향으로 가장 가까운 정수로 반올림되요. 결과가 대상 형식의 범위를 벗어나면 오버플로 검사 컨텍스트에 따라 달라지는데, checked에서는 xref:System.OverflowException이 발생하고 unchecked에서는 지정되지 않은 값을 결과로 냅니다.

  • doublefloat로 변환하면 double 값이 가장 가까운 float 값으로 반올림돼요. 이때 double 값이 float에 담기기 너무 작거나 크다면 결과는 0 또는 무한대가 됩니다.

  • floatdoubledecimal로 변환하면 원본 값이 decimal 표현으로 변환되고, 필요하면 소수점 이하 28번째 자리 뒤에서 가장 가까운 수로 반올림됩니다. 원본 값에 따라 결과는 달라질 수 있어요.

    • 원본 값이 decimal로 표현하기 너무 작으면 결과는 0이 됩니다.
    • 원본 값이 NaN(숫자가 아님), 무한대, 또는 decimal로 표현하기 너무 크면 xref:System.OverflowException이 발생해요.
  • decimalfloatdouble로 변환하면 원본 값이 각각 가장 가까운 float 또는 double 값으로 반올림됩니다.

    [!NOTE] Starting in .NET 11, conversions between decimal and float or double are correctly rounded. Previously, these conversions could round through intermediate values or discard significant digits. For more information, see Decimal and BigInteger floating-point conversions are correctly rounded.

참고로 .NET 11부터는 decimalfloat/double 사이의 변환이 올바르게(정확히) 반올림되도록 바뀌었어요. 그 이전에는 중간 값을 거쳐 반올림되거나 유효 숫자가 버려질 수 있었는데, 이제는 그런 문제가 없어졌죠.

더 알아보기