[Unity脚本运行时更新]C#6新特性

C#

浏览数:44

2019-8-30

洪流学堂,让你快人几步!本文首发于洪流学堂微信公众号。

本文是该系列《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.LogMathf.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<int, string> {
    [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开发者朋友,或许你能帮到他。

作者:郑洪智_技术探路者