CS4L23——特殊语法

本章代码关键字

1
2
3
4
5
6
7
8
9
10
var                                    // 隐式类型,用于声明临时变量,编译器会根据=右边的值的类型来判断变量是什么类型的,而不需要再指定变量类型
new() // 在已知变量类型的情况下,使用new实例化对象时可以直接省略掉new关键字后面的类型
? // 可空类型
Nullable<> // 可空值类型的包装
nullable<>.HasValue // 检查可空值类型是否为null
nullable<>.GetValueOrDefault() // 安全获取可空值类型值,如果为值为空,返回默认值类型的默认值,如果此时还传入了参数,会返回传入的参数
?. // 空检查字符串
?? // 空合并操作符,如果左边值为null就返回右边值,否则返回左边值
??= // 空合并操作符的复合运算符,相当于若变量为null,就对其赋值
$"" // 内插字符串

var​ 隐式类型

var​ 是一种隐式类型,用于声明临时变量,编译器会根据 =​ 右边的值的类型来判断变量是什么类型的,而不需要我们自己再指定类型

因此,var​ 不能作为类的成员,只能用于临时变量申明时,也就是说一般写在函数语句块中。且 var​ 必须初始化
var​ 也不能用于里氏替换,也就是父类装子类的情况,父类必须显式指定

1
2
3
4
5
6
7
static void Main()
{
var i = 5;
var s = "Hello, world";
var array = new int[] { 1, 2, 3, 4 };
var list = new List<int>();
}

如果需要知道 var​ 声明的变量实际是什么类型的,直接通过 IDE 来查看即可:

image

目标类型化 new

注意!此语法是 C# 9 及其以后的版本才可以使用的,这意味着 Unity 2020 及以前的版本不可使用该方法!
具体内容详见:CS5L11——CSharp 9 功能和语法

在已知变量类型的情况下,使用 new​ 实例化对象时可以直接省略掉 new​ 关键字后面的类型,使变量的赋值语句变得更简单
相比 var​,它可以不仅可以用于临时变量初始化时,还可以用于类的成员变量以及对变量的重新赋值的情况

实际上,new()​ 在所有可以推导出类型的上下文中,都可以使用,包括在初始化器内部,只要能够确定赋值类型是什么

不适用此特性的场景包括:

  • 该方法不适用于里氏替换,也就是父类装子类的情况,new​ 的子类必须显式指定,否则会出现歧义!

  • 使用 as​ 操作时无法正确推导,以 int​ 为例

    1
    Console.Write(new() as int);     // error: "new()" 没有目标类型
  • 使用 var​ 时也无法推导

    1
    var x = new();    // error: "new()" 没有目标类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Test {}

class Program
{
List<int> numbers = new();
Test obj = new();
static Dictionary<string, List<Test>>? testDict;

static void Main()
{
testDict = new()
{
{"111", new() { new() }}, //这里的第一个new()是List<Test>,后面的new()是Test
{"222", new() { new(), new() }}, //这里的第一个new()是List<Test>,后面的new()是Test
};
}
}

设置对象初始值

声明对象时,可以通过直接写大括号的形式初始化公共成员变量和属性
圆括号在有无参构造的情况下可以省略(使用目标类型化 new()​ 时不可省略!),
加了圆括号相当于调用了构造函数,再按照大括号快速初始化一遍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person
{
private int money;
public bool sex;
public string? Name { get; set; }
public int Age { get; set; }
}

class Program
{

static void Main()
{
Person p = new()
{
sex = true,
Age = 18,
Name = "MrTang"
};
// 等同于:
// Person p = new();
// p.sex = true;
// p.Age = 18;
// p.Name = "MrTang";
}
}

设置集合初始值

声明集合对象(例如 List<>​ 等)时,也可以通过大括号直接初始化内部属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int[] array2 = new int[] { 1, 2, 3, 4, 5 };

List<int> listInt = new List<int>() { 1, 2, 3, 4, 5, 6 };

List<Person> listPerson = new List<Person>()
{
new Person(200),
new Person(100) {Age = 10},
new Person { sex = true, Age = 18, Name = "liuqi" }
};

Dictionary<int, string> dic = new Dictionary<int, string>()
{
{1, "123"},
{2, "222"}
};

配合目标类型化 new()​ 可以进一步省略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Person
{
private int money;
public bool sex;
public Person() {}
public Person(int money)
{
this.money = money;
}
public string? Name { get; set; }
public int Age { get; set; }
}

class Program
{

static void Main()
{
int[] array2 = new int[] { 1, 2, 3, 4, 5 };

List<int> listInt = new() { 1, 2, 3, 4, 5, 6 };

List<Person> listPerson = new()
{
new(200),
new(100) { Age = 10 },
new() { sex = true, Age = 18, Name = "MrTang" }
};

Dictionary<int, string> dic = new()
{
{1, "123"},
{2, "222"}
};
}
}

匿名类型

var​ 变量可以申明为自定义的匿名类型,不能直接往里面装方法

1
2
3
var v = new { age = 10, money = 11, name = "小明" };
Console.WriteLine(v.age);
Console.WriteLine(v.name);

输出:

1
2
10
小明

可空类型

一般来说,值类型是不能赋值为空的,但是如果在声明变量时,在值类型后面加 ?​,可以赋值为空

1
2
int i1 = null;      // error: 无法将 null 转换为“int”,因为后者是不可为 null 的值类型
int? i2 = null;

判断可空值类型是否为空

可空值类型本质上是 Nullable<T>​,也就是 C# 为值类型做了一个包装,也就是说可空值类型和原本的值类型并不一样
Nullable<T>​ 有一个 HasValue​ 属性可以判断是否为 null

1
2
3
4
5
6
int? i = null;
if (i.HasValue)
{
Console.WriteLine(i);
Console.WriteLine(i.Value);
}

安全获取可空类型值

如果为值为空,返回默认值类型的默认值,如果此时还传入了参数,会返回传入的参数

1
2
3
int? i = null;
Console.WriteLine(i.GetValueOrDefault());
Console.WriteLine(i.GetValueOrDefault(100));

输出:

1
2
0
100

空引用检查

注意!Unity 的 C# 项目默认关闭了这个,即使设置开启也会被 Unity 关闭,因此这里只针对纯 C# 项目使用,Unity 项目可无视此内容

值得一提的是,C# 8 及其以后的版本为了尽量避免 NullReferenceException​ 异常(即常见的空引用异常),
引入了空引用检查,并在 .NET 6 及以后的版本默认开启此功能(在 csproj​ 文件内可关闭,需将 <Nullable>​ 改为 disable​)

启用空引用检查的情况下,引用类型默认将不允许为 null​,包括但不限于以下常见情形会出现警告:

  • 对引用类型变量赋值为 null​,编译器会发出警告
  • 对于返回值为不可为 null​ 的引用类型的函数,如果返回 null​,会被编译器发出警告
  • 对于类的构造函数,如果存在类型不可为 null​ 且没有初始化的成员变量或自动属性,构造函数内也没有初始化,那么此构造函数将被警告
  • 一个不可为 null​ 的变量接受了一个会返回可空类型的函数的返回值,编译器会发出警告
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Program
{
string text;

// warning: 在退出构造函数时,不可为 null 的 字段 "text" 必须包含非 null 值。请考虑添加 "required" 修饰符或将该 字段 声明为可为 null。
public Program() {}

static void Main()
{
string str = null; // warning: 将 null 文本或可能的 null 值转换为不可为 null 类型
string str2 = NullableString(); // warning: 将 null 文本或可能的 null 值转换为不可为 null 类型
}

public string TestNullable()
{
return null; // warning: 可能返回 null 引用
}

public static string? NullableString()
{
return null;
}
}

编译器对空引用进行严格检查后,我们即可根据警告去处理问题:

  • 如果一个变量,返回值,参数可能为 null​,那就必须要在类型前加上 ?​ 表示为可空类型

  • 不可为 null​ 的成员变量必须要初始化,或者在构造函数内初始化,要么加上 ?​ 表示为可空类型

  • 根据空引用检查,IDE 可以帮助我们判断一个变量是否可能为 null​,以帮助我们准确的添加 null​ 判断

    imageimage

借助空引用检查,我们可以很好的避免 NullReferenceException​ 异常

空检查运算符

对于一个可能为空的引用类型变量,直接调用其方法可能会出现 NullReferenceException​ 异常
为此我们需要在调用时使用 if​ 语句判断空,而 C# 提供了 ?.​ 空检查运算符,
相比直接用 .​ 调用,它会在发现对象为 null​ 时不调用方法

注意!大部分 Unity 提供的引用类型,诸如 GameObject​、MonoBehaviour​ 等不可以使用此方法来判断此对象是否存在,
因为 Unity 提供的引用类型重载了 !=​,==​ 运算符判空时的逻辑,使用非 C# 默认的逻辑以判断这些对象是否在 Unity 内被销毁,
?.​ 依然使用了 C# 默认的逻辑,就有可能出现虽然不为 null​,但实际上在 Unity 内部已经被销毁的情况,导致出错

1
2
3
4
5
6
7
8
9
10
11
12
13
object? obj = null;
string? str = obj?.ToString()?.ToLower();
// 等效于
// object? obj = null;
// string? str;
// if (obj != null)
// {
// str = obj.ToString();
// if (str != null)
// {
// str = str.ToLower();
// }
// }

索引器(?[]​),委托也可以使用

1
2
3
4
5
6
7
8
9
10
11
12
int[] arrayInt = null;
Console.WriteLine(arrayInt?[0]); //不加问号的话这样写会报错

//委托也可以使用
Action action = null;
action?.Invoke();

// 等效于
// if (action != null)
// {
// action();
// }

空合并操作符

空合并操作符 ??​,使用方法:左边值 ??​ 右边值
如果左边值为 null​ 就返回右边值 否则返回左边值,只要是可以为 null​ 的类型都可以用

1
2
3
4
5
6
7
8
int? intV = null;
int intI = intV == null ? 100 : intV.Value; //注意这里int?不能直接转换为int,需要.Value
intI = intV ?? 100; //上下两句是结果是一样的,这里就不需要value,因为??会自动完成.Value的操作
Console.WriteLine(intI);

string? str = null;
str = str ?? "haha";
Console.WriteLine(str);

输出:

1
2
100
haha

空复合分配运算符

+=​,-=​ 类似,当出现 variable = variable ?? value​ 时,可以直接使用复合分配:variable ??= value​,
相当于如果一个变量为 null​,就对其赋值

1
2
3
4
string? str = null;
str = str = "haha";
str ??= "haha"; //上下两句是结果是一样的
Console.WriteLine(str);

内插字符串

关键符号:$​,用 $​ 来构造字符串,让字符串中可以拼接变量

1
2
3
string name = "XiaoLi";
int age = 18;
Console.WriteLine($"好好学习,{name},年龄:{age}");

输出:

1
好好学习,XiaoLi,年龄:18

单句逻辑省略写法

if​ 或者循环只有一句代码时,就可以省略大括号而直接缩进,有多句代码就不可以省略

1
2
3
4
5
6
7
string str = "";

if (str == null)
Console.WriteLine("123123");

for (i = 0; i < 10; i++)
Console.WriteLine(i);

类里的属性,函数等如果代码块只有一句代码,就可以直接用 =>​ 而不需要大括号 且不需要 return
其中,属性(索引器同理)只有 get​ 且 get​ 语句块只有一句代码时,属性的整个语句块,都可以使用 =>​ 来简写,=>​ 后跟要返回对应的表达式语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test
{
public int GetConstNumber() => 100;
public void Output(string text) => Console.WriteLine(text);

private bool isSurvive;
public bool IsSurvive => isSurvive;

private int age;
public int Age
{
get => age;
set => Age = value;
}
}