C# là ngôn ngữ kiểu tĩnh: mọi biến, field và biểu thức đều có kiểu được biết lúc biên dịch. Ngôn ngữ có một tập kiểu có sẵn (built-in type) với từ khóa riêng (int, double, string...). Mỗi từ khóa là bí danh của một kiểu trong namespace System, nên int và System.Int32 là cùng một kiểu, string và System.String cũng là cùng một kiểu.
Các kiểu có sẵn
| Kiểu | Kiểu .NET | Kích thước | Phạm vi hoặc độ chính xác | Literal |
|---|---|---|---|---|
bool | Boolean | 1 byte | true hoặc false | true |
byte | Byte | 1 byte | từ 0 đến 255 | 200 |
sbyte | SByte | 1 byte | từ -128 đến 127 | -5 |
short | Int16 | 2 byte | từ -32.768 đến 32.767 | 1200 |
ushort | UInt16 | 2 byte | từ 0 đến 65.535 | 60000 |
int | Int32 | 4 byte | từ -2.147.483.648 đến 2.147.483.647 | 42 |
uint | UInt32 | 4 byte | từ 0 đến 4.294.967.295 | 42u |
long | Int64 | 8 byte | khoảng ±9,2 × 10^18 | 42L |
ulong | UInt64 | 8 byte | từ 0 đến khoảng 1,8 × 10^19 | 42ul |
float | Single | 4 byte | khoảng ±3,4 × 10^38, ~6 đến 9 chữ số | 2.5f |
double | Double | 8 byte | khoảng ±1,8 × 10^308, ~15 đến 17 chữ số | 2.5 hoặc 2.5d |
decimal | Decimal | 16 byte | khoảng ±7,9 × 10^28, 28 đến 29 chữ số | 2.5m |
char | Char | 2 byte | một đơn vị mã UTF-16, từ U+0000 đến U+FFFF | 'A' |
string | String | tham chiếu | văn bản dài tùy ý, bất biến | "hello" |
object | Object | tham chiếu | kiểu gốc của mọi kiểu | new object() |
Literal số nguyên không có hậu tố là int, hoặc nếu quá lớn so với int thì là kiểu đầu tiên trong uint, long và ulong chứa được nó, còn literal có dấu chấm thập phân là double. Đó là lý do float f = 2.5; và decimal d = 2.5; không biên dịch được: literal là double, và không có phép chuyển đổi ngầm từ double sang float hay decimal. Hãy thêm hậu tố: 2.5f, 2.5m.
Mọi kiểu số đều cung cấp giới hạn của mình dưới dạng hằng số:
Đầu ra:
byte: 0 to 255
short: -32768 to 32767
int: -2147483648 to 2147483647
long: -9223372036854775808 to 9223372036854775807
decimal max: 79228162514264337593543950335
double max: 1.798E+308
sizes: int 4, long 8, double 8, char 2
255 161 3000000000
Trong thực tế, phần lớn code dùng bốn kiểu: int cho số đếm và chỉ số, long cho ID và tổng lớn, double cho số đo và toán học, và decimal cho tiền.
decimal và double
double (và float) lưu số ở dạng nhị phân. Hầu hết phân số thập phân, kể cả 0,1, không có dạng nhị phân chính xác, nên chúng được lưu bằng giá trị biểu diễn được gần nhất và sai số nhỏ cộng dồn lại. decimal lưu một số nguyên hệ 10 kèm hệ số tỷ lệ, nên 0,1 đúng là 0,1.
Đầu ra:
False
True
0.30000000000000004
0.3
double sum == 1.0: False
decimal sum == 1.0: True
Total: 59.97
Split three ways: 19.99
Dùng decimal cho tiền, giá, thuế và bất cứ thứ gì mà một người sẽ cộng tay và so sánh. Nó giữ các số 0 ở cuối từ đầu vào (1.50m in ra 1.50) và có từ 28 đến 29 chữ số có nghĩa.
Dùng double cho số đo vật lý, thống kê, hình học và game. Nó nhanh hơn vài lần, dùng một nửa bộ nhớ, và phạm vi cực lớn. Đừng bao giờ so sánh hai giá trị double đã tính toán bằng ==; hãy so hiệu của chúng với một sai số cho phép: Math.Abs(x - y) < 1e-9.
decimal không phải phép màu: 1m / 3m vẫn là 0.3333333333333333333333333333, và phép tính double không ném lỗi khi chia cho 0 (nó cho ra vô cực, hoặc NaN với 0.0 / 0.0), trong khi phép chia cho 0 với decimal và số nguyên ném ra DivideByZeroException.
Tràn số nguyên và checked
Phép tính số nguyên vượt quá phạm vi của kiểu sẽ quay vòng theo mặc định, một cách âm thầm. Trình biên dịch bắt được tràn số trong biểu thức hằng, nhưng không bắt được khi giá trị đến từ biến:
Đầu ra:
-2147483648
OverflowException
-1294967296
2147483648
checked(...) hoặc khối checked { ... } biến tràn số thành OverflowException. Bạn cũng có thể bật cho cả dự án bằng <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>. Viết int x = int.MaxValue + 1; với hằng số là lỗi biên dịch (CS0220), vì trình biên dịch nhìn thấy được tràn số.
Dòng cuối cho thấy cách sửa phần lớn lỗi tràn số: chuyển sang kiểu rộng hơn trước phép tính. (long)(max + 1) sẽ sai, vì phép cộng đã quay vòng trước khi phép ép kiểu chạy.
Kiểu giá trị và kiểu tham chiếu
Mọi kiểu trong C# hoặc là kiểu giá trị (value type), hoặc là kiểu tham chiếu (reference type), và khác biệt này quyết định phép gán làm gì.
- Kiểu giá trị: mọi kiểu số,
bool,char,DateTime, enum và mọistruct. Biến chứa dữ liệu; phép gán sao chép dữ liệu. - Kiểu tham chiếu:
string, mảng,objectvà mọiclass. Biến chứa tham chiếu đến một đối tượng; phép gán sao chép tham chiếu, nên cả hai biến cùng thấy một đối tượng.
Đầu ra:
a=10 b=99
first[0]=99
s1=cat s2=cats
string là kiểu tham chiếu nhưng trong thực tế hoạt động như giá trị: nó bất biến, và == so sánh văn bản chứ không so sánh tham chiếu. Xem thêm về kiểu giá trị do người dùng định nghĩa ở trang struct.
char và bool
Một char là một đơn vị mã UTF-16, viết trong dấu nháy đơn. Bên dưới nó là một kiểu số, nên nó chuyển được sang int và hỗ trợ phép tính:
Đầu ra:
66
C
True
True
7
b
True
bool có đúng hai giá trị và không chuyển đổi qua lại với số: if (count) là lỗi biên dịch, không phải phép kiểm tra khác 0. Hãy viết if (count != 0). Ký tự nằm ngoài Basic Multilingual Plane, như phần lớn emoji, chiếm hai giá trị char (một cặp surrogate), nên Length của một string đếm đơn vị UTF-16, không phải ký tự nhìn thấy được.
Giá trị mặc định
Field, phần tử mảng và default(T) nhận giá trị mặc định của kiểu: 0 với số, false với bool, '\0' với char, và null với kiểu tham chiếu. Biến cục bộ không có giá trị mặc định và phải được gán trước khi dùng.
Đầu ra:
0
0
False
True
0001-01-01
0,0,0
Từ C# 7.1, bạn có thể viết literal default mà không cần kiểu khi trình biên dịch suy ra được (int x = default;). Kiểu giá trị không thể là null, và đó là việc của int? (kiểu giá trị nullable); xem kiểu nullable.
Số nguyên theo kích thước nền tảng và số nguyên lớn
Còn hai kiểu số nữa cho các trường hợp đặc biệt. nint và nuint (C# 9) là số nguyên có kích thước bằng con trỏ, 32 hoặc 64 bit tùy nền tảng, dùng trong interop và code cấp thấp. System.Numerics.BigInteger chứa số nguyên lớn tùy ý, đổi lại là tốc độ chậm hơn:
using System.Numerics;
BigInteger factorial = 1;
for (int i = 2; i <= 30; i++) factorial *= i;
// 265252859812191058636308480000000
Câu hỏi thường gặp
C# có những kiểu dữ liệu nào?
Các kiểu có sẵn gồm các kiểu số nguyên (sbyte, byte, short, ushort, int, uint, long, ulong), các kiểu số thực dấu phẩy động float và double, decimal cho phép tính thập phân chính xác, char cho một ký tự UTF-16, bool, và các kiểu tham chiếu string và object. Mỗi từ khóa là bí danh của một kiểu .NET: int là System.Int32, string là System.String.
Nên dùng decimal hay double cho tiền trong C#?
Dùng decimal. double lưu phân số nhị phân, nên những giá trị như 0,1 không biểu diễn chính xác được và tổng bị lệch dần (0.1 + 0.2 == 0.3 là false). decimal lưu chữ số hệ 10, giữ từ 28 đến 29 chữ số có nghĩa, và cho kết quả đúng như kế toán mong đợi. double nhanh hơn và có phạm vi lớn hơn rất nhiều, nên hãy dùng nó cho khoa học, đồ họa và số đo.
Giá trị lớn nhất của int trong C# là bao nhiêu?
int.MaxValue là 2.147.483.647 và int.MinValue là -2.147.483.648 (số nguyên có dấu 32 bit). Cộng 1 vào int.MaxValue sẽ âm thầm quay vòng về int.MinValue, trừ khi code chạy trong ngữ cảnh checked, khi đó nó ném ra OverflowException. Dùng long (tối đa khoảng 9,2 tỷ tỷ) cho giá trị lớn hơn.
float và double trong C# khác nhau thế nào?
float dài 32 bit với khoảng 6 đến 9 chữ số có nghĩa; double dài 64 bit với khoảng 15 đến 17. Literal số thực mặc định là double, nên float cần hậu tố f: float speed = 2.5f;. Hãy dùng double, trừ khi bộ nhớ hoặc một API (như Vector3 của Unity) yêu cầu float.
Kiểu giá trị và kiểu tham chiếu trong C# khác nhau thế nào?
Biến kiểu giá trị (int, double, bool, DateTime, mọi struct) chứa chính dữ liệu, và phép gán sao chép dữ liệu đó. Biến kiểu tham chiếu (string, mảng, mọi class) chứa tham chiếu đến một đối tượng trên heap, và phép gán sao chép tham chiếu, nên hai biến có thể cùng trỏ đến một đối tượng.