【Cangjie】编程语言学习02——仓颉操作符(运算符)_A篇

注:本教程假定读者是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?.idexpr?[expr]expr?(expr)expr?{expr}
!3按位求反、逻辑非!expr右结合
-3一元负号-expr右结合
**4幂运算expr ** expr右结合
*/5乘法,除法expr * exprexpr / expr左结合
%5取模expr % expr左结合
+-6加法,减法expr + exprexpr - 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
is9类型检查expr is Type
as9类型转换expr as Type
==10判等expr == expr
!=10判不等expr != expr
&11按位与expr & expr左结合
^12按位异或expr ^ expr左结合
|13按位或expr | expr左结合
&&14逻辑与expr && expr左结合
||15逻辑或expr || expr左结合
??16coalescing 操作符expr ?? expr右结合
|>17pipeline 操作符id |> expr左结合
~>17composition 操作符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 类型

不支持任何操作符。

## 题目重述 题目要求对数组、字符串进行语法糖扩展,以实现如下功能: - `println([1,2,3]+[4,5,6])` 输出 `[1,2,3,4,5,6]` - `println(["123"]+["456"])` 输出 `["123","456"]` - `println(!a)` 对于 `a=[1,2,3]` 输出 `[3,2,1]`(即反转数组) - `println(!!"123")` 输出 `true` - `println(!!"")` 输出 `false` 请在下方进行解答。 ## 给出答案 下面是符合题目要求的仓颉语言代码: ```cangjie // 扩展数组和字符串的功能 // 定义加法操作符重载以实现两个数组相加 Array.prototype.+ = function(other) { return this.concat(other); } // 定义非运算符!,用于翻转数组 Array.prototype.! = function() { return this.slice().reverse(); } // 定义双非运算符!!,用于将字符串转换为布尔值 String.prototype.!! = function() { return this.length > 0; } // 测试代码 println([1, 2, 3] + [4, 5, 6]); // 输出 [1,2,3,4,5,6] println(["123"] + ["456"]); // 输出 ["123","456"] let a = [1, 2, 3]; println(!a); // 输出 [3,2,1] println(!!"123"); // 输出 true println(!!""); // 输出 false ``` ## 代码解析 1. **数组加法**:通过定义 `Array.prototype.+` 方法,允许两个数组使用 `+` 操作符相加,并返回合并后的新数组。 2. **数组反转**:通过定义 `Array.prototype.!` 方法,允许使用 `!` 操作符反转数组,并返回新的反转后的数组。 3. **字符串转布尔值**:通过定义 `String.prototype.!!` 方法,允许使用 `!!` 操作符检查字符串是否为空,并返回相应的布尔值。 ## 知识点 1. **操作符重载**:在编程语言中,通过自定义对象的行为来实现操作符的特殊功能。 2. **原型链扩展**:JavaScript/仓颉语言中,可以通过修改对象的原型来添加新方法或属性。 3. **数组和字符串方法**:如 `concat()` 用于连接数组,`reverse()` 用于反转数组,`length` 属性用于获取字符串长度。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值