C# 类型系统

C# 是强类型语言。 每个变量和常量都有一个类型,计算结果为值的每个表达式也一样。 C# 主要使用 规范类型系统规范类型系统使用名称来标识每种类型。 在 C#、 structclassinterface 类型(包括 record 类型)中,都由其名称标识。 每个方法声明都为每个参数和返回值指定名称、类型和类型(值、引用或输出)。 .NET 类库定义表示各种构造的内置数值类型和复杂类型。 这些构造包括文件系统、网络连接、集合和对象数组以及日期。 典型的 C# 程序使用类库中的类型和用户定义的类型,这些类型对特定于程序问题域的概念进行建模。

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# 提供一组标准的内置类型。 这些类型表示整数、浮点值、布尔表达式、文本字符、小数值和其他类型的数据。 该语言还包括内置 stringobject 类型。 可以在任何 C# 程序中使用这些类型。 有关内置类型的完整列表,请参阅 内置类型

自定义类型

使用 元组 创建结构类型以存储相关数据成员。 这些类型提供一个包含多个成员的结构。 元组的行为有限。 它们是用于存储值的容器。 这些是你可以创建的最简单类型。 以后您可能会决定需要某种特定的行为。 在这种情况下,可以将元组转换为structclass中的任意一个。

使用 structclassinterfaceenumrecord 构造来创建自定义类型。 .NET 类库本身是可在自己的应用程序中使用的自定义类型的集合。 默认情况下,类库中最常用的类型在任何 C# 程序中都可用。 通过显式添加对提供这些类型的包的引用,使其他类型变得可用。 编译器引用包后,可以在源代码中声明该包程序集中声明的类型和常量。

定义类型时做出的第一个决定是决定要用于类型的构造。 以下列表有助于做出初始决策。 某些选项重叠。 在大多数情况下,多个选项是合理的选择。

  • 如果数据类型不是应用域的一部分,并且不包含行为,请使用结构类型。
  • 如果数据存储大小较小,不超过 64 个字节,请选择 structrecord struct
  • 如果类型不可变,或者需要非破坏性突变,请选择structrecord struct
  • 如果类型应该具有用于比较相等性的值语义,请选择 record classrecord struct
  • 如果类型主要用于存储数据,但行为最少,请选择 record classrecord struct
  • 如果该类型是继承层次结构的一部分,请选择 record classclass
  • 如果类型使用多态性,请选择 class
  • 如果主要目标是行为,请选择一个class

还可以选择一个 interface 来为 契约建模:行为是由不相关类型能够实现的成员所描述的。 接口是抽象的,声明必须由继承自该接口的所有 classstruct 类型实现的成员。

通用类型系统

通用类型系统支持继承原则。 类型可以派生自其他类型,称为 基类型。 派生类型继承方法、属性和基类型的其他成员(有一些限制)。 基类型反过来可以派生自某些其他类型,在这种情况下,派生类型继承其继承层次结构中这两种基类型的成员。

所有类型,包括内置数值类型,如 System.Int32 (C# 关键字: int),最终派生自单个基类型,即 System.Object (C# 关键字: object)。 这种统一类型层次结构称为 通用类型系统 (CTS)。 有关 C# 中的继承的详细信息,请参阅 “继承”。

CTS 中的每个类型都定义为 值类型引用类型。 这些类型包括 .NET 类库中的所有自定义类型,以及你自己的用户定义的类型:

  • 使用 structrecord struct 关键字定义的类型是值类型。 所有内置数值类型都是 structs
  • 使用classrecord classrecord关键字定义的类型是引用类型。

引用类型和值类型具有不同的编译时规则和不同的运行时行为。

注释

最常用的类型都组织在命名空间中 System 。 但是,包含类型的命名空间与它是值类型还是引用类型无关。

类和结构是 .NET 中通用类型系统的基本构造中的两个。 每个构造本质上是一个数据结构,用于封装一组作为逻辑单元属于一起的数据和行为。 数据和行为是类、结构或记录 的成员 。 成员包括其方法、属性、事件等,如本文后面部分所列。

类、结构或记录声明类似于用于在运行时创建实例或对象的蓝图。 如果定义名为 Person类、结构或记录, Person 则为类型的名称。 如果声明和初始化 p类型的变量 Person,则 p 据说是 Person的对象或实例。 可以创建同一 Person 类型的多个实例,每个实例在其属性和字段中可以具有不同的值。

类是引用类型。 创建类型的对象时,为其分配对象的变量只保留对该内存的引用。 向新变量分配对象引用时,新变量引用原始对象。 通过一个变量所做的更改反映在另一个变量中,因为它们都引用了相同的数据。

结构是值类型。 创建结构时,为其分配结构的变量保存结构的实际数据。 将结构分配给新变量时,将复制该结构。 因此,新变量和原始变量包含同一数据的两个单独的副本。 对一个副本所做的更改不会影响另一个副本。

记录类型可以是引用类型(record class)或值类型(record struct)。 记录类型包含支持值相等性的方法。

一般情况下,使用类对更复杂的行为进行建模。 类通常存储创建类对象后修改的数据。 结构最适合小型数据结构。 结构通常存储创建结构后不修改的数据。 记录类型是一种数据结构类型,其具有编译器额外合成的成员。 记录通常存储创建对象后不修改的数据。

值类型

值类型派生自 System.ValueType,派生自 System.Object。 派生自 System.ValueType 的类型在 CLR 中具有特殊行为。 值类型变量直接包含其值。 结构的内存在声明变量的任何上下文中进行内联分配。 可以声明 record struct 类型为值类型,并包括用于记录的合成成员。

存在两类值类型: structenum

内置数值类型是结构,它们具有可以访问的字段和方法:

// constant field on type byte.
byte b = byte.MaxValue;

但是,声明并为其赋值,就像它们是简单的非聚合类型一样:

byte num = 0xA;
int i = 5;
char c = 'Z';

值类型是密封的。 不能从任何值类型派生类型,例如 System.Int32。 无法定义要从任何用户定义的类或结构继承的结构,因为结构只能继承自 System.ValueType。 但是,结构可以实现一个或多个接口。 可以将结构体类型转换为其实现的任何接口类型。 这将导致“装箱”操作,以将结构包装在托管堆上的引用类型对象内。 当你将值类型传递给使用 System.Object 或任何接口类型作为输入参数的方法时,就会发生装箱操作。 有关详细信息,请参阅 装箱和拆箱

使用 结构 关键字创建自己的自定义值类型。 通常,结构用作一小部分相关变量的容器,如以下示例所示:

public struct Coords(int x, int y)
{
    public int X { get; init; } = x;
    public int Y { get; init; } = y;
}

有关结构的详细信息,请参阅 结构类型。 有关值类型的详细信息,请参阅 “值类型”。

另一类值类型是 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.IO.FileMode

所有枚举都继承自 System.Enum,而它又继承自 System.ValueType。 应用于结构的所有规则也适用于枚举。 有关枚举的详细信息,请参阅 枚举类型

引用类型

定义为 classrecord classrecorddelegate 数组或 interface 的类型是 reference type

声明变量 reference type时,它将包含值 null ,直到使用该类型的实例分配该值,或使用运算符创建一个 new 。 以下示例演示类的创建和赋值:

MyClass myClass = new();
MyClass myClass2 = myClass;

不能使用new运算符直接实例化interface。 而是创建并分配实现接口的类的实例。 请看下面的示例:

MyClass myClass = new();

// Declare and assign using an existing value.
IMyInterface myInterface = myClass;

// Or create and assign a value in a single statement.
IMyInterface myInterface2 = new MyClass();

创建对象时,系统会在托管堆上分配内存。 该变量仅保留对对象位置的引用。 对于托管堆上的类型,在分配内存和回收内存时都会产生开销。 垃圾回收 是 CLR 的自动内存管理功能,用于执行回收。 但是,垃圾回收也是高度优化的,在大多数情况下,它不会创建性能问题。 有关垃圾回收的详细信息,请参阅 自动内存管理

所有数组都是引用类型,即使这些数组的元素是值类型。 数组隐式派生自 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# 中,编译器将类型分配给文本值。 可以通过在数字末尾追加一个字母来指定数字文本应采用的类型。 例如,若要指定应将值4.56视为一个float值,请在数字后面追加“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 上一示例中的对象)。 有关详细信息,请参阅 泛型

元组和匿名类型

如果不打算使用公共 API 存储或传递这些值,则为简单的相关值集创建类型可能会不方便。 为此,可以创建 元组匿名类型 。 有关详细信息,请参阅 元组匿名类型

可以为 null 的值类型

普通值类型不能具有值null。 但是,可以通过在类型后面追加一个?。 例如, int? 是一种 int 也可以具有值 null的类型。 可以为 null 的值类型是泛型结构类型 System.Nullable<T> 的实例。 在将数据传入和传出数据库(数值可能为 null)时,可为空的值类型特别有用。 有关详细信息,请参阅 可以为 Null 的值类型

隐式类型声明

使用 var 关键字隐式键入局部变量(但不是类成员)。 变量仍会在编译时接收类型,但编译器提供该类型。 有关详细信息,请参阅 隐式类型局部变量

编译时类型和运行时类型

变量可以具有不同的编译时间和运行时类型。 编译时类型是源代码中变量的声明或推断类型。 运行时类型是该变量引用的实例的类型。 这两种类型通常相同,如以下示例所示:

string message = "This is a string of characters";

在其他情况下,编译时类型不同,如以下两个示例所示:

object anotherMessage = "This is another string of characters";
IEnumerable<char> someCharacters = "abcdefghijklmnopqrstuvwxyz";

在上述两个示例中,运行时类型都是一种 string。 编译时类型位于 object 第一行和第 IEnumerable<char> 二行中。

如果变量的两种类型不同,请务必了解编译时类型和运行时类型何时适用。 编译时类型决定了编译器执行的所有动作。 这些编译器操作包括方法调用解析、重载解析以及显式和隐式转换。 运行时类型确定在运行时解析的所有操作。 这些运行时操作包括调度虚拟方法调用、计算 isswitch 表达式,以及其他类型测试 API。 若要更好地了解代码如何与类型交互,请识别哪些作适用于哪种类型。

如需了解更多信息,请参阅以下文章:

C# 语言规范

有关详细信息,请参阅 C# 语言规范。 语言规范是 C# 语法和用法的明确来源。