注:本教程假定读者是0基础未接触过编程的小白,因此文章的编写会较为啰嗦。如果是有其他编程语言学习经验的,可直接看加粗字体的段落,那些内容基本是比较重要的,对于0基础的读者,一定按照我的建议读此文章,否则很容易刚开始就学不下去了。
C语言的运算符,在仓颉中称为操作符。仓颉有47个操作符。笔者在本篇先讲常用的操作符,因为有一些运算符笔者没见过,也没用过,不知是否是仓颉原创的,总之只能随着学习的深入,再去介绍和领会。
除了讲操作符以外,本章还要完整的介绍仓颉的一个重要概念表达式,表达式常常与操作符一起出现,但不绝对,这章很重要,也会布置很多练习帮助巩固。
因此关于本章节,看完 A 篇就可以直接看下一章 03 《控制流表达式》,B 篇笔者也不知道什么时候出。
仓颉所有操作符的集合我放在附录中了可直接查看,附录中还集中说明了仓颉基础数据类型支持的操作符。
一、操作符
本篇文章要介绍的操作符有 算术操作符、位操作符、关系操作符、自增和自减操作符、赋值操作符、复合赋值操作符、逻辑操作符,除此之外就剩下一些比较特殊的操作符。
在介绍这些操作符的使用方法前,读者必须要先有一个非常重要的概念,仓颉语言的大部分二元操作符(就是要两个元素才能运算的操作符,比如加法操作符+)都显式要求参与运算的两个操作数必须是相同类型,否则编译器报错。
当你使用一个二元操作符时,优先考虑它是否要求两个操作数类型相同。
(事实上,像C、C++这样的语言的二元运算符也有同样的要求,只是它们的编译器会将两个操作数隐式转换为相同的类型,再进行运算。而仓颉则不支持这种隐式转换)
(一)算术操作符
算术操作符包括:一元负号(-
)、加法(+
)、减法(-
)、乘法(*
)、除法(/
)、取模(%
)、幂运算(**
)。
-
除了一元负号(
-
)和幂运算(**
),其他操作符要求左右操作数是相同的类型 -
**
的左操作数只能为Int64
类型或Float64
类型 -
%
的操作数只支持整数类型
var ia : Int64 = 128
var ib : Int32 = 64
var ic : Int64 = 127
var fa : Float64 = 128.0
var fb : Float32 = 64.0
var fc : Float64 = 127.0
ia = ia + ib // 错误 操作符两边操作数类型不相同
ia = ia + ic
print("ia = ${ia} ic = ${ic}\n")// 手动加入换行符
// 取模操作符 就是求 a / b 的余数 比如: 3 % 4 = 0……3 3就是模 余数是3 5 % 4 = 1……1 1就是模
// 那么下面的打印就是 255 % 127 = 1
print("ia % ic = ${ia % ic}")
fa = fa + fb // 错误 操作符两边操作数类型不相同
fa = fa + fc
fa = fa % fc // 错误 % 操作符只支持整数类型
读者可以多尝试几个操作符。
关于 幂运算操作符,仓颉指出两点要注意的:
- 当左操作数类型为
Int64
时,右操作数只能为UInt64
类型,表达式的类型为Int64
。 - 当左操作数类型为
Float64
时,右操作数只能为Int64
类型或Float64
类型,表达式的类型为Float64
。
幂运算的使用,见如下示例:
let p1 = 2 ** 3 // p1 = 8
let p2 = 2 ** UInt64(3 ** 2) // p2 = 512
let p3 = 2.0 ** 3 // p3 = 8.0
let p4 = 2.0 ** 3 ** 2 // p4 = 512.0
let p5 = 2.0 ** 3.0 // p5 = 8.0
let p6 = 2.0 ** 3.0 ** 2.0 // p6 = 512.0
1、表达式
我们注意到,刚刚仓颉提到了 “表达式的类型” 的概念,首先来了解仓颉语言中表达式的概念。
在仓颉编程语言中,简化并延伸了表达式的传统定义——凡是可求值的语言元素都是表达式。
(传统表达式的概念,0基础了解即可,不做要求。在一些传统编程语言中,一个表达式由一个或多个【操作数(operand)】通过零个或多个【操作符(operator)】组合而成,表达式总是【隐含着一个计算过程】,因此【每个表达式都会有一个计算结果】,对于【只有操作数而没有操作符的表达式,其计算结果就是操作数自身】,对于包含操作符的表达式,计算结果是对操作数执行操作符定义的计算而得到的值。在这种定义下的表达式也被称为算术运算表达式。)
表达式的概念抽象而广泛。我们可以先激进的理解这个概念,只要有一个最终结果的都是表达式。
比如我们刚刚练习的所有包含操作符的代码,都是一个表达式,因为它们都有一个最终的结果。不过大多都是一个复合表达式。
比如在复合表达式 【 ia = ia + ic 】 中,第一个表达式是 【 ia + ic 】,第二个表达式是 【 ia = (ia+ic) 】,它们展开就是一个复合表达式。
现在来理解 “表达式的类型” 的概念就很简单了,它的意思就是 表达式的求值结果的类型。
还记得上一章提到了插值字符串的插值表达式吗,也就是说 只要是表达式,我们都可以尝试放进去看看是否合法。
(二)位操作符
位操作符包括:按位求反(!
)、左移(<<
)、右移(>>
)、按位与(&
)、按位异或(^
)、按位或(|
)。注意,按位与、按位异或和按位或操作符要求左右操作数是相同的整数类型。
还记得上一章提到了 计算机数据的存储格式都是 二进制的补码。
当我们使用位操作符时,则不需要考虑补码,直接使用二进制原码就可以了。
var a : UInt8 = 0b1000_0001
a = !a // 按位取反 每一个二进制位取相反值 则结果为 0b0111_1110 126
a = a >> 1 // 右移 1 位 则结果为 0b0011_1111 63 右移1位也相当于 a / 2
a = a << 1 // 左移 2 位 则结果为 0b1111_1100 252 左移 2 位相当于 a * (2**2)
var b : UInt8 = 0b1110_0100
var c : UInt8 = 0b0110_1110
var d = b & c // 按位与 对应位 都为1则结果为1,否则为0
// 0b1110_0100 b
// 0b0110_1110 c
// 0b0110_0100 d
d = b ^ c // 按位异或 对应位 相同结果为0,不同则为1
// 0b1110_0100 b
// 0b0110_1110 c
// 0b1000_1010 d
d = b | c // 按位或 对应位 有一个为1结果为1,否则结果为0
// 0b1110_0100 b
// 0b0110_1110 c
// 0b1110_1110 d
如果觉得笔者解释的不好、不详细、不明白,也可以直接百度【位运算】 。
(三)关系操作符
小于(<
)、大于(>
)、小于等于(<=
)、大于等于(>=
)、相等(==
)、不等(!=
)。要求关系操作符的左右操作数是相同的类型。
关系表达式的求值类型是布尔类型(Bool)。
这个数学上就已经学过了,就不多说了,值得注意的是,不建议浮点数直接使用 相等( == )运算符,浮点数在使用过算术操作符后,就很难直接比对,原理的话需要了解 【浮点数的存储机制】,这个很复杂,笔者不打算扩展,可自行百度。
当我们需要比较两个浮点数是否相等时,看如下示例:
var fa64 : Float64 = 128.0
var fb64 : Float64 = 127.0
var ret64 : Bool = (fa64 - fb64 < 1e-10f64) && (fa64 - fb64 > -1e-10f64) // 1e-10 就是 0.0000000001 要什么精度根据自己需求
// 当满足 -0.0000000001 < (fa64-fb64) < 0.0000000001 条件时
// 就可以认为 fa64 与 fab64 相等 要什么精度根据自己诉求调整
var fa32 : Float32 = 64.0
var fb32 : Float32 = 127.0
var ret32 : Bool = (fa32 - fb32 < 1e-6f32) && (fa32 - fb32 > -1e-6f32)
var fa16 : Float16 = 32.0
var fb16 : Float16 = 16.0
var ret16 : Bool = (fa16 - fb16 < 1e-3) && (fa16 - fb16 > -1e-3) // 不写后缀也没关系,编译器会自动将 1e-3 转换为
&& 操作符这里不解释,总之它让 (fa64 - fb64 < 1e-10f64) && (fa64 - fb64 > -1e-10f64) 的含义与 -0.0000000001 < (fa64-fb64) < 0.0000000001 相同。
这也引出一个新问题,仓颉允许 算术操作符连续使用,如:64+64+64 这样的表达式是合法的,但是,关系操作符很明显不能连续使用。
还记得我说仓颉是一个强类型语言吗?再联想关系表达式的求值类型,明白了吧。举个例子:
var fa64 : Float64 = 128.0
var fb64 : Float64 = 127.0
var ret64 : Bool = -1e-10f64 < (fa64 - fb64) < 1e-10f64
// 首先 仓颉先对括号里面的 表达式求值 为 1.0
// 接着 仓颉先对表达式 -1e-10f64 < 1.0 求值 左边的值很明显小于1.0 则为 true
// 然后 对表达式 true < 1e-10f64 求值
// 看出来没有,关系运算符要求 左右操作数类型相同
// 很明显 左右操作数的类型不一致,因此编译报错
不过仓颉直接禁止了这种用法,因此它的报错并不是类型不一致,而是直接警告不允许链接。
是吧,理解好了仓颉的概念逻辑,就很容易推导和理解一些规则。因此,表达式的概念要好好理解,而且一定要抽象理解!绝不能死记硬背。编程语言最讲道理了,但只要遵守规则又能非常的灵活,自由,以后代码样式见多了就能理解了。
(四)自增和自减操作符
自增(++
)和自减(--
)。
对于有基础的读者,一定要注意下面的事项。
注意:
- 仓颉中的自增和自减操作符只能作为一元后缀操作符使用。
- 仓颉的自增和自减操作符与其他语言的不一样,表达式的求值结果并不是操作数引用!!!
这个表达式的返回类型为 Unit,它的求值结果是 ()
自增和自减操作符的作用是,让操作数的值+1或-1。操作数必须是可变左值(也就是一个可变变量)。(虽然左值的概念也很抽象,但是也可以慢慢理解,只要代码量上来了,自然而然理解)
var ia : Int64 = 128
let ret = ia++
println("${ret}") // 打印 ()
println("${ia}") // 打印 129
自减的练习就交给读者了,不可懈怠。
(五)逻辑操作符
逻辑操作符包括:逻辑非 !
,逻辑与 &&
,逻辑或 ||。
逻辑操作符主要运用于 布尔类型的表达式(或操作数)。在关系运算符中我们就用到了 逻辑与 &&。
逻辑非 ! 与按位取反类似,都是取相反值。
逻辑与 && 左右表达式的求值结果 为 真 true 时,逻辑与表达式结果为 true,其他为 假 false。
左表达式 | 右表达式 | 求值结果 |
真 true | 真 true | 真 true |
真 true | 假 false | 假 false |
假 false | 真 true | 假 false |
假 false | 假 false | 假 false |
逻辑或 ||
左右表达式的求值结果只要有一个为 真 true,逻辑或表达式的求值结果为 true,否则为 假 false
左表达式 | 右表达式 | 求值结果 |
真 true | 真 true | 真 true |
真 true | 假 false | 真 true |
假 false | 真 true | 真 true |
假 false | 假 false | 假 false |
由此可知 表达式求值结果为 布尔(Bool)类型。
var a : Bool = true
var b : Bool = false
var c : Bool = true
var d : Bool = false
var ret = a && b // false
ret = a && c // true
ret = a || b // true
ret = b || d // false
ret = !a // 逻辑非 取反 结果为 false
ret = !b // true
读者可以配合关系操作符完成其他练习。
关于逻辑操作符还有一个知识点,短路求值。
逻辑与表达式 和 逻辑或表达式 有一个特殊的点就是 短路求值。
当逻辑与表达式中,如果左表达式 为 假 false 时,整个表达式的求值就是 假 false。所以,这时就不需要对右表达式求值了。
当逻辑或表达式中,如果左表达式为 真 true 时,整个表达式的求值就是 真 true。所以,这时就不需要对右表达式求值了。
验证一下:
var ia : Int64 = 128
var ic : Int64 = 127
println("${ic}")
var ret = (ia == ia) && ((ic=12) == ()) // 因为 (ic=12) 求值结果是 Unit类型 () 所以要 (ic=12) == ()
println("${ret} ${ic}")
ret = (ia != ia) && ((ic = 15) == ())
println("${ret} ${ic}")
我们先解析代码,在第一条逻辑与表达式中,先看左表达式, ia==ia 的求值结果必然为 真 true 吧,因此可以执行右表达式中的 ic=12 赋值,因此此时ic=12;再看第二条 逻辑与表达式, ia!=ia左表达式的求值结果必然为 假 false,因此右表达式也就不执行了,因为已经可以明确知道逻辑与表达式的求值结果了,因此 ic还是12。
它的运行结果如下:
逻辑或表达式的短路求值的验证就交给你了。快写代码试试吧!
(六)赋值操作符和复合赋值操作符
赋值操作符即 =
,复合赋值操作符包括:+=
、-=
、*=
、/=
、%=
、**=
、<<=
、>>=
、&=
、^=
、|=、&&=、||=
。
注意:所有赋值表达式的类型都是 Unit ,而不是返回对左表达式求值结果的引用。
赋值操作符用的比较多,就不说了。
复合赋值操作符用代码理解吧:
var a : Int64 = 128
var b : Int64 = 64
a += b // 类似于 a = a + b 但是效率的话,可能是符合赋值操作符的更高,只是可能
a -= a + b // 复合赋值操作符的 两边要求是表达式,因此也可以使用复合表达式,但是左边一定要是左值
关于其他的复合赋值操作符,只要两边的表达式都符合 第一个操作符的要求即可,就留给读者自己练习吧。
二、操作符的优先级
当不同的操作符组成一个复合表达式时,根据操作符的优先级来决定,子表达式的求值顺序。
比如:
var a : Int64 = 3 + 2 * 6 // 很明显先对子表达式 2 * 6 求值
var b : Bool = (a == 3 + 2 * 6) && (a != (3 + 2) * 6)
// 这里明显以 逻辑与操作符 区分为两个子表达式 (a == 3 + 2 * 6)、(a != (3 + 2) * 6)
// 根据 逻辑与操作符 的短路求值, 先对左表达式求值,后对右表达式求值
// 左表达式中 ==操作符优先级 < +操作符优先级 < *操作符优先级
// 所以先对 2*6 求值, 后对 +表达式求值,再对 == 表达式求值
// 左边的 ()操作符返回表达式的求值结果,为true,接着对右表达式求值
// 右表达式中 != 操作符优先级最低,虽然 +操作符优先级 低于 *操作符优先级
// 但是 它用 () 操作符包含起来了(这个操作符不是函数调用的意思)
// 所以需要先执行 3+2 子表达式,然后 ()操作符返回 3+2表达式的求值结果
// 然后对 *表达式求值,再对 !=表达式求值
// 然后右边的 ()操作符返回表达式的求值结果
// 最后 由 &&检查两边表达式的 布尔值,再返回整个复合表达式的求值结果
和数学中的一样,有 ()操作符的,先算 ()里的表达式。
当多个操作符的优先级相同时,就要参考结合方向了,比如当一个复合表达式只有两个+操作符3个操作数时,就应该从左至右相加。
没有结合方向的则表示不允许连续使用,比如前面举的不允许连续使用关系操作符的例子:
-0.0000000001 < (fa64-fb64) < 0.0000000001
应该使用 &&操作符 或 ||操作符 链接起来:
(fa64 - fb64 < 1e-10f64) && (fa64 - fb64 > -1e-10f64) // 这里即使去掉两边的括号也能正常运行
如果觉得注释字多理解不了,不先勉强,也不焦虑,以后写了更多的代码之后,自然而然理解。到时再回来看,就能形成非常正确的理解,接着再看时,可能就会觉得我的原理描述可能存在错误了。温故而知新。
三、总结
本篇的重点除了操作符以外,就是表达式的概念了。只要好好理解了表达式的概念,以后学仓颉的其他语法将如虎添翼。不过还没有深刻地理解,不需要担心,代码量上来了,自然理解。
B篇应该会在 讲完函数章节之后再出。
关于本章讲的所有操作符,都应该掌握它们的作用。如果现在觉得记得有些吃力,不用焦虑,编程语言的学习既是抽象的,更是需要实践的。只要写的代码够多,就能好好抽象理解。别人能设计出来的东西,我们一定可以学会、学好,加油!
附录.
操作符
仓颉官方操作符https://docs.cangjie-lang.cn/docs/0.53.18/user_manual/source_zh_cn/Appendix/operator.html
下表列出了仓颉支持的所有操作符的优先级及结合性,其中优先级一栏数值越小,对应操作符的优先级越高。
有一点需要提的是,() 括号操作符(不是函数调用操作符) 居然不被包含在里面,就是 括号内的表达式优先求值的操作符,但是仓颉可以使用这个操作符。不知是否遗漏的,还是其他原因,这在C语言中也算一个运算符。
比如: a+(b+c) ,这里应该会先对表达式 b+c 求值。
下表中, expr 表示 表达式的意思,var表示 左值,id应该表示一个具体的对象。
结合方向指的是 当多个相同优先级的运算符连续出现时,它们是按照从左到右还是从右到左的顺序进行计算。
比如:
var ret : Int64 = 13 + 11 - 5 // 从左至右,先对表达式 13+11 求值,再对 表达式24-5求值
截止至 2025.6.26
操作符 | 优先级 | 含义 | 示例 | 结合方向 |
---|---|---|---|---|
@ | 0 | 宏调用 | @id | 右结合 |
. | 1 | 成员访问 | expr.id | 左结合 |
[] | 1 | 索引 | expr[expr] | 左结合 |
() | 1 | 函数调用 | expr(expr) | 左结合 |
++ | 2 | 自增 | var++ | 无 |
-- | 2 | 自减 | var-- | 无 |
? | 2 | 问号 | expr?.id , expr?[expr] , expr?(expr) , expr?{expr} | 无 |
! | 3 | 按位求反、逻辑非 | !expr | 右结合 |
- | 3 | 一元负号 | -expr | 右结合 |
** | 4 | 幂运算 | expr ** expr | 右结合 |
* , / | 5 | 乘法,除法 | expr * expr , expr / expr | 左结合 |
% | 5 | 取模 | expr % expr | 左结合 |
+ , - | 6 | 加法,减法 | expr + expr , expr - expr | 左结合 |
<< | 7 | 按位左移 | expr << expr | 左结合 |
>> | 7 | 按位右移 | expr >> expr | 左结合 |
.. | 8 | 区间操作符 | expr..expr | 无 |
..= | 8 | 含步长的区间操作符 | expr..=expr | 无 |
< | 9 | 小于 | expr < expr | 无 |
<= | 9 | 小于等于 | expr <= expr | 无 |
> | 9 | 大于 | expr > expr | 无 |
>= | 9 | 大于等于 | expr >= expr | 无 |
is | 9 | 类型检查 | expr is Type | 无 |
as | 9 | 类型转换 | expr as Type | 无 |
== | 10 | 判等 | expr == expr | 无 |
!= | 10 | 判不等 | expr != expr | 无 |
& | 11 | 按位与 | expr & expr | 左结合 |
^ | 12 | 按位异或 | expr ^ expr | 左结合 |
| | 13 | 按位或 | expr | expr | 左结合 |
&& | 14 | 逻辑与 | expr && expr | 左结合 |
|| | 15 | 逻辑或 | expr || expr | 左结合 |
?? | 16 | coalescing 操作符 | expr ?? expr | 右结合 |
|> | 17 | pipeline 操作符 | id |> expr | 左结合 |
~> | 17 | composition 操作符 | expr ~> expr | 左结合 |
= | 18 | 赋值 | id = expr | 无 |
**= | 18 | 复合运算符 | id **= expr | 无 |
*= | 18 | 复合运算符 | id *= expr | 无 |
/= | 18 | 复合运算符 | id /= expr | 无 |
%= | 18 | 复合运算符 | id %= expr | 无 |
+= | 18 | 复合运算符 | id += expr | 无 |
-= | 18 | 复合运算符 | id -= expr | 无 |
<<= | 18 | 复合运算符 | id <<= expr | 无 |
>>= | 18 | 复合运算符 | id >>= expr | 无 |
&= | 18 | 复合运算符 | id &= expr | 无 |
^= | 18 | 复合运算符 | id ^= expr | 无 |
|= | 18 | 复合运算符 | id |= expr | 无 |
&&= | 18 | 复合运算符 | id &&= expr | 无 |
||= | 18 | 复合运算符 | id ||= expr | 无 |
各基础数据类型支持的操作符
整数类型
整数类型默认支持的操作符包括:算术操作符、位操作符、关系操作符、自增和自减操作符、赋值操作符、复合赋值操作符。
浮点类型
浮点类型默认支持的操作符包括:算术操作符、关系操作符、赋值操作符、复合赋值操作符。浮点类型不支持自增和自减操作符。
布尔类型
布尔类型支持的操作符包括:逻辑操作符(逻辑非 !
,逻辑与 &&
,逻辑或 ||
)、部分关系操作符(==
和 !=
)、赋值操作符、部分复合赋值操作符(&&=
和 ||=
)。
字符类型
字符类型仅支持关系操作符:小于(<
)、大于(>
)、小于等于(<=
)、大于等于(>=
)、相等(==
)、不等(!=
)。比较的是字符的 Unicode 值(关于 Unicode 值自行了解,笔者不赘述)。
字符串类型
字符串类型支持使用关系操作符进行比较,支持使用 +
进行拼接。比如:
let s1 = "abc"
var s2 = "ABC"
let r2 = s1 + s2
println("The result of 'abc' + 'ABC' is: ${r2}")
// 打印结果为:The result of 'abc' + 'ABC' is: abcABC
元组类型
仅支持 赋值操作符(=)。
数组类型
数组类型支持 赋值操作符(=)、成员访问操作符(.)、索引操作符([])。
区间类型
.. | 8 | 区间操作符 | expr..expr | 无 |
..= | 8 | 含步长的区间操作符 | expr..=expr | 无 |
Unit 类型
除了赋值(=)、判等(==)和判不等(!=)外,Unit
类型不支持其他操作。
Noting 类型
不支持任何操作符。