加载中...
仓颉文档阅读-语言规约IV: 表达式(II)

仓颉文档阅读-语言规约IV: 表达式(II)

周五 9月 26 2025
8063 字 · 38 分钟

此样式内容, 表示文档原文内容

表达式

表达式通常由一个或多个操作数(operand)构成, 多个操作数之间由操作符(operator)连接, 每个表达式都有一个类型, 计算表达式值的过程称为对表达式的求值(evaluation)

在仓颉编程语言中, 表达式几乎无处不在, 有表示各种计算的表达式(如算术表达式、逻辑表达式等), 也有表示分支和循环的表达式(如if表达式、循环表达式等)

对于包含多个操作符的表达式, 必须明确每个操作符的优先级、结合性以及操作数的求值顺序

优先级和结合性规定了操作数与操作符的结合方式, 操作数的求值顺序规定了二元和三元操作符的操作数求值顺序, 它们都会对表达式的值产生影响

注: 本章中对于各操作符的操作数类型的规定, 均建立在操作符没有被重载的前提下

循环表达式

仓颉编程语言支持三种循环表达式:for-in表达式、while表达式和do-while表达式

循环表达式的语法定义为:

PLAINTEXT
loopExpression
  : forInExpression
  | whileExpression
  | doWhileExpression
  ;

for-in表达式

一个完整的 for-in表达式 具有如下形式:

PLAINTEXT
for (p in e where c) {
    s
}

其中pattern guard``where c是非必须的, 因此更简易的 for-in表达式 具有如下形式:

PLAINTEXT
for (p in e) {
    s
}

for-in表达式 的语法定义为:

PLAINTEXT
forInExpression
    : 'for' '(' patternsMaybeIrrefutable 'in' expression patternGuard? ')' block
    ;
    
patternsMaybeIrrefutable
    : wildcardPattern
    | varBindingPattern
    | tuplePattern
    | enumPattern
    ;

patternGuard
    : 'where' expression
    ;

上述语法定义中, 关键字for之后只能是那些一定或可能为 irrefutable 的 pattern(见 模式的分类)

在语义检查阶段, 会检查for之后的 pattern 是否真的是 irrefutable, 如果不是 irrefutable pattern, 则编译报错

另外, 如果for之后的 pattern 中存在 binding pattern, 相当于新声明了一个(或多个)let变量, 每个变量的作用域从它第一次出现的位置到循环体结束

for(pattern in expression patternGuard)

中要求 pattern 必须 一定或可能 irrefutable, 也就是不能总是 refutable

这意味着, for-in表达式, 允许像模式匹配那样, 匹配遍历expression中的元素

但, 必须要能够匹配expression中的所有元素?

就像这样?

CANGJIE
for ((i, j) in [(1, 2), (3, 4)]) {
    println("Sum = ${i + j}")
}

从语法定义来看, 还可以通配符模式、绑定模式、枚举模式进行匹配

但都要满足 irrefutable

且, 如果存在 绑定模式, 相当于声明了新的变量, 也就可能触发遮盖:

CANGJIE
let x = 20;
for (x in [1, 2, 3, 4, 5, 6, 7]) {
    println(x)
}
// for 中的 x 是绑定模式, 相当于新声明一个变量, 触发遮盖

for-in会先对expression求值, 再调用其iterator()函数, 获取一个类型为Iterator<T>的值

程序通过调用Iterator<T>next()函数开始执行循环, 我们可以使用pattern匹配迭代的元素, 如果匹配成功(如果存在patternGuard, 也必须同时满足patternGuard的条件), 则执行循环体block, 然后在开始处重新调用next()继续循环, 当next()返回None时循环终止

Iterable<T>Iterator<T>可在标准库中查阅

CANGJIE
main(): Int64 {
    let intArray: Array<Int32> = [0, 1, 2, 3, 4]
    for (item in intArray) {
        print(item)          // output: 01234
    }

    let intRange = 0..5
    for (number in intRange where number > 2) {
        print(number)        // output: 34
    }

    return 0
}

for (pattern in expression patternGuard)

pattern 匹配的是expreesion的迭代器元素, 循环体执行完, 自动调用迭代器的next()函数, 获取迭代下一个值

while表达式

while表达式的语法定义为:

PLAINTEXT
whileExpression
    : 'while' '(' ('let' deconstructPattern '<-')? expression ')' block
    ;

其中while是关键字, while之后是一个小括号, 小括号内可以是一个表达式或者一个let声明的解构匹配, 接着是一个块

一个基础的while表达式举例:

CANGJIE
main(): Int64 {
    var hundred = 0
    while (hundred < 100) { // until hundred = 100
        hundred++
    }
    
    return 0
}

while表达式首先对while之后的表达式进行求值(要求表达式的类型为Bool), 如果表达式的值等于true, 则执行它之后的块, 接着重新计算表达式的值并判断是否重新执行一次循环; 如果表达式的值等于false, 则终止循环

如果while()的括号内是普通Bool类型表达式, 那么就与C/C++中的while表达式一样

如果表达式值为true, 那么就去执行循环体

不过, 仓颉中的while还有一种let解构匹配的模式

对于包含letwhile表达式, 我们称之为while-let表达式

我们可以用while-let表达式来做一些简单的解构操作

一个基础的while-let表达式举例:

CANGJIE
main(): Int64 {
    var x: Option<Int64> = Option<Int64>.Some(100)
    // while-let expression
    while (let Some(v) <- x) { 
        print("x has value")
        x = ...
    }
    
    return 0 
}

while-let表达式首先对<-之后的表达式进行求值(表达式的类型为可以是任意类型), 如果表达式的值能匹配let之后的 pattern, 则执行它之后的块, 接着重新计算表达式的值然后再次匹配并判断是否重新执行一次循环; 如果匹配失败, 则终止当前的while循环

let之后的 pattern 支持常量模式、通配符模式、绑定模式、Tuple模式、enum模式

while-let与之前的if-let是类似的(见条件表达式)

do-while表达式

do-while 表达式的语法定义为:

PLAINTEXT
doWhileExpression
 : 'do' block 'while' '(' expression ')' 
 ;

while表达式不同的是:

while表达式在第一次循环迭代时, 如果表达式expression的值为false, 则循环体不会被执行;

然而对于do-while表达式, 第一次循环迭代时, 先执行循环体block, 然后再根据表达式expression的值决定是否再次执行循环体, 也就是说do-while表达式中的循环体会至少执行一次

例如:

CANGJIE
main(): Int64 {
 var hundred = 0
 do {
     hundred++
 } while (hundred < 100)
 return 0
}

循环表达式总结

for-inwhiledo-while这三种循环表达式的表达能力是等价的

通常, 在知道循环的次数或遍历一个序列中的所有元素时使用for-in表达式;

在不知道循环的次数, 但知道循环终止条件时使用whiledo-while表达式

三种循环表达式的类型均为Unit

由于breakcontinue表达式必须有包围着它们的循环体, 所以对于三种循环表达式, 其循环条件中出现的breakcontinue均会绑定到其最近的外层循环;

如外层不存在循环, 则报错

例如:

CANGJIE
while (true) {
    println("outer") // printed once
    do {
        println("inner") // printed once
    } while (break)  // stop the execution of the outer loop
    println("unreached") // not printed
}

try表达式

根据是否涉及资源的自动管理, 将try表达式分为两类: 不涉及资源自动管理的普通 try 表达式, 以及会进行资源自动管理的try-with-resources表达式

try表达式的语法为:

PLAINTEXT
tryExpression
    : 'try' block 'finally' block
    | 'try' block ('catch' '(' catchPattern ')' block)+ ('finally' block)?
    | 'try' '(' resourceSpecifications ')' block ('catch' '(' catchPattern ')' block)* ('finally' block)?
    ;

普通try表达式 的主要目的是错误处理, 详见[异常]章节

try-with-resources表达式的主要目的是自动释放非内存资源, 详见[异常]章节

没了解过其他现代语言, 只了解C++中的异常

所以对于普通try有一定的了解, 仓颉中的try在异常时具体再看

控制转移表达式

控制转移表达式会改变程序的执行顺序

控制转移表达式的类型是Nothing类型, 该类型是任何类型的子类型

仓颉编程语言提供如下控制转移表达式:

  • break
  • continue
  • return
  • throw

控制转移表达式可以像其他表达式一样, 作为子表达式成为复杂表达式的一部分, 但是有可能会导致产生不可达代码(不可达部分会编译告警):

CANGJIE
main(): Int64 {
    return return 1 // warning: the left return expression is unreachable
}

仓颉中明确表示, break``continue``return``throw表达式类型是Nothing

控制转移表达式中:

breakcontinue必须有包围着它们的循环体, 且该循环体无法穿越函数边界;

return必须有包围着它的函数体, 且该函数体无法穿越;

throw不作要求

“包围着的循环体”无法穿越”函数边界”

在下面的例子中, break出现在函数f中, 外层的while循环体不被视作包围着它的循环体;

continue出现在lambda表达式 中, 外层的while循环体不被视作包围着它的循环体

CANGJIE
while (true) {
    func f() {
        break         // Error: break 必须直接在循环内使用
    }
    let g = { =>
        continue     // Error: continue 必须直接在循环内使用
    }
}

控制转义表达式的语法为:

CANGJIE
jumpExpression
  : 'break'
  | 'continue'
  | 'return' expression?
  | 'throw'  expression
  ;

目前看来, 仓颉的break与C/C++不同, 仓颉的break只能直接用在循环体内

break表达式

break表达式只能出现在循环表达式的循环体中, 并将程序的执行权交给被终止循环表达式之后的表达式

例如, 下面的代码通过在while循环体内使用break表达式, 实现在区间[1, 49]内计算46的最小公倍数

CANGJIE

main(): Int64 {
    var index: Int32 = 0
    while (index < 50) {
        index = index + 1
        if ((index % 4 == 0) && (index % 6 == 0)) {
            print("${index} is divisible by both 4 and 6") // output: 12
            break
        }
    }
    return 0
}

需要注意的是, 当break出现在嵌套的循环表达式中时, 只能终止直接包围它的循环表达式, 外层的循环并不会受影响

例如, 下面的程序将输出 5 次12 is divisible by both 4 and 6, 且每次同时会输出i的值:

CANGJIE
main(): Int64 {
    var index: Int64 = 0
    for (i in 0..5) {
        index = i
        while (index < 20) {
            index = index + 1
            if ((index % 4 == 0) && (index % 6 == 0)) {
                print("${index} is divisible by both 4 and 6")
                break
            }    
        }
        print("${i}th test")
    }
    return 0
}

仓颉的break用法基本与C/C++中break在循环体中的用法是一致的

只能跳出所在层的循环, 对更外层循环无用

continue表达式

continue表达式只能出现在循环表达式的循环体中, 用于提前结束离它最近循环表达式的当前迭代, 然后开始新一轮的循环(并不会终止循环表达式)

例如, 下面的代码输出区间[1,49]内所有可以同时被46整除的数(12243648), 对于其他不满足要求的数, 同样会显式地输出

CANGJIE
main(): Int64 {
    var index: Int32 = 0
    while (index < 50) {
        index = index + 1
        if ((index % 4 == 0) && (index % 6 == 0)) {
            print("${index} is divisible by both 4 and 6")
            continue
        }
        print("${index} is not what we want")
    }
    return 0
}

continue的用法, 也基本与C/C++一致

return表达式

return表达式只能出现在函数体中, 它可以在任意位置终止函数的执行并返回, 实现控制流从被调用函数到调用函数的转移

return表达式有两种形式:returnreturn expr(expr是一个表达式)

  • 若为return expr的形式, 我们将expr的值作为函数的返回值, 所以要求expr的类型与函数定义中的返回类型保持一致

    CANGJIE
    
    // return expression
    func larger(a: Int32, b: Int32): Int32 {
        if (a >= b) {
            return a
        } else {
            return b
        }
    }
  • 若为return的形式, 我们将其视为return ()的语法糖, 所以要求函数的返回类型也为Unit

    CANGJIE
    // return expression
    func equal(a: Int32, b: Int32): Unit {
        if (a == b) { 
            print("a is equal to b")
            return
        } else {
            print("a is not equal to b")
        }     
    }

需要说明的是, return表达式作为一个整体, 其类型并不由后面跟随的表达式决定(return后面跟随的表达式为()), 而是Nothing类型

return的用法, 也基本与C/C++保持一致

但, 仓颉中return拥有具体类型:Nothing. 且恒为Nothing

returnreturn ()的语法糖, 就像C/C++中, 如果函数返回值类型为void, 可以不使用return, 也可以执行return;

throw表达式

throw表达式用于抛出异常, 在调用包含throw表达式的代码块时, 如果throw表达式被执行到, 就会抛出相应的异常, 并由事先定义好的异常处理逻辑进行捕获和处理, 从而改变程序的执行流程

下面的例子中, 当除数为 0 时, 抛出算术异常:

CANGJIE
func div(a: Int32, b: Int32): Int32 {
    if (b != 0) {
        return a / b
    } else {
        throw ArithmeticException()
    }     
}

关于returnthrow表达式, 本节只做了最简单的使用举例, 有关它们的详细介绍, 请分别参见函数和异常章节

如果没有了解过任何编程语言, 要深入理解还是要参阅具体的文档

数值类型转化表达式

数值类型转换表达式用于实现数值类型间的转换, 它的值是类型转换后的值, 它的类型是转换到的目标类型(但原表达式的类型不受目标类型影响), 详细的转换规则可参见 类型转换

数值类型转换表达式的语法定义为:

PLAINTEXT
numericTypeConvExpr
    : numericTypes '(' expression ')'
    ;
    
numericTypes
    : 'Int8'
    | 'Int16'
    | 'Int32'
    | 'Int64'
    | 'UInt8'
    | 'UInt16'
    | 'UInt32'
    | 'UInt64'
    | 'Float16'
    | 'Float32'
    | 'Float64'
    ;

仓颉的数值类型转换表达式, 其实就是C/C++中的最普通的强制类型转换

只不过, 仓颉只允许数值类型(整型、浮点型)之间进行强制类型转换

thissuper表达式

thissuper表达式分别使用thissuper表示

this可以出现在所有实例成员函数和构造函数中, 表示当前实例

super只能出现在class类型定义中, 表示当前定义的类型的直接父类的实例(详见[类])

禁止使用单独的super表达式

thissuper表达式的语法定义为:

PLAINTEXT
thisSuperExpression
    : 'this'
    | 'super'
    ;

仓颉的this我能理解等价于C++类中的this指针, 表示当前实例

super我也理解是存在继承关系的类中, 表示直接父类实例

但我不理解什么叫禁止单独使用super表达式?

不就是应该像使用this一样, 直接通过super访问父类的成员吗? 意思是必须通过super访问、调用成员? 不能直接访问super本身?

spawn表达式

spawn 表达式用于创建并启动一个thread, 详见[并发]章节

看来是仓颉中, 创建线程的方式

synchronized表达式

synchronized 表达式用于同步机制中, 详见[并发]章节

仓颉用于线程同步机制的方式

括号表达式

括号表达式是指使用圆括号括起来的表达式

圆括号括起来的子表达式被视作一个单独的计算单元被优先计算

括号表达式的语法定义为:

PLAINTEXT
parenthesizedExpression
    : '(' expression ')'
    ;

括号表达式举例:

CANGJIE
1 + 2 * 3 - 4    // The result is 3.
(1 + 2) * 3 - 4  // The result is 5.

就是数学中可以将表达式作为整体运算的括号

后缀表达式

后缀表达式 由表达式加上后缀操作符构成

根据后缀操作符的不同, 分为: 成员访问表达式、函数调用表达式、索引表达式

在成员访问表达式、函数调用表达式、索引表达式中的后缀操作符的前面, 可以使用可选的?操作符, 以实现Option类型对这些后缀操作符的支持

关于?操作符, 详见下文介绍

后缀表达式的语法定义为:

PLAINTEXT
postfixExpression
    : atomicExpression
    | type '.' identifier
    | postfixExpression '.' identifier typeArguments?
    | postfixExpression callSuffix
    | postfixExpression indexAccess
    | postfixExpression '.' identifier callSuffix? trailingLambdaExpression
    | identifier callSuffix? trailingLambdaExpression
    | postfixExpression ('?' questSeperatedItems)+
    ;

从文本介绍来看, 后缀表达式应该类似于:obj.mem``func1()``arr[0]

但, 语法定义中描述了许多内容

还有?操作符, 不过后面有介绍

成员访问表达式

成员访问表达式的语法定义为上述后缀表达式语法的第 3 条:

PLAINTEXT
postfixExpression '.' identifier typeArguments?

成员访问表达式可以用于访问classinterfacestruct等的成员

果然, 成员访问表达式 即为 类、接口、结构体访问成员的表达式

成员访问表达式的形式为T.a

T可以表示为特定的实例或类型名, 我们将其称为成员访问表达式的主体

a表示成员的名字

  • 如果T是类的实例化对象, 通过这种方式可以访问类或接口中的非静态成员
  • 如果Tstruct的实例, 允许通过实例名访问struct内的非静态成员
  • 如果T是类名、接口名或struct名, 允许直接通过类型名访问其静态成员

需要注意的是: 类、接口和struct的静态成员的访问主体只能是类型名

  • Tthis: 在类或接口的作用域内, 可以通过this关键字访问非静态成员
  • Tsuper: 在类或接口作用域内, 可以通过super关键字访问当前类对象直接父类的非静态成员

C/C++中因为存在值和指针, 所以有两种访问成员的方式.->

不过, 仓颉中统一通过.访问成员

要注意: 仓颉中静态成员, 只能通过类型名访问, 不能通过具体实例访问

thissuper只能在类或接口内部通过.访问非静态成员

对于成员访问表达式e.a, 如果e是类型名:

  • ae可变静态成员变量时, e.a可变的, 其他情况下e.a不可变

如果e是表达式(假设e的类型是T):

  • T是引用类型时, 如果aT可变成员变量, 则e.a可变的, 否则e.a不可变的;
  • T是值类型时, 如果e可变aT可变成员变量, 则e.a可变的, 否则e.a不可变

仓颉通过类型只能访问静态成员, 通过实例只能访问非静态成员

仓颉中的不可变成员, 无论什么时候都不可变

仓颉中的可变成员, 引用类型都可变, 值类型只有实例时可变时, 成员才可变

函数调用表达式

函数调用表达式的语法定义为上述后缀表达式语法的第 4 条, 其中callSuffixvalueArgument的语法定义为:

PLAINTEXT
callSuffix
    : '(' (valueArgument (',' valueArgument)*)? ')'
    ;
    
valueArgument
    : identifier ':' expression
    | expression
    | refTransferExpression
    ;

refTransferExpression
    : 'inout' (expression '.')? identifier
    ;

函数调用表达式用于调用函数, 函数详见第 5 章

对于函数调用表达式f(x), 假设f的类型是T

如果T是函数类型, 则调用名为f的函数, 否则, 如果T重载了函数调用操作符(), 则f(x)会调用其()操作符重载函数(参见 [可以被重载的操作符])

函数调用的后缀是(), 可加参数可不加参数

加参数可以指定形参进行传参:func1(param1: 1, param2: 2), 不过只适用于函数定义时的命名参数, 且命名参数只能命名传参

非命名参数就只能非命名传参

refTransferExpression意思是引用传参? 应该与C++中的引用传参一样, 可以通过形参直接访问实参(具体还要阅读到函数章节再了解)

索引访问表达式

索引访问表达式的语法定义为上述后缀表达式语法的第 5 条, 其中indexAccess的语法定义为:

PLAINTEXT
indexAccess
    : '[' (expression | rangeElement) ']'
    ;

rangeElement
    :  '..'
    | ('..=' | '..' ) expression
    | expression '..'
    ;

索引访问表达式用于那些支持索引访问的类型(包括Array类型和Tuple类型)通过下标来访问其具体位置的元素, 详见第 2 章中关于Array类型Tuple类型的介绍

对于索引访问表达式e[a](假设e的类型是T):

  • T是元组类型时, e[a]不可变的;
  • T不是元组类型时, 如果T重载了 set 形式的操作符[](参见 [可以被重载的操作符]), 则e[a]可变的, 否则e[a]不可变

对于索引访问表达式e1[e2], 仓颉语言总是先求值e1v1, 再将e2求值至v2, 最后根据下标v2选取对应的值或调用相应的重载了的[]操作符

记得在阅读Tuple类型介绍时, 文档中提到使用[]访问元组的元素, []里只能是Int64类型字面量, 应该不能是Range

问号操作符

问号操作符?为一元后缀操作符, 它必须和上文介绍的后缀操作符., (), {}[]一起使用(出现在后缀操作符之前), 实现Option类型对这些后缀操作符的支持, 例如:a?.b, a?(b), a?[b]等等

其中()是函数调用, 当函数调用最后一个实参是lambda时, 可以使用尾闭包语法a?{b}

将包含?.?()?{}?[]的表达式称为 optional chaining 表达式

optional chaining 表达式的语法定义为上述后缀表达式语法的最后一条, 其中questSeperatedItems的语法定义为:

PLAINTEXT
questSeperatedItems
    : questSeperatedItem+
    ;

questSeperatedItem
    : itemAfterQuest (callSuffix | callSuffix? trailingLambdaExpression | indexAccess)?
    ;

itemAfterQuest
    : '.' identifier typeArguments?
    | callSuffix
    | indexAccess
    | trailingLambdaExpression
    ;

关于 optional chaining 表达式, 规定:

  1. 对于表达式e, 将e中的所有?删除, 并且将紧邻?之前的表达式的类型由Option<T>替换为T之后, 得到表达式e1

    如果e1的类型是Option类型, 则在e之后使用.(){}[]时, 需要在e和这些操作符之间加上?; 否则, 不应该加?

  2. Optional chaining 表达式的类型是Option<T>(即无论其中有几个?, 类型都只有一层Option), 类型T为 optional chaining 中最后一个表达式(变量或函数名、函数调用表达式、下标访问表达式)的类型

  3. 一旦 optional chaining 中的某个Option类型的表达式的值为None, 则整个 optional chaining 表达式的值为None

    如果 optional chaining 中每个Option类型的表达式的值都等于某个Some值, 则整个表达式的值为Some(v)(v的类型是最后一个表达式的类型)

解释一下这部分文档内容

?问号操作符实际是作用于Option类型的

即, 如果存在一个变量e的类型是Option<T>类型的, 就可以使用e?.e?()e?{}e?[]来尝试访问成员、调用函数、调用lambda、通过索引访问数据

?之后的操作符, 是根据Option<T>中 这个实际的T决定的, T是类、结构体、interface就用., 是函数就用(), 是lambda就可以用尾闭包{}, 是数组、元组等就可以用[]

?操作符, 通过Option类型, 来安全判断None的情况, 你的Option<T>数据如果是None, 使用?就会取值None, 并终止之后的操作, 整个表达式值就是None. 这对应文档中, 规则3上半部分:

一旦 Optional chaining 中的某个 Option 类型的表达式的值为None, 则整个 Optional chaining 表达式的值为None

比如:

CANGJIE
class Test {
    let value = 20
}

func getTest(number: Int64) : Option<Test> {
    if (number == 20) {
        return Test()
    }

    return None
}

main(): Int64 {
    let res = getTest(20)
    println(res?.value)
    // println(res.value)

    return 0
}

如果尝试不用?访问成员, 是会报错的, 因为resOption类型的, 可能是None, 直接访问成员是不安全的

这段代码的执行结果为:

这个结果也对应文档中的规则3的下半部分:

如果 optional chaining 中每个 Option 类型的表达式的值都等于某个Some值, 则整个表达式的值为Some(v)(v的类型是最后一个表达式的类型)

即, 如果一个完整的 Optional chaining, 链中不存在None, 那么最终结果就是Some(value), value是最终结果的实际类型的数据

其次, 无论Optional chaining有多长, 最终的结果也就只有一层Option, 即a?.b?.c?[2]?()也就只有一层Option, 不是None就是Some(value). 这是规则2的内容

但是对于规则1, 不是很理解. 因为按照规则3, Optional chaining的最终结果一定是Option类型, 如果访问成员或调用函数是一定要加?的, 但规则1说有另外的判断

规则1的意思容易理解为 要根据最终Some(value), value的实际类型来判断是否需要再加?, 但是从实际情况来看无论value是什么类型, 最终Optional chaining的结果是一定要加?

但是经过不断地测试和验证, 规则1 针对的不是一个完整的Optional chaining

规则1针对的应该是一个Optional chaining的中间过程, 即 如果一个Optional chaining很长, 那么并不是所有的成员访问都需要加?

可能存在a?.b?.c?.d, 也可能存在a?.b?.c.d, 规则1针对的是类似这种情况, 在Optional chaining的过程中, 如果出现访问到的某个元素是Option类型的, 那么要用此元素访问其成员或调用函数等操作, 不需要加?, 但最终整个Optional chaining是Option类型的

类似:

CANGJIE
class BaseTest {
    let value: Int64 = 10
}

class Test {
    let value: Int64 = 20
    let base = BaseTest()
}

func getTest(number: Int64) : Option<Test> {
    if (number == 20) {
        return Test()
    }

    return None
}

main(): Int64 {
    let res = getTest(20)
    println(res?.base.value)

    return 0
}

过程中访问到res的成员base, base是非Option类型的, 所以不加?访问其成员

但整个Optional chaining的最终类型就是Option类型

以表达式a?.b, c?(d)e?[f]为例, 说明如下:

  1. 表达式a的类型需要是某个Option<T1>T1包含实例成员b

    表达式c的类型需要是某个Option<(T2)->U2>d的类型为T2

    表达式e的类型需要是某个Option<T3>T3支持下标操作符

  2. 表达式a?.b, c?(d)e?[f]的类型分别为Option<U1>, Option<U2>Option<U3>, 其中U1T1中实例成员b的类型, U2是函数类型(T2)->U2的返回值类型, U3T3执行下标操作的返回类型

  3. a, ce的值分别等于Some(v1), Some(v2)Some(v3)时, a?.b, c?(d)e?[f]的值分别等于Option<U1>.Some(v1.b), Option<U2>.Some(v2(d))Option<U3>.Some(v3[f])

    a, ce的值分别等于None时, a?.b, c?(d)e?[f]的值分别等于Option<U1>.None, Option<U2>.NoneOption<U3>.None(注意这里的b, df都不会被求值)

事实上, 表达式a?.b, c?(d)e?[f]分别等价于如下match表达式:

CANGJIE
// a?.b 等同于以下匹配表达式
match (a) {
    case Some(v) => Some(v.b)
    case None => None<U1>
}

// c?(d) 等同于以下匹配表达式
match (c) {
    case Some(v) => Some(v(d))
    case None => None<U2>
}

// e?[f] 等同于以下匹配表达式
match (e) {
    case Some(v) => Some(v[f])
    case None => None<U3>
}

即, 对于a?.bc?(d)e?[f], 只要ace不是None, 那么最终结果就是Some(a.b)Some(c(d))Some(e[f])

结果的最终类型是根据最终Some(value)value的类型决定的, Option<T>T即为value的实际类型

再来看一个包含多个?的多层访问的例子a?.b.c?.d(以?.为例, 其他操作类似, 不再赘述):

  1. 表达式a的类型需要是某个Option<Ta>Ta包含实例成员b, b的类型中包含实例成员变量cc的类型是某个Option<Tc>, Tc包含实例成员d

  2. 表达式a?.b.c?.d的类型为Option<Td>, 其中TdTc的实例成员d的类型;

  3. a的值等于Some(va)va.b.c的值等于Some(vc)时, a?.b.c?.d的值等于Option<Td>.Some(vc.d)

    a的值等于Some(va)va.b.c的值等于None时, a?.b.c?.d的值等于Option<Td>.None(d不会被求值)

    a的值等于None时, a?.b.c?.d的值等于Option<Td>.None(b, cd都不会被求值)

表达式a?.b.c?.d等价于如下match表达式:

CANGJIE
// a?.b.c?.d 等同于以下匹配表达式
match (a) {
    case Some(va) =>
        let x = va.b.c
        match (x) {
            case Some(vc) => Some(vc.d)
            case None => None<Td>
        }
    case None =>
        None<Td>
}

这个例子中的b的类型就是非Option类型

Optional chaining 也可以作为左值表达式(参见 [赋值表达式]), 例如a?.b = e1, a?[b] = e2, a?.b.c?.d = e3

赋值表达式的左侧是 Optional chaining 表达式时, 要求 Optional chaining 表达式是可变的(参见 [赋值表达式])

因为函数类型是不可变的, 所以只需要考虑?.?[]这两种情况, 并且它们都可以归纳到a?.b = ca?[b] = c这两种基本的场景(假设a的类型为Option<T>), 规定:

  • 对于a?.b, 仅当T是引用类型且b可变时, a?.b可变的, 其他情况下a?.b不可变
  • 对于a?[b], 仅当T是引用类型且重载了 set 形式的操作符[]时, a?[b]可变的, 其他情况下a?[b]不可变

a?.b(或a?[b])可变时, 如果a的值等于Option<T>.Some(v), 将c的值赋值给v.b(或v[b])

如果a的值等于Option<T>.None, 什么也不做(bc也不会被求值)

类似地, 表达式a?.b = e1, a?[b] = e2a?.b.c?.d = e3分别等价于如下match表达式

CANGJIE
// a?.b = e1 等同于以下匹配表达式
match (a) {
    case Some(v) => v.b = e1
    case None => ()
}

// a?[b] = e2 等同于以下匹配表达式
match (a) {
    case Some(v) => v[b] = e2
    case None => ()
}

// a?.b.c?.d = e3 等同于以下匹配表达式
match (a) {
    case Some(va) => 
        match (va.b.c) {
            case Some(vc) => vc.d = e3
            case None => ()
        }
    case None => 
        ()
}

Option chaining可以当作左值被赋值, 且仅当最后一部分访问符(.[])前的变量实际是引用类型, 且成员可变或重载set()时, 才能当作左值被赋值

因为从给的例子来看:

CANGJIE
// a?.b.c?.d = e3 等同于以下匹配表达式
match (a) {
    case Some(va) => 
        match (va.b.c) {
            case Some(vc) => vc.d = e3
            case None => ()
        }
    case None => 
        ()
}

模式匹配表达式, 中间是直接匹配的va.b.c, 没有其他额外的规则, 只要匹配到了, 即只要最终c是引用类型, 且c.d是可变的就可以进行赋值

下面几个例子, 可以很好的说明, 可以测试一下:

CANGJIE
// 全都为引用类型
class C {
    var d = 20
}

class B {
    let value: Int64 = 10
    let c = Some(C())
}

class A {
    let b = B()
}

main(): Int64 {
    let a = Some(A())
    a?.b.c?.d = 30
    println(a?.b.c?.d)

    return 0
}

// 只有最后一部分为引用类型
class C {
    var d = 20
}

struct B {
    let value: Int64 = 10
    let c = Some(C())
}

struct A {
    let b = B()
}

main(): Int64 {
    let a = Some(A())
    a?.b.c?.d = 30
    println(a?.b.c?.d)

    return 0
}

// 最后一部分不为引用类型
struct C {
    var d = 20
}

class B {
    let value: Int64 = 10
    let c = Some(C())
}

class A {
    let b = B()
}

main(): Int64 {
    let a = Some(A())
    a?.b.c?.d = 30
    println(a?.b.c?.d)

    return 0
}

最后一个例子是编译不过的, 前两个可以:

操作符?应用举例:

CANGJIE
// ?. 用法
class C {
    var item: Int64 = 100
}
let c = C()
let c1 = Option<C>.Some(c)
let c2 = Option<C>.None
let r1 = c1?.item              // r1 = Option<Int64>.Some(100)
let r2 = c2?.item              // r2 = Option<Int64>.None
func test1() {
    c1?.item = 200             // c.item = 200
    c2?.item = 300             // no effect
}

// ?() 用法
let foo = {i: Int64 => i + 1}
let f1 = Option<(Int64) -> Int64>.Some(foo)
let f2 = Option<(Int64) -> Int64>.None
let r3 = f1?(1)               // r3 = Option<Int64>.Some(2)
let r4 = f2?(1)               // r4 = Option<Int64>.None

// ?[] 对于元组访问的用法
let tuple = (1, 2, 3)
let t1 = Option<(Int64, Int64, Int64)>.Some(tuple)
let t2 = Option<(Int64, Int64, Int64)>.None
let r7 = t1?[0]               // r7 = Option<Int64>.Some(1)
let r8 = t2?[0]               // r8 = Option<Int64>.None
func test3() {
    t1?[0] = 10               // error: 't1?[0]' is immutable
    t2?[1] = 20               // error: 't2?[0]' is immutable
}

Thanks for reading!

仓颉文档阅读-语言规约IV: 表达式(II)

周五 9月 26 2025
8063 字 · 38 分钟