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# 程序。 有关内置类型的完整列表,请参阅 内置类型。
自定义类型
可以使用 struct
、class
、interface
、enum
和 record
构造来创建自己的自定义类型。 .NET 类库本身是可在自己的应用程序中使用的自定义类型的集合。 默认情况下,类库中最常用的类型在任何 C# 程序中都可用。 仅当显式添加对定义它们的程序集的项目引用时,其他人才可用。 编译器对程序集的引用后,可以在源代码中声明该程序集中声明的类型(和常量)。 有关详细信息,请参阅 .NET 类库。
定义类型时做出的第一个决定是决定要用于类型的构造。 以下列表有助于做出初始决策。 选项中存在重叠。 在大多数情况下,多个选项是合理的选择。
- 如果数据存储大小较小,不超过 64 个字节,请选择
struct
或record struct
。 - 如果类型不可变,或者需要非破坏性突变,请选择
struct
或record struct
。 - 如果类型应该具有用于比较相等性的值语义,请选择
record class
或record struct
。 - 如果类型主要用于存储数据,而不是行为,请选择
record class
或record struct
。 - 如果该类型是继承层次结构的一部分,请选择
record class
或class
。 - 如果类型使用多态性,请选择
class
。 - 如果主要目标是行为,请选择一个
class
。
通用类型系统
了解 .NET 中类型系统的两个基本点非常重要:
- 它支持继承原则。 类型可以派生自其他类型,称为 基类型。 派生类型继承方法、属性和基类型的其他成员(有一些限制)。 基类型反过来可以派生自某些其他类型,在这种情况下,派生类型继承其继承层次结构中这两种基类型的成员。 所有类型,包括内置数值类型(如 System.Int32 (C# 关键字:
int
),最终派生自单个基类型,即 System.Object (C# 关键字:object
)。 这种统一类型层次结构称为 通用类型系统 (CTS)。 有关 C# 中的继承的详细信息,请参阅 “继承”。 - CTS 中的每个类型都定义为 值类型 或 引用类型。 这些类型包括 .NET 类库中的所有自定义类型,以及你自己的用户定义的类型。 使用
struct
关键字定义的类型是值类型;所有内置数值类型都是structs
。 使用class
或record
关键字定义的类型是引用类型。 引用类型和值类型具有不同的编译时规则和不同的运行时行为。
下图显示了 CTS 中的值类型和引用类型之间的关系。
注释
可以看到最常用的类型都在命名空间中 System 组织。 但是,包含类型的命名空间与它是值类型还是引用类型无关。
类和结构是 .NET 中通用类型系统的基本构造中的两个。 每个结构本质上都是一个数据结构,它封装一组作为逻辑单元属于一起的数据和行为。 数据和行为是类、结构或记录 的成员 。 成员包括其方法、属性、事件等,如本文后面部分所列。
类、结构或记录声明类似于用于在运行时创建实例或对象的蓝图。 如果定义名为 Person
类、结构或记录, Person
则为类型的名称。 如果声明和初始化 p
类型的变量 Person
,则 p
据说是 Person
的对象或实例。 可以创建同一 Person
类型的多个实例,并且每个实例在其属性和字段中可以具有不同的值。
类是引用类型。 创建类型的对象时,为其分配对象的变量只保留对该内存的引用。 将对象引用分配给新变量时,新变量引用原始对象。 通过一个变量所做的更改反映在另一个变量中,因为它们都引用了相同的数据。
结构是值类型。 创建结构时,为其分配结构的变量保存结构的实际数据。 将结构分配给新变量时,将复制该结构。 因此,新变量和原始变量包含同一数据的两个单独的副本。 对一个副本所做的更改不会影响另一个副本。
记录类型可以是引用类型(record class
)或值类型(record struct
)。 记录类型包含支持值相等性的方法。
一般情况下,类用于对更复杂的行为进行建模。 类通常存储在创建类对象后要修改的数据。 结构最适合小型数据结构。 结构通常存储创建结构后不打算修改的数据。 记录类型是具有其他编译器合成成员的数据结构。 记录通常存储创建对象后不打算修改的数据。
值类型
值类型派生自 System.ValueType,派生自 System.Object。 派生自 System.ValueType 的类型在 CLR 中具有特殊行为。 值类型变量直接包含其值。 结构的内存在声明变量的任何上下文中进行内联分配。 值类型变量没有单独的堆分配或垃圾回收开销。 可以声明 record struct
类型为值类型,并包括用于记录的合成成员。
值类型有两个类别: struct
和 enum
。
内置数值类型是结构,它们具有可以访问的字段和方法:
// 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
{
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.IO.FileMode。
所有枚举都继承自 System.Enum,而 System.Enum 又继承自 。 应用于结构的所有规则也适用于枚举。 有关枚举的详细信息,请参阅 枚举类型。
引用类型
定义为class
、record
、delegate
、数组或interface
的类型是reference type
。
当您声明reference type
变量时,它的值是null
,直到您为它分配该类型的一个实例或使用new
运算符创建一个为止。 以下示例演示了类的创建和赋值:
MyClass myClass = new MyClass();
MyClass myClass2 = myClass;
不能使用interface
运算符直接实例化new
对象。 而是创建并分配实现接口的类的实例。 请看下面的示例:
MyClass myClass = new MyClass();
// 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
,则可以在编译时引发错误。 有关详细信息,请参阅 泛型。
隐式类型、匿名类型和可为 null 的值类型
可以使用关键字隐式键入局部变量(但不能键入 var
类成员)。 变量仍会在编译时接收类型,但该类型由编译器提供。 有关详细信息,请参阅 隐式类型局部变量。
不方便为不打算存储或传递外部方法边界的简单相关值集合创建命名类型。 可以为此创建 匿名类型 。 有关详细信息,请参阅 匿名类型。
普通值类型不能具有值null
。 但是,可以通过在类型后面追加一个?
。 例如, int?
是一种 int
也可以具有值 null
的类型。 可以为 null 的值类型是泛型结构类型 System.Nullable<T> 的实例。 在将数据传入和传出数据库(数值可能为 null
)时,可为空的值类型特别有用。 有关详细信息,请参阅 可以为 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。 若要更好地了解代码如何与类型交互,请识别哪些作适用于哪种类型。
相关章节
如需了解更多信息,请参阅以下文章:
C# 语言规范
有关详细信息,请参阅 C# 语言规范。 语言规范是 C# 语法和用法的明确来源。