CS5L9——CSharp 7 功能和语法

C#7的新语法更新重点主要是 代码简化
今天学习的out和ref新用法,弃元、本地函数都是相对比较重要的内容,可以给我们带来很多便捷性

而元组和模式匹配知识点 是C# 7中引入的最重要的两个知识点
他们可以帮助我们更效率的完成一些功能需求,建议大家常用他们

本章代码关键字

1
2
3
4
5
6
//不愧是C#,语法糖爆炸多,本章知识不建议看这里的关键字,尤其是元组,模式匹配,你在这里看是看不懂的XD
out //在out后面可以直接声明一个对应的变量
ref //ref修饰值变量可以在赋值时使用引用传递,而非值传递,使变量指向另一个变量的内存空间
() //元组
is //is后面不仅可以跟类型,还可以跟常量,直接判断是否同类型的同时,是否值相同,也可以跟变量,只要类型相同,is前面的变量的值直接就可以赋值给后面的变量
case //case可以用于在判断类型的同时,将判断的值赋值给类型后面的变量

C# 7 对应的Unity版本

Unity 2018.3支持 C# 7,Unity 2019.4支持 C# 7.3,7.1, 7.2,7.3 相关内容都是基于 7 的一些改进

C# 7 的新增功能和语法

  1. 字面值改进
  2. out 参数相关 和 弃元 知识点
  3. ref 返回值
  4. 本地函数
  5. 抛出表达式
  6. 元组
  7. 模式匹配

字面值改进

基本概念:在声明数值变量时,为了方便查看数值,可以在数值之间插入 _​ 作为分隔符
主要作用:方便数值变量的阅读,而 _​ 本身对程序没有影响(我们不能在数值开头使用_​​

1
2
3
4
int i = 9_9123_1239;
print(i);
int i2 = 0xAB_CD_17;
print(i2);

out变量的快捷使用 和 弃元

out的快捷使用(内联声明)

用法:不需要再使用带有 out​ 参数的函数之前,声明对应变量
作用:简化代码,提高开发效率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Start()
{
//C#7以前我们只能这么写
//int a, b;
//Calc(out a, out b);
//在C#7以后,我们可以直接在out关键字后面声明变量,并在后续使用它
Calc(out int a, out int b);
print(a);
print(b);
}

public void Calc(out int a, out int b)
{
a = 10;
b = 20;
}

结合 var​ 类型更简便
使用 var​ 后,编译器会根据参数需要的类型,自动帮我们判断该变量的类型

1
Calc(out var x, out var y);

但是这种写法在存在重载时不能正常使用,必须明确调用的是谁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void Start()
{
//Calc(out var x, out var y); //当遇到参数数量相同但参数类型不同的重载时,这样写编译器无法帮我们判断类型
Calc(out int x, out var y); //因此我们必须要在声明指明类型使编译器可以明确使用的是何种重载
}

public void Calc(out int a, out int b)
{
a = 10;
b = 20;
}

public void Calc(out float a, out float b)
{
a = 10;
b = 20;
}

弃元

可以使用 _​ 弃元符号,省略不想使用的参数

1
2
3
4
5
6
7
8
9
10
11
void Start()
{
Calc(out int c, out _);
print(c);
}

public void Calc(out int a, out int b)
{
a = 10;
b = 20;
}

ref修饰临时变量和返回值

基本概念:使用 ref​ 修饰临时变量和函数返回值,可以让赋值变为引用传递
作用:用于修改数据对象中的某些值类型变量

  1. 修饰值类型临时变量:

    在以前,将值类型变量a赋值给另一个值类型变量b,修改b并不能将a的值也修改掉,因为它们分别使用的是内存上不同的数据

    1
    2
    3
    4
    int testI = 100;
    int testI2 = testI; //这里是值传递,也就是说,testI2新开辟了一个新的内存空间来存储testI的数据
    testI2 = 900; //因此,在这里修改testI2并不能影响testI1的数据,因为它们指向的内存空间是不同的
    print(testI);

    而使用 ref​ 关键字,对赋值的两边添加 ref​ ,会使b指向a在内存的数据,这会导致修改b,也会修改a的值,因为它们使用的是内存上相同的数据

    1
    2
    3
    4
    int testI = 100;
    ref int testI2 = ref testI; //通过ref来修饰临时变量,使这个赋值变成了引用传递,也就是说,testI2指向的testI1的内存空间
    testI2 = 900; //因此,在这里修改testI2可以改变testI1的数据,因为它们指向的内存空间是相同的
    print(testI);

    结构体是值类型,因此使用结构体变量赋值另一个结构体变量时,会新开辟一块内存空间,因此修改新的结构体变量,并不能修改原来的结构体变量
    而在在赋值时使用 ref​ 修饰临时变量,可以使两个变量指向同一个内存,修改其中一个也能修改另一个

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public struct TestRef
    {
    public int atk;
    public int def;

    public TestRef(int atk, int def)
    {
    this.atk = atk;
    this.def = def;
    }
    }

    public class Lesson8 : MonoBehaviour
    {
    void Start()
    {
    TestRef r1 = new TestRef(5, 5);
    ref TestRef r2 = ref r1; //用ref修饰临时变量,因此这里是引用传递,也就是说,r2和r1指向了同一个内存空间
    r2.atk = 10; //因此修改r2的某个成员变量,会使r1的成员变量也被修改,因为它们指向的内存空间是相同的,类似于语言类型
    print(r1.atk);
    }
    }
  2. 获取对象中的参数,使变量指向该对象的参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public struct TestRef
    {
    public int atk;
    public int def;

    public TestRef(int atk, int def)
    {
    this.atk = atk;
    this.def = def;
    }
    }

    public class Lesson8 : MonoBehaviour
    {
    void Start()
    {
    ref int atk = ref r1.atk; //通过ref修饰临时变量,使atk指向结构体变量的某个成员存储数据的内存空间
    atk = 99; //因此修改atk,就会修改r1.atk的值,因为atk指向的使r1.atk的内存空间
    print(r1.atk);
    }
    }
  3. 函数返回值

    将方法的返回值前都加上 ref​,并使用 ref​ 修饰的变量接收返回的引用传递(不能缺少任意一个 ref​​ ,否则会导致引用传递失效或者语法错误)
    这样可以使变量接收到的返回值为引用传递,或者说让变量指向返回的对象的内存空间,修改变量的值就会同时修改返回的对象的成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    void Start()
    {
    int[] numbers = new int[] { 1, 2, 3, 45, 5, 65, 4532, 12 };
    //函数返回的是数组某个元素的引用,由一个ref修饰的变量接收引用传递,使该变量指向返回的元素的内存空间
    ref int number = ref FindNumber(numbers, 5);
    number = 98765; //修改这个变量就会修改方法返回的数组的某一个元素的值
    print(numbers[4]);
    }

    //要使返回值为值类型的方法,返回的是某个值类型的引用,我们需要用ref来修饰该方法,并让ref来修饰return
    public ref int FindNumber(int[] numbers, int number)
    {
    for (int i = 0; i < numbers.Length; i++)
    {
    if (numbers[i] == number)
    return ref numbers[i]; //返回numbers的第i个元素的引用
    }
    return ref numbers[0]; //返回numbers的第一个元素的引用
    }

本地函数

基本概念:在函数内部声明一个临时函数
注意:本地函数只能在声明该函数的函数内部使用,本地函数可以使用声明自己的函数中的变量
作用:方便逻辑的封装
建议:把本地函数写在主要逻辑的后面,方便代码的查看

可以理解为是本地函数就像是函数自己的私有函数,就像类的私有函数那样,
函数里可以直接调用这个本地函数,本地函数的局部变量生命周期也仅限于本地函数内部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void Start()
{
print(TestInt(10));
}

public int TestInt(int i)
{
bool b = false;
i += 10;
Calc();
print(b);
return i;

void Calc()
{
i += 10;
b = true;
}
}

抛出表达式

throw​:抛出表达式,就是指抛出一个错误,一般的使用方式 都是 throw new 一个异常类

**异常基类:**​Exception

1
throw new System.Exception("出错了!");

C#自带的异常类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//常用的异常类
IndexOutOfRangeException:当一个数组的下标超出范围时运行时引发。
NullReferenceException:当一个空对象被引用时运行时引发。
ArgumentException:方法的参数是非法的
ArgumentNullException: 一个空参数传递给方法,该方法不能接受该参数
ArgumentOutOfRangeException: 参数值超出范围
SystemException:其他用户可处理的异常的基本类
OutOfMemoryException:内存空间不够
StackOverflowException 堆栈溢出
//不常用的
ArithmeticException:出现算术上溢或者下溢
ArrayTypeMismatchException:试图在数组中存储错误类型的对象
BadImageFormatException:图形的格式错误
DivideByZeroException:除零异常
DllNotFoundException:找不到引用的DLL
FormatException:参数格式错误
InvalidCastException:使用无效的类
InvalidOperationException:方法的调用时间错误
MethodAccessException:试图访问思友或者受保护的方法
MissingMemberException:访问一个无效版本的DLL
NotFiniteNumberException:对象不是一个有效的成员
NotSupportedException:调用的方法在类中没有实现
InvalidOperationException:当对方法的调用对对象的当前状态无效时,由某些方法引发。

在C# 7中,可以在更多的表达式中进行错误抛出
好处:更节约代码量

  1. 空合并操作符后用 throw

    1
    2
    private string jsonStr;
    private void InitInfo(string str) => jsonStr = str ?? throw new System.ArgumentNullException(nameof(str));
  2. 三目运算符后面用 throw

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void Start()
    {
    GetInfo("1,2,3", 4); //因为第一个参数被分割后并不能获取到索引为4的子字符串,因此抛出异常
    }

    private string GetInfo(string str, int index)
    {
    string[] strs = str.Split(',');
    return str.Length > index ? strs[index] : throw new System.IndexOutOfRangeException(nameof(str));
    }
  3. =>符号后面直接throw

    1
    System.Action action = () => throw new System.Exception("错误,该委托不可调用!");

元组

基本概念:
多个值的集合,相当于是一种快速构建数据结构类的方式
一般在函数存在多返回值时可以使用元组 (返回值1类型, 返回值2类型,…) 来声明返回值
在函数内部返回具体内容时通过 (返回值1, 返回值2, …) 进行返回****主要作用:
提升开发效率,更方便的处理多返回值等需要用到多个值时的需求

  1. 无变量名元组的声明
    (获取值:Item'N'​ 作为从左到右依次的参数,N​ 从1开始)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    (int, float) yz = (1, 5.5f);
    print(yz.Item1);
    print(yz.Item2);

    (int, float,bool,string) yz1 = (1, 5.5f, true, "123");
    print(yz1.Item1);
    print(yz1.Item2);
    print(yz1.Item3);
    print(yz1.Item4);
  2. 有变量名元组的声明
    (有变量名的话,就不需要什么 item1​ 这些,直接使用元组名+变量名获取元组的成员值)

    1
    2
    3
    4
    5
    (int i, float f, bool b, string str) yz2 = (1, 5.5f, true, "123");
    print(yz2.i);
    print(yz2.f);
    print(yz2.b);
    print(yz2.str);

    值得一提的是,元组赋值时也可以用类似于函数命名参数那样在参数前加上变量名,但是我们赋值时仍然不能改变赋值的顺序

    1
    2
    (int i, float f, bool b, string str) yz2 = (i: 1, f: 5.5f, b: true, str: "123");
    //(int i, float f, bool b, string str) yz2 = (i: 1, b: true, f: 5.5f, str: "123"); //即使我们这样写,也不能改变赋值的顺序
  3. 元组可以进行等于和不等于的判断
    但是,数量相同才能比较,类型相同才能比较,否则会报错! 每一个参数的比较是通过 ==​ 比较 如果都是 true​ 则认为两个元组相等

    1
    2
    3
    4
    5
    6
    7
    (int i, float f, bool b, string str) yz1 = (1, 5.5f, true, "123");
    (int i, float f, bool b, string str) yz2 = (1, 5.5f, true, "123");

    if (yz1 == yz2)
    print("相同");
    else
    print("不同");

元组不仅可以作为临时变量 成员变量也是可以的

1
2
3
4
5
6
7
8
9
public class Lesson9 : MonoBehaviour
{
public (int, float) yz;

void Start()
{
print(this.yz.Item1);
}
}

函数多返回值

  • 无变量名函数返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void Start()
    {
    var info = GetInfo();
    print(info.Item1);
    print(info.Item2);
    print(info.Item3);
    }

    private (string, int, float) GetInfo()
    {
    return ("123", 2, 5.5f);
    }
  • 有变量名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void Start()
    {
    var info = GetInfo();
    print(info.str);
    print(info.i);
    print(info.f);
    }

    private (string str, int i, float f) GetInfo()
    {
    return ("123", 2, 5.5f);
    }
  • 元组的解构赋值

    相当于把多返回值元组拆分到不同的变量中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void Start()
    {
    (string myStr, int myInt, float myFloat) = GetInfo();
    print(myStr);
    print(myInt);
    print(myFloat);
    }

    private (string str, int i, float f) GetInfo()
    {
    return ("123", 2, 5.5f);
    }

    或者也可以这样写,要注意不能与上面的写法混用,要么声明都写在里面,要么声明都写在外面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    void Start()
    {
    string myStr;
    int myInt;
    float myFloat;
    (myStr, myInt, myFloat) = GetInfo();
    print(myStr);
    print(myInt);
    print(myFloat);
    }

    private (string str, int i, float f) GetInfo()
    {
    return ("123", 2, 5.5f);
    }
  • 丢弃参数

    利用传入 下划线 _​ 达到丢弃该参数不使用的作用

    1
    2
    (string ss, _, _) = GetInfo();
    print(ss)

字典多变量键

当字典中的键 需要用多个变量来控制时

1
2
3
4
5
6
7
8
Dictionary<(int i, float f), string> dic = new Dictionary<(int i, float f), string>();
dic.Add((1, 2.5f), "123");

if (dic.ContainsKey((1, 2.5f)))
{
print("有这个键");
print(dic[(1, 2.5f)]);
}

模式匹配

基本概念:
模式匹配是一种语法元素,可以测试一个值是否满足某种条件,并可以从值中提取信息
在C#7中,模式匹配增强了两个现有的语言结构

  1. is​ 表达式,is​ 表达式可以在右侧写一个模式语法,而不仅仅是一个类型
  2. switch​ 语句中的 case

主要作用:
节约代码量,提高编程效率

  1. 常量模式(is 常量):
    用于判断输入值是否等于某个值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    object o = 1;
    //旧写法
    if (o is int)
    {
    print("o是int");
    }
    //新写法
    if (o is 1)
    {
    print("o是1");
    }
    //等价于:
    if (o is int && o = 1)
    {
    print("o是1");
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    object o = null;
    if (o is 1)
    {
    print("o是1");
    }
    if (o is null)
    {
    print("o是null");
    }
  2. 类型模式(is 类型 变量名case 类型 变量名):
    用于判断输入值类型,如果类型相同,将输入值提取出来
    判断某一个变量是否是某一个类型,如果满足会将该变量存入你申明的变量中
    注意,该变量是生命周期是整个函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    object o = 1;
    if ( o is int i )
    {
    print(i);
    }
    //等同于下面这个写法
    //if (o is int)
    //{
    // int i = (int)o;
    // print(i);
    //}

    类型模式还可以用于 switch

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    object o = 1;
    switch (o)
    {
    case int value:
    print("int: " + value);
    break;
    case float value:
    print("float: " + value);
    break;
    case null:
    print("null");
    break;
    default:
    break;
    }
  3. var模式:
    用于将输入值放入与输入值相同类型的新变量中,相当于是将变量装入一个和自己类型一样的变量中

    1
    2
    3
    4
    5
    6
    7
    object o = 1;
    if (o is var v)
    {
    print(o);
    print(v);
    }
    //实际就等同于var v = o

    var​ 模式可以用于让一个变量装载一个不确定类型的返回值,由编译器帮我们判断是何种类型,且该过程可以写入一句表达式内,以缩减代码行数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int GetInt()
    {
    return 1;
    }

    if (o is var v)
    {
    print(v);
    }
    //旧写法
    int kk = GetInt();
    if (kk >= 0 && kk <= 10) { }
    //新写法,这里旧省略了kk这个中间变量,这里还省略了类型转换,由编译器直接帮我们判断这个函数返回的是何种类型,并将该过程直接并入表达式内
    if (GetInt() is var k && k >= 0 && k <= 10) { }