C# 语言类型系统
C# 是一种强类型语言。 每个变量
和常量
都有一个类型,每个求值的表达式也是如此。 每个方法声明都为每个输入参数和返回值指定名称、参数数量以及类型和种类(值
、引用
或输出
)。 .NET
类库定义了一组内置数值类型以及表示各种逻辑构造的更复杂类型(如文件系统、网络连接、对象的集合和数组以及日期)。C#
程序可以使用类库中的类型,以及用户自定义得类型。
类型中可存储的信息包括以下项:
- 类型变量所需的存储空间。
- 可以表示的最大值和最小值。
- 包含的成员(方法、字段、事件等)。
- 继承自的基类型。
- 它实现的接口。
- 允许执行的运算种类。
编译器使用类型信息来确保在代码中执行的所有操作都是类型安全的。 例如,如果声明 int
类型的变量,那么编译器允许在加法和减法
运算中使用此变量。如果尝试对 bool
类型的变量执行这些相同操作,则编译器将生成错误,如以下示例所示:
int a = 5;
int b = a + 2; //OK
bool test = true;
// Error. Operator '+' cannot be applied to operands of type 'int' and 'bool'.
int c = a + test;
注意:C 和 C++ 开发人员请注意,在 C# 中, bool 不能转换为 int 。
编译器将类型信息作为元数据嵌入可执行文件中。公共语言运行时 (CLR
) 在运行时使用元数据,以在分配和回收内存时进一步保证类型安全性。
在变量声明中指定类型
当在程序中声明变量
或常量
时,必须指定其类型
或使用 var
关键字让编译器推断类型。以下示例显示了一些使用内置数值类型和复杂用户定义类型的变量声明:
// Declaration only:
float temperature;
string name;
MyClass myClass;
// Declaration with initializers (four examples):
char firstLetter = 'C';
var limit = 3;
int[] source = { 0, 1, 2, 3, 4, 5 };
var query = from item in source
where item <= limit
select item;
方法声明指定方法参数的类型
和返回值
。以下示例显示了需要 int
作为输入参数并返回字符串
的方法:
public string GetName(int ID)
{
if (ID < names.Length)
return names[ID];
else
return String.Empty;
}
private string[] names = { "Spencer", "Sally", "Doug" };
声明变量后,不能使用新类型重新声明该变量,并且不能分配与其声明的类型不兼容的值。 例如,不能声明 int
后再向它分配 true
的布尔值。 不过,可以将值转换成其他类型。例如,在将值分配给新变量或作为方法自变量传递时。编译器会自动执行不会导致数据丢失的类型转换。如果类型转换可能会导致数据丢失,必须在源代码中进行 显式转换
。
内置类型``
C# 提供了一组标准的内置类型来表示整数
、浮点值
、布尔表达式
、文本字符
、十进制值
和其他数据类型
。 还有内置的 string
和 object
类型。 这些类型可供在任何 C#
程序中使用。 有关内置类型的完整列表。
下表列出了 C#
内置值类型
:
类型别名 | 类型 |
---|---|
bool | System.Boolean |
byte | System.Byte |
sbyte | System.SByte |
char | System.Char |
decimal | System.Decimal |
double | System.Double |
float | System.Single |
int | System.Int32 |
uint | System.UInt32 |
nint | System.IntPtr |
nuint | System.UIntPtr |
long | System.Int64 |
ulong | System.UInt64 |
short | System.Int16 |
ushort | System.UInt16 |
下表列出了 C#
内置引用类型
:
类型别名 | 类型 |
---|---|
object | System.Object |
string | System.String |
dynamic | System.Object |
在上表中,左侧列中的每个 C#
类型关键字(nint
、nuint
和 dynamic
除外)都是相应 .NET
类型的别名。 它们是可互换的。 例如,以下声明声明了相同类型的变量:
int a = 123;
System.Int32 b = 123;
第一个表的最后两行中的 nint
和 nuint
类型是本机大小的整数。 在内部它们由所指示的 .NET
类型表示,但在任意情况下关键字和 .NET
类型都是不可互换的。 编译器为 nint
和 nuint
的整数类型提供操作和转换,而不为指针类型 System.IntPtr
和 System.UIntPtr
提供。
自定义类型
可以使用 struct
、 class
、 interface
、 enum
和 record
构造来创建自己的自定义类型。 .NET
类库本身是一组自定义类型,以供你在自己的应用程序中使用。 默认情况下,类库中最常用的类型在任何 C#
程序中均可用。对于其他类型,只有在显式
添加对定义这些类型的程序集的项目引用时才可用。编译器引用程序集之后,你可以声明在源代码的此程序集中声明的类型的变量
(和常量
)。
通用类型系统
对于 .NET
中的类型系统,请务必了解以下两个基本要点:
它支持
继承原则
。类型可以派生
自其他类型(称为基类型
)。派生类型继承(有一些限制)基类型的方法
、属性
和其他成员
。 基类型可以继而从某种其他类型派生,在这种情况下,派生类型继承其继承层次结构中的两种基类型的成员。 包括System.Int32
(C# 关键字: int )等内置数值类型在内的所有类型最终都派生自单个基类型,即System.Object
(C# 关键字: object )。这样的统一类型层次结构称为通用类型系统 (CTS)
。CTS
中的每种类型被定义为值类型
或引用类型
。 这些类型包括.NET
类库中的所有自定义类型以及你自己的用户定义类型。 使用struct
关键字定义的类型是值类型;所有内置数值类型都是structs
。 使用class
或record
关键字定义的类型是引用类型。 引用类型和值类型遵循不同的编译时规则和运行时行为。
下图展示了 CTS 中值类型和引用类型之间的关系。
你可能会发现,最常用的类型全都被整理到了 System
命名空间中。不过,包含类型的命名空间与类型是值类型还是引用类型没有关系。
类 和 结构 是 .NET
通用类型系统的两种基本构造。本质上都是一种数据结构,其中封装了同属一个逻辑单元的一组数据和行为。数据
和行为
是类、结构或记录的成员,包括方法、属性和事件等(本文稍后将具体列举)。
类
、结构
或记录
声明类似于一张蓝图,用于在运行时创建实例或对象。如果定义名为 Person
的类、结构或记录,则 Person
是类型的名称。 如果声明和初始化 Person
类型的变量 p
,那么 p
就是所谓的 Person
对象或实例。可以创建同一 Person
类型的多个实例,每个实例都可以有不同的属性和字段值。
类
或记录
是引用类型。 创建类型的对象后,向其分配对象的变量仅保留对相应内存的引用。 将对象引用分配给新变量后,新变量会引用原始对象。通过一个变量所做的更改将反映在另一个变量中,因为它们引用相同的数据。结构是值类型。创建结构时,向其分配结构的变量保留结构的实际数据。 将结构分配给新变量时,会复制结构。因此,新变量和原始变量包含相同数据的副本(共两个)。 对一个副本所做的更改不会影响另一个副本。
一般来说,类用于对更复杂的行为或应在类对象创建后进行修改的数据建模。结构最适用于所含大部分数据不得在结构创建后进行修改的小型数据结构。记录类型可用于所含大部分是不得在创建对象后修改的数据的大型数据结构。
值类型
值类型派生自System.ValueType
(派生自 System.Object
)。 派生自 System.ValueType
的类型在 CLR
中具有特殊行为。值类型变量直接包含它们的值,这意味着在声明变量的任何上下文中内联分配内存。 对于值类型变量,没有单独的堆分配或垃圾回收开销。
值类型分为两类: struct
和 enum
。
内置的数值类型是结构(struct)
,它们具有可访问的字段和方法:
// constant field on type byte.
byte b = byte.MaxValue;
byte num = 0xA;
int i = 5;
char c = 'Z';
值类型已密封
,这意味着不能从任何值类型(例如 System.Int32
)派生类型。 不能将结构定义为从任何用户定义的类
或结构继承
,因为结构只能从 System.ValueType
继承。 但是,一个结构可以实现一个或多个接口
。 可将结构类型强制转换为它实现的任何接口类型;强制转换会导致装箱
操作发生,以将结构包装在托管堆上的引用类型对象内。 当你将值类型传递给使用 System.Object
或任何接口类型
作为输入参数的方法时,就会发生装箱
操作。
使用 struct
关键字可以创建你自己的自定义值类型。 结构通常用作一小组相关变量的容器,如以下示例所示:
public struct Coords
{
public int x, y;
public Coords(int p1, int p2)
{
x = p1;
y = p2;
}
}
另一种值类型是 枚举(enum)
。枚举定义的是一组已命名的整型常量。 例如,.NET
类库中的System.IO.FileMode
枚举包含一组已命名的常量整数,用于指定打开文件应采用的方式。 下面的示例展示了具体定义:
public enum FileMode
{
CreateNew = 1,
Create = 2,
Open = 3,
OpenOrCreate = 4,
Truncate = 5,
Append = 6,
}
System.IO.FileMode.Create
常量的值为 2
。 因为,名称对于阅读源代码的人来说更有意义,所以,最好使用枚举,而不是常量数字文本。
所有枚举从 System.Enum
(继承自 System.ValueType
)继承。 适用于结构的所有规则也适用于枚举。
引用类型
定义为 class
、 record
、 delegate
、数组
或 interface
的类型是引用类型。 在运行时,当声明引用类型的变量时,该变量会一直包含值 null
,直至使用 new
运算符显式创建对象,或者为该变量分配已经在其他位置使用new
创建的对象,如下所示:
MyClass mc = new MyClass();
MyClass mc2 = mc;
接口必须与实现它的类对象一起初始化。 如果 MyClass
实现 IMyInterface
,则按以下示例所示创建IMyInterface
的实例:
IMyInterface iface = new MyClass();
创建对象后,内存会在托管堆上进行分配,并且变量只保留对对象位置的引用。 对于托管堆上的类型,在分配内存和 CLR
自动内存管理功能GC
(称为“垃圾回收
”)回收内存时都会产生开销。 但是,垃圾回收已是高度优化,并且在大多数情况下,不会产生性能问题。
所有数组
都是引用类型,即使元素是值类型,也不例外。 虽然数组隐式派生自 System.Array
类,但可以使用 C#
提供的简化语法声明和使用数组,如以下示例所示:
// Declare and initialize an array of integers.
int[] nums = { 1, 2, 3, 4, 5 };
// Access an instance property of System.Array.
int len = nums.Length;
引用类型完全支持继承
。创建类时,可以从其他任何未定义为密封的接口或类继承,而其他类可以从你的类继承并重写虚拟方法。
文本值的类型
在 C# 中,文本值从编译器接收类型。可以通过在数字末尾追加一个字母来指定数字文本应采用的类型。 例如,若要指定应按 float
来处理值 4.56
,则在该数字后追加一个“f
”或“F
”,即 4.56f
。 如果没有追加字母,那么编译器就会自己去推断文本值的类型。、
由于文本已类型化,且所有类型最终都是从 System.Object
派生,因此可以编写和编译如下所示的代码:
string s = "The answer is " + 5.ToString();
// Outputs: "The answer is 5"
Console.WriteLine(s);
Type type = 12345.GetType();
// Outputs: "System.Int32"
Console.WriteLine(type);
泛型类型
可使用一个或多个类型参数声明、作为客户端代码在创建类型实例时将提供的实际类型(具体类型)的占位符的类型。 这种类型称为泛型类型
。 例如,.NET
类型 System.Collections.Generic.List<T>
具有一个类型参数,它按照惯例被命名为 T
。 当创建类型的实例时,指定列表将包含的对象的类型,例如 string
:
List<string> stringList = new List<string>();
stringList.Add("String example");
// compile time error adding a type other than a string:
stringList.Add(4);
通过使用类型参数,可重新使用相同类以保存任意类型的元素,且无需将每个元素转换为对象。泛型集合类称为强类型集合,因为编译器知道集合元素的具体类型,并能在编译时引发错误,例如当尝试向上面示例中的stringList
对象添加整数时。
隐式类型、匿名类型和可以为 null 的值类型
你可以使用 var
关键字隐式键入一个局部变量(但不是类成员)。变量仍可在编译时获取类型,但类型是由编译器提供。不方便为不打算存储或传递外部方法边界的简单相关值集合创建命名类型。 因此,可以创建 匿名类型
。
普通值类型不能具有 null
值。 不过,可以在类型后面追加 ?
,创建可为空的值类型
。 例如, int?
是还可以包含值 null
的 int
类型。 可以为 null
的值类型是泛型结构类型System.Nullable<T>
的实例。 在将数据传入和传出数据库(数值可能为 null
)时,可为空的值类型特别有用。
编译时类型和运行时类型
变量可以具有不同的编译时和运行时类型。编译时类型是源代码中变量的声明或推断类型。 运行时类型是该变量所引用的实例的类型。这两种类型通常是相同的,如以下示例中所示:
string message = "This is a string of characters";
在其他情况下,编译时类型是不同的,如以下两个示例所示:
object anotherMessage = "This is another string of characters";
IEnumerable<char> someCharacters = "abcdefghijklmnopqrstuvwxyz";
在上述两个示例中,运行时类型为 string
。 编译时类型在第一行中为 object
,在第二行中为IEnumerable<char>
。
如果变量的这两种类型不同,请务必了解编译时类型和运行时类型的应用情况。编译时类型确定编译器执行的所有操作。这些编译器操作包括方法调用解析、重载决策以及可用的隐式和显式强制转换。 运行时类型确定在运行时解析的所有操作。 这些运行时操作包括调度虚拟方法调用、计算 is
和 switch
表达式以及其他类型的测试 API。 为了更好地了解代码如何与类型进行交互,请识别哪个操作应用于哪种类型。