C# 7.0 的新特性
Out 变量(Out variables)
在之前的 C# 版本中,使用 out 参数并不像我们期盼的那样流畅。在你能够使用 out 参数来调用一个函数之前,你首先需要声明待传入的变量。同时由于你一般不会初始化这些变量(它们毕竟会被这些方法覆写),你也无法使用 var 来声明它们,而是需要指定完整的类型:
public void PrintCoordinates(Point p)
{
int x, y; // 需要“预声明”
p.GetCoordinates(out x, out y);
WriteLine("({x}, {y})");
}
请注意,这些变量位于包含它们的代码块的作用域,因此之后的代码可以使用它们。许多种类的语句不会建立它们自己的代码块,因此在这些语句中声明的 out 变量通常被引入到(这个)封闭作用域中。
由于 out 变量直接以 out 参数的形式声明,编译器通常可以分辨它们的类型应该是什么(除非有冲突的重载),所以完全可以用 var 替代类型来声明它们:
p.GetCoordinates(out var x, out var y);
Out 参数的一个常见使用场景是会返回一个指示是否成功的 Try… 模式,然后 out 参数来携带获得的结果:
public void PrintStars(string s)
{
if (int.TryParse(s, out var i)) { WriteLine(new string('', i)); }
else { WriteLine("Cloudy - no stars tonight!"); }
}
我们也允许以 _(下划线)形式“舍弃” out 参数,来使你忽略你不关心的参数:
p.GetCoordinates(out var x, out _); // 我只关心 x
模式匹配(Pattern matching)
C# 7.0 引入了模式匹配的概念,一种从抽象的角度来说,指可以测试一个值是否有某种特定的“形状”、并在满足这一条件的时候从值中提取信息的句法元素。
C# 7.0 中的模式的例子有:
c(c 为 C# 中的一个常量表达式)形式的常量模式(Constant pattern),来测试输入是否等于 c
T x(T 为一个类型,x 为一个标识符)形式的类型模式(Type pattern),来测试输入是否有类型 T,并在满足条件的时候将值提取成全新的 T 类型的变量 x
var x(x 为一个标识符)形式的变量匹配(Var patterns),这种匹配总是能够成功,并会将输入的值简单的放入一个全新的与输入类型相同的变量 x 中。
这只是个开始——模式现在是 C# 中的一种新的语言元素了,我们也希望在未来能向 C# 中加入更多的模式。
在 C# 7.0 中我们用模式改进了两个已有的语言结构:
is 表达式的右边现在可以是表达式,而不仅仅是类型了
switch 语句中的 case 子句现在可以匹配模式,而不仅仅是常量了
在未来的 C# 版本中我们会添加更多可以使用模式的地方。
具有模式的 Is 表达式
这里有一个用常量模式和类型模式来使用 is 表达式的例子:
public void PrintStars(object o)
{
if (o is null)return; // 常量模式 “null”
if (!(o is int i)) return; // 类型模式 “int i”
WriteLine(new string('', i));
}
如你所见,模式变量(pattern variables)——通过模式引入的变量——和之前描述过的 out 变量很像,都可以在表达式中声明,也可以在最近的作用域中使用。和 out 变量一样,模式变量也是可以修改的。我们经常以“表达式变量”来统称 out 变量和模式变量。
模式和 Try 模式通常可以被很好地组合使用:
if (o is int i || (o is string s && int.TryParse(s, out i)) { /* 使用 i */ }
具有模式的 Switch 语句
我们正在使 switch 语句一般化,因此:
你可以筛选任意类型(不仅仅是原生类型)
模式可以被用在 case 子句中
Case 子句可以有额外的限制条件
这是个简单的例子:
switch(shape)
{
case Circle c:
WriteLine("{s.Length} x {s.Height} square");
break;
case Rectangle r:
WriteLine("found {names.Item1} {names.Item3}.");
Item1 等是元组元素的默认名称,并且总是可用的。但是这样描述性不是非常好,因此你可以选择性地使用另一种更好的方法:
(string first, string middle, string last) LookupName(long id) // 元组元素拥有了名称
现在元组的接收者可以使用更具描述性的名字了:
var names = LookupName(id);
WriteLine("found {first} {last}.");
在一个解构声明中,你可以为独立的变量声明使用 var:
(var first, var middle, var last) = LookupName(id1); // 在内部使用 var
甚至把一个单独的 var 放在括号外作为缩写:
var (first, middle, last) = LookupName(id1); // 在外部使用 var
你也可以通过 解构分配(deconstructing assignment)将其解构到已存在的变量上:
(first, middle, last) = LookupName(id2); // 解构分配
解构不仅仅适用于元组。任何类型都可以被解构,只要它拥有一个如下形式的(实例或扩展)解构方法(deconstructor method):
public void Deconstruct(out T1 x1, ..., out Tn xn) { ... }
Out 参数构成解构的结果。
(为什么它使用 out 参数而不是返回一个元组?那是因为这样一来你可以对不同的值的数量拥有不同的重载了)。
class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) { X = x; Y = y; }
public void Deconstruct(out int x, out int y) { x = X; y = Y; }
}
(var myX, var myY) = GetPoint(); // 调用 Deconstruct(out myX, out myY);
它将成为一种常见的模式,通过以这种方式“对称地”拥有构造器和解构器。
如同 out 变量,我们允许在解构中“舍弃”你不关心的部分:
(var myX, _) = GetPoint(); // 我只关心 myX
本地方法(Local functions)
有时一个辅助函数只在某个使用到它的函数中有用。现在你可以在其他函数体内将这类函数定义为 本地函数(local function):
public int Fibonacci(int x)
{
if (x < 0) throw new ArgumentException("Less negativity please!", nameof(x));
return Fib(x).current;
(int current, int previous) Fib(int i)
{
if (i == 0) return (1, 0);
var (p, pp) = Fib(i - 1);
return (p + pp, p);
}
}
作用域内的参数和本地变量都在本地方法中可用,就如同在 lambda 表达式中一样。
例如,被实现为迭代器的方法通常需要一个非迭代的包装函数以在调用时检查参数。(迭代器本身在 MoveNext 被调用之前不会开始)。本地方法完美的适用于这种场景:
public IEnumerable<T> Filter<T>(IEnumerable<T> source, Func<T, bool> filter)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (filter == null) throw new ArgumentNullException(nameof(filter));
return Iterator();
IEnumerable<T> Iterator()
{
foreach (var element in source)
{
if (filter(element)) { yield return element; }
}
}
}
如果 Iterator 是 Filter 旁的私有函数,它可能会被其他成员意外地直接使用(而没有参数检查)。同时,它还需要接收与 Filter 相同的参数,而不是直接在作用域中使用它们。
字面量改进
C# 7.0 允许 _(下划线)在数字字面量中作为数字分隔符 (digit separator):
var d = 123_456;
var x = 0xAB_CD_EF;
你可以将它们放置在任意位置来增强可读性。它们不会影响值。
同时,C# 7.0 引入了二进制字面量(binary literals),这样你可以直接指定位模板而不用将十六进制记号牢记于心。
var b = 0b1010_1011_1100_1101_1110_1111;
引用返回和引用本地变量(Ref returns and locals)
就像你可以在 C# 中以引用方式传值(使用 ref 修饰符)一样,你现在可以以引用方式返回值,并将它们以引用的方式存在本地变量中。
public ref int Find(int number, int[] numbers)
{
for (int i = 0; i < numbers.Length; i++)
{
if (numbers[i] == number)
{
return ref numbers[i]; // 返回储存的位置,而不是值
}
}
throw new IndexOutOfRangeException($"{nameof(number)} not found");
}
int[] array = { 1, 15, -39, 0, 7, 14, -12 };
ref int place = ref Find(7, array); // 为 7 在数列中的位置起个别名
place = 9; // 在数列中以 9 替换 7
WriteLine(array[4]); // 输出 9
这对向很大的数据结构中传递占位符来说非常有用。例如,一个游戏可能将它的数据存在一个庞大的预先分配好的结构体数组(以避免垃圾回收的停顿)中。现在方法可以返回直接指向这种解构的引用,调用者可以借此来读或者修改数据。
为了确保这样做是安全的,有一些限制:
你只能返回“可以安全返回”的引用:一种是传给你的,另一种是指向对象中的字段的。
引用本地变量被初始化到一个确定的储存位置,且不可被修改为指向另一个(引用变量)。
更加一般化的 Async 返回类型
在此之前,C# 中的 async 方法只能返回 void,Task 或是 Task<T> 中的一个。C# 7.0 允许用这样的方式定义其他的类型以使它们可以从被 async 方法所返回。
例如,我们现在有一个 ValueTask<T> 结构类型。它被用来防止 async 操作的结果在仍在 await 的时候就可用的情况下的 Task<T> 对象的创建。对大多数 async 场景,例如使用到缓存,这可以大幅减少内存分配并可以获得巨大的性能提升。
你可以想象得到,有许多种能使这种“类 Task”类型非常有用的方法。正确的创建它们可能不是那么直观,因此我们并不期待大多数人来造自己的轮子,但是它们将会出现在框架和 API 中,然后调用者们就可以像今天使用 Task 一样地返回并 await 它们了。
更多的表达式体成员(Expression bodied members)
表达式体方法,属性等是 C# 6.0 中的一大亮点,但我们并未所有成员上启用它。C# 7.0 在可以拥有表达式体的列表中添加了访问器、构造器和析构器:
class Person
{
private static ConcurrentDictionary<int, string> names = new ConcurrentDictionary<int, string>();
private int id = GetId();
public Person(string name) => names.TryAdd(id, name); // 构造器
~Person() => names.TryRemove(id, out *); // 析构器
public string Name
{
get => names[id]; // get 访问器
set => names[id] = value; // set 访问器
}
}
这是由社区贡献的特性的一个例子,而不是微软 C# 编译器团队(贡献的)。对,开源!
抛出表达式(Throw expressions)
在表达式中抛出异常非常简单:只要调用一个为你做这件事的方法!但是在 C# 7.0 中我们直接允许将 throw 在特定位置作为一个表达式:
class Person
{
public string Name { get; }
public Person(string name) => Name = name ?? throw new ArgumentNullException(nameof(name));
public string GetFirstName()
{
var parts = Name.Split(" ");
return (parts.Length > 0) ? parts[0] : throw new InvalidOperationException("No name!");
}
public string GetLastName() => throw new NotImplementedException();
}