最新文章
泰课在线 | 微信拼团成功后如何获取课程?
08-09 17:57
Unity教程 | 使用ARKit为iOS开发AR应用
07-31 17:23
Unity Pro专业版7折订阅四选一工具包之VR开发与艺术设计
07-28 11:47
网友使用虚幻UE4实现CAVE 多通道立体渲染的沉浸式环境
07-27 11:57
VR晕动症调查:未来5年内大部分VR晕动症将得到解决
07-27 11:26
AMD CEO:未来3-5年最重要 希望5年达1亿VR用户
07-27 10:44
C#7.0中有哪些新特性
以下将详细描述 C# 7.0 中所有计划的语言特性。随着 Visual Studio “15” Preview 4 版本的发布,这些特性中的大部分将得以启用。现在,是时候展示这些特性了,也欢迎大家分享自己的想法!
C# 7.0 新增了许多功能,主要聚焦于数据消费、简化代码以及性能改善。其中,元组和模式匹配或许是最为突出的特性。元组能够轻松实现多个返回结果,而模式匹配则可依据数据的“形”来简化代码。我们期望将它们结合使用,让代码更加简洁高效,使开发者更愉悦且富有成效。
若您对某些特性有意见,或者有关于提升这些特性的想法,请点击 Visual Studio 窗口顶部的反馈按钮告知我们。需要注意的是,Preview 4 版本中还有许多功能尚未实现。接下来,我会介绍一些在最终版本中将会生效的特性,以及一些若不起作用就会被删除的特性。我支持对这些计划进行调整,尤其是根据大家反馈所做出的改变。当最终版本发布时,部分特性可能会有所修改或被删除。
如果您对这些特性的设计过程感兴趣,可以在 Roslyn GitHub site 上找到大量的设计笔记和讨论。
希望 C# 7.0 能为您带来愉悦的编程体验!
输出变量
在当前的 C# 中,使用输出参数并不如我们期望的那般便捷。在调用一个带有输出参数的方法之前,必须先声明一个变量并将其传递给该方法。若未初始化这些变量,就无法使用 var 来声明它们,除非指定完整的类型,示例如下:
public void PrintCoordinates(Point p)
{
int x, y; // 必须“预先声明”
p.GetCoordinates(out x, out y);
WriteLine($"({x}, {y})");
}
在 C# 7.0 中,我们新增了输出变量,并且可以直接声明一个作为输出实参传递的变量,示例如下:
public void PrintCoordinates(Point p)
{
p.GetCoordinates(out int x, out int y);
WriteLine($"({x}, {y})");
}
需要注意的是,变量处于封闭块的作用域内,后续仍可使用。大多数类型的声明不会创建自己的作用域,因此在其中声明的变量通常会被引入到封闭作用域。
Note:在 Preview 4 中,作用域规则更为严格,输出变量的作用域仅限于声明它们的语句,所以上述示例要到下个版本发布时才能正常使用。
由于输出变量直接作为实参传递给输出形参,编译器通常能够推断出它们的类型(除非存在冲突过载),因此使用 var 来声明它们是更好的选择,示例如下:
p.GetCoordinates(out var x, out var y);
输出参数的一种常见用法是 Try 模式,其中布尔返回值表示操作是否成功,输出参数则携带获取的结果,示例如下:
public void PrintStars(string s)
{
if (int.TryParse(s, out var i)) { WriteLine(new string('*', i)); }
else { WriteLine("Cloudy - no stars tonight!"); }
}
注意:这里的 i 仅在 if 语句中定义,所以 Preview 4 能够很好地处理这种情况。
我们计划允许使用以 a* 形式的“通配符”作为输出参数,这样可以忽略不关心的参数,示例如下:
p.GetCoordinates(out int x, out *); // 只关心 x
Note:C# 7.0 中是否会包含通配符尚不确定。
模式匹配
C# 7.0 引入了模式的概念。从抽象层面来讲,模式是一种句法元素,可用于测试数据是否具有某种“形”,并在应用时从值中提取有效信息。
C# 7.0 中的模式示例如下:
- 常量模式:形式为
C(C是 C# 中的常量表达式),用于测试输入是否等于C。 - 类型模式:形式为
T X(T是一种类型,X是一个标识符),用于测试输入是否为T类型,若是,则将输入值提取为T类型的新变量X。 - Var 模式:形式为
Var x(x是一个标识符),它总是匹配成功,并将输入值以其原本的类型存入新变量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));
}
可以看到,模式变量(由模式引入的变量)与前文描述的输出变量类似,它们可以在表达式中间声明,并在最近的作用域内使用。和输出变量一样,模式变量是可变的。
注:和输出变量一样,Preview 4 适用严格的作用域规则。
模式和 Try 方法可以很好地协同工作,示例如下:
if (o is int i || (o is string s && int.TryParse(s, out i))) { /* 使用 i */ }
具有模式的 Switch 语句
我们对 Switch 语句进行了扩展:
- 可以为任何类型设置
Switch语句(不仅限于原始类型)。 case语句中可以使用模式。Case语句可以有特殊的条件。
以下是一个简单的示例:
switch (shape)
{
case Circle c:
WriteLine($"circle with radius {c.Radius}");
break;
case Rectangle s when (s.Length == s.Height):
WriteLine($"{s.Length} x {s.Height} square");
break;
case Rectangle r:
WriteLine($"{r.Length} x {r.Height} rectangle");
break;
default:
WriteLine("<unknown shape>");
break;
case null:
throw new ArgumentNullException(nameof(shape));
}
关于新扩展的 switch 语句,有几点需要注意:
Case语句的顺序:现在变得重要了。就像catch语句一样,case语句的作用域可能会相交,第一个匹配的case会被选中。此外,编译器会通过去除明显不会执行的case来提供帮助。在此之前,甚至不需要考虑判断顺序,所以这并非是使用case语句的重大改变。- 默认语句的判断顺序:默认语句仍然最后被判断。尽管
null的case语句在默认语句之前出现,但它会在默认语句被选中之前进行测试,这与现有Switch语义兼容。不过,通常建议将默认语句放在最后。 Switch不会匹配到最后的null语句:这是因为当前IS表达式的类型匹配不会匹配到null,这确保了空值不会被意外地与任何类型模式匹配;必须明确如何处理它们(或使用默认语句)。
通过 case 引入的模式变量,其标签仅在相应的 Switch 作用域内有效。
元组
从方法中返回多个值是一种常见的需求,但目前可用的选项并非最优:
- 输出参数:使用起来较为繁琐(即使有上述改进),并且在异步方法中无法使用。
System.Tuple<...>返回类型:存在冗余使用的问题,并且需要分配一个元组对象。- 方法的定制传输类型:对于类型而言,会产生大量的代码开销,其目的仅仅是暂时组合一些值。
- 通过动态返回类型返回匿名类型:性能开销较大,且没有静态类型检查。
为了改善这种情况,C# 7.0 增加了元组类型和元组文字,示例如下:
(string, string, string) LookupName(long id) // 元组返回类型
{
... // 从数据存储中检索 first, middle 和 last
return (first, middle, last); // 元组文字
}
该方法可以有效地返回三个字符串,以元素的形式包含在一个元组值中。
调用此方法将得到一个元组,并且可以单独访问其中的元素,示例如下:
var names = LookupName(id);
WriteLine($"found {names.Item1} {names.Item3}.");
Item1 等是元组元素的默认名称,可以一直使用,但它们缺乏描述性,因此可以选择添加更具描述性的名称,示例如下:
(string first, string middle, string last) LookupName(long id) // 元组元素有名称
现在,元组的接收者可以使用多个具有描述性的名称,示例如下:
var names = LookupName(id);
WriteLine($"found {names.first} {names.last}.");
也可以直接在元组文字中指定元素名称,示例如下:
return (first: first, middle: middle, last: last); // 文字中的命名元组元素
通常,可以为元组类型分配一些彼此无关的名称,只要各个元素是可分配的,元组类型就可以自如地转换为其他元组类型。不过也存在一些限制,特别是对于元组文字,常见的错误如不慎交换元素名称时会出现错误。
Note:这些限制在 Preview 4 中尚未实现。
元组是值类型,其元素是公开且可变的。如果所有元素都成对相等(并且具有相同的哈希值),那么两个元组也是相等的(并且具有相同的哈希值)。
这使得元组在需要返回多个值的情况下非常有用。例如,在需要多个键值的字典中使用元组作为键,或者在每个位置需要多个值的列表中使用元组进行搜索,都能很好地工作。
Note:元组依赖于一组基本类型,但 Preview 4 中不包含这些。为了使该特性正常工作,可以通过 NuGet 获取它们:
- 右键单击 Solution Explorer 中的项目,然后选择“管理的 NuGet 包......”。
- 选择“Browse”选项卡,选中“Include prerelease”,选择“nuget.org”作为“Package source”。
- 搜索“System.ValueTuple”并安装它。
解构
另一种使用元组的方式是对其进行解构。解构声明是一种将元组(或其他值)分割成部分,并分别分配到新变量的语法,示例如下:
(string first, string middle, string last) = LookupName(id1); // 解构声明
WriteLine($"found {first} {last}.");
在解构声明中,可以使用 var 来声明单独的变量,示例如下:
(var first, var middle, var last) = LookupName(id1); // var 在内部
或者将一个单独的 var 作为缩写放在圆括号外面,示例如下:
var (first, middle, last) = LookupName(id1); // var 在外部
也可以使用解构赋值将元组解构成现有的变量,示例如下:
(first, middle, last) = LookupName(id2); // 解构赋值
解构并不局限于元组,任何类型只要具有(实例或扩展)解构方法,都可以被解构,示例如下:
public void Deconstruct(out T1 x1, ..., out Tn xn) { ... }
输出参数构成了解构结果中的值。
(为什么使用参数而不是返回一个元组呢?这是为了针对不同的值拥有多个重载。)
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);
这是一种常见的模式,以对称的方式包含了构建和解构。
对于输出变量,我们计划在解构中加入通配符,以简化对不关心变量的处理,示例如下:
(var myX, *) = GetPoint(); // 只关心 myX
Note:通配符是否会出现在 C# 7.0 中尚不确定。
局部函数
有时,一个辅助函数只需要在另一个独立函数内部起作用。现在,可以在其他函数内部以局部函数的方式声明这样的函数,示例如下:
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; }
}
}
}
如果迭代器有一个私有方法传递给过滤器,那么当其他成员意外使用迭代器时,迭代器也可能变得可用(即使没有参数检查)。此外,局部函数会采用与过滤器相同的实参,以替换作用域内的参数。
注意:在 Preview 4 中,局部函数必须在调用之前声明。这个限制将会放宽,以便局部函数在从定义分配中读取时能够被调用。
文字改进
C# 7.0 允许使用 _ 作为数字分隔符,示例如下:
var d = 123_456;
var x = 0xAB_CD_EF;
可以在任意数字之间插入 _ 以提高可读性,它们不会影响值的大小。
此外,C# 7.0 引入了二进制文字,这样就可以直接指定二进制模式,而无需了解十六进制,示例如下:
var b = 0b1010_1011_1100_1101_1110_1111;
引用返回和局部引用
在 C# 中,不仅可以通过引用来传递参数(使用引用修改器),现在还可以通过引用来返回参数,并且可以将其存储为局部变量,示例如下:
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; // 将数组中的 7 替换为 9
WriteLine(array[4]); // 输出 9
这是一种绕过占位符访问大数据结构的有效方法。例如,游戏可能会将数据保存在大型预分配的阵列结构中(以避免垃圾回收机制暂停),方法可以直接返回对结构的引用,调用者可以通过该引用读取和修改数据。
不过,为了确保安全,存在一些限制:
- 只能返回“安全返回”的引用,即传递给方法的引用或指向对象中的引用。
- 本地引用必须初始化为本地存储,并且不能指向其他存储。
异步返回类型
到目前为止,C# 的异步方法必须返回 void、Task 或 Task<T>。C# 7.0 允许定义其他类型作为异步方法的返回类型。
例如,我们计划创建一个 ValueTask<T> 结构类型。它的设计目的是在异步运行的结果在等待时已可用的情况下,避免对 Task<T> 进行分配。对于许多涉及缓冲的异步场景,这可以大大减少分配的数量,并显著提升性能。
Note:异步返回类型在 Preview 4 中尚未提供。
更多的 expression bodied 成员
expression bodied 的方法和属性是 C# 6.0 的重大改进。C# 7.0 在 expression bodied 事件列表中增加了访问器、构造器和终结器。
class Person
{
private static ConcurrentDictionary<int, string> names = new ConcurrentDictionary<int, string>();
private int id = ...
}