第 8 章 / 共 58 章 卷二 运算与控制

分支结构

程序从此有了"看情况办事"的能力——if 家族一次讲透。

本章导读

前面七章写的程序都是"一条道走到黑":从第一行执行到最后一行。真实程序处处是岔路——成绩够不够及格?用户输入合不合法?var 里装的到底是数字还是文本?本章学习 xlang 的分支结构:if / then / elseif / else / end。

xlang 的分支语法讲究"显式收尾":每个 if 块必须以 end 结束,多路分支用 elseif 一个词(不是两个词),条件表达式必须严格是 bool——没有 C 家族"非零即真"的潜规则。这些纪律初学时略显啰嗦,换来的好处是:分支边界永远肉眼可见,条件永远不存在歧义。

本章学习目标

掌握 if/then/elseif/else/end 全形态;树立"条件必须 bool"的纪律;会用嵌套分支与块级作用域;会把第 7 章的 is 用作条件;懂得三元表达式与 if 语句的选型;会用 if + return 写守卫式代码。

8.1 if 基本形态

最简单的分支:条件成立就执行一段代码。

basic.xl
int temperature = 38

if temperature >= 37 then
    println("发热,请休息")
end

三个要点:if 后面是条件表达式;then 标志条件部分结束、分支体开始;end 关闭整个 if 语句。分支体必须另起一行——then 后面同一行直接写语句是语法错误(见坑 4)。

两路分支加 else:

two_way.xl
int age = 17

if age >= 18 then
    println("成年人")
else
    println("未成年人")
end
> xl run two_way.xl 未成年人

结构上:else 没有自己的条件,它收编"前面所有条件都不成立"的剩余情况。一个 if 语句里 else 最多出现一次,且必须在 end 之前。

8.2 elseif 多路分支

三路及以上用 elseif——注意是一个词,写成 else if 是语法错误(见坑 1):

score.xl
int score = 86

if score >= 90 then
    println("优秀")
elseif score >= 75 then
    println("良好")
elseif score >= 60 then
    println("及格")
else
    println("不及格")
end
> xl run score.xl 良好

求值规则:从上往下逐个检查,命中第一个真条件就执行对应分支,然后整条语句结束——后面的条件不再求值。上例 86 分命中第二个条件(>= 75),"及格"分支的条件根本没被检查。

这条"短路"规则带来一个实用推论:条件顺序就是优先级。把最严格、最特殊的条件放前面,把兜底的放后面。上例若把 score >= 60 排到最前,86 分会被误判为"及格"——条件本身没错,顺序错了。

8.3 条件必须是 bool

C 语言里 if (n) 合法,0 为假、非零为真。xlang 明确拒绝这套"真值(truthy)"潜规则:if/elseif 的条件表达式的类型必须是 bool,否则编译期 [sem] 报错:

# bad1.xl 内容:if 1 then println("hi") end > xl run bad1.xl bad1.xl:1:4: [sem] if 条件必须是 bool
# bad2.xl 内容:string s = "abc"; if s then ... > xl run bad2.xl bad2.xl:2:4: [sem] if 条件必须是 bool

数字、字符串、null 都不能直接当条件。想表达"非零""非空""有值",就写出来:

explicit.xl
int count = 3
if count != 0 then          // 显式比较,而不是 if count
    println("有库存")
end

var name = "xlang"
if name != null then        // 显式判空,而不是 if name
    println(name as string)
end

复杂条件用第 6 章的 and / or / not 组合,它们同样要求两边都是 bool,且短路求值:

logic.xl
int score = 86
if score >= 80 and score % 2 == 0 then
    println("80+ 且为偶数")
end

初看繁琐,实则受益:if count != 0 把意图写在明处,读者不用猜"这个 if 到底在判断什么"。这也是 xlang"显式优于隐式"设计哲学的又一体现(第 3、7 章的转换规则同源)。

8.4 嵌套与块级作用域

分支体里可以再写 if,层数不限:

nest.xl
int a = 3
int b = 7
if a > 0 then
    if b > 5 then
        println("both")
    else
        println("only a")
    end
end
> xl run nest.xl both

每个分支体都是一个块,遵守第 4 章确立的块级作用域规则:块内声明的变量只属于这个块,出了 end 立即失效:

# scope.xl 内容: # if true then # int y = 1 # println(string(y)) # end # println(string(y)) ← 第 5 行,块外使用 > xl run scope.xl scope.xl:5:16: [sem] undeclared identifier 'y'

这条规则常常塑造代码结构:想在分支之后还用某个值,就要在 if 之前声明好,分支内只负责赋值:

hoist.xl
string level          // 先声明(第 4 章:声明可不带初始化)
int score = 86
if score >= 60 then
    level = "通过"     // 块内只赋值
else
    level = "未通过"
end
println(level)         // 块外可用
嵌套深度的工程纪律

语法允许无限嵌套,工程上建议最多两层。超过两层的嵌套通常有两种解药:用 elseif 拍平(条件互斥时),或用 if + return 提前退出(8.7 节)。嵌套每深一层,读者脑子里的"条件栈"就压一层。

8.5 is 条件模式

第 7 章的 is 放进 if 条件里,就是处理 var 值的标准姿势——先判断、后取出:

guard.xl
var v = 42
if v is int then
    int n = v as int          // is 确认过,as 才安全
    println("int: " + string(n))
end

var s = "hi"
if s is string then
    println("string: " + (s as string))
end
> xl run guard.xl int: 42 string: hi

回忆第 7 章坑 3:as 失败是 fatal,进程直接终止且无法捕获。所以 is + as 必须成对出现——is 是安检门,as 是取货窗。注意 xlang 当前不做"控制流类型收窄":进入 if v is int 的分支后,v 的类型仍是 var,不会自动变成 int,该写的 as 一次不能少。

8.6 三元 vs if 的选型

第 6 章学过三元表达式 cond ? a : b。它与 if 语句的分工很清晰:

场景选择理由
根据条件取一个值三元表达式直接嵌进赋值/拼接/传参
根据条件做一件事(或多件事)if 语句语句块能装任意多条语句
三路及以上if/elseif嵌套三元可读性急剧恶化
pick.xl
int x = 7
string tag = x % 2 == 0 ? "even" : "odd"   // 取值:三元一步到位
println(tag)

if x > 100 then                          // 做事:用 if 语句
    println("big")
    println("really big")
end

一句话:值用三元,动作用 if;两个分支以内考虑三元,三个以上必须 if/elseif。

8.7 if 与 return 的配合

函数体内,if 常与 return 搭档写成"守卫子句(guard clause)":异常情况先检查、先返回,主逻辑保持在最浅缩进:

grade.xl
func grade(int s) as string
    if s >= 90 then
        return "A"
    elseif s >= 60 then
        return "B"
    else
        return "C"
    end
end

println(grade(92))
println(grade(70))
println(grade(30))
> xl run grade.xl A B C

函数将在第 11 章系统讲解,这里只需记住模式:func 名(参数) as 返回类型 ... end,分支内 return 立即结束函数。当每个分支都 return 时,整条 if 语句就是函数的"分拣器"。

深入细节

拾遗 1:then 其实可以省略

笔者实测发现:if x > 3 后面不写 then、直接换行写分支体,编译器同样接受。但本书所有示例都写 then,理由有三:官方文档示例全部带 then,它是语言的"标准仪态";then 让条件与分支体的分界一目了然;团队代码里少一个"可选项"就少一种风格分歧。把 then 当作必须写,是最省心的纪律。

拾遗 2:没有 switch/match——这不是缺陷,是取舍

很多语言有 switch(C/Java)或 match(Rust/Python 3.10+)。xlang 当前版本只有 if/elseif 链。功能清单中模式匹配相关的枚举、联合类型(union/derive)已列入语言设计,但以值匹配为核心的多路分支,现状就是 elseif。好在 elseif 链表达能力不弱:条件可以是任意 bool 表达式,比只能匹配常量值的 switch 更自由。真要按值分派且分支很多,第 16 章的字典查表法是惯用替代。

拾遗 3:if 是语句,不是表达式

Rust 里 let x = if cond { 1 } else { 2 }; 合法——if 本身有值。xlang 的 if 是纯语句,不能出现在赋值号右边,要"按条件取值"请用三元表达式。这与第 2 章"语句与表达式泾渭分明"的设计一致:语句做事,表达式算值,谁也不越界。想看 if 的实现成本?用 xl cgen 观察生成的 C 代码会发现:xlang 的 if/else 几乎一对一映射为 C 的 if/else,分支本身零开销。

⚠️ 常见错误

以下报错全部来自真实编译器输出(文件路径已缩短)。

坑 1:把 elseif 写成 else if
# bad.xl 内容: # if x > 10 then # println("a") # else if x > 3 then ← 两个词 # println("b") # end > xl run bad.xl bad.xl:4:6: [parse] 语法错误

原因:xlang 的多路分支关键字是 elseif 一个词。else 后面只能直接换行跟分支体,不能再接 if。报错位置指向那个多出来的 if。

坑 2:用非 bool 值当条件
# bad.xl 内容: # if 1 then # println("hi") # end > xl run bad.xl bad.xl:1:4: [sem] if 条件必须是 bool

原因:xlang 没有 truthy 规则,整数、字符串、null 都不能当条件。正确做法:写出显式比较——if n != 0、if s != ""、if v != null。这条错误信息本身就是最好的老师。

坑 3:条件里写成赋值号
# bad.xl 内容: # int x = 5 # if x = 5 then ← 想写 == # println("five") # end > xl run bad.xl bad.xl:2:6: [parse] 语法错误

原因:条件位置不允许出现赋值语句,= 在此直接被解析器拒绝。C 家族"if 里误写赋值却悄悄通过"的经典灾难在 xlang 里从语法层面被消灭——这正是不提供 truthy 规则的深层收益之一。

坑 4:then 后面同一行写语句
# bad.xl 内容: # if ok then println("go") ← 语句与 then 同行 # end > xl run bad.xl bad.xl:2:37: [parse] 语法错误

原因:then 之后必须换行,分支语句从下一行开始。xlang 的分支体是"行导向"的块,没有 C 的单语句简写。乖乖换行即可。有趣的是,反向操作合法:最后一条语句结束后用分号把 end 接在同一行(println("x"); end)——但本书不推荐,独立一行最清晰。

本章小结

知识点写法/规则要点
单分支if cond then ... endthen 后换行,end 收尾
两分支else最多一次,无自身条件
多分支elseif cond then一个词;命中即短路;顺序即优先级
条件类型必须是 bool无 truthy;[sem] 编译期拦截
作用域分支体是块块内声明块外失效;跨块值提前声明
is 条件if v is T then ... v as T ...is/as 成对;无自动类型收窄
三元选型cond ? a : b取值用三元,动作用 if

复习题

✍️ 先遮住答案想一想
  1. elseif 链中,为什么"条件顺序就是优先级"?举一个顺序错误导致结果错误的例子。
  2. xlang 为什么拒绝 if n 这种写法?坑 3 与此有什么内在联系?
  3. 分支体内声明的变量为什么不能在 end 之后使用?想让分支结果带出块外该怎么做?
  4. if v is int then int n = v + 1 end 能通过编译吗?为什么?正确写法是什么?
  5. 什么场景该用三元而不是 if 语句?反过来呢?
参考答案
  1. 命中第一个真条件后整条语句短路结束。如成绩分级:若 >= 60 排在 >= 90 前,95 分会被判为"及格"而非"优秀"。
  2. 为了消灭"非零即真"的歧义,让条件意图显式可见。正因为条件必须 bool,if x = 5 这种赋值误写才必然被解析器拦下——bool 纪律与坑 3 的防护是同一条设计链。
  3. 块级作用域:块内变量随 end 销毁。办法是在 if 之前声明变量,分支内只赋值。
  4. 不能。v + 1 的结果类型是 var,赋给 int 变量报 [sem] 类型不匹配;且没有控制流收窄,分支内 v 仍是 var。应写 int n = (v as int) + 1——先 as 取出,再运算。(顺带一提:println(v + 1) 打印 43 是合法的,var 参与运算没问题,卡住的是"var 值进静态类型变量"这一步。)
  5. 按条件取一个值嵌进表达式时用三元;要执行一条或多条语句、或分支超过两路时用 if/elseif。

编程练习

练习 1:BMI 分类器

编写 bmi.xl:给定 float weight = 68.0、float height = 1.75,计算 BMI = weight / (height * height),按 <18.5 偏瘦、<24 正常、<28 超重、否则肥胖 四档打印结论,并打印 BMI 数值。

参考思路:elseif 链按从低到高的阈值顺序排列;注意第 6 章的坑——** 幂运算符不接受浮点操作数,平方请写 height * height。

练习 2:var 分拣台

编写 sort_var.xl:对一组 var 值(至少含 int、float、string、bool、null 各一个),逐个用 if/elseif 的 is 链判断类型,int 打印其两倍,string 与其自身拼接打印,其余各打印一句类型说明。

参考思路:复用 8.5 节 is + as 模式;null 分支用 v is null。体会"先安检后取货"的节奏。

练习 3:守卫式最大值

编写 max3.xl:函数 max3(int a, int b, int c) as int 返回三者最大值。先用嵌套 if 写一版,再用"if + return 提前返回"的扁平风格重写,对比两种写法的可读性。

参考思路:扁平版形如 if a >= b and a >= c then return a end ...。体会 8.7 节"守卫子句让主逻辑保持最浅缩进"的收益。