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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容