C# 语言值类型
值类型和引用类型是 C#
类型的两个主要类别。值类型的变量包含类型的实例。它不同于引用类型的变量,后者(引用类型)包含对类型实例的引用。默认情况下,在分配中,通过将实参传递给方法并返回方法结果来复制变量值。对于值类型变量,会复制相应的类型实例。 以下示例演示了该行为:
using System;
public struct MutablePoint
{
public int X;
public int Y;
public MutablePoint(int x, int y) => (X, Y) = (x, y);
public override string ToString() => $"({X}, {Y})";
}
public class Program
{
public static void Main()
{
var p1 = new MutablePoint(1, 2);
var p2 = p1;
p2.Y = 200;
Console.WriteLine($"{nameof(p1)} after {nameof(p2)} is modified: {p1}");
Console.WriteLine($"{nameof(p2)}: {p2}");
MutateAndDisplay(p2);
Console.WriteLine($"{nameof(p2)} after passing to a method: {p2}");
}
private static void MutateAndDisplay(MutablePoint p)
{
p.X = 100;
Console.WriteLine($"Point mutated in a method: {p}");
}
}
// 预计输出:
// p1 after p2 is modified: (1, 2)
// p2: (1, 200)
// Point mutated in a method: (100, 200)
// p2 after passing to a method: (1, 200)
如上面的的示例所示,对值类型变量的操作只影响存储在变量中的值类型实例。
如果值类型包含引用类型的数据成员,则在复制值类型实例时,只会复制对引用类型实例的引用。 副本和原始值类型实例都具有对同一引用类型实例的访问权限。以下示例演示了该行为:
using System;
using System.Collections.Generic;
public struct TaggedInteger
{
public int Number;
private List<string> tags;
public TaggedInteger(int n)
{
Number = n;
tags = new List<string>();
}
public void AddTag(string tag) => tags.Add(tag);
public override string ToString() => $"{Number} [{string.Join(", ", tags)}]";
}
public class Program
{
public static void Main()
{
var n1 = new TaggedInteger(0);
n1.AddTag("A");
Console.WriteLine(n1); // output: 0 [A]
var n2 = n1;
n2.Number = 7;
n2.AddTag("B");
Console.WriteLine(n1); // output: 0 [A, B]
Console.WriteLine(n2); // output: 7 [A, B]
}
}
若要使代码更不易出错、更可靠,请定义并使用不可变的值类型。本文仅为演示目的使用可变值类型。
值类型的种类以及类型约束
值类型可以是以下种类之一:
- 结构类型,用于封装数据和相关功能
- 枚举类型,由一组命名常数定义,表示一个选择或选择组合
可为 null
值类型 T?
表示其基础值类型 T
的所有值及额外的 null
值。 不能将 null
分配给值类型的变量,除非它是可为 null
的值类型。
你可使用 struct
约束指定类型参数为不可为 null
的值类型。 结构类型和枚举类型都满足 struct
约束。 从 C# 7.3
开始,你可以在基类约束中使用 System.Enum
(称为枚举约束),以指定类型参数为枚举类型。
内置值类型
C# 提供以下内置值类型,也称为“简单类型”:
- 整型数值类型
- 浮点型数值类型
- bool,表示布尔值
- char,表示 Unicode UTF-16 字符
所有简单值类型都是结构类型
,它们与其他结构类型的不同之处在于,它们允许特定的额外操作:
- 可以使用文字为简单类型提供值。例如,
'A'
是类型char
的文本,2001
是类型int
的文本。 - 可以使用
const
关键字声明简单类型的常数。不能声明具有其他结构类型的常数。 - 常数表达式的操作数都是简单类型的常数,在编译时进行评估。
从 C# 7.0
开始,C# 支持值元组
。 值元组是值类型,而不是简单类型。
整型数值类型
整型数值类型表示整数
。所有的整型数值类型均为值类型
,也是简单类型,可以使用文本进行初始化。所有整型数值类型都支持算术
、位逻辑
、比较
和相等
运算符。
C# 支持以下预定义整型类型:
类型别名 | 范围 | 说明 | 类型名称 |
---|---|---|---|
sbyte | -128 到 127 | 8 位带符号整数 | System.SByte |
byte | 0 到 255 | 无符号的 8 位整数 | System.Byte |
short | -32,768 到 32,767 | 有符号 16 位整数 | System.Int16 |
ushort | 0 到 65,535 | 无符号 16 位整数 | System.UInt16 |
int | -2,147,483,648 到 2,147,483,647 | 带符号的 32 位整数 | Syste,.Int32 |
uint | 0 到 4,294,967,295 | 无符号的 32 位整数 | System.UInt32 |
long | -9,223,372,036,854,775,808到9,223,372,036,854,775,807 | 64 位带符号整数 | Sustem.Int64 |
ulong | 0 到18,446,744,073,709,551,615 | 无符号 64 位整数 | System.UInt64 |
nint | 取决于平台 | 带符号的 32 位或 64 位整数 | System.IntPtr |
nuint | 取决于平台 | 无符号的 32 位或 64 位整数 | System.UIntPtr |
在除最后两行之外的所有表行中,最左侧列中的每个 C# 类型关键字都是相应 .NET
类型的别名。 关键字和 .NET
类型名称是可互换的。 例如,以下声明声明了相同类型的变量:
int a = 123;
System.Int32 b = 123;
每个整型类型的默认值都为零 0
。除本机大小的类型外,每个整型类型都有 MinValue
和 MaxValue
常量,提供该类型的最小值和最大值。System.Numerics.BigInteger
结构用于表示没有上限或下限的带符号整数。
整数文本
整数文本可以是:
- 十进制:不使用任何前缀
- 十六进制:使用
0x
或0X
前缀 - 二进制:使用
0b
或0B
前缀(在 C# 7.0 和更高版本中可用)
下面的代码演示每种类型的示例:
var decimalLiteral = 42;
var hexLiteral = 0x2A;
var binaryLiteral = 0b_0010_1010;
上面的示例还演示了如何将 _
用作数字分隔符(从 C# 7.0 开始提供支持)。可以将数字分隔符用于所有类型的数字文本。
整数文本的类型由其后缀确定,如下所示:
- 如果文本没有后缀,则其类型为以下类型中可表示其值的第一个类型:
int
、uint
、long
、ulong
。 - 如果文本以
U
或u
为后缀,则其类型为以下类型中可表示其值的第一个类型:uint
、ulong
。 - 如果文本以
L
或l
为后缀,则其类型为以下类型中可表示其值的第一个类型:long
、ulong
。 - 如果文本的后缀为
UL
、Ul
、uL
、ul
、LU
、Lu
、lU
或lu
,则其类型为ulong
。
注意:可以使用小写字母 l 作为后缀。 但是,这会生成一个编译器警告,因为字母 l 可能与数字 1 混淆。 为清楚起见,请使用 L 。
如果由整数字面量所表示的值超出了 UInt64.MaxValue
,则将出现编译器错误 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;
数值类型转换
可以将任何整型数值类型转换为其他整数数值类型。如果目标类型可以存储源类型的所有值,则转换是隐式的。否则,需要使用强制转换表达式来执行显式转换。
浮点数值类型
浮点数值类型表示实数
。所有浮点型数值类型均为值类型
,也是简单类型,可以使用文本进行初始化。 所有浮点数值类型都支持算术
、比较
和相等
运算符。
C# 支持以下预定义浮点类型:
类型别名 | 范围 | 说明 | 大小 | 类型名称 |
---|---|---|---|---|
float | ±1.5 x 10(-45次方)次方 到 ±3.4 x 10(38次方) | 大约 6-9 位数字 | 4 个字节 | System.Single |
double | ±5.0 × 10(-324次方)到 ±1.7 × 10(308次方) | 大约 15-17 位数字 | 8 个字节 | System.Double |
decimal | ±1.0 x 10(-28次方)到 ±7.9228 x 10(28次方) | 28-29 位 | 16 个字节 | System.Decimal |
在上表中,最左侧列中的每个 C# 类型关键字都是相应 .NET
类型的别名。它们是可互换的。例如,以下声明声明了相同类型的变量:
double a = 12.3;
System.Double b = 12.3;
每个浮点类型的默认值都为零 0
。每个浮点类型都有 MinValue
和 MaxValue
常量,提供该类型的最小值和最大有限值。float
和 double
类型还提供可表示非数字和无穷大值的常量。 例如,double
类型提供以下常量:
Double.NaN
Double.NegativeInfinity
Double.PositiveInfinity
当所需的精度由小数点右侧的位数决定时, decimal
类型是合适的。 此类数字通常用于财务应用程序、货币金额(例如 $1.00)、利率(例如 2.625%)等。精确到只有一个小数的偶数用 decimal
类型处理会更准确,例如:0.1
可以由 decimal
实例精确表示,而没有精确表示 0.1
的 double
或 float
实例。
由于数值类型存在这种差异,因此当你对十进制数据使用 double
或 float
时,算术计算可能会出现意外的舍入错误。 当性能比精度更重要时,可以使用 double
代替 decimal
。然而,除了大多数计算密集型应用程序之外,所有应用程序都不会注意到性能上的任何差异。避免使用 decimal
的另一个可能原因是为了最大限度地降低存储需求。 例如,ML.NET
使用 float
,因为对于非常大的数据集,4
个字节与 16
个字节之间的差异还是很明显的。
可在表达式中将整型类型
与 float
和 double
类型混合使用。在这种情况下,整型类型隐式转换为其中一种浮点类型,且必要时, float
类型隐式转换为 double
。 此表达式的计算方式如下:
- 如果表达式中有
double
类型,则表达式在关系比较和相等比较中求值得到double
或bool
。 - 如果表达式中没有
double
类型,则表达式在关系比较和相等比较中求值得到float
或bool
。
你还可在表达式中混合使用整型类型
和 decimal
类型。在这种情况下,整型类型隐式
转换为 decimal
类型,并且表达式在关系比较和相等比较中求值得到 decimal
或 bool
。
不能在表达式中将 decimal
类型与 float
和 double
类型混合使用。在这种情况下,如果你想要执行算术运算、比较运算或相等运算,则必须将操作数显式
转换为 decimal
或反向转换,如下例所示:
double a = 1.0;
decimal b = 2.1m;
Console.WriteLine(a + (double)b);
Console.WriteLine((decimal)a + b);
浮点文本
浮点文本的类型由其后缀确定,如下所示:
- 不带后缀的文本或带有
d
或D
后缀的文本的类型为double
- 带有
f
或F
后缀的文本的类型为float
- 带有
m
或M
后缀的文本的类型为decimal
下面的代码演示每种类型的示例:
double d = 3D;
d = 4d;
d = 3.934_001;
float f = 3_000.5F;
f = 5.4f;
decimal myMoney = 3_000.5m;
myMoney = 400.75M;
上面的示例还演示了如何将 _
用作数字分隔符(从 C# 7.0
开始提供支持)。可以将数字分隔符用于所有类型的数字文本。
还可以使用科学记数法,即指定真浮点文本的指数部分,如以下示例所示:
double d = 0.42e2;
Console.WriteLine(d); // 输出 42
float f = 134.45E-2f;
Console.WriteLine(f); // 输出: 1.3445
decimal m = 1.5E6m;
Console.WriteLine(m); // 输出: 1500000
浮点类型转换
浮点数值类型之间只有一种隐式转换:从 float
到 double
。 但是,可以使用显式强制转换将任何浮点类型转换为任何其他浮点类型。
布尔类型
bool
类型关键字是 .NETSystem.Boolean
结构类型的别名,它表示一个布尔值,可为 true
或 false
。
若要使用 bool
类型的值执行逻辑运算,请使用布尔逻辑运算符。 bool
类型是 比较
和相等
运算符的结果类型。bool 表达式可以是 if
、do
、while
和 for
语句中以及条件运算符 ?:
中的控制条件表达式。
bool
类型的默认值为 false
。
初始化
可使用 true
和 false
来初始化 bool
变量或传递 bool
值:
bool check = true;
Console.WriteLine(check ? "Checked" : "Not checked"); // 输出: Checked
Console.WriteLine(false ? "Checked" : "Not checked"); // 输出: Not checked
char类型
char
类型关键字是 .NETSystem.Char
结构类型的别名,它表示 Unicode UTF-16
字符。
char
类型的默认值为 \0
,即 U+0000
。
char
类型支持比较、相等、增量和减量运算符。此外,对于 char
操作数,算数和逻辑位运算符对相应的字符代码执行操作,并得出 int
类型的结果。
字符串类型将文本表示为 char
值的序列。
char文本
可以使用以下命令指定 char
值:
- 字符文本。
Unicode
转义序列,它是\u
后跟字符代码的十六进制表示形式(四个符号)。十六进制转义序列,它是
\x
后跟字符代码的十六进制表示形式。var chars = new[]
{
'j',
'\u006A',
'\x006A',
(char)106,
};
Console.WriteLine(string.Join(" ", chars)); // 输出: j j j j
如上面的示例所示,你还可以将字符代码的值转换为相应的 char
值。
- 对于
Unicode
转义序列,必须指定全部四位十六进制值。也就是说,\u006A
是一个有效的转义序列,而\u06A
和\u6A
是无效的。 - 对于十六进制转义序列,可以省略前导零。 也就是说,
\x006A
、\x06A
和\x6A
转义序列是有效的,并且对应于同一个字符。
char类型转换
char
类型可隐式转换为以下整型类型: ushort
、 int
、 uint
、 long
和 ulong
。它也可以隐式转换为内置浮点数值类型: float
、 double
和 decimal
。它可以显式转换为 sbyte
、 byte
和 short
整型类型。
无法将其他类型隐式转换为 char
类型。 但是,任何整型或浮点数值类型都可显式转换为 char
。
枚举类型
枚举类型是由基础整型数值类型的一组命名常量定义的值类型。若要定义枚举类型,请使用 enum
关键字并指定枚举成员 的名称:
enum Season
{
Spring,
Summer,
Autumn,
Winter
}
默认情况下,枚举成员的关联常数值为类型 int
。它们从零
开始,并按定义文本顺序递增 1
。可以显式指定任何其他整数数值类型作为枚举类型的基础类型。还可以显式指定关联的常数值,如下面的示例所示:
enum ErrorCode : ushort
{
None = 0,
Unknown = 1,
ConnectionLost = 100,
OutlierReading = 200
}
不能在枚举类型的定义内定义方法。若要向枚举类型添加功能,请创建扩展方法。
枚举类型 E
的默认值是由表达式 (E)0
生成的值,即使零
没有相应的枚举成员也是如此。可以使用枚举类型,通过一组互斥值或选项组合来表示选项。若要表示选项组合,请将枚举类型定义为位标志。
作为位标志的枚举类型
如果希望枚举类型表示选项组合,请为这些选项定义枚举成员,以便单个选项成为位字段。也就是说,这些枚举成员的关联值应该是2 的幂
。然后,可以使用按位逻辑运算符 |
或 &
分别合并选项或交叉组合选项。若要指示枚举类型声明位字段,请对其应用 Flags
属性。 如下面的示例所示,还可以在枚举类型的定义中包含一些典型组合。
[Flags]
public enum Days
{
None = 0b_0000_0000, // 0
Monday = 0b_0000_0001, // 1
Tuesday = 0b_0000_0010, // 2
Wednesday = 0b_0000_0100, // 4
Thursday = 0b_0000_1000, // 8
Friday = 0b_0001_0000, // 16
Saturday = 0b_0010_0000, // 32
Sunday = 0b_0100_0000, // 64
Weekend = Saturday | Sunday
}
public class FlagsEnumExample
{
public static void Main()
{
Days meetingDays = Days.Monday | Days.Wednesday | Days.Friday;
Console.WriteLine(meetingDays);
// 输出:
// Monday, Wednesday, Friday
Days workingFromHomeDays = Days.Thursday | Days.Friday;
Console.WriteLine($"Join a meeting by phone on {meetingDays & workingFromHomeDays}");
// 输出:
// Join a meeting by phone on Friday
bool isMeetingOnTuesday = (meetingDays & Days.Tuesday) == Days.Tuesday;
Console.WriteLine($"Is there a meeting on Tuesday: {isMeetingOnTuesday}");
// 输出:
// Is there a meeting on Tuesday: False
var a = (Days)37;
Console.WriteLine(a);
// 输出:
// Monday, Wednesday, Saturday
}
}
System.Enum 类型和枚举约束
System.Enum
类型是所有枚举类型的抽象基类。它提供多种方法来获取有关枚举类型及其值的信息。
从 C# 7.3
开始,你可以在基类约束中使用 System.Enum
(称为枚举约束),以指定类型参数为枚举类型。 所有枚举类型也都满足 struct
约束,此约束用于指定类型参数为不可为 null
的值类型。
枚举类型转换
对于任何枚举类型,枚举类型与其基础整型类型之间存在显式转换。 如果将枚举值转换为其基础类型,则结果为枚举成员的关联整数值。
public enum Season
{
Spring,
Summer,
Autumn,
Winter
}
public class EnumConversionExample
{
public static void Main()
{
Season a = Season.Autumn;
Console.WriteLine($"Integral value of {a} is {(int)a}");
// 输出: Integral value of Autumn is 2
var b = (Season)1;
Console.WriteLine(b); // output: Summer
var c = (Season)4;
Console.WriteLine(c); // output: 4
}
}
使用 Enum.IsDefined
方法来确定枚举类型是否包含具有特定关联值的枚举成员。
对于任何枚举类型,都存在分别与 System.Enum
类型的装箱和取消装箱相互转换。
结构类型
结构类型(“structuretype
”或“struct type
”)是一种可封装数据和相关功能的值类型。使用 struct
关键字定义结构类型。由于篇幅原因,会在后续章节中讲解。
元组类型
元组功能在 C# 7.0
及更高版本中可用,它提供了简洁的语法,用于将多个数据元素分组成一个轻型数据结构。由于篇幅原因,会在后续章节中讲解。
可为空的值类型
可为 null
值类型 T?
表示其基础值类型 T
的所有值及额外的 null
值。 例如,可以将以下三个值中的任意一个指定给 bool?
变量: true
、 false
或 null
。 基础值类型 T
本身不能是可为空的值类型。由于篇幅原因,会在后续章节中讲解。