洪流学堂,让你快人几步!本文首发于洪流学堂微信公众号。
本文是该系列《Unity脚本运行时更新带来了什么?》的第5篇。
洪流学堂公众号回复runtime
,获取本系列所有文章。
Unity2017-2018.2中的4.x运行时已经支持到C#6,之前的文章已经介绍完毕。Unity2018.3将支持到C# 7.3,今天我们先来看看C#7.0新特性能给代码带来什么吧,不过这些特性得等到Unity2018.3才可以用哦。
C#7 新特性
C#7.0增加了许多新功能,并将重点放在数据消耗,代码简化和性能上。最大的更新有:元组,它可以轻松获得多个结果;模式匹配可以简化以数据类型为条件的代码。但是还有许多其他大小不一的新功能。
Out变量
在旧版本的C#中,使用out参数并不是很流畅。在调用有out参数的方法之前,首先必须声明要传递给它的变量。由于通常不会初始化这些变量(毕竟它们将被方法覆盖),也无法使用var来声明它们,需要指定完整类型:
public void PrintCoordinates(Point p)
{
int x, y; // 必须要预先声明
p.GetCoordinates(out x, out y);
WriteLine($"({x}, {y})");
}
在C# 7.0中添加了out变量,可以直接在作为out参数传递的位置声明变量:
public void PrintCoordinates(Point p)
{
p.GetCoordinates(out int x, out int y);
WriteLine($"({x}, {y})");
}
变量的作用范围是当前代码块内,后续代码可以使用它们。
由于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参数,使用下划线_可以忽略你不需要的out参数:
p.GetCoordinates(out var x, out _); // 我只在乎x
模式匹配
C# 7.0引入了*模式的概念,它是一个语法元素,可以测试一个值具有某种“形状”,并可以从值中提取信息。
C#7.0中的模式示例如下:
- 常量模式c(c是C#中的常量表达式),用于判断输入值是否等于c
- 类型模式T x(T是一个类型并且x是一个标识符),它判断输入值的类型是否为T,如果是,则将输入值提取到x中
- Var模式var x(其中x是标识符),它总是匹配,并简单地将输入的值放入与输入x相同类型的新变量中。
这只是一个开始 - 模式是C#中一种新的语法,将来会将更多的元素添加到C#中。
在C#7.0中,模式增强了两个现有的语言结构:
- is 表达式现在可以在右侧有一个模式,而不仅仅是一个类型
- case switch语句中的子句现在可以用模式匹配,而不仅仅是常量值
具有模式的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));
}
模式变量,模式引入的变量,类似于前面描述的out变量,因为它们可以在表达式的中间声明,并且可以在最近的作用范围内使用。也像out变量一样,模式变量是可变的。我们经常将out变量和模式变量共同称为“表达式变量”。
模式和try方法一起用经常能达到神器的效果:
if (o is int i || (o is string s && int.TryParse(s, out i)) { /* use 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会被选中。此外,就像使用catch一样,编译器会帮你标记永远无法访问到的case。C#7.0之前,你不用考虑case的顺序,所以这个特性不会改变已有代码的行为。
-
default语句会在最后匹配:即使上面的
case null
是最后一个,也会在default之前匹配。这是为了与现有的switch语法兼容,但是良好的做法通常会将default放在最后。 - 最后的case null不会匹配不到:这是因为is表达式中的类型模式不匹配null,这可以确保不会因为先出现的任何类型模式而意外地捕获null。你必须明确地处理它们(或将它们保留到default里处理)。
- case ...:标签引入的模式变量的作用范围是对应的case范围内。
Tuple 元组
你可能希望从方法返回多个值,在旧版C#中可能的解决方案有:
- out参数:使用很笨重(即使有上面的改进),也不能使用异步方法。
- System.Tuple<...>返回类型:使用麻烦并需要分配元组对象。
- 为每个方法自定义返回类型:一个类型需要写大量代码,只是为了临时分组
- 通过dynamic返回的匿名类型:性能开销大,无静态类型检查。
这些解决方案都不是很好,为了更好的实现这一目标,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) // tuple elements have names
现在,该元组的接收者可以使用描述性名称:
var names = LookupName(id);
WriteLine($"found {names.first} {names.last}.");
您还可以直接在元组定义中指定元素名称:
return (first: first, middle: middle, last: last); // named tuple elements in a literal
通常,只要元组各个元素可赋值,元组就可以自由转换为其他元组类型,无论元组元素的名称是否一样。
元组是值类型,它们的元素是公共的,可变的字段。它们具有值相等性,这意味着如果两个元组的所有元素对应相等(并且具有相同的哈希码),则它们是相等的(并且具有相同的哈希码)。
这使得元组可用于多个返回值之外的许多其他情况。例如,如果你需要一个包含多个键的字典,可以使用元组作为key。如果你需要在每个位置具有多个值的列表,可以使用元组。
Deconstruction 解构
使用元组的另一种方法是解构后使用。解构声明可以将元组(或其他值)分割成单独的部分来接收对应的变量。
(string first, string middle, string last) = LookupName(id1); // deconstructing declaration
WriteLine($"found {first} {last}.");
也可以使用var:
(var first, var middle, var last) = LookupName(id1); // var inside
var 也可以放到外面。
var (first, middle, last) = LookupName(id1); // var outside
也可以解构到已有的变量中,称作解构赋值:
(first, middle, last) = LookupName(id2); // deconstructing assignment
解构不仅仅可以用于元组,任何类型都可以被解构,只要它有一个如下类型的解构方法(实例方法或扩展方法):
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(); // calls Deconstruct(out myX, out myY);
以这种方式使构造函数和解构函数“对称”是一种常见的模式。
就像输出变量一样,解构中可以“丢弃”你不关心的部分:
(var myX, _) = GetPoint(); // I only care about myX
本地函数
有时辅助函数只在使用它的单个方法中有意义。现在可以在函数体内将这些函数声明为本地函数:
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 中可以使用下划线 _ 作为数字字面值的分隔符:
var d = 123_456;
var x = 0xAB_CD_EF;
可以将它们放在数字之间的任何位置以提高可读性。它们对值没有影响。
此外,C#7.0引入了二进制字面值,因此你可以直接使用位模式,而不必使用心算来计算十六进制表示。
var b = 0b1010_1011_1100_1101_1110_1111;
Ref返回和临时变量
就像在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]; // return the storage location, not the value
}
}
throw new IndexOutOfRangeException($"{nameof(number)} not found");
}
int[] array = { 1, 15, -39, 0, 7, 14, -12 };
ref int place = ref Find(7, array); // aliases 7's place in the array
place = 9; // replaces 7 with 9 in the array
WriteLine(array[4]); // prints 9
这对于将占位符传递到大数据结构非常有用。例如,游戏经常把数据保存在一个大的预分配结构体数组中(以避免GC造成的性能影响)。方法现在可以直接将引用返回给这样的结构体,调用者可以通过读取和修改这个结构体。
有一些限制来确保这是安全的:
- 只能返回“可以安全返回”的引用:一个是传递进来的引用,另一个是指向对象中的字段的引用。
- Ref本地变量初始化在特定的存储位置,并且不能改变指向另一个。
异步返回类型的广泛支持
到目前为止,C#中的异步方法必须返回void,Task或者Task<T>。C#7.0允许以特定方式定义其他类型,以便从异步方法返回它们。
例如,我们现在有一个ValueTask<T>结构体类型。它的是为了防止在Task<T>等待时异步操作的结果已经可用的情况下再分配对象。例如,对于涉及缓冲的许多异步场景,这可以大大减少分配数量并导致显着的性能提升。
你还可以通过许多其他方式创建自定义“类任务”类型。创建它们并不是很简单,但它们很可能会开始出现在框架和API中,然后调用者就可以返回await它们了,就像现在的Task一样。如果你想具体了解:https://github.com/dotnet/roslyn/blob/master/docs/features/task-types.md
更多表达式化的成员体
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); // constructors
~Person() => names.TryRemove(id, out _); // finalizers
public string Name
{
get => names[id]; // getters
set => names[id] = value; // setters
}
}
抛出表达式
在表达式中间抛出异常很容易:只需调用执行此操作的方法!但是在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();
}
小结
本文讲解了C#7的新特性中对Unity编程有影响的新特性,不过这些特性得等到Unity2018.3才可以用哦。
洪流学堂公众号回复runtime
,获取本系列所有文章。
把今天的内容分享给其他Unity开发者朋友,或许你能帮到他。