C# 정수 계열 숫자 형식
C# 정수 계열 숫자 형식 (Integral numeric types)
프로그램에서 가장 자주 만나는 숫자는 아마 정수일 거예요. C#에서 정수를 다루는 형식(타입)이 어떤 것들이 있고, 각 형식이 담을 수 있는 값의 범위와 메모리 크기가 어떻게 다른지, 리터럴은 어떻게 쓰는지 차근차근 짚어볼게요. 이 글을 끝까지 보면 "정수 형식은 뭘 골라야 하지?"라는 고민이 한결 가벼워질 거예요.
출처: Microsoft Learn
본문
정수 계열 숫자 형식(integral numeric types)은 정수를 나타내요. 모든 정수 계열 숫자 형식은 값 형식(value type)이고, 단순 형식(simple type)에 속하며, 리터럴로 초기화해요. 그리고 이 형식들은 모두 산술(arithmetic), 비트 논리(bitwise logical), 비교(comparison), 같음(equality) 연산자를 지원해요.
정수 형식의 특성
C#이 제공하는 미리 정의된 정수 형식은 다음과 같아요.
| C# 형식/키워드 | 범위 | 크기 | .NET 형식 |
|---|---|---|---|
sbyte |
-128 ~ 127 | 부호 있는 8비트 정수 | xref:System.SByte?displayProperty=nameWithType |
byte |
0 ~ 255 | 부호 없는 8비트 정수 | xref:System.Byte?displayProperty=nameWithType |
short |
-32,768 ~ 32,767 | 부호 있는 16비트 정수 | xref:System.Int16?displayProperty=nameWithType |
ushort |
0 ~ 65,535 | 부호 없는 16비트 정수 | xref:System.UInt16?displayProperty=nameWithType |
int |
-2,147,483,648 ~ 2,147,483,647 | 부호 있는 32비트 정수 | xref:System.Int32?displayProperty=nameWithType |
uint |
0 ~ 4,294,967,295 | 부호 없는 32비트 정수 | xref:System.UInt32?displayProperty=nameWithType |
long |
-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 | 부호 있는 64비트 정수 | xref:System.Int64?displayProperty=nameWithType |
ulong |
0 ~ 18,446,744,073,709,551,615 | 부호 없는 64비트 정수 | xref:System.UInt64?displayProperty=nameWithType |
nint |
플랫폼에 따라 다름(런타임에 계산) | 부호 있는 32비트 또는 64비트 정수 | xref:System.IntPtr?displayProperty=nameWithType |
nuint |
플랫폼에 따라 다름(런타임에 계산) | 부호 없는 32비트 또는 64비트 정수 | xref:System.UIntPtr?displayProperty=nameWithType |
표에서 마지막 두 줄을 제외한 모든 줄에서, 맨 왼쪽의 C# 형식 키워드는 해당 .NET 형식의 별칭(alias)이에요. 다시 말해 키워드와 .NET 형식 이름은 서로 바꿔 쓸 수 있어요. 예를 들어 아래 두 선언은 사실상 같은 형식의 변수를 만듭니다.
int a = 123;
System.Int32 b = 123;
표 마지막 두 줄의 nint와 nuint는 네이티브 크기 정수(native-sized integers)예요. 네이티브 크기 정수를 정의하려면 nint와 nuint 컨텍스트 키워드를 사용해요. 이 형식들은 32비트 프로세스에서 실행 중일 때는 32비트 정수, 64비트 프로세스에서 실행 중일 때는 64비트 정수가 됩니다. 주로 interop 시나리오나 저수준 라이브러리, 그리고 정수 연산을 많이 수행하는 곳에서 성능을 최적화할 때 사용해요.
네이티브 크기 정수 형식은 내부적으로 .NET 형식인 xref:System.IntPtr?displayProperty=nameWithType와 xref:System.UIntPtr?displayProperty=nameWithType로 표현돼요. nint와 nuint는 그 밑에 깔린 형식의 별칭입니다.
각 정수 형식의 기본값은 0, 즉 0이에요.
각 정수 형식에는 그 형식이 담을 수 있는 최솟값과 최댓값을 알려주는 MinValue와 MaxValue 속성이 있어요. 이 속성들은 네이티브 크기 형식(nint, nuint)을 제외하고는 모두 컴파일 타임 상수예요. 네이티브 크기 형식의 MinValue와 MaxValue는 런타임에 계산되는데, 크기가 프로세스 설정에 따라 달라지기 때문이에요.
위아래 한계가 없는 부호 있는 정수를 표현해야 한다면 xref:System.Numerics.BigInteger?displayProperty=nameWithType 구조체를 사용해요.
정수 리터럴
정수 리터럴은 다음 세 가지 형태 중 하나로 쓸 수 있어요.
- 10진수: 접두사 없이
- 16진수:
0x또는0X접두사 사용 - 2진수:
0b또는0B접두사 사용
아래 코드가 각각의 예시를 보여줍니다.
var decimalLiteral = 42;
var hexLiteral = 0x2A;
var binaryLiteral = 0b_0010_1010;
앞의 예시에서 _를 자릿수 구분자(digit separator)로 쓴 것도 보셨죠. 자릿수 구분자는 모든 종류의 숫자 리터럴에 사용할 수 있어요.
접미사에 따라 정수 리터럴의 형식이 정해지는 규칙은 다음과 같습니다.
-
접미사가 없으면, 값이 표현될 수 있는 형식 중 다음 순서에서 처음 만나는 형식으로 컴파일러가 정해요:
int,uint,long,ulong.[!NOTE] 컴파일러는 리터럴을 항상 양수 값으로 해석해요. 예를 들어
0xFF_FF_FF_FF라는 리터럴은int형식의-1과 같은 비트 표현을 갖고 있지만,uint형식의4,294,967,295로 해석됩니다. 특정 형식의 값이 필요하면 리터럴을 그 형식으로 캐스팅해요. 리터럴 값이 대상 형식에 표현될 수 없다면unchecked연산자를 사용해요. 예를 들어unchecked((int)0xFF_FF_FF_FF)는-1을 만들어냅니다. -
리터럴에
U또는u접미사가 있으면, 값이 표현될 수 있는 형식 중 다음 순서에서 처음 만나는 형식으로 컴파일러가 정해요:uint,ulong. -
리터럴에
L또는l접미사가 있으면, 값이 표현될 수 있는 형식 중 다음 순서에서 처음 만나는 형식으로 컴파일러가 정해요:long,ulong.[!NOTE] 접미사로 소문자
l을 쓸 수 있긴 해요. 그런데l은 숫자1과 혼동될 수 있어서 컴파일러 경고가 발생해요. 명확하게 쓰려면L을 사용하세요. -
리터럴에
UL,Ul,uL,ul,LU,Lu,lU,lu중 하나의 접미사가 있으면, 컴파일러가 형식을ulong으로 정해요.
정수 리터럴이 표현하는 값이 xref:System.UInt64.MaxValue?displayProperty=nameWithType를 초과하면 컴파일러 오류 CS1021이 발생해요.
컴파일러가 정수 리터럴의 형식을 int로 결정했고, 리터럴이 표현하는 값이 대상 형식의 범위 안에 있다면, 그 값은 sbyte, byte, short, ushort, uint, ulong, nint, nuint로 묵시적으로 변환될 수 있어요.
byte a = 17;
byte b = 300; // CS0031: Constant value '300' cannot be converted to a 'byte'
앞의 예시에서 보듯, 리터럴의 값이 대상 형식의 범위를 벗어나면 컴파일러 오류 CS0031이 발생해요.
캐스트를 사용해 정수 리터럴이 표현하는 값을 리터럴의 결정된 형식이 아닌 다른 형식으로 변환할 수도 있어요.
var signedByte = (sbyte)42;
var longVariable = (long)42;
변환 (Conversions)
모든 정수 계열 숫자 형식은 다른 어떤 정수 계열 숫자 형식으로든 변환할 수 있어요. 대상 형식이 원본 형식의 모든 값을 저장할 수 있으면 변환은 묵시적(implicit)이에요. 그렇지 않으면 캐스트 식를 사용해 명시적(explicit) 변환을 수행해요. 자세한 내용은 기본 제공 숫자 변환을 참고하세요.
네이티브 크기 정수
네이티브 크기 정수 형식은 저장 크기가 대상 머신의 자연 정수 크기와 일치하기 때문에 특별한 동작을 보여요.
-
런타임에 네이티브 크기 정수의 크기를 얻으려면
sizeof()를 사용해요. 단, 이 코드는 unsafe 컨텍스트에서 컴파일되어야 해요. 예를 들면 다음과 같아요.:::code language="csharp" source="snippets/shared/NativeIntegerTypes.cs" id="SizeOf":::
정적 속성인 xref:System.IntPtr.Size?displayProperty=nameWithType와 xref:System.UIntPtr.Size?displayProperty=nameWithType에서도 같은 값을 얻을 수 있어요.
-
런타임에 네이티브 크기 정수의 최솟값과 최댓값을 얻으려면
nint와nuint키워드와 함께MinValue,MaxValue를 정적 속성으로 사용해요. 예를 들면 다음과 같아요.:::code language="csharp" source="snippets/shared/NativeIntegerTypes.cs" id="MinMax":::
-
nint와nuint의 전체 범위는 더 클 수 있지만, 컴파일 타임 상수는 32비트 범위로 제한돼요: -
컴파일러는 다른 숫자 형식으로의 묵시적·명시적 변환을 제공해요. 자세한 내용은 기본 제공 숫자 변환을 참고하세요.
-
네이티브 크기 정수 리터럴을 위한 직접적인 문법은 없어요.
long을 뜻하는L같은, 리터럴이 네이티브 크기 정수임을 나타내는 접미사가 없기 때문이에요. 대신 다른 정수 값의 묵시적·명시적 캐스트를 사용해요. 예를 들면 다음과 같아요.nint a = 42 nint a = (nint)42;
C# 언어 사양
자세한 내용은 C# 언어 사양의 다음 섹션을 참고하세요.