C# 是静态类型语言:每个变量、字段和表达式都有一个在编译时就已知的类型。这门语言有一组拥有专属关键字的内置类型(int、double、string 等)。每个关键字都是 System 命名空间中某个类型的别名,所以 int 和 System.Int32 是同一个类型,string 和 System.String 也是同一个类型。
内置类型
| 类型 | .NET 类型 | 大小 | 范围或精度 | 字面量 |
|---|---|---|---|---|
bool | Boolean | 1 字节 | true 或 false | true |
byte | Byte | 1 字节 | 0 至 255 | 200 |
sbyte | SByte | 1 字节 | -128 至 127 | -5 |
short | Int16 | 2 字节 | -32,768 至 32,767 | 1200 |
ushort | UInt16 | 2 字节 | 0 至 65,535 | 60000 |
int | Int32 | 4 字节 | -2,147,483,648 至 2,147,483,647 | 42 |
uint | UInt32 | 4 字节 | 0 至 4,294,967,295 | 42u |
long | Int64 | 8 字节 | 约 ±9.2 × 10^18 | 42L |
ulong | UInt64 | 8 字节 | 0 至约 1.8 × 10^19 | 42ul |
float | Single | 4 字节 | 约 ±3.4 × 10^38,约 6 至 9 位有效数字 | 2.5f |
double | Double | 8 字节 | 约 ±1.8 × 10^308,约 15 至 17 位有效数字 | 2.5 或 2.5d |
decimal | Decimal | 16 字节 | 约 ±7.9 × 10^28,28 至 29 位有效数字 | 2.5m |
char | Char | 2 字节 | 一个 UTF-16 码元,U+0000 至 U+FFFF | 'A' |
string | String | 引用 | 任意长度的文本,不可变 | "hello" |
object | Object | 引用 | 所有类型的基类 | new object() |
不带后缀的整数字面量是 int;如果它大到 int 装不下,就是 uint、long、ulong 中第一个能容纳它的类型。带小数点的字面量是 double。这就是为什么 float f = 2.5; 和 decimal d = 2.5; 无法编译:字面量是 double,而 double 不能隐式转换为 float 或 decimal。加上后缀即可:2.5f、2.5m。
每种数值类型都以常量形式提供了自己的上下限:
输出:
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
实际代码中最常用的是其中四种:int 用于计数和索引,long 用于 ID 和大额总数,double 用于测量值和数学计算,decimal 用于金额。
decimal 与 double
double(以及 float)以二进制形式存储数字。大多数十进制小数,包括 0.1,都没有精确的二进制形式,所以存储的是最接近的可表示值,小误差会不断累积。decimal 存储的是一个十进制整数加一个小数位数,所以 0.1 就是精确的 0.1。
输出:
False
True
0.30000000000000004
0.3
double sum == 1.0: False
decimal sum == 1.0: True
Total: 59.97
Split three ways: 19.99
金额、价格、税费以及任何人们会手工相加并核对的数字,都用 decimal。它会保留输入中末尾的零(1.50m 打印为 1.50),有 28 到 29 位有效数字。
物理测量、统计、几何和游戏用 double。它快好几倍,内存只占一半,取值范围也极大。永远不要用 == 比较两个计算得到的 double,而是把它们的差与一个容差比较:Math.Abs(x - y) < 1e-9。
decimal 也不是万能的:1m / 3m 的结果仍然是 0.3333333333333333333333333333。另外,double 运算除以零不会抛出异常(结果是无穷大,0.0 / 0.0 则是 NaN),而 decimal 和整数除以零会抛出 DivideByZeroException。
整数溢出与 checked
默认情况下,超出类型范围的整数运算会悄无声息地回绕。编译器能发现常量表达式中的溢出,但值来自变量时就发现不了:
输出:
-2147483648
OverflowException
-1294967296
2147483648
checked(...) 或 checked { ... } 块会把溢出变成 OverflowException。你也可以用 <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> 为整个项目开启溢出检查。用常量写 int x = int.MaxValue + 1; 会产生编译错误(CS0220),因为编译器能看出溢出。
最后一行展示了大多数溢出 bug 的修复方法:在运算之前先转换为更宽的类型。写成 (long)(max + 1) 是错的,因为执行强制转换时,加法早已回绕了。
值类型与引用类型
C# 的每种类型要么是值类型,要么是引用类型,这个区别决定了赋值时会发生什么。
- 值类型:所有数值类型、
bool、char、DateTime、枚举以及每个struct。变量直接包含数据;赋值时复制数据。 - 引用类型:
string、数组、object以及每个class。变量包含指向对象的引用;赋值时复制引用,所以两个变量看到的是同一个对象。
输出:
a=10 b=99
first[0]=99
s1=cat s2=cats
string 是引用类型,但实际用起来像值:它不可变,而且 == 比较的是文本,而不是引用。更多关于用户自定义值类型的内容,见 结构体。
char 和 bool
char 是一个 UTF-16 码元,用单引号书写。它的底层是数值类型,所以可以转换为 int,也支持算术运算:
输出:
66
C
True
True
7
b
True
bool 只有两个值,不能与数字互相转换:if (count) 是编译错误,而不是判断是否非零。请写 if (count != 0)。基本多文种平面之外的字符(比如大多数 emoji)占用两个 char 值(一个代理对),所以 string 的 Length 统计的是 UTF-16 码元,而不是可见字符。
默认值
字段、数组元素和 default(T) 会得到该类型的默认值:数字是零,bool 是 false,char 是 '\0',引用类型是 null。局部变量没有默认值,使用之前必须先赋值。
输出:
0
0
False
True
0001-01-01
0,0,0
从 C# 7.1 开始,当编译器能推断出类型时,你可以直接写不带类型的 default 字面量(int x = default;)。值类型不能为 null,int?(可空值类型)正是为此而生;见 可空类型。
本机大小整数和大整数
还有两种用于特殊场景的数值类型。nint 和 nuint(C# 9)是与指针同样大小的整数,根据平台是 32 位或 64 位,用于互操作和底层代码。System.Numerics.BigInteger 可以保存任意大小的整数,代价是速度:
using System.Numerics;
BigInteger factorial = 1;
for (int i = 2; i <= 30; i++) factorial *= i;
// 265252859812191058636308480000000
常见问题
C# 有哪些数据类型?
内置类型包括整数类型(sbyte、byte、short、ushort、int、uint、long、ulong)、浮点类型 float 和 double、用于精确十进制运算的 decimal、表示一个 UTF-16 字符的 char、bool,以及引用类型 string 和 object。每个关键字都是一个 .NET 类型的别名:int 就是 System.Int32,string 就是 System.String。
C# 中表示金额应该用 decimal 还是 double?
用 decimal。double 存储的是二进制小数,所以像 0.1 这样的值无法精确表示,累加起来会产生偏差(0.1 + 0.2 == 0.3 的结果是 false)。decimal 存储的是十进制数字,有 28 到 29 位有效数字,能得到会计期望的结果。double 更快,取值范围也大得多,适合科学计算、图形和测量。
C# 中 int 的最大值是多少?
int.MaxValue 是 2,147,483,647,int.MinValue 是 -2,147,483,648(32 位有符号整数)。给 int.MaxValue 加 1 会悄悄回绕成 int.MinValue,除非代码在 checked 上下文中运行,那时会抛出 OverflowException。更大的值请用 long(最大约 922 亿亿)。
C# 中 float 和 double 有什么区别?
float 是 32 位,约有 6 到 9 位有效数字;double 是 64 位,约有 15 到 17 位有效数字。浮点字面量默认是 double,所以 float 需要加 f 后缀:float speed = 2.5f;。除非内存或某个 API(比如 Unity 的 Vector3)要求用 float,否则请用 double。
C# 中值类型和引用类型有什么区别?
值类型变量(int、double、bool、DateTime、任何 struct)直接保存数据本身,赋值时会复制数据。引用类型变量(string、数组、任何 class)保存的是指向堆上对象的引用,赋值时复制的是引用,所以两个变量可以指向同一个对象。