类型
C#是一种强类型语言,每个变量和常量都有一个类型,每个求值的表达式也是如此。
类型定义了数据的结构和行为。类型声明可以包含其成员、基类型、实现的接口和该类型允许的操作。
编译器将类型信息作为元数据嵌入可执行文件中。CLR在运行时使用元数据,以在代码中执行的所有操作(分配、使用和回收内存)都是类型安全的。类型中可存储的信息包括:
- 类型变量所需的存储空间。
- 可以表示的最大值和最小值。
- 对该类型定义的任何特性。
- 类型的可访问性。
- 继承自的基类型。
- 实现的接口。
- 包含的成员(方法、字段等)。
- 允许执行的运算种类。
通用类型系统
通用类型系统(Common Type System,CTS)定义了如何在CLR中声明、使用和管理类型,同时也是运行时跨语言集成支持的一个重要组成部分。CTS具有以下功能:
- 建立一个支持跨语言集成、类型安全和高性能代码执行的框架。
- 提供一个支持完整实现多种编程语言的面向对象的模型。
- 定义各语言必须遵守的规则,有助于确保用不同语言编写的对象能够交互作用。
- 提供包含应用程序开发中使用的基元数据类型的库。
.NET类库定义了内置数值类型和表示各种构造的复杂类型。这些类型支持继承原则,并且被定义为值类型(Value types)、引用类型(Reference types)或者指针类型(Pointer types)。此外,.NET允许使用struct
、enum
、class
、interface
和record
来创建自定义类型。
值类型存储数据的值,引用类型则存储数据的地址(引用),因此修改引用类型会改变所有持有该引用的对象的值。
int a = 10;
int b = a;
b = 20; // a不变
int[] array = new int[1, 2];
int[] array2 = array;
array2[0] = 10; // array[0]改变
值类型
值类型派生自System.ValueType
(派生自System.Object
),直接存储其值。值类型总是分配在它声明的地方,作为局部变量存储在栈上,作为类的字段则跟随类存储在堆中。
值类型分为struct
和enum
两类。内置的数值类型都属于struct
,具有可访问的字段和方法,是非聚合类型,可为其声明并赋值。
值类型已密封,无法派生类型,只能通过struct
自定义值类型。
值类型进一步分为简单类型(整型sbyte
,short
,int
,long
,byte
,ushort
,uint
,ulong
;浮点float
,double
,decimal
;布尔bool
;字符char
)、枚举enum
、结构体struct
、元组、可为null值类型。所有简单类型都是struct
类型;可以使用文本为简单类型初始化,可以用const
定义声明简单类型的常量。
整型
整型表示整数,默认类型为int
,默认值为0
,且具有MinValue
和MaxValue
属性。
整数文本可以是十进制、十六进制(前缀为0x
或0X
)、二进制(前缀为0b
或0B
)。整数文本类型由其后缀确定:若无后缀,则其类型为以下可表示其值的第一个类型int
、uint
、long
、ulong
;若后缀为U
或u
,则其类型依次为uint
、ulong
;若为L
或l
,则其类型依次为long
、ulong
;若为UL
或LU
(包含其他大小写形式),则其类型为ulong
。可将_
用作数字分隔符。数字分隔符可用于所有类型的数字文本。
var decimalLiteral = 42;
var hexLiteral = 0x2A;
var binaryLiteral = 0b_0010_1010;
可以将任何整数类型转换为其他整数类型。如果目标类型可以存储源类型的值,则转换为隐式(范围小的转换为大的);否则,需用显示转换(范围大的转化为范围小的,溢出风险)。任意整型可以通过ToString()
转换为字符串类型;字符串类型可以通过Parse()
和TryParse()
转换为相应整型,前提是字符串符合整型格式。
浮点型
浮点型表示实数,默认类型为double
,默认值为0
,且具有MinValue
和MaxValue
属性。float
和double
还提供可表示非数字和无穷大的属性。浮点数通常是不准确的,并且这种不精确会随着运算次数的增加而增加。
实数文本亦由其后缀决定:不带后缀或后缀为d
或D
的为double
;为f
或F
的为float
,为m
或M
的为decimal
。可将_
用作数字分隔符。数字分隔符可用于所有类型的数字文本。
double d = 2.5;
float f = 3.6f;
decimal dec = 1_000.75m;
在表达式中,整型可以和float
和double
混合使用,此时整型会隐式转换为其中一种浮点类型:若表达式中有double
,表达式结果为double
或bool
;若表达式中无double
,则结果为float
或bool
。整型也可与decimal
混合使用,此时整型会隐式转化为decimal
,表达式结果为decimal
或bool
。decimal
不能和float
或double
混合使用,需要先进行显示转换。
浮点型之间只有一种隐式转换:float
到double
,但可将任何浮点类型显示转换为其他浮点类型。任意浮点型可以通过ToString()
转换为字符串类型;字符串类型可以通过Parse()
和TryParse()
转换为相应浮点型,前提是字符串符合浮点型格式。decimal
可以通过To[类型]()
方法转化为相应类型。
布尔
布尔类型bool
(System.Boolean
)表示一个布尔值,可为true
或false
,默认值为false
,1字节。可用true
和false
初始化bool
变量或传递bool
值。
bool
可用布尔逻辑运算符执行逻辑运算,是比较运算符的结果类型。bool
表达式可以是if
、do
、while
、for
语句以及条件运算符?:
中的控制条件表达式。
任意布尔类型可以通过ToString()
转换为True或False字符串;任何大小写形式的True或False字符串都可以通过Parse()
和TryParse()
转换为相应布尔值。
字符
字符类型char
(System.Char
)表示Unicode UTF-16字符,默认值为\0
,2字节,范围为U+0000到U+FFFF。
每⼀个字符都对应⼀个整数,遵循ASCII表。可用字符文本、Unicode转义序列(\u
后跟字符代码的十六进制表示形式,四个符号)、十六进制转义序列(\x
后跟字符代码的十六进制表示形式,四个符号)和整数(每个Unicode字符代码的值)为char
指定值,除整数外其他形式需要被单引号'
包裹。
var chars = new[] { 'j', '\u006A', '\x006A', (char)106 };
Console.WriteLine(string.Join(" ", chars));
// output: j j j j
char
支持比较运算符。对于算术和位运算符,会对相应的字符代码执行操作,并得出int
结果。
char
可隐式转换为ushort
、int
、uint
、long
和ulong
,也可隐式转换为float
、double
和decimal
。char
可以显示转换为sbyte
、byte
和short
。无法将其他类型隐式转换为char
,但可以将整型或浮点型显示转换为char
。字符类型可以通过ToString()
转换为仅包含一个字符的字符串;可以通过Parse()
和TryParse()
将仅包含一个字符的字符串转换为字符类型。
枚举
枚举类型是被命名的、由一组基础整型常量定义的集合,使用enum
关键字并指定枚举类型名称和枚举成员以声明枚举。枚举名通常以E
或E_
开头。枚举不能继承或传递继承,不能在方法语句块中声明。
// 声明枚举
enum ESeason
{
Spring,
Summer,
Autumn,
Winter
}
// 声明枚举变量
ESeason currentSeason = ESeason.Spring;
默认情况下,枚举类型关联常数类型为int
,从零开始并按文本定义顺序依次递增1。可以显示指定任意整型作为枚举类型的基础类型和关联常数。
enum ErrCode : ushort
{
None, // 关联常数为0
Unknow, // 关联常数为1
Error = 100,
ConectionLost // 关联常数为101
}
不能在枚举中定义方法,只能使用扩展方法为枚举类型添加功能。
枚举类型可以使用一组互斥的值或选项组合来表示选项。若要表示选项组合,需用位标记特性[Flags]
标记枚举类型,成员的关联值应是2的幂。这样可使用位逻辑运算符|
或&
合并选择或交叉组合选项。
[Flags]
enum Days
{
None = 0, // 0b_0000_0000
Monday = 1, // 0b_0000_0001
Tuesday = 2, // 0b_0000_0010
Wednesday = 4, // 0b_0000_0100
Thursday = 8, // 0b_0000_1000
Friday = 16, // 0b_0001_0000
Saturday = 32, // 0b_0010_0000
Sunday = 64 // 0b_0100_0000
Weekend = Saturday | Sunday; // 0b_0110_0000
}
Days MeetingDays = Monday | Wednesday | Fruday; // 0b_0001_0101
Days WorkingAtHomeDays = Thursday | Friday; // 0b_0001_1000
Days MeetingAtHomeDay = MeetingDays & WorkingAtHomeDays; // 0b_0001_0000
对于任意枚举类型,与其基础整型之间存在显示转换。使用ToString()
可以将枚举类型转换成字符串,其中如果枚举类型没有使用[Flages]
标记,则返回选项名称的字符串。若使用了[Flags]
标记,对于选项返回名称;对于选项组合,若枚举类型中定义了该组合,则返回选项组合的名称,未定义则返回其数值形式的字符串。使用Parse()
和TryParse()
可以将一个或多个枚举选项的名称或数值的字符串转换为相应的枚举类型。
结构体
结构体是一种可以封装数据和相关方法(功能)的值类型,使用struct
关键字定义。结构体可以包含字段、属性、含参构造函数(无参构造函数默认是自动定义的,且不能被改变)、方法、索引、运算符方法和事件。
结构体中声明的字段不能直接初始化,并且不能为自身类型。
构造函数没有返回值,名称必须与结构体或类的名字相同,通常用于初始化,可以重载。对于结构体,若声明了构造函数,则必须在构造函数内对全字段赋值。结构体不能包含显示无参构造函数和析构函数。
struct Color
{
// 字段和属性
private int r;
public int R
{
get { return r; }
set { r = value; }
}
public int G { get; private set; }
public int B { get; private set; }
// 构造函数
public Color(int r, int g, int b)
{
this.r = r;
G = g;
B = b;
}
}
使用new
操作符创建一个结构体对象时,会调用适当的构造函数来创建结构,而不分配堆内存。与类不同,结构体可以不使用new
即可被实例化。如果不使用new
,只有在所有的普通字段都被初始化之后,才能调用方法。
结构体不能继承其他的结构或类,也不能作为其他结构体或类的基础结构。结构体可实现接口。结构体成员不能指定为 abstract
、virtual
或protected
。
结构体通常用于封装重数据、轻行为的数据结构,如颜色、坐标等。
元组
元组类型(Tuple)提供了封装少量不同类型数据元素的功能,使用()
包裹数据元素即可声明元组类型,声明时需要指定其所有数据成员的类型。可以在元组初始化表达式或元组类型定义中显式指定字段名;如果没有指定,则可以从初始化表达式中相应变量的名称中推断出字段名。元组字段的默认名称是Item1
、Item2
等,即使显式或推断指定了字段名也可以使用默认名称。
(double, int) t1 = (4.5, 3);
var t = (Sum: 4.5, Count: 3);
Console.WriteLine($"Sum of {t.Count} elements is {t.Sum}.");
var sum = 4.5;
var count = 3;
t = (sum, count);
Console.WriteLine($"Sum of {t.count} elements is {t.sum}.");
var a = 1;
t = (a, b: 2, 3);
Console.WriteLine($"The 1st element is {t.Item1} (same as {t.a}).");
Console.WriteLine($"The 2nd element is {t.Item2} (same as {t.b}).");
Console.WriteLine($"The 3rd element is {t.Item3}.");
元组是值类型,支持==
和!=
运算符。其元素是公共字段,这使得元组成为可变值类型。可以用任意数量的元素定义元组。
(double, int) t1 = (4.5, 3);
var t = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26);
Console.WriteLine(t.Item26); // output: 26
var t = (Sum: 4.5, Count: 3);
Console.WriteLine($"Sum of {t.Count} elements is {t.Sum}.");
var sum = 4.5;
var count = 3;
var t = (sum, count);
Console.WriteLine($"Sum of {t.count} elements is {t.sum}.");
C#支持两个元素数量相同且各元素可隐式转换的元组之间的赋值。元组元素值按照元组元素的顺序赋值,忽略字段名。C#支持两个元素数量相同且各元素按顺序支持比较的元组之间的比较(==
和!=
),比较时也不考虑字段名。==
和!=
运算符以短路的方式比较元组,即一旦遇到一对不相等的元素或到达元组的末端,操作就会停止。然而,在进行比较之前,所有元组元素都会被求值。
(int, double) t1 = (17, 3.14);
(double First, double Second) t2 = (0.0, 1.0);
t2 = t1;
元组通常用于方法返回结果,即不用定义out
参数即可获取多个不同类型的结果。
需要注意,元组与System.Tuple
不同:
- 元组是值类型,
System.Tuple
是引用类型。 - 元组是可变的,
System.Tuple
是不可变的。 - 元组的数据成员是字段,
System.Tuple
的数据成员是属性。
可为null值类型
可为null值类型T?
表示其底层值类型T
的所有值和一个附加的null
。例如,可以将true
、false
或null
中的任何一个赋值给bool?
变量。底层值类型T
本身不能为可为null值类型。
任何可为null值类型都是泛型System.Nullable<T>
的实例,因此可以以Nullable<T>
或T?
的形式引用可为null值类型。
由于值类型可以隐式地转换为相应的可为null值类型,因此可以将值分配给可为null值类型的变量,就像对其底层值类型那样。此外,还可以赋值null
。可为null值类型的默认值为null
,
double? pi = 3.14;
char? letter = null;
int m2 = 10;
int? m = m2;
bool? flag = null;
int?[] arr = new int?[10];
可以使用is
、与null
进行比较或HasValue
确定可为null值类型变量是否具有其底层类型的值,当HasValue
为true
时使用Value
获取其底层类型的值。
int? a = 10;
if (a.HasValue) // 等价于if (a is int)或if (a != null)
{
Console.WriteLine(a.Value);
}
else
{
Console.WriteLine("a = null");
}
可以使用??
运算符或GetValueOrDefault()
方法将可为null值类型的值分配给值类型。
int? a = null;
int b = a ?? -1;
值类型支持的运算符也支持相应的可为null值类型,但如果一个或两个操作数为null
则结果为null
。
引用类型
引用类型只存储对数据(称为“对象”)的引用(存储在栈或堆上),而引用指向的数据(数据、类型对象指针、同步块索引)存储在堆内存,即会进行堆分配,具有垃圾回收开销。 声明引用类型时,其默认值为null
,直到分配或new
运算符创建一个该类型的实例。
引用类型进一步可分为字符串string
、数组、对象object
、类class
、接口interface
、委托delegate
、dynamic
。
字符串
字符串string
类型,即System.String
,派生自object
,用于表示字符的有序集合,是一个引用类型,其对象值存储在托管堆中。string
类型允许给变量分配任何字符串值。string
在声明但未赋值时为null
,区别于空字符串""
。
因为string
是char
的有序集合,其长度Length
就是字符数组的字符个数。⼀个string
可以当做⼀个char
数组。因此可以通过索引[]
访问其中指定位置字符或使用foreach
遍历字符串。使用==
或!=
比较时比较其值,即所有位置上的字符是否都相等,而不是引用。
string
不允许使用new string()
的方式创建实例。
string
类型的值可以通过两种文本形式进行分配:引号和@
引号。@
(称作逐字字符串)将转义字符\
当作普通字符对待。@
字符串中可以任意换行,换行符及缩进空格都计算在字符串长度之内。
string str = "runoob.com";
string str = @"C:\Windows";
字符串可以使用+
进行拼接。在拼接时,会自动调用操作数的ToString()
方法。此外,字符串可以使用格式字符串和内插字符串进行拼接。
string str = "" + 1 + 2;
string.Format("{0}{1}{2}", 1, 2, 3);
str = $"{1}{2}{3}";
string
对象是不可变 (只读) 的。字符串创建后,存储在静态存储区中,不能修改其值。用于修改其值的方法实际上会返回一个新的string
对象。
由于字符串的不可变性,在大量使用字符串操作时,会导致创建大量的字符串对象,带来极大的性能损失。为此string
具有驻留性:相同的字符串在内存(堆)中只分配一次,第二次申请字符串时,发现已经有该字符串便直接返回已有字符串的地址。因此,多个变量指向同一字符串时,静态存储区仅存在一个副本,这些变量具有同样的引用。
字符串驻留基本原理:CLR初始化时在内存创建驻留池。驻留池本质是一个哈希表,存储被驻留的字符串及其内存地址。驻留池是进程级别的,多个AppDomain共享,不受GC控制,生命周期随进程。当分配字符串时,首先到驻留池中查找,如找到,则返回已有相同字符串的地址,不会创建新字符串对象;如果没有找到,则创建新字符串,并把字符串添加到驻留池中。当然,为了防止大量字符串都驻留到内存里而得不到释放造成内存爆炸,并非所有字符串都会驻留,只有通过IL指令ldstr
创建的字符串才会留用。string
类提供两个静态方法:
String.Intern(String)
可以主动驻留一个字符串。String.IsInterned(String)
检测指定字符串是否驻留,如果驻留则返回字符串,否则返回null
。
在使用字符串时,尽量:
- 在使用线程锁的时候,不要锁定一个字符串对象,因为字符串的驻留性,可能会引发不可以预料的问题。
- 理解字符串的不变性,尽量避免产生额外字符串。
- 在处理大量字符串连接的时候,尽量使用
StringBuilder
,在使用StringBuilder
时,尽量设置一个合适的长度初始值。 - 少量字符串连接建议使用
String.Concat()
和String.Join()
代替。
数组
数组是内存中一块线性连续的区域,用于存储一组相同类型的多个变量。数组是这些变量的有序、有限集合。这些变量拥有相同的标识,通过索引访问。通过指定数组的元素类型来声明数组。如果希望数组存储任意类型的元素,可将其类型指定为object
。
- 数组可是一/多维的。
- 在创建数组实例时,必须确定维度数量和维度长度(即元素数量),且在实例的生存期内无法更改,同时元素均初始化为元素类型的默认值:数值数组元素默认值为
0
,布尔类型默认值为false
,引用元素默认值为null
。 - 数组索引从
0
开始,通过[]
访问元素。 - 元素可以是任何类型,包括数组类型和自定义类型。
- 数组类型是从抽象的基类型
Array
派生的引用类型,所有数组都会实现IList
和IEnumerable
,一维数组还实现IList<T>
和IEnumerable<T>
。 foreach
可以按行简单、明了的循环遍历数组。- 数组在作实参传递到方法时,传递的是引用,方法内部对数组的操作会改变原数组。
- 可以使用
var
关键字声明、创建隐式类型数组,数组实例类型通过初始化中的元素类型推断。初始化中的隐式类型数组不需要使用var
关键字。
一维数组通过new
运算符指定数组类型和元素数量。可在声明数组时初始化数组的元素,此时不需要说明长度(根据初始化列表中的元素数量推断得出)。在声明并初始化数组时,可省略new
表达式和数组类型,即隐式类型化数组。在尚未创建数组实例的情况下初始化时必须使用new
运算符向此变量分配新数组。
// 数组声明与创建实例
type[] arrayName;
arrayName = new type[length];
type[] arrayName = new type[length];
// 在声明数组时并初始化值,可省略长度
type[] arrayName = new type[length] { value1, value2 }; // 容量显示声明
type[] arrayName = new type[] { value1, value2 }; // 容量隐式分配
// 隐式类型化数组
type[] arrayName = { value1, value2 };
// 非声明时初始化必须用new运算符
int[] array3;
array3 = new int[] { 1, 3, 5, 7, 9 }; // OK
// Error: array3 = {1, 3, 5, 7, 9};
多维数组的声明与初始化与一维数组相同,使用,
分隔各维元素。
// 二维数组声明
int[,] int2DArray = new int[2,2];
int2DArray[0,0] = 1;
交错数组的每个元素是一维数组,其长度可能不同。由于交错数组是数组的数组,因此其元素为引用类型且被初始化为null
。
// 交错数组声明
int[][] intCrossArray;
// 创建实例
intCrossArray = new int[10][];
// 初始化:必须初始化后才能使用
intCrossArray[0] = new int[10];
intCrossArray[1] = new int[] { 1, 2 };
// 可以在声明时初始化
int[][] intCrossArray2 = new int[][]
{
new int[] { 1, 2 },
new int[] { 3 };
}