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

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

周一 9月 29 2025
4588 字 · 20 分钟

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

表达式

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

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

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

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

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

流表达式

流表达式是包含流操作符的表达式

流操作符包括两种: 表示数据流向的中缀操作符|>(称为pipeline)和表示函数组合的中缀操作符~>(称为composition)

|>~>的优先级相同, 并介于||和赋值操作符=之间

|>~>的结合性均为左结合, 详情参考下文

流表达式的语法定义为:

PLAINTEXT
flowExpression
    : logicDisjunctionExpression (flowOperator logicDisjunctionExpression)*
    ;

flowOperator
    : '|>' | '~>'
    ;

C++中也存在流, 只不过与仓颉中的流 好像并不是同一种东西?

这里两个操作符的优先级比较重要, 当然对于自己不确定的优先级, 我更喜欢使用()来进行分明

pipeline操作符

pipeline表达式是单个参数函数调用的语法糖, 即e1 |> e2let v = e1; e2(v)的语法糖(即先对|>操作符左边的e1求值)

这里e2是函数类型的表达式, e1的类型是e2的参数类型的子类型

或者e2的类型重载了函数调用操作符()(参见 [可以被重载的操作符])

注意: 这里的f不能是initsuper构造函数

CANGJIE
func f(x: Int32): Int32 { x + 1 }

let a: Int32 = 1
var res = a |> f // ok
var res1 = a |> {x: Int32 => x + 1} // ok

func h(b: Bool) { b }
let res3 = a < 0 || a > 10  |> h // Equivalence: (a < 0 || a > 10)  |> h

func g<T>(x: T): T { x }
var res4 = a |> g<Int32> // ok

class A {
    let a: Int32
    let b: Int32
    init(x: Int32) {
        a = x
        b = 0
    }
    init(x: Int32, y: Int32) {
        x |> init // error:`init`is not a valid expression
        b = y
    }
}

// PIPELINE with operator`()`overloading
class A {
    operator func ()(x: Int32) {
        x
    }
}
let obj = A()
let a: Int32 = 1
let res = a |> obj // Equivalence: obj(a)

从此例子中看, |>可以将左边的操作数作为参数传递给右边的函数、lambda作为参数

不过右边的函数必须是单参数的

composition操作符

composition表达式表示两个单参函数的组合

也就是说, composition表达式e1 ~> e2let f = e1; let g = e2; {x => g(f(x))}的语法糖(即先对~>操作符左边的e1求值)

这里的f, g均为函数类型的表达式或者其类型重载了单参的函数调用操作符()(参见 [可以被重载的操作符]), 则会有以下四种情况:

e1 ~> e2对应的lambda表达式
e1e2是函数类型, 且e1的返回值类型是e2的参数类型的子类型let f = e1; let g = e2;
{x => g(f(x))}
类型f实现了单参数操作符()的重载函数, 而g是一个函数类型, f.operator()的返回值类型是g的参数类型的子类型let f = e1; let g = e2;
{x => g(f.operator()(x))}
f是一种函数类型, 而g的类型实现了单参数操作符()的重载函数, 且f的返回值类型是g.operator()参数类型的子类型let f = e1; let g = e2;
{x => g.operator()(f(x))}
fg的类型都实现了单参数运算符()的重载函数, 且f.operator()的返回值类型是g.operator()参数类型的子类型let f = e1; let g = e2;
{x => g.operator()(f.operator()(x))}

注意: 这里的e1, e2求值后不能是initsuper构造函数

CANGJIE
func f(x: Int32): Float32 { Float32(x) }
func g(x: Float32): Int32 { Int32(x) }

var fg = f ~> g                         // 等价于: {x: Int32 => g(f(x))}

let lambdaComp = {x: Int32 => x} ~> f     // ok

func h1<T>(x: T): T { x }
func h2<T>(x: T): T { x }
var hh = h1<Int32> ~> h2<Int32>         // ok

class A {
    operator func ()(x: Int32): Int32 {
        x
    }
}
class B {
    operator func ()(x: Float32): Float32 {
        x
    }
}
let objA = A()
let objB = B()
let af = objA ~> f // ok
let fb = f ~> objB // ok
let aa = objA ~> objA // ok

composition操作符~>, 作用是将两个单参数函数组合成一个新lambda表达式

如果存在e1e2均为单参数函数, 那么e1 ~> e2, 就是组合成一个 将e1执行返回值作为e2的参数的新lambda, e1的返回值类型需要时e2参数的子类型

赋值表达式

赋值表达式是包含赋值操作符的表达式, 用于将左操作数的值修改为右操作数的值, 要求右操作数的类型是左操作数类型的子类型

对赋值表达式求值时, 总是先计算=右边的表达式, 再计算=左边的表达式, 最后进行赋值

对于复合赋值表达式(+=``-=…)求值时, 总是先计算=左边的表达式的左值, 然后根据这个左值取右值, 然后将该右值与=右边的表达式做计算(若有短路规则会继续遵循短路规则), 最后赋值

除了子类型允许的赋值外, 如果右操作数是字符串字面量, 而左操作数的类型是ByteRune, 则字符串值将分别被强制赋值为ByteRune, 并对强制赋值进行赋值

赋值操作符分为普通赋值操作符和复合赋值操作符, 赋值表达式的语法定义为:

PLAINTEXT
assignmentExpression
    : leftValueExpressionWithoutWildCard assignmentOperator flowExpression
    | leftValueExpression '=' flowExpression
    | tupleLeftValueExpression`=`flowExpression
    | flowExpression
    ;

tupleLeftValueExpression
    :`(`(leftValueExpression | tupleLeftValueExpression) (`, `(leftValueExpression | tupleLeftValueExpression))+`, `?`)`
    ;

leftValueExpression
    : leftValueExpressionWithoutWildCard
    | '_'
    ;

leftValueExpressionWithoutWildCard
    : identifier
    | leftAuxExpression '?'? assignableSuffix
    ;

leftAuxExpression   
    : identifier typeArguments?
    | type
    | thisSuperExpression
    | leftAuxExpression ('?')? '.' identifier typeArguments?
    | leftAuxExpression ('?')? callSuffix
    | leftAuxExpression ('?')? indexAccess
    ;

assignableSuffix
    : fieldAccess
    | indexAccess
    ;

fieldAccess
    : '.' identifier
    ;

assignmentOperator
    : '=' | '+=' | '-=' | '**=' | '*=' | '/=' | '%=' | '&&=' | '||=' 
    | '&=' | '|=' | '^=' | '<<=' | '>>='
    ;

赋值表达式就是=作为赋值时相关的表达式

C++与仓颉都拥有的赋值运算符, 使用上大致都相同, 但&&=||=是C++中没有的, 这两个是操作符左右需要为Bool类型, 且遵循短路规则

其次, 文档中提到的如果左操作数是RuneByte, 右操作数是字符串字面量, 字符串字面量会强制赋值为RuneByte, 然后再赋值. 这里提到的字符串字面量, 其实只是单字符字符串字面量, 如果是多字符字符串, 是不允许复制的

出现在(复合)赋值操作符左侧的表达式称为左值表达式(上述定义中的leftValueExpression)

语法上, 左值表达式可以是一个identifier_, 或者一个leftAuxExpression后接assignableSuffix(包含fieldAccessindexAccess两类), leftAuxExpressionassignableSuffix之间可以有可选的?操作符(对 Option Type 的实例进行赋值的语法糖)

leftAuxExpression可以是以下语法形式:

  1. 一个包含可选类型实参(typeArguments)的identifier
  2. thissuper
  3. 一个leftAuxExpression后接一个.(二者之间可以有可选的?操作符)和一个存在可选类型实参的identifier
  4. 一个leftAuxExpression后接一个函数调用后缀callSuffix或索引访问后缀indexAccess(callSuffixindexAccess之前可以有可选的?操作符)

语义上, 左值表达式只能是如下形式的表达式:

  1. identifier表示的变量(参见 变量名和函数名)

  2. 通配符_, 意味着忽略=右侧表达式的求值结果(复合赋值表达式禁止使用通配符)

  3. 成员访问表达式e1.a或者e2?.a(参见成员访问表达式)

  4. 索引访问表达式e1[a]或者e2?[a](参见索引访问表达式)

注: 其中e1e2必须是满足leftAuxExpression语法的表达式

左值表达式是否合法, 取决于左值表达式是否是可变的: 仅当左值表达式可变时, 它才是合法的

关于上述表达式的可变性, 可参见对应章节

文档已经将左值表达式的形式列出来了, 其实就是普通变量、通配符_还有索引访问和成员访问

赋值表达式的类型是Unit, 值是(), 这样做的好处是可以避免类似于错误地将赋值表达式当做判等表达式使用等问题的发生

在下面的例子中, 如果先执行(a = b), 则返回值是(), 而()不能出现在=的左侧, 所以执行()=0时就会报错

同样地, 由于if之后的表达式必须为Bool类型, 所以下例中的if表达式也会报错

另外, =是非结合的, 所以类似于a = b = 0这样的同一个表达式中同时包含两个以上=的表达式是被禁止的(无法通过语法检查)

CANGJIE
main(): Int64 {
    var a = 1
    var b = 1
    a = (b = 0)     // semantics error
    if (a = 5) {      // semantics error
    }
    a = b = 0       // syntax error
    
    return 0
}

仓颉中规定 赋值表达式类型恒为Unit且值恒为()

而且, 仓颉中的=是非结合的, 即 禁止连续赋值, 从语法上是禁止的

复合赋值表达式a op= b不能简单看做赋值表达式与其他二元操作符的组合a = a op b(其中op可以是算术操作符、逻辑操作符和位操作符中的任意二元操作符, 操作数ab的类型为操作符op所要求的类型)

在仓颉语言中, a op= b中的a只会被求值一次(副作用也只发生一次), 而a = a op b中的a会被求值两次(副作用也发生两次)

因为复合赋值表达式也是一个赋值表达式, 所以复合赋值操作符也是非结合的

复合赋值表达式同样要求两个操作数的类型相同

下面举例说明复合赋值表达式的使用:

CANGJIE
a **= b
a *= b 
a /= b
a %= b 
a += b 
a -= b 
a <<= b
a >>= b
a &&= b
a ||= b
a &= b 
a ^= b 
a |= b 

从语言特性来看, a op= b是比a = a op b更优的

不过编译器可能会进行优化

最后, 如果用户重载了***/%+-<<>>&^|操作符, 那么仓颉语言会提供其对应的复合赋值操作符**=*=/=%=+=-=<<=>>=&=^=|=的默认实现

但有些额外的要求, 否则无法为a = a op b提供赋值语义:

  1. 重载后的操作符的返回类型需要与左操作数的类型一致或是其子类型, 即对于a op= b中的a, b, op, 它们需要能通过a = a op b的类型检查

    例如 当有子类型关系A <: B <: C时, 若用户重载的+的类型是(B, Int64) -> B(B, Int64) -> A, 则仓颉语言可以提供默认实现

    若用户重载的+的类型是(B, Int64) -> C, 则仓颉语言不会为其提供默认实现

  2. 要求a op= b中的a必须是可被赋值的, 例如 是一个变量

仓颉会根据重载的原始操作符, 提供对应的复合赋值操作符

但需要满足类型检查

多赋值表达式是一种特殊的赋值表达式, 多赋值表达式等号左边必须是一个tuple, 这个tuple 里面的元素必须都是左值, 等号右边的表达式也必须是tuple类型, 右边tuple每个元素的类型必须是对应位置左值类型的子类型

注意: 当左侧tuple中出现_时, 表示忽略等号右侧tuple对应位置处的求值结果(意味着这个位置处的类型检查总是可以通过的)

多赋值表达式可以将右边的tuple类型的值, 一次性赋值给左边tuple内的对应左值, 省去逐个赋值的代码

CANGJIE
main(): Int64 {
    var a: Int64
    var b: Int64

    (a, b) = (1, 2) // a == 1, b == 2
    (a, b) = (b, a) // swap, a == 2, b == 1
    (a, _) = (3, 4) // a == 3
    (_, _) = (5, 6) // no assignment

    return 0
}

多赋值表达式可以看成是如下形式的语法糖

赋值表达式右侧的表达式会优先求值, 再对左值部分从左往右逐个赋值

CANGJIE
main(): Int64 {
    var a: Int64
    var b: Int64
    (a, b) = (1, 2)
    
    // desugar
    let temp = (1, 2)
    a = temp[0]
    b = temp[1]
    
    return 0
}

Lambda表达式

Lambda表达式是函数类型的值, 详见第 5 章函数

Quote表达式

Quote表达式用于引用代码, 并将其表示为可操作的数据对象, 主要用于元编程, 详见第 14 章元编程

宏调用表达式

宏调用表达式用于调用仓颉定义的宏, 主要用于元编程, 详见第 14 章元编程

引用传值表达式

引用传值表达式只可用于 C 互操作中调用CFunc场景中, 详见第 13 章互操作中inout参数一节

原来引用传值表达式 适用来与 C 互操作的

操作符的优先级和结合性

对于包含两个或两个以上操作符的表达式, 它的值由操作符和操作数的分组结合方式决定, 而分组结合方式取决于操作符的优先级和结合性

简单来讲, 优先级规定了不同操作符的求值顺序, 结合性规定了具有相同优先级的操作符的求值顺序

如果一个表达式中包含多个不同优先级的操作符, 那么它的计算顺序是: 先计算包含高优先级操作符的子表达式, 再计算包含低优先级操作符的子表达式

在包含多个同一优先级操作符的子表达式中, 计算次序由操作符的结合性决定

下表列出了各操作符的优先级、结合性、功能描述、用法以及表达式的类型

其中越靠近表格顶部, 操作符的优先级越高:

操作符结合性描述用法表达式类型
@右结合宏调用表达式@expr1 @expr2Unit
.左结合成员访问Name.name成员name的类型
[]索引访问varName[expr]varName中元素的类型
()函数调用funcName(expr)funcName返回值的类型
++None后缀自增varName++Unit
--后缀自减varName--Unit
?问号expr1?.expr2 etc.Option<T>``Texpr2的类型
!右结合按位逻辑非!exprexpr的类型
-一元 负-expr
**右结合求幂expr1 ** expr2expr1的类型
*左结合乘法expr1 * expr2expr1expr2的类型, 因为expr1expr2类型相同
/除法expr1 / expr2
%取余expr1 % expr2
+左结合expr1 + expr2expr1expr2的类型, 因为expr1expr2类型相同
-expr1 - expr2
<<左结合按位左移expr1 << expr2expr1的类型, 其中expr1expr2可以有不同的类型
>>按位右移expr1 >> expr2
..None范围操作符expr1..expr2:expr3Range类型
..=expr1..=expr2:expr3
<None小于expr1 < expr2除了expr as userType的类型是Option<userType>
其他表达式具有Bool类型
<=小于等于expr1 <= expr2
>大于expr1 > expr2
>=大于等于expr1 >= expr2
is类型检查(判断)expr is type
as类型转换expr as userType
==None判等expr1 == expr2Bool
!=判不等expr1 != expr2
&左结合按位与expr1 & expr2expr1expr2的类型, 因为expr1expr2类型相同
^左结合按位异或expr1 ^ expr2expr1expr2的类型, 因为expr1expr2类型相同
|左结合按位或expr1 | expr2expr1expr2的类型, 因为expr1expr2类型相同
&&左结合逻辑与expr1 && expr2Bool
||左结合逻辑或expr1 || expr2Bool
??右结合coalescingexpr1 ?? expr2expr2的类型
|>左结合Pipelineexpr1 |> expr2
~>Compositionexpr1 ~> expr2expr1 ~> expr2的类型是 lambda 表达式{x=>expr2(expr1(x))}的类型
=None赋值leftValue = exprUnit
**=复合赋值leftValue **= expr
*=leftValue *= expr
/=leftValue /= expr
%=leftValue %= expr
+=leftValue += expr
-=leftValue -= expr
<<=leftValue <<= expr
>>=leftValue >>= expr
&=leftValue &= expr
^=leftValue ^= expr
|=leftValue |= expr
&&=leftValue &&= expr
||=leftValue ||= expr

注:?.(){}[]一起使用时, 是一种语法糖形式, 不会严格按照它们固有的优先级和结合性进行求值, 详见问号操作符

表达式求值顺序

表达式的求值顺序规定了计算操作数的值的顺序, 显然只有包含二元操作符的表达式才存在求值顺序的概念

仓颉编程语言的默认求值顺序为:

  1. 对于包含逻辑与(&&)、逻辑或(||)和coalescing(??)的表达式, 仅当操作符的右操作数的值会影响整个表达式的值时, 才计算右操作数的值, 否则只计算左操作数的值

    因此, &&||??的求值顺序为: 先计算左操作数的值, 再计算右操作数的值

  2. 对于 optional chaining 表达式, 其中的?会将表达式分隔成若干子项, 按从左到右的顺序对各子项依次求值(子项内按使用到的操作符的求值顺序进行求值)

  3. 对于其他表达式(如算术表达式、关系表达式、位运算表达式等), 同样按从左往右的顺序求值


Thanks for reading!

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

周一 9月 29 2025
4588 字 · 20 分钟