原书第7版
点击查看第二章
C# 7.0核心技术指南
C# 7.0 in a Nutshell
[美] 约瑟夫·阿坝哈瑞(Joseph Albahari)
本·阿坝哈瑞(Ben Albahari) 著
刘夏 译
Beijing Boston Farnham Sebastopol Tokyo
O’Reilly Media, Inc.授权机械工业出版社出版
机械工业出版社
第1章
C#和.NET Framework简介
C#是一种通用的,类型安全的面向对象编程语言。其目标是提高程序员的生产力,为此,需要在简单性、表达性和性能之间进行权衡。C#语言的首席架构师Anders Hejlsberg随该语言的第一个版本一直走到了今天(他也是Turbo Pascal的发明者和Delphi的架构师)。C#语言和平台无关,且可以与诸多平台下的编译器和框架(尤其是Windows下的Microsoft .NET Framework)协同工作。
1.1 面向对象
C#实现了丰富的面向对象范式,包括封装、继承和多态。封装意味着在对象周围创建一个边界,将其外部(公有)行为与内部(私有)实现细节隔离。C#面向对象特性包括:
统一的类型系统
C#中的基础构件是一种称为类型的数据与函数的封装单元。C#拥有统一的类型系统,其中的所有类型都共享一个公共的基类。这意味着所有类型,不论它们是表示业务对象还是表示数字这样的基元类型,都共享相同的基本功能。例如,任何类型的实例都可以通过调用ToString方法将自身转换为一个字符串。
类与接口
在传统面向对象范式中,唯一的类型就是类。然而C#还有其他几种类型,其中之一是接口(interface)。接口与类相似,但它仅仅描述成员。而实现接口的类型将实现接口定义的这些成员。接口在需要多继承的情形下非常有用(与C++和Eiffel等语言不同,C#并不支持类的多继承)。
属性、方法和事件
在纯粹的面向对象范式中,所有的函数都是方法(Smalltalk就是这样)。而在C#中,方法只是函数成员之一。除此之外还有属性、事件及其他的形式。属性是封装了一部分对象状态的函数成员,例如按钮的颜色或者标签的文本。事件则是简化对象状态变化处理的函数成员。
虽然C#首先是一种面向对象的语言,但它也借鉴了函数式编程的范式。例如:
可以将函数作为值看待
C#使用委托(delegate)将函数作为值传递给其他函数或者从其他函数中返回。
C#支持纯函数模式
函数式编程的核心是避免使用值可以变化的变量,或称为声明式模式。C#拥有支持该模式的若干关键功能。包括支持可以捕获变量的匿名函数(Lambda表达式),通过查询表达式(query expression)执行列表式或响应式编程;它还可以使用只读字段(readonly field)和属性创建不可变的(immutable)类型。
1.2 类型安全性
C#是一种类型安全(type-safe)的语言。这意味着类型的实例只能通过它们定义的协议进行交互,从而保证了每种类型的内部一致性。例如,C#不允许将字符串类型作为整数类型进行处理。
更具体地说,C#支持静态类型化(static typing),即在编译时会执行安全性检查。此外,在运行时也会同样执行类型安全性检查。
静态类型化能够在程序运行之前排除大量的错误。它将大量的运行时单元测试转移到编译器中,确保程序中所有类型之间都是相互适配的,使大型程序更易于管理、更具预测性并更加健壮。而且静态类型化可以借助一些工具,例如Visual Studio的IntelliSense来提供更好的编程辅助。因为它知道某个特定变量的类型,自然也知道该变量上能够调用的方法。
C#允许部分代码通过dynamic关键字来动态定义指定类型。然而,C#在大多数情况下仍然是一门静态类型化语言。
C#还是一门强类型语言(strongly typed language),因为它的类型规则(不论是静态还是运行时)非常严格。例如,不能用一个浮点类型的参数来调用一个接受整数类型参数的函数,必须显式将这个浮点数转换为整数。这可以防止编码错误。
强类型也是C#代码能够在沙盒(sandbox)中运行的原因之一。沙盒环境中的所有安全性均由宿主控制。因此在沙盒中,用户无法越过类型规则而随意破坏对象的状态。
1.3 内存管理
C#依靠运行时来实现自动内存管理。公共语言运行时的垃圾回收器会作为程序的一部分运行,并负责回收那些不再被引用的对象所占用的内存,程序员无须显式释放对象的内存,从而避免诸如C++等语言中错误使用指针而造成的问题。
C#并未抛弃指针,只是在大多数编程任务中是不需要使用指针的。对于性能优先的热点和互操作性,仍然可以在标记为unsafe的程序块内使用指针并进行显式内存分配。
1.4 平台支持
C#以往几乎只用于编写在Windows平台上执行的代码。但最近,Microsoft和一些公司将其推广到了其他的平台上,包括Linux、macOS、iOS以及Android。Xamarin可以使用C#进行跨平台的移动应用开发,而可移植的类型库(Portable Class Library)也得到了广泛的应用。Microsoft发布的ASP.NET Core是一个跨平台的轻量级网络宿主框架,并可以在.NET Framework以及.NET Core (开源的跨平台运行时)上运行。译注1
1.5 C#和CLR
C#依赖运行时环境,它含有许多特性,如自动化内存管理和异常处理。Microsoft .NET Framework的核心公共语言运行时(Common Language Runtime,CLR)就提供了这些运行时特性。(.NET Core以及Xamarin框架也提供了类似的运行时。)CLR和语言无关。开发者可以使用多种语言,例如C#、F#、Visual Basic.NET,以及托管C++来构建应用程序。
与其他的托管语言一样,C#也会将代码编译为托管代码。托管代码以中间语言(Intermediate Language,IL)的形式表示。CLR通常会在执行前,将IL转换为机器(例如x86或x64)原生代码,称为即时(Just-In-Time,JIT)编译。除此之外,还可以使用提前编译(ahead-of-time compilation)技术来改善拥有大程序集,或在资源有限的设备上运行的程序的启动速度(包括那些使用Xamarin开发的)满足iOS应用商店规则的应用。
托管代码的容器称为程序集(assembly)或可移植程序集(portable executable)。程序集可以是一个可执行文件(.exe)也可以是一个库(.dll)。它们不仅包含IL,还包含称为元数据
(metadata)的类型信息。元数据的引入使程序集无须额外的文件就可以引用其他程序集中的类型。
使用Microsoft的ildasm工具可以反编译并查看程序集的IL。而其他工具,例如ILSpy、dotPeek(JetBrains)以及Reflector(Red Gate)则可以将IL代码进一步反编译为C#。IL的层次相比原生机器代码要高得多,因此反编译器可以高质量地重建C#代码。
程序也可以通过反射(reflection)查询其元数据,甚至在运行时生成新的IL(reflection.emit)。
1.6 CLR和.NET Framework
.NET Framework是由CLR和大量的程序库组成的。这些程序库由核心库(本书主要介绍)和应用库组成,应用库依赖于核心库。图1-1是这些程序库的可视化概况(也可以作为本书的导航图)。
核心库又称为基础类库(Base Class Library, BCL)。而整个框架称为框架类库(Framework Class Library)。
1.7 其他框架
Microsoft .NET Framework是一个全面而成熟的框架,但仅仅运行在Microsoft Windows(桌面版本和服务器)上。在若干年里,陆续出现了支持其他平台的框架。目前除.NET Framework之外,还存在三个其他的框架。而且这三个框架都是属于Microsoft的。
Universal Windows Platform (UWP)
为了编写Windows 10应用商店的应用,并使其可以在支持Windows 10的设备(手机、XBox、Surface Hub以及Hololens)上执行。应用必须在沙盒中执行以降低恶意软件的威胁,并防止类似读写任意文件这样的操作。
.NET Core以及ASP.NET Core
这是一个用来开发易于部署的Internet应用程序和微服务的开源框架(最初基于一个削减功能后的.NET Framework)。用它书写的应用可以在Windows、macOS以及Linux上运行。与.NET Framework不同,.NET Core可以将Web应用程序打包并进行xcopy部署(自包含的部署)。
Xamarin
该框架用于书写iOS、Android以及Windows Mobile等移动应用。微软已于2016年将Xamarin公司收购。
表1-1比较了上述几种框架所支持的平台。
四种框架支持的平台、基础库和应用场景各有不同。但是可以说在.NET Core 2.0发布之后,它们都公布了相似的核心框架(BCL),而这也是本书的着眼点。甚至我们可以利用这种共性,书写可以在所有四种框架上工作的类库(有关.NET Standard 2.0的内容请参见第5章)。
UWP在内部使用了.NET Core,因此从技术上说,.NET Core可以在Windows 10设备上运行(尽管这与为ASP.NET Core提供框架的目的不同)。这和表1-1的内容有些出入。可以预测,未来.NET Core 2会得到更加广泛的使用。
1.7.1遗留框架和小众框架
以下的框架仍然可以在旧有的平台上运行:
- 用于Windows 8/8.1的Windows Runtime(现被UWP取代)。
- Windows Phone 7/8(现被UWP取代)。
- 用于游戏开发的Microsoft XNA(现被UWP取代)。
- Silverlight(由于HTML5和JavaScript的兴起而不再继续开发)。
- .NET Core 1.x(.NET Core 2.0的前序版本,其功能非常有限)。
同样值得一提的是以下的小众框架:
- .NET Micro Framework是在资源非常受限的嵌入式设备上运行.NET代码的框架(大小在1MB以内)。
- Mono是Xamarin开发的开源框架,包含了用于开发跨平台(Linux、macOS以及Windows)桌面应用的类库。该框架并未支持所有的特性。
除此之外,我们还可以在SQL Server上执行托管代码。SQL Server的CLR集成环境支持在SQL中调用C#开发的自定义函数、存储过程以及聚合函数。它虽然使用.NET Framework,但是其沙盒是由特殊的CLR宿主提供的,以保护SQL Server进程无恙。
1.7.2 Windows Runtime
C#还支持和Windows Runtime (WinRT)的互操作。WinRT是
- 支持Windows 8及以上操作系统的语言无关的面向对象的执行接口。
- 植入Windows 8及以上操作系统的库。该类库与上述接口兼容。
“WinRT”这个词汇容易令人误解。这是因为它曾经有两个含义:
- 其一是UWP的前身。即Windows 8/8.1商店应用(有时称为Metro或者Modern应用)的开发平台。
- 其二是Microsoft在2011年发布的基于精简指令集(RISC)的平板电脑操作系统(该系统已经被废弃)。
所谓执行接口(execution interface)是一个调用(潜在的)由其他语言书写的代码的协议。Microsoft Windows曾以低层次的C语言形式提供了原生的执行接口,组成了Win32 API。
WinRT则更加丰富。从局部看,它是一个支持.NET、C++和JavaScript的增强版本的COM(组件对象模型)。和Win32不同,它是面向对象的,并拥有相对丰富的类型系统。这意味着从C#中引用WinRT库就像是引用.NET库一样,你甚至不会意识到你正在使用WinRT。
Windows 10的WinRT库是UWP平台的关键组成部分(UWP依托于WinRT和.NET Core库)。如果目标平台是标准.NET Framework平台,则引用Windows 10的WinRT库是可选的。但是当需要访问Windows 10特有的,并未被.NET Framework涵盖的功能时,WinRT库就非常有用了。
Windows 10的WinRT库支持UWP用户界面,可开发沉浸式触摸优先的应用。它还支持移动设备相关的功能,例如传感器、文本消息等(Windows 8、8.1以及10的新功能是通过WinRT而非Win32开放的)。WinRT库还提供了文件I/O定制功能使其能够在UWP沙盒中顺畅运行。
WinRT和普通COM的区别是WinRT的程序库支持多种语言,包括C#、VB、C++和JavaScript,因此每一种语言(几乎)都将WinRT类型视为自己的专属类型。例如,WinRT将根据目标语言的要求调整大小写规则,甚至还会重新对一些函数与接口进行映射。WinRT程序集还在.winmd文件中包含了丰富的元数据,而其格式与.NET文件相同,不需要特殊处理就可以无缝对接。事实上,除了命名空间存在区别之外,开发者甚至不知道使用的是WinRT而非.NET类型。此外,WinRT类型遵循COM风格限制,并对继承和泛型提供了有限的支持。
C#不仅可以消费WinRT库,还可以创建新的库(并在JavaScript应用程序中调用)。
1.8 C#简史
下文将倒序介绍C#各个版本的新特性以方便熟悉旧版本语言的读者。
1.8.1 C# 7.0新特性
(C# 7.0随Visual Studio 2017发布。)
1.8.1.1 数字字面量的改进
C# 7中,数字字面量可以使用下划线来改善可读性、它们称为数字分隔符而被编译器忽略:
int million = 1_000_000;
二进制字面量可以使用0b前缀进行标识:
var b = 0b1010_1011_1100_1101_1110_1111;
1.8.1.2 输出变量及参数忽略
C# 7中,调用含有out参数的方法将更加容易。首先,可以非常自然地声明输出变量:
bool successful = int.TryParse ("123", out int result);
Console.WriteLine (result);
当调用含有多个out参数的方法时,可以使用下划线字符忽略你并不关心的参数:
SomeBigMethod (out _, out _, out _, out int x, out _, out _, out _);
Console.WriteLine (x);
1.8.1.3 模式
is运算符也可以自然地引入变量了,称为模式变量(请参见3.2.2.5节):
void Foo (object x)
{
if (x is string s)
Console.WriteLine (s.Length);
}
switch语句同样支持模式,因此我们不仅可以选择常量还可以选择类型(请参见2.11.3.5节);可以使用when子句来指定一个判断条件;或是直接选择null:
switch (x)
{
case int i:
Console.WriteLine ("It's an int!");
break;
case string s:
Console.WriteLine (s.Length); // We can use the s variable
break;
case bool b when b == true: // Matches only when b is true
Console.WriteLine ("True");
break;
case null:
Console.WriteLine ("Nothing");
break;
}
1.8.1.4 局部方法
局部方法是声明在其他函数内部的方法(请参见3.1.2.4节):
void WriteCubes()
{
Console.WriteLine (Cube (3));
Console.WriteLine (Cube (4));
Console.WriteLine (Cube (5));
int Cube (int value) => value value value;
}
局部方法仅仅在其包含函数内可见,它们可以像Lambda表达式那样捕获局部变量。
1.8.1.5 更多的表达式体成员
C# 6引入了以“胖箭头”语法表示的表达式体的方法、只读属性、运算符以及索引器。而C# 7更将其扩展到了构造函数、读/写属性和终结器中:
public class Person
{
string name;
public Person (string name) => Name = name;
public string Name
{
get => name;
set => name = value ?? "";
}
~Person () => Console.WriteLine ("finalize");
}
1.8.1.6 解构器
C# 7引入了解构器模式。构造器一般接受一系列值(作为参数)并将其赋值给字段,而解构器则正相反,它将字段反向赋值给变量。以下示例为Person类书写了一个解构器(不包含异常处理):
public void Deconstruct (out string firstName, out string lastName)
{
int spacePos = name.IndexOf (' ');
firstName = name.Substring (0, spacePos);
lastName = name.Substring (spacePos + 1);
}
解构器以特定的语法进行调用:
var joe = new Person ("Joe Bloggs");
var (first, last) = joe; // Deconstruction
Console.WriteLine (first); // Joe
Console.WriteLine (last); // Bloggs
1.8.1.7 元组
也许对于C# 7来说最值得一提的改进当属显式的元组(tuple)支持(请参见4.10节)。元组提供了一种存储一系列相关值的简单方式:
var bob = ("Bob", 23);
Console.WriteLine (bob.Item1); // Bob
Console.WriteLine (bob.Item2); // 23
C#的新元组实质上是使用System.ValueTuple<...>泛型结构的语法糖。多亏了编译器的“魔力”,我们还可以对元组的元素进行命名:
var tuple = (Name:"Bob", Age:23);
Console.WriteLine (tuple.Name); // Bob
Console.WriteLine (tuple.Age); // 23
有了元组,函数再也不必通过一系列out参数来返回多个值了:
static (int row, int column) GetFilePosition() => (3, 10);
static void Main()
{
var pos = GetFilePosition();
Console.WriteLine (pos.row); // 3
Console.WriteLine (pos.column); // 10
}
元组隐式地支持解构模式,因此很容易解构为若干独立的变量。因此,上述Main方法中的GetFilePosition返回的元组将存储于两个局部变量row和column中:
static void Main()
{
(int row, int column) = GetFilePosition(); // Creates 2 local variables
Console.WriteLine (row); // 3
Console.WriteLine (column); // 10
}
1.8.1.8 throw表达式
在C# 7之前,throw一直是一个语句。现在,它也可以作为表达式出现在表达式体函数中:
public string Foo() => throw new NotImplementedException();
throw表达式也可以出现在三无判断运算符中:
string Capitalize (string value) =>
value == null ? throw new ArgumentException ("value") :
value == "" ? "" :
char.ToUpper (value[0]) + value.Substring (1);
1.8.1.9 其他改进
C#还包含一系列针对特定的场景进行专门的微小优化的功能(请参见2.8.5节和2.8.6节)。同时,我们可以在异步方法声明中包含返回类型而非Task/Task。
1.8.2 C# 6.0新特性
随Visual Studio 2015一起发布的C# 6.0采用了下一代的,完全使用C#编写的编译器,即“Roslyn”项目。新的编译器将一整条编译流水线通过程序库进行开放,使得对各种源代码进行分析成为可能(见第27章)。编译器本身是开源的,可以从github.com/dotnet/roslyn获得其源代码。
此外,C# 6.0为了改善代码的清晰性引入了一系列小而精的改进。
null条件(“Elvis”)运算符(请参见2.10节)可以避免在调用方法或访问类型的成员之前显式地编写用于null判断的语句。在以下示例中,result将会为null而不会抛出NullReferenceException:
System.Text.StringBuilder sb = null;
string result = sb?.ToString(); // result is null
表达式体函数(expression-bodied function)(请参见3.1.2节)可以以Lambda表达式的形式书写仅仅包含一个表达式的方法、属性、运算符以及索引器,使代码更加简短:
public int TimesTwo (int x) => x * 2;
public string SomeProperty => "Property value";
属性初始化器(property initializer,参见第3章)可以对自动属性进行初始赋值:
public DateTime TimeCreated { get; set; } = DateTime.Now;
这种初始化也支持只读属性:
public DateTime TimeCreated { get; } = DateTime.Now;
只读属性也可以在构造器中进行赋值,这令创建不可变(只读)类型变得更加容易了。
索引初始化器(index initializer)(见第4章)可以一次性初始化具有索引器的任意类型:
var dict = new Dictionary()
{
[3] = "three",
[10] = "ten"
};
字符串插值(string interploation)(参见2.6.2节)用更加简单的方式替代了string.Format:
string s = $"It is {DateTime.Now.DayOfWeek} today";
异常过滤器(exception filters)(请参见4.5节)可以在catch块上再添加一个条件:
string html;
try
{
html = new WebClient().DownloadString ("http://asef");
}
catch (WebException ex) when (ex.Status == WebExceptionStatus.Timeout)
{
...
}
using static(参见2.12节)指令可以引入一个类型的所有静态成员,这样就可以不用书写类型而直接使用这些成员:
using static System.Console;
...
WriteLine ("Hello, world"); // WriteLine instead of Console.WriteLine
nameof(见第3章)运算符返回变量、类型或者其他符号的名称。这样在Visual Studio中就可以避免变量重命名造成不一致的代码:
int capacity = 123;
string x = nameof (capacity); // x is "capacity"
string y = nameof (Uri.Host); // y is "Host"
最后值得一提的是,C# 6.0可以在catch和finally块中使用await。
1.8.3 C# 5.0新特性
C# 5.0最大的新特性是通过两个关键字,async和await,支持异步功能(asynchronous function)。异步功能支持异步延续(asynchronous continuation),从而简化响应式和线程安全的富客户端应用程序的编写。它还有利于编写高并发和高效的I/O密集型应用程序,而不需要为每一个操作绑定一个线程资源。
第14章将详细介绍异步功能。
1.8.4 C# 4.0新特性
C# 4.0增加的新特性有:
动态绑定
可选参数和命名参数
用泛型接口和委托实现类型变化
改进COM互操作性
动态绑定(参见第4章和第20章)将绑定过程(解析类型与成员的过程)从编译时推迟到运行时。这种方法适用于一些需要避免使用复杂反射代码的场合。动态绑定还适合于实现动态语言以及COM组件的互操作。
可选参数(见第2章)允许函数指定参数的默认值,这样调用者就可以省略一些参数,而命名参数则允许函数的调用者按名字而非按位置指定参数。
类型变化规则在C# 4.0进行了一定程度的放宽(见第3章和第4章),因此泛型接口和泛型委托类型参数可以标记为协变(covariant)或逆变(contravariant),从而支持更加自然的类型转换。
COM互操作性(见第25章)在C# 4.0中进行了三个方面的改进。第一,参数可以通过引用传递,并无须使用ref关键字(特别适用于与可选参数一同使用)。第二,包含COM 互操作(interop)类型的程序集可以链接而无须引用。链接的互操作类型支持类型相等转换,无须使用主互操作程序集(Primary Interop Assembly),并且解决了版本控制和部署的难题。第三,链接的互操作类型中的函数若返回COM变体类型,则会映射为dynamic而不是object,因此无须进行强制类型转换。
1.8.5 C# 3.0新特性
C# 3.0增加的特性主要集中在语言集成查询(Language Integrated Query, LINQ)上。LINQ令C#程序可以直接编写查询并以静态方式检查其正确性。它可以查询本地集合(如列表或XML文档),也可以查询远程数据源(如数据库)。C# 3.0中和LINQ相关的新特性还包括隐式类型局部变量、匿名类型、对象构造器、Lambda表达式、扩展方法、查询表达式和表达式树。
隐式类型局部变量(var关键字,见第2章)允许在声明语句中省略变量类型,然后由编译器推断其类型。这样可以简化代码并支持匿名类型(见第4章)。匿名类型是一些即时创建的类,它们常用于生成LINQ查询的最终输出结果。数组也可以隐式类型化(见第2章)。
对象初始化器(见第3章)允许在调用构造器之后以内联的方式设置属性,从而简化对象的构造过程。对象初始化器不仅支持命名类型也支持匿名类型。
Lambda表达式(见第4章)是由编译器即时创建的微型函数,适用于创建“流畅的”LINQ查询(见第8章)。
扩展方法(见第4章)可以在不修改类型定义的情况下使用新的方法扩展现有类型,使静态方法变得像实例方法一样。LINQ表达式的查询运算符就是使用扩展方法实现的。
查询表达式(见第8章)提供了编写LINQ查询的更高级语法,大大简化了具有多个序列或范围变量的LINQ查询的编写过程。
表达式树(见第8章)是赋值给一种特殊类型Expression的Lambda表达式的DOM(文档对象模型,Document Object Model)模型。表达式树使LINQ查询能够远程执行(例如在数据库服务器上),因为它们可以在运行时进行转换和翻译(例如变成SQL语句)。
C# 3.0还添加了自动化属性和分部方法。
自动化属性(见第3章)对在get/set中对私有字段直接读写的属性进行了简化,并将字段的读写逻辑交给编译器自动生成。分部方法(Partial Method,见第3章)可以令自动生成的分部类(Partial Class)自定义需要手动实现的钩子函数,而该函数可以在没有使用的情况下“消失”。
1.8.6 C# 2.0新特性
C# 2提供的新特性包括泛型(见第3章)、可空类型(nullable type)(见第4章)、迭代器(见第4章)以及匿名方法(Lambda表达式的前身)。这些新特性为C# 3引入LINQ铺平了道路。
C# 2还添加了分部类、静态类以及许多细节功能,例如对命名空间别名、友元程序集和定长缓冲区的支持。
泛型需要在运行时仍然能够确保类型的正确性,因此需要引入新的CLR(CLR 2.0)才能达成该目标。