windows C#-类型系统(上)
C# 是一种强类型语言。 每个变量和常量都有一个类型,每个求值的表达式也是如此。 每个方法声明都为每个输入参数和返回值指定名称、类型和种类(值、引用或输出)。 .NET 类库定义了内置数值类型和表示各种构造的复杂类型。 其中包括文件系统、网络连接、对象的集合和数组以及日期。 典型的 C# 程序使用类库中的类型,以及对程序问题域的专属概念进行建模的用户定义类型。
类型中可存储的信息包括以下项:
- 类型变量所需的存储空间;
- 可以表示的最大值和最小值;
- 包含的成员(方法、字段、事件等);
- 继承自的基类型;
- 它实现的接口;
- 允许执行的运算种类;
编译器使用类型信息来确保在代码中执行的所有操作都是类型安全的。 例如,如果声明 int 类型的变量,那么编译器允许在加法和减法运算中使用此变量。 如果尝试对 bool 类型的变量执行这些相同操作,则编译器将生成错误,如以下示例所示:
int a = 5;
int b = a + 2; //OKbool 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 sourcewhere item <= limitselect item;
方法声明指定方法参数的类型和返回值。 以下签名显示了需要 int 作为输入参数并返回字符串的方法:
public string GetName(int ID)
{if (ID < names.Length)return names[ID];elsereturn String.Empty;
}
private string[] names = { "Spencer", "Sally", "Doug" };
声明变量后,不能使用新类型重新声明该变量,并且不能分配与其声明的类型不兼容的值。 例如,不能声明 int 后再向它分配 true 的布尔值。 不过,可以将值转换成其他类型。例如,在将值分配给新变量或作为方法自变量传递时。 编译器会自动执行不会导致数据丢失的类型转换。 如果类型转换可能会导致数据丢失,必须在源代码中进行显式转换。
内置类型
C# 提供了一组标准的内置类型。 这些类型表示整数、浮点值、布尔表达式、文本字符、十进制值和其他数据类型。 还有内置的 string 和 object 类型。 这些类型可供在任何 C# 程序中使用。
自定义类型
可以使用 struct、class、interface、enum 和 record 构造来创建自己的自定义类型。 .NET 类库本身是一组自定义类型,以供你在自己的应用程序中使用。 默认情况下,类库中最常用的类型在任何 C# 程序中均可用。 其他类型只有在显式添加对定义这些类型的程序集的项目引用时才可用。 编译器引用程序集之后,你可以声明在源代码的此程序集中声明的类型的变量(和常量)。
通用类型系统
对于 .NET 中的类型系统,请务必了解以下两个基本要点:
它支持继承原则。 类型可以派生自其他类型(称为基类型)。 派生类型继承(有一些限制)基类型的方法、属性和其他成员。 基类型可以继而从某种其他类型派生,在这种情况下,派生类型继承其继承层次结构中的两种基类型的成员。 所有类型(包括 System.Int32 (C# keyword: int) 等内置数值类型)最终都派生自单个基类型,即 System.Object (C# keyword: object)。 这样的统一类型层次结构称为通用类型系统 (CTS)。 若要详细了解 C# 中的继承,请参阅继承。
CTS 中的每种类型被定义为值类型或引用类型。 这些类型包括 .NET 类库中的所有自定义类型以及你自己的用户定义类型。 使用 struct 关键字定义的类型是值类型;所有内置数值类型都是 structs。 使用 class 或 record 关键字定义的类型是引用类型。 引用类型和值类型遵循不同的编译时规则和运行时行为。
下图展示了 CTS 中值类型和引用类型之间的关系。
类和结构是 .NET 通用类型系统的两种基本构造。 C# 9 添加记录,记录是一种类。 每种本质上都是一种数据结构,其中封装了同属一个逻辑单元的一组数据和行为。 数据和行为是类、结构或记录的成员。 这些行为包括方法、属性和事件等,本文稍后将具体列举。
类、结构或记录声明类似于一张蓝图,用于在运行时创建实例或对象。 如果定义名为 Person 的类、结构或记录,则 Person 是类型的名称。 如果声明和初始化 Person 类型的变量 p,那么 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 或任何接口类型作为输入参数的方法时,就会发生装箱操作。
使用 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。 不过,名称对于阅读源代码的人来说更有意义,因此,最好使用枚举,而不是常量数字文本。