通过


C# 类型系统

小窍门

开发软件的新手? 首先开始 学习入门 教程。 它们将引导你完成编写程序并介绍类型。

是否在其他语言中有经验? 如果已经了解类型系统,请跳过 值与参考 区别,并选择 哪种类型 指南,然后跳转到有关特定类型的文章。

C# 是强类型语言。 每个变量、常量和表达式都有一个类型。 编译器通过检查代码中的每个操作是否对所涉及的类型有效来强制实施 类型安全性 。 例如,可以添加两个int值,但不能添加一个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 来让编译器根据赋值内容推断其类型:

// Explicit type:
int count = 10;
double temperature = 36.6;

// Compiler-inferred type:
var name = "C#";
var items = new List<string> { "one", "two", "three" };

方法参数和返回值也有类型。 以下方法采用stringint,并返回string

static string GetGreeting(string name, int visitCount)
{
    return visitCount switch
    {
        1 => $"Welcome, {name}!",
        _ => $"Welcome back, {name}! Visit #{visitCount}."
    };
}

声明变量后,无法更改其类型或分配与声明类型不兼容的值。 可以将值转换为其他类型的值。 编译器执行不会自动丢失数据的 隐式转换显式转换 (强制转换)需要您在代码中明确指定转换。 有关详细信息,请参阅 强制转换和类型转换

内置类型和自定义类型

C# 为常见数据提供内置类型:整数、浮点数、bool浮点数和charstring。 每个 C# 程序都可以使用这些内置类型,而无需任何额外的引用。

除了内置类型之外,还可以使用多个构造创建自己的类型:

  • - 用于建模行为和复杂对象的引用类型。 支持继承和多态性。
  • 结构 - 小型轻型数据的值类型。 每个变量都有自己的副本。
  • 记录 — 拥有编译器生成的相等性的类或结构,ToString,并通过with表达式实现非破坏性突变。
  • 接口 — 定义任何类或结构都可以实现的成员的协定。
  • 枚举 - 命名的整数常量集,例如星期几或文件访问模式。
  • 元组 - 轻量级结构类型,用于对相关值进行分组,而无需定义命名类型。
  • 泛型——通过诸如 List<T>Dictionary<TKey, TValue> 这样的类型参数化结构,在重用相同逻辑以处理不同类型时提供类型安全性。

值类型和引用类型

C# 中的每个类型都是 值类型引用类型。 此区别决定了变量存储数据的方式和分配的工作原理。

值类型 直接保存其数据。 将值类型分配给新变量时,运行时将复制数据。 对一个变量的更改不会影响另一个变量。 结构、枚举和内置数值类型都是值类型。

引用类型 保存对托管堆上对象的引用。 将引用类型分配给新变量时,这两个变量都指向同一对象。 通过一个变量进行的更改通过另一个变量可见。 类、数组、委托和字符串是引用类型。

以下示例显示了差异。 第一个块显示记录结构的定义,该结构 Coords 是值类型。 第二个块显示值类型和引用类型的不同行为。

public readonly record struct Coords(int X, int Y);
// Value type: each variable holds its own copy
var point1 = new Coords(3, 4);
var point2 = point1;
Console.WriteLine($"point1: ({point1.X}, {point1.Y})");
Console.WriteLine($"point2: ({point2.X}, {point2.Y})");
// point1 and point2 are independent copies

// Reference type: both variables refer to the same object
var list1 = new List<int> { 1, 2, 3 };
var list2 = list1;
list2.Add(4);
Console.WriteLine($"list1 count: {list1.Count}"); // 4 — same object

所有类型最终派生自 System.Object。 值类型派生自 System.ValueType,而 System.ValueType 又派生自 。 此统一层次结构称为 通用类型系统 (CTS)。 有关继承的详细信息,请参阅 “继承”。

选择哪种类型

当您定义一个新类型时,您选择的种类决定了代码的行为方式。 使用以下准则做出初始决策:

  • 元组 - 不需要命名类型或行为的值的临时分组。
  • structrecord struct - 小型数据(大约 64 字节或更少)、值语义或不可变性。 记录结构添加基于值的相等性和 with 表达式。
  • record class — 主要是基于值的相等性数据, ToString以及非破坏性突变。 支持继承。
  • class — 复杂行为、多态性或可变状态。 大多数自定义类型都是类。
  • interface — 不相关类型可以实现的接口。 定义功能而不是标识。
  • enum — 一组固定的命名常量,例如状态代码或选项。

多个选项通常是合理的。

编译时类型和运行时类型

变量可以在编译时和运行时具有不同的类型。 编译时类型是源代码中声明或推断的类型。 运行时类型是变量引用的实例的实际类型。 运行时类型必须与编译时类型相同,或者是派生自它或实现它的类型。 仅当从运行时类型到编译时类型(例如标识、引用、装箱或数字转换)存在隐式转换时,分配才有效。

// Compile-time and run-time types match:
string message = "Hello, world!";

// Compile-time type differs from run-time type:
object boxed = "This is a string at run time";
IEnumerable<char> characters = "abcdefghijklmnopqrstuvwxyz";

在前面的示例中, boxed 具有一种编译时类型, object 但运行时类型为 string。 工作分配的工作原理是 string 派生自 object. 同样,characters 具有编译时类型 IEnumerable<char>,并且该赋值是可行的,因为 string 实现了该接口。 编译时类型控制重载分辨率和可用转换。 运行时类型控制虚拟方法调度、 is 表达式和 switch 表达式。

另见

C# 语言规范

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