洪流学堂,让你快人几步!本文首发于洪流学堂微信公众号。
本文是该系列《Unity脚本运行时更新带来了什么?》的第4篇。
洪流学堂公众号回复runtime
,获取本系列所有文章。
Unity2017-2018.2中的4.x运行时已经支持到C#6,Unity2018.3将支持到C# 7.3,看看C#6新特性能给代码带来什么吧。
C#6 新特性
String填空
String.Format
非常常用,但使用起来很麻烦而且容易出错。在格式字符串中需要使用类似{0}的占位符,还得单独提供对应的参数:
var s = String.Format("{0} is {1} year{{s}} old", p.Name, p.Age);
字符串填空可以让你直接将表达式放在字符串的“空”中,就是在最前面加上$
:
var s = $"{p.Name} is {p.Age} year{{s}} old";
和 String.Format
类似,可选的对齐和格式都可以指定:
var s = $"{p.Name,20} is {p.Age:D3} year{{s}} old";
填空的内容可以是任何表达式:
var s = $"{p.Name} is {p.Age} year{(p.Age == 1 ? "" : "s")} old";
请注意,条件表达式是在括号里,因此:"s"
不会与格式说明符混淆。
自动属性的增强
自动属性的初始化
现在可以给自动属性赋初始值了。
public class Customer
{
public string First { get; set; } = "Jane";
public string Last { get; set; } = "Doe";
}
这个初始化直接赋值给属性的后备字段(自动生成的隐藏字段),并没有通过set方法。初始化的时机和字段初始化的时机一致。
和字段初始化一致,自动属性初始化时无法引用this
,毕竟初始化是在对象完全初始化之前进行的。
自动属性可以只设置Get
自动属性现在可以只设置Get,不设置Set
public class Customer
{
public string First { get; } = "Jane";
public string Last { get; } = "Doe";
}
只有Get方法的自动属性的后备字段被隐式声明为readonly
(尽管仅用于反射)。这个属性可以在属性声明时直接初始化,就像上面代码一样。也可以在类的构造函数中初始化,会直接赋值给后备字段。
public class Customer
{
public string Name { get; }
public Customer(string first, string last)
{
Name = first + " " + last;
}
}
表达式化的方法体
现在Lambda表达式可以用于成员方法的方法体。
表达式化的成员方法
方法、运算符可以用lambda的箭头来定义表达式主体。
public Point Move(int dx, int dy) => new Point(x + dx, y + dy);
public static Complex operator +(Complex a, Complex b) => a.Add(b);
public static implicit operator string(Person p) => p.First + " " + p.Last;
效果与带有单个return语句的块代码完全相同。
对于返回void的方法以及返回Task的异步方法,箭头语法仍然适用,但箭头后面的表达式必须是语句表达式(就像lambdas的规则一样):
public void Print() => Debug.Log(First + " " + Last);
表达式化的成员属性
属性和索引器可以有getter和setter。表达式主体可用于编写只有getter的属性和索引器,其中getter的主体由表达式主体提供:
public string Name => First + " " + Last;
public Customer this[long id] => store.LookupCustomer(id);
注意这里没有get
关键字。
Using static
该功能允许导入类型的所有可访问的静态成员,使其在后续代码中无需使用类型限定符即可使用:
using UnityEngine;
using static UnityEngine.Debug;
using static UnityEngine.Mathf;
class CS6Updates : MonoBehaviour
{
void Start()
{
Log(Sqrt(3 * 3 + 4 * 4));
}
}
如果你经常需要使用一些静态方法时,这个新功能就很棒,可以减少很多的代码量。如上面代码中本来应该写Debug.Log
和Mathf.Sqrt
。
扩展方法
扩展方法是静态方法,但使用的时候是实例方法。using static不会将扩展方法引入到全局范围内,还是需要通过实例方法去调用。
using static System.Linq.Enumerable; // 具体类型,不是命名空间
class Program
{
static void Main()
{
var range = Range(5, 17); // Ok: not extension
var odd = Where(range, i => i % 2 == 1); // Error, not in scope
var even = range.Where(i => i % 2 == 0); // Ok
}
}
Null条件运算符
有时候代码中会充斥着null检查。null条件运算符可以让你仅在对象非null的情况下访问对象成员,否则返回null。
int? length = customers?.Length; // null if customers is null
Customer first = customers?[0]; // null if customers is null
null条件运算符经常和空接合运算符??
一起使用:
int length = customers?.Length ?? 0; // 0 if customers is null
null条件运算符采用就近原则,我们先看一下以下的代码:
int? first = customers?[0].Orders.Count();
上面的代码等价于(除了 customers
只会计算一次):
int? first = (customers != null) ? customers[0].Orders.Count() : null;
null条件运算符可以链式计算:
int? first = customers?[0].Orders?.Count();
注意调用带括号的委托类型变量时不能直接使用 ?
,这会导致很多语法歧义。你可以使用Invoke调用:
if (predicate?.Invoke(e) ?? false) { … }
触发事件时建议这么调用:
PropertyChanged?.Invoke(this, args);
在触发事件之前,这是一种检查null的简单且线程安全的方法。它是线程安全的原因是该功能仅计算左侧一次,并将其保存在临时变量中。
nameof表达式
有些时候你可能想知道一个变量的变量名是什么。
使用字符串可以达到这个目的,但是容易出错。nameof表达式本质上是一种奇特的字符串文字,其中编译器检查你是否具有给定名称的内容,并且Visual Studio知道它引用的内容,因此导航和重构起作用。
if (x == null) throw new ArgumentNullException(nameof(x));
WriteLine(nameof(person.Address.ZipCode)); // prints "ZipCode"
索引初始化
对象和集合初始化对于初始化对象的字段、属性或为集合提供一组初始数据非常有用。但使用索引初始化字典和其他对象不太优雅。对象初始化现在可以使用一个新语法,可以通过索引将值设置为Key。
var numbers = new Dictionary {
[7] = "seven",
[9] = "nine",
[13] = "thirteen"
};
异常过滤器
try { … }
catch (MyException e) when (myfilter(e))
{
…
}
如果括号内表达式的计算结果为true,则运行catch块,否则异常将不被catch。
异常过滤器比捕获和重新抛出更好用,因为它可以保持堆栈不受破坏。如果稍后的异常导致堆栈被转储,你可以看到它最初来自哪里,而不仅仅是它重新抛出的最后一个位置。
“滥用”异常过滤器也是常见且被接受的一种方式:例如日志记录。他们可以在不拦截异常的情况下检查“飞过”的异常。在这些情况下,过滤器通常会调用一个错误返回的辅助函数来执行:
private static bool Log(Exception e) { /* log it */ ; return false; }
…
try { … } catch (Exception e) when (Log(e)) {}
catch和finally中的异步
Resource res = null;
try
{
res = await Resource.OpenAsync(…); // You could do this.
…
}
catch(ResourceException e)
{
await Resource.LogAsync(res, e); // Now you can do this …
}
finally
{
if (res != null) await res.CloseAsync(); // … and this.
}
小结
本文讲解了C#6的新特性中对Unity编程有影响的新特性。
洪流学堂公众号回复runtime
,获取本系列所有文章。
把今天的内容分享给其他Unity开发者朋友,或许你能帮到他。