前一章中,我们写了一些简单的方法,语句都是按顺序执行的。但为了解决现实世界的问题,我们往往需要根据情况在方法中选择不同的执行路径。在本章中,我们将具体介绍相关内容。
布尔值
理解布尔值
如果你在第二章中有认真看过C#中的基元数据类型,你应当会注意到一种特殊的类型:bool
。
和三次元生活不同,C#的世界非错即对。假如有一个整型变量 x
,我们将 99
赋值给它,然后询问: x
与 10
哪个更大?显然,答案是 x
更大。所以表达式 x > 10
的值为 true
。你可以简单地把布尔值(bool)理解为真假。
声明布尔变量
我们可以用之前学到过的语法来声明布尔变量,记得在合适的作用域中声明:
bool flag; flag = true; Console.WriteLine(flag); //输出:true
使用布尔操作符
布尔操作符是求值为 true
或 false
的操作符。组合不同的布尔操作符和操作数可以写出条件表达式。条件表达式的结果为布尔值。
求反运算符
C#中最简单的布尔操作符是求反,用感叹号 !
表示。在上面的例子中,如果 flag
的值为 true
,那么 !flag
就是 false
。
判等运算符
为了比较两个值是否相等,C#提供了相等和不等操作符,分别为 ==
和 !=
。这两个二元操作符判断一个值是否与相同类型的另一个值相等,结果也是一个 bool
值。如下面的例子:
bool flag = 10 != 9; // 10 != 9的求值结果为true,该结果被保存到flag中
注意,判等时的运算符为两个等号,一个等号表示赋值。
关系运算符
我们在小学就学过的 >
<
≥
≤
这些符号在C#中也有对应的操作符,不过大于(小于)等于符号写作 >=
(<=
)。它们判断一个值是小于还是大于同类型的另一个值。如下面的例子:
int a = 50; bool flag1 = a > 0; //true bool flag2 = a < 25; //false bool flag3 = a >= 50; //true bool flag4 = a <= 50; //true
条件逻辑操作符
在生活和模组编写的过程中我们经常听到“如果A而且B或C时”这样的话。在C#中,我们可以使用 &&
和 ||
两个二元操作符运算布尔值。它们分别表示逻辑与和逻辑或。
逻辑与
你可以直观地把它理解为“且”。
当且仅当 &&
两侧的两个布尔值均为 true
时,该表达式的结果才为 true
,否则为 false
。
bool flag1 = 100 > 0 && 200 < 100; //false bool flag2 = 0 > 100 && 100 > 200; //false bool flag3 = 100 > 0 && 50 < 100; //true
最常用的例子是使用 &&
判断某个变量是否在某个范围内。新手很容易写出下面这样的错误代码:
float number = 50f; bool flag = number >= 10f && <= 100f; //典型错误写法1 bool flag = 10 <= number <= 100f; //更典型的错误写法 //要不是C#不允许将bool和数值比大小甚至会导致难以发现的Bug
正确的写法应该是下面这样的:
float number = 50f; bool flag = number >= 10f && number <= 100f; //OK bool flag = 10f <= number && number <= 100f; //有些人很喜欢这样写,当然是可以的
逻辑或
你可以直观地把它理解为“或”。
只要 ||
两侧有一个值为 true
,表达式的返回值就为 true
。如下面的例子:
float number = 50f; bool flag = number >= 100f || 50 > 0; //flag 为true,因为50>0为true。
我们经常使用下面的写法判断某个变量是否不在某个范围内:
bool flag = number > 100f || number < 0f; //表示number不在区间(0,100)内 //number取0和100时也为真,想一想>和<的含义
优先级问题
逻辑与(&&
)的优先级高于逻辑或(||
)。但在实际环境下我们往往直接用小括号指定运算顺序。小括号和之前介绍过的一样,用于覆盖运算的优先级。这样写是为了让表达式看上去更直观,而不需要思考表达式的求值顺序。
例如,如果你已经熟练掌握了前面的运算符,你可以用这种写法判断某个变量是否不在某个范围内:
bool flag = !(0f <= number && number <= 100f); //也是一种常用的写法,注意是否取等。
短路求值问题
操作符 &&
和 ||
都支持短路求值。短路求值的含义是,有时根本没必要对两个操作数都求值。例如,&&
的左操作数为 false
,或 ||
的左操作数为 true
时,便无需再继续对右操作数进行求值。因此,在写mod时这样写不会出现错误:
if (npc != null && npc.active) { //... }
如果没有短路求值,即使 npc
为null,程序也会继续求值 npc.active
,从而引发 NullReferenceException
异常。
但短路求值往往也会引起奇怪的问题,比如下面的例子:
int a = 0; bool flag = true || a++;
flag
的值肯定是 true
,那执行完第二行后 a
的值是多少呢?乍一看似乎 a
会递增,但由于短路求值的存在,a++
根本没有执行。因此,我建议你将条件判定和变量递增分开写,以免被短路求值背刺。
利用短路求值的特性,我们往往将更容易求值的布尔表达式优先求值(如简单的比较和判等)放在前面,而将复杂的表达式放在后面(如需要调用方法的复杂求值)。
操作符和优先级和结合性总结
下面的列表总结了迄今为止我们接触过的所有操作符的优先级和结合性。
操作符的优先级按类别从高到低排列,同类操作符优先级相同。除赋值运算符外,其他运算符均为左结合性。
-
主要操作符
()
:小括号,用于覆盖优先级++
和--
:后缀形式递增递减
-
一元操作符
!
:取反/逻辑非+
和-
:事实上是正负号++
和--
:前缀形式递增递减
-
乘运算
*
和/
:算数乘除%
:取模(求余)
-
加运算
+
和-
:算数加减
-
关系运算
>
和<
:大于和小于>=
和<=
:大于等于和小于等于
-
相等
==
:判等!=
:判不等
-
逻辑与
-
逻辑或
-
赋值
使用if语句
之前的学习中,我们的代码总是按顺序执行。现在,我们将学习使用if
语句根据布尔表达式的结构选择执行两个不同的代码块。
理解if语句的语法
if
语句的语法如下:
if ( 布尔表达式 ) 语句1; else 语句2; //if和else是C#的关键字
如果布尔表达式的值为true
,就执行语句1,否则执行语句2。注意,else
后面的部分是可选的,如果不写 else
,那么在表达式为 false
时什么都不会发生。例如,如果我们要写一个计时器,我们可以使用下面的语句实现满60进1的功能:
int seconds; //操作seconds //... if (seconds == 59) seconds = 0; else seconds++;
此外,布尔表达式也可以是一个布尔变量,或是一个返回值为 bool
的方法调用,只要求值为 bool
就可以。
使用代码块分组语句
在前面的 if
语句语法中,我们只根据表达式的真假执行一条语句。而实际情况(比如你在写Boss的AI的时候)中你往往需要根据表达式的真假分别执行很多个语句。因此,我们引入了代码块。代码块是用大括号封闭的一组语句。
例如下面的写法在可以实现在 seconds
满60时向 minutes
进1:
int seconds = 0, minutes = 0; //... if (seconds == 59) { seconds = 0; minutes++; } else { seconds++; }
遗漏大括号将导致非常严重的后果,因此我建议你在使用 if
的任何时候都用大括号将语句括起来,即使其中只有一个语句。
此外,这个大括号也会定义一个新的作用域。在此作用域中定义的变量会在 }
出现,即代码块结束时消失。
if语句的嵌套
你可以在if语句中使用套娃(不许禁止套娃!),这样就可以链接一系列布尔表达式。
这里用裙裙教程里的例子:如果我们想让武器在肉前、肉后、白天、黑夜射出不同的弹幕,我们可以用下面的写法:
if (!Main.hardMode && Main.dayTime) { // 肉山前,白天 type = ProjectileID.LightBeam; } else if (!Main.hardMode && !Main.dayTime) { // 肉山前,夜晚 type = ProjectileID.NightBeam; } else if (Main.hardMode && Main.dayTime) { // 肉山后,白天 type = ProjectileID.SwordBeam; } else if (Main.hardMode && !Main.dayTime) { // 肉山后,夜晚 type = ProjectileID.TerraBeam; }
如果你不理解上面的语句,你可以参考主教程,这里只是为了演示 if
语句的嵌套。
使用switch语句
为什么引入switch语句
当使用嵌套的 if
语句时,你往往会发现所有的 if
语句都很相似,因为是在对完全相同的表达式进行求值,唯一区别是每个 if
语句都将表达式的结果与不同的值进行比较,例如下面的代码:
if (day == 0) { dayName = "Sunday"; } else if (day == 1) { dayName = "Monday"; } //... else { dayName = "Unknown"; }
这时可以引入switch语句简化编程并增强可读性。
switch语句的语法
switch
的语法如下:
switch (控制表达式) { case 常量表达式1: 代码块; break; case 常量表达式2: 代码块; break; //... default: 代码块; break; }
控制表达式只求值一次,必须写在圆括号中,然后C#会逐个检查常量表达式,找到与控制表达式匹配的一项,并执行它标识的代码块(常量表达式那一行称为 case
标签)。进入代码块后,程序会顺序执行到 break
语句。遇到 break;
后,switch
语句结束,程序从结束大括号后的第一个语句继续执行。如果没有任何 case
标签匹配控制表达式,就会执行 default
标签下的代码块。
例如,我们可以把上面的嵌套 if
改成 switch
如下:
switch (day) { case 0: dayName = "Sunday"; break; case 1: dayName = "Monday"; break; //更多case标签 //... default: dayName = "Unknown"; break; }
switch语句的限制
尽管 switch
语句很好用,但它有许多严格的限制:
switch
语句的控制表达式必须是整型(如int
,char
,long
等)或string
,不能为浮点型。case
标签必须是常量表达式,换言之,必须在编译时能确定,不能把变量用作case
标签。case
标签必须唯一。- 可以连续写多个
case
标签表示多个情况执行同一代码块,但不能在两个标签之间插入其他代码。
例如,下面的伪代码(可别拿去跑,只是展示逻辑):
switch (card) { case Hearts: case Diamonds: color = "Red"; //合法,两种情况执行相同的代码 break; case Clubs: color = "Black"; //非法,控制不能从一个case标签贯穿到另一个case标签 case Spades: color = "Black"; break; }
如果有更复杂的情况,还是用回嵌套 if
吧。
在下一章中,我们将介绍复合赋值和循环语句。