C# 是一种通用的、类型安全的 面向对象编程语言。它的目标是提高程序员的生产力,为此,需要在简单性、表达性和性能之间进行权衡。C# 语言与平台无关,可以和多种特定平台下的运行时协同工作。
1.1 面向对象
C#实现了丰富的面向对象范式,包括封装、继承和多态。C# 面向对象特性包括:
- 统一的类型系统:C# 中的基础构件是一种称为类型的数据与函数的封装单元。C# 拥有统一的类型系统,其中的所有类型都共享一个公共的基类。因此所有类型,不论它们是表示业务对象还是表示数字这样的基元类型,都拥有相同的基本功能。
- 类与接口:在传统面向对象范式中,唯一的类型就是类。然而C#还有其他几种类型,其中之一是接口(interface)。它仅可用于定义行为(而非状态)。这样,接口不但可以实现多重继承 ,还可以将标准与实现隔离。
- 属性、方法和事件:在纯粹的面向对象范式中,所有的函数都是方法。而在 C# 中,方法只是函数成员之一,除此之外还有属性(property)、事件及其他的形式。属性是封装了一部分对象状态的函数成员,例如,按钮的颜色或者标签的文本。
1.2 类型安全性
C# 是一种类型安全(type-safe)的语言。即类型的实例只能通过它们的协议进行交互。这确保了每种类型的内部已执行。例如,C#不允许将字符串类型作为整数类型进行处理。
更具体地说,C# 是静态类型化(static typing)语言,它在编译时会执行安全性检查。当然,在运行时也会同样执行类型安全性检查。
静态类型化能够在程序运行之前消除大量错误。它将大量在运行时单元执行的测试转移到编译器中,确保程序中所有类型之间都是相互适配的,从而使大型程序更易于管理、更具预测性并更加健壮。此外,静态类型化可以借助一些工具,例如Visual Studio的IntelliSense来提供更好的编程辅助。它们能够知晓某个特定变量的类型,自然也知道该变量上能够调用的方法。
C#允许部分代码通过 dynamic 关键字来动态定义指定类型。然而,C# 在大多数情况下仍然是一门静态类型化语言。
C# 还是一门强类型语言(strongly typed language),因为它拥有非常严格的类型规则(不论是静态还是运行时均是如此)。例如,不能将浮点类型的参数在调用时传递到接受整数类型参数的函数中,而必须显式将这个浮点数转换为整数。这可以防止编码错误。
1.3 内存管理
C# 依靠运行时来实现自动内存管理。公共语言运行时的垃圾回收器会作为程序的一部分运行,并负责回收那些不再被引用的对象所占用的内存,程序员不必显式释放对象的内存,从而避免在 C++ 等语言中错误使用指针而造成的问题。
C# 并未抛弃指针,只是在大多数编程任务中是不需要使用指针的。在性能优先的热点和互操作领域,你仍然可以在标记为 unsafe 的程序块内使用指针和显式内存分配。
1.4 平台支持
C#在以下平台均具备运行时支持:
- Windows 7~11桌面系统(支持富客户端、Web、服务器和命令行应用程序)
- macOS(支持富客户端、Web与命令行应用)
- Linux和macOS(支持Web与命令行应用程序)
- Android和iOS(移动应用程序)
- Windows 10设备(XBOX、Surface Hub和HoloLens)
除此之外,还有名为 Blazor 的技术。该技术能够将 C# 编译为可以在浏览器上运行的 Web Assembly。
1.5 CLR、BCL 和运行时
执行 C# 程序的运行时由公共语言运行时(Common Language Runtime)和基础类库构成。运行时还可以包含更高层次的应用程序层,其中包含用于开发富客户端、移动或Web应用程序(参见图1-1)的类库。不同的运行时可以在不同的平台开发多种类型的应用程序。
### 1.5.1 公共语言运行时
公共语言运行时(Common Language Runtime,CLR)提供必需的运行时服务,例如,自动化内存管理与异常处理。其中"公共"指其他托管编程语言(如F#、Visual Basic和托管C++),也能共享该运行时。
C# 是一种托管语言,因为它也会将源代码编译为托管代码,托管代码以中间语言(Intermediate Language,IL)的形式表示。CLR 通常会在执行前将 IL 转换为机器(例如 X64 或 X86)原生代码,该技术称为即时(Just-In-Time,JIT)编译。
除此之外,还可以使用提前编译(ahead-of-time compilation)的方式改善那些拥有大量程序集,或需要在资源有限的设备上运行的程序的启动速度(以确保开发的移动端应用程序能够达到iOS应用商店标准。)
托管代码的容器称为程序集(assembly)。它们不仅包含 IL,还包含类型信息(元数据)。元数据的引入使程序集无须额外的文件就可以引用其他程序集中的类型。
Microsoft 的 ildasm 工具可以反编译程序集或查看程序集的内容。其他工具(例如 ILSpy 与 JetBrains 的 dotPeek)则可以将 IL 代码进一步反编译为 C#。IL 的层次相比原生机器代码要高得多,因此反编译器可以高质量地重建 C# 代码。
程序也可以通过反射(reflection)查询其中的元数据,甚至在运行时生成新的 IL(reflection.emit)。
1.5.2 基础类库
CLR 总是与一组程序集一同发行。这组程序集称为基础类库(Base Class Library,BCL)。BCL 向程序员提供了最核心的编程能力。例如,集合、输入/输出、文本处理、XML/JSON处理、网络编程、加密、互操作、并发和并行编程。
BCL 还实现了 C# 语言本身所需的类型(为了支持枚举、查询和异步等功能),并可以让你显式访问 CLR 功能,例如反射与内存管理。
1.5.3 运行时
运行时(或称为框架)是一个可下载并安装的部署单元。运行时包含CLR(以及 BCL),还可以包含开发特定应用程序(例如 Web、移动应用、富客户端应用程序等)所需的应用程序层。若只是开发命令行控制台应用程序或非UI类库,则无须应用程序层。
在开发应用程序时,需要选定一个特定的目标运行时,即应用程序依赖该运行时提供的功能。运行时的选择也决定了应用程序支持的平台。
下表列出了主流的运行时选项:
图1-2使用图形的方式展示了上述内容,并展示了本书涵盖的内容。
#### 1.5.3.1 .NET 6
.NET 6 是 Microsoft 的旗舰级开源运行时。它可以在 Windows、Linux 和 macOS 上运行 .NET Web 与命令行应用程序,也可以在 Windows 7~11、macOS 与在 iOS 和 Android 移动应用程序上运行 .NET 富客户端应用程序。本书将关注 .NET 6 CLR 与 BCL。
和 .NET Framework 不同,.NET 6 并未随 Windows 预先安装在机器上。在运行 .NET 6 应用程序时,若无法找到正确的运行时则会出现提示信息,并指引你访问运行时下载页面。你可以使用自包含部署方式(self-contained deployment)避免上述情况。这种部署方式会将应用程序所需的部分运行时包含在程序中。
.NET 6 是 .NET 5 的更新版本,而更早的版本是 .NET Core 3。Microsoft 去掉了"Core"并跳过了 4 这个版本号,以避免和 .NET Framework 4.x 混淆。
因此,使用 .NET Core 1、2 和 3 或 .NET 5 编译的程序集在绝大多数情况下不加修改就可以在 .NET 6 上运行。相反,使用(任何版本的).NET Framework 编译的程序集通常和 .NET 6 是不兼容的。
.NET 6 的 BCL 和 CLR 与 .NET 5(和 .NET Core 3)非常相似,它们的不同之处主要体现在性能和部署方式上。
1.5.3.2 MAUI
MAUI(Multiple-platform App UI,多平台应用程序 UI,于 2022 年初发布)用于设计支持 iOS 和 Android 的移动应用程序,以及支持 macOS 和 Windows 的跨平台桌面应用程序。MAUI 由 Xamarin 进化而来,允许单一项目支持多种平台。
1.5.3.3 UWP 和 WinUI3
UWP(Universal Windows Platform) 用于编写沉浸式触控优先的应用程序。这些应用程序可以运行在 Windows 10+ 桌面系统以及 Xbox、Surface Hub和Hololens等设备上。UWP 应用运行在沙盒之内,并可通过 Windows Store 发行。Windows 10 操作系统中预装了 UWP。UWP 基于 .NET Core 2.2 CLR/BCL,目前看来这种依存关系会持续下去。其后继者 WinUI 3 则作为Windows App SDK 的组成部分随之发布。
1.5.3.4 .NET Framework
.NET Framework 是 Microsoft 最初的 Windows 独占运行时,用于编写(只)运行于 Windows 桌面系统和服务器系统的网络应用程序与富客户端应用程序。虽然 Microsoft 会继续支持和维护当前的 4.8 版本以保证现有应用程序的执行,但该框架目前已没有后续的发布计划。
.NET Framework 中的 CLR 和 BCL 与应用层集成在一起。使用 .NET Framework 编写的应用程序通常在进行少许修改后就可以在 .NET 6 中重新编译。.NET Framework 中含有一些 .NET 6 并不具备的特性,反之亦然。
Windows 预装了.NET Framework,会通过 Windows 升级服务自动升级。若将目标框架设置为 .NET Framework 4.8,则可以使用 C# 7.3 及之前版本的语言功能。
.NET 这个词之前一直用以指代任何与 .NET 相关的技术(例如 .NET Framework、.NET Core、.NET Standard 等)。
而微软将 .NET Core 重新命名为 .NET 不可避免会造成误解。在本书中,我们使用 .NET 表示 .NET 5+,而使用".NET Core 和 .NET 5+"指代 .NET Core 及其后续版本。
更令人费解的是 .NET 5(+) 本身也是一个框架(framework),但它和之前的 .NET Framework 截然不同。因此我们尽可能使用"运行时"这个术语而不用"框架"这个词。
1.5.4 小众运行时
除了上述提到的运行时外,还有以下小众运行时:
- .NET Micro Framework 是在资源非常受限的嵌入式设备上运行 .NET 代码的框架(大小在 1MB 以内)。
- Unity 是一个游戏开发平台。它使用 C# 作为脚本语言进行游戏逻辑的开发。
1.6 C# 简史
下文将倒序介绍C#各个版本的新特性以方便熟悉旧版本语言的读者。
1.6.1 C# 10 的新特性
C# 10 随 Visual Studio 2022 发布。C# 10.0 可用于目标运行时为 .NET 6 的程序。
1.6.1.1 文件范围命名空间
通常情况下,一个文件中的所有类型都会定义在单一命名空间中。因此,C# 10 中的文件范围命名空间能够有效地避免代码混乱同时消除多余的缩进:
c#
namespace MyNamespace; // Applies to everything that follows in the file
class Class1 {}; // inside MyNamespace
class Class2 {}; // inside MyNamespace
1.6.1.2 全局 using 指令
在 using 指令前添加 global 关键字可以将该指令应用到当前工程(project)下的所有文件中:
c#
global using System;
global using System.Colleciton.Generic;
``
global using 指令可以避免在每一个文件中重复书写相同的指令。此外,global using 指令也支持 using static 写法。
除了全局 using 指令外,.NET 6 工程还支持隐式全局 using 指令。当工程文件中的 ImplicitUsings 元素的值为 true 时。编译器将自动(根据 SDK 工程类型)导入最常见的命名空间。请参见 2.12.3 节。
#### 1.6.1.3 在匿名对象上应用非破坏性更改
C# 9 使用 with 关键字在 record 上执行非破坏性更改。在 C# 10 中,这种做法同样适用于匿名对象:
```c#
var a1 = new { A = 1, B = 2, C = 3, D = 4, E = 5 };
var a2 = a1 with { E = 10 };
Console.WriteLine(a2);
1.6.1.4 新的解构语法
C# 7 可以在元组或任意实现了 Deconstruct 方法的类型上使用解构语法。而 C# 10 能够在解构时同时进行赋值和声明:
c#
var point = (3, 4);
double x = 0;
(x, double y) = point;
1.6.1.5 结构体的字段初始化器与午餐构造函数
从 C# 10 开始,我们可以在结构体(请参见3.4)中引入字段初始化器与无参构造器。当然,这些功能只在显式调用构造器的情况下才会生效,因此我们可以轻易地越过这些机制------例如,使用 default 关键字。该功能主要用于 record struct 类型。
1.6.1.6 record struct
C# 9 引入了 record,它是一种由编译器增强的类(class)。而在 C# 10 中,record 也支持 struct:
C#
record struct Point (int x, int y);
两类 record 的规则是相似的:record struct 与 class struct 的功能相近(请参见4.12节),但编译器为 record struct 生成的属性是可写的。如果需生成只读属性,则需要在 record 声明之前辅以 readonly 关键字。
1.6.1.7 Lambda 表达式的功能增强
C# 10 对 Lambda 表达式的语法进行了多项增强。首先,可以使用 var 隐式类型声明 Lambda 表达式:
C#
var greater = () => "Hello, world";
Lambda 表达式的隐式类型或为 Action 委托或为 Func 委托。因此本例中 greeter 的类型为 Func<string>
。而对于含有参数的表达式则必须显式指定每一个参数的类型:
C#
var square = (int x) => x* x;
其次,Lambda 表达式可以指定返回类型:
c#
var sqr = int (int x) => x;
这种举措主要是为了改善编译器处理复杂嵌套表达式的性能问题。
再次,我们可以将 Lambda 表达式传递到参数类型为 object、Delegate 或 Expression 的方法中。
C#
void M1(object x) {}
var M2(Delegate x) {}
void M3(Expression x) {}
M1(() => "test"); // Implicitly yped to Func<string>
M2(() => "test"); // Implicitly yped to Func<string>
M3(() => "test"); // Implicitly yped to Expression<string>
最后,我们可以在 Lambda 表达式编译生成的方法上添加特性(除此之外,还可以在其参数和返回值上添加特性):
C#
Action a = [Description("test")] () => {};
有关Lambda表达式特性的更多细节,请参见4.14.4节。
1.6.1.8 嵌套属性模式
C# 10 支持嵌套属性模式匹配(请参见 4.13.6 节),因此以下的语法是合法的:
C#
var obj = new Uri("http://www.linqpad.net");
if (obj is Url { Scheme.Length: 5 })...
上述语法等价于:
C#
if (obj is Url { Scheme: { Length: 5 } })...
1.6.1.9 CallerArgumentExpression 特性
若在方法的参数上应用 [CallerArgumentExpression] 特性,则编译器将捕获方法调用者的参数表达式,并将其传递给该参数:
c#
Print(Math.PI * 2);
var Print(double number, [CallerArgumentExpression] string expr = null) => Console.WriteLine(expr);
// Output: Math.PI * 2
该特性主要用于验证库和断言库的开发(请参见4.15.1节)。
1.6.1.10 其他新特性
C# 10 增强了 #line 预处理指令的功能。现在我们可以在该指令中指定列与范围。
在 C# 10 中,如果字符串插值中的值为常量(字符串),则插值后的字符串仍然可以是常量。
在 C# 10 中,record 可以将 ToString() 方法标记为 seal 的,以便派生类能够使用相同的表示形式。