5077 字
25 分钟
仓颉文档阅读-开发指南III: 基础数据类型(I) - 基本操作符
NOTE

阅读文档版本:

语言规约 Cangjie-0.53.18-Spec

具体开发指南 Cangjie-LTS-1.0.4

在阅读 了解仓颉的语言规约时, 难免会涉及到一些仓颉的示例代码, 但 我们对仓颉并不熟悉, 所以可以用 仓颉在线体验 快速验证

有条件当然可以直接 配置Canjie-SDK

WARNING

博主在此之前, 基本只接触过C/C++语言, 对大多现代语言都没有了解, 所以在阅读过程中遇到相似的概念, 难免会与C/C++中的相似概念作类比, 见谅

且, 本系列是文档阅读, 而不是仓颉的零基础教学, 所以如果要跟着阅读的话最好有一门编程语言的开发经验

WARNING

在阅读仓颉编程语言的开发指南之前, 已经大概阅读了一遍 仓颉编程语言的语言规约, 已经对仓颉编程语言有了一个大概的了解

所以在阅读开发指南时, 不会对类似: 类、函数、结构体、接口等解释起来较为复杂名称 做出解释

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

基础数据类型#

基本操作符#

操作符是执行特定的数学运算或逻辑操作的符号. 例如, 数学运算符号的加号(+)可将两个数相加(如: let i = 1 + 2), 逻辑操作符号的逻辑与(&&)可用于组合并确保多个条件判断均满足(如: if (i > 0 && i < 10))

仓颉编程语言不仅支持各种常用的操作符, 同时为了减少常见编码错误对它们做了部分改进

如: 赋值表达式(包含赋值操作符的表达式)的类型是Unit, 值是(), 如果将if(a == 3)写成if(a = 3), 赋值表达式的返回值不是布尔类型, 因此会编译报错, 这样可以避免将判等操作符(==)误写成赋值操作符(=)的问题

算术操作符(+-*/%等)的结果会被检测并禁止值溢出, 以此来避免保存变量时由于变量大于或小于其类型所能承载的范围时导致的异常结果

仓颉编程语言还提供了区间操作符, 例如a..ba..=b, 这方便表达一个区间内的数值

本章节只描述了仓颉编程语言中的基本操作符, 其他操作符参见附录中的[操作符]

如何进行自定义类型的操作符重载参见[操作符重载]章节

仓颉对于=赋值操作符特别规定, 赋值表达式的类型和值, 恒为Unit()

这表示, 仓颉中基础数据的赋值表达式是禁止连续赋值的, 因为单个赋值表达式的值恒为()

赋值操作符=#

用于将左操作数的值修改为右操作数的值, 要求右操作数的类型是左操作数类型的子类型

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

main(): Int64 {
var a = 1
var b = 1
a = (b = 0) // 编译错误, 赋值表达式的类型是 Unit, 值是 ()
if (a = 5) { // 编译错误, 赋值表达式的类型是 Unit, 值是 ()
}
a = b = 0 // 语法错误, 不支持链式使用赋值
return 0
}

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

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

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

main(): Int64 {
var a: Int64
var b: Int64
(a, b) = (1, 2) // a = 1, b = 2
(a, b) = (b, a) // 交换, a = 2, b = 1
(a, _) = (3, 4) // a = 3
(_, _) = (5, 6) // 无赋值
return 0
}

赋值操作符=, 可以将=右边表达式的值 赋值给=左边的表达式, 总是先计算右边的表达式, 且右边表达式的值要为左边表达式的子类型

仓颉还可以使用元组, 对多个变量一次性进行赋值:

(变量1, 变量2, ...) = (值1, 值2, ...)

但要保证, 按照编码顺序, 变量类型与值类型保持一直或值为变量子类型

左边元组中的变量, 可以使用通配符_代替, 对应位置将不会出现变量赋值

算术操作符-+-*/%**#

仓颉编程语言支持的算术操作符包括: 一元负号(-)、加(+)、减(-)、乘(*)、除(/)、取余(%)、求幂(**)

除了一元负号是一元前缀操作符, 其他操作符均是二元中缀操作符

一元负号(-)的操作数只能是数值类型的表达式

一元前缀负号表达式的值等于操作数取负的值, 类型和操作数的类型相同:

let num1: Int64 = 8
let num2 = -num1 // num2 = -8, 其数据类型为“Int64"
let num3 = -(-num1) // num3 = 8, 其数据类型为“Int64"

对于二元操作符*/%+-, 要求两个操作数的类型相同

其中%的操作数只支持整数类型;*/+-的操作数可以是任意数值类型

注意:

  • 除法(/)的操作数为整数时, 将非整数值向0的方向舍入为整数

  • 整数取余运算a % b的值定义为a - b * (a / b)

  • 加法操作符也可用于字符串的拼接

let a = 2 + 3 // a = 5
let b = 3 - 1 // b = 2
let c = 3 * 4 // c = 12
let d = 7 / 3 // d = 2
let e = 7 / -3 // e = -2, 当遇到“-"时, 它具有更高的优先级
let f = -7 / 3 // f = -2
let g = -7 / -3 // g = 2, 当遇到“-"时, 它具有更高的优先级
let h = 4 % 3 // h = 1
let i = 4 % -3 // i = 1, 当遇到“-"时, 它具有更高的优先级
let j = -4 % 3 // j = -1
let k = -4 % -3 // k = -1, 当遇到“-"时, 它具有更高的优先级
let s1 = "abc"
var s2 = "ABC"
let r1 = s1 + s2 // r1 = "abcABC"

仓颉中的算术操作符, 要保证操作数类型完全相同

算术操作符的-+-*, 都没有什么需要特别注意的, 就是小学的四则运算, 但要注意优先级, 不确定的可以使用()

+可以用于字符串之间的拼接

其他的有一定需要注意的特性:

  1. /, 除 只有一点需要注意

    如果运算数为整数, 运算结果将小数部分向0的方向舍入

    即, 如果结果是负数, 整数部分+1小数部分舍去; 如果结果是正数, 小数部分舍去

  2. %, 取余 操作数只能为整型, 且需要了解正确的计算方式

    a - b * (a / b)

** 表示求幂运算(如x**y表示计算底数xy次幂)

**的左操作数只能为Int64类型或Float64类型

注意:

当左操作类型为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

求幂操作符**, 是C/C++中不存在的操作符, x**y表示计算底数xy次幂

作为幂 的操作数:

在底为Int64类型时, 只能为UInt64类型

在底为Float64类型时, 只能为Int64Float64类型

复合赋值操作符#

仓颉编程语言也提供**=*=/=%=+=-=<<=>>=&=^=|=&&=||=复合赋值操作符

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

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

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

var a: Int64 = 10
a += 2 // a = 12
a -= 2 // a = 10
a **= 2 // a = 100
a *= 2 // a = 200
a /= 10 // a = 20
a %= 6 // a = 2
a <<= 2 // a = 8

复合赋值操作符, 总是先对=左边的表达式取值, 再对=右边的表达式取值, 然后将左值与右值根据复合操作符进行计算, 最后赋值给左边变量

关系操作符#

关系操作符包括六种: 相等(==)、不等(!=)、小于(<)、小于等于(<=)、大于(>)、大于等于(>=)

关系操作符都是二元操作符, 并且要求两个操作数的类型是一样的

关系表达式的类型是Bool类型, 即值只可能是truefalse

关系表达式举例:

main(): Int64 {
3 < 4 // true
3 <= 3 // true
3 > 4 // false
3 >= 3 // true
3.14 == 3.15 // false
3.14 != 3.15 // true
return 0
}

对于元组类型, 当且仅当所有元素均支持使用==进行值判等(使用!=进行值判不等)时, 此元组类型才支持使用==进行值判等(使用!=进行值判不等)

否则, 此元组类型不支持==!=(如果使用==!=, 编译报错)

两个同类型的元组实例相等, 当且仅当相同位置(index)的元素全部相等(意味着它们的长度相等)

var isTrue: Bool = (1, 3) == (0, 2) // false
isTrue = (1, "123") == (1.0, 2) // 编译错误, 两个操作数的类型不一致
isTrue = (1, _) == (1.0, _) // 编译错误, 通配符不可作为元组中元素进行匹配

仓颉的关系操作符的关系表达式, 类型是Bool值只能为truefalse, 且操作数的类型只能是相同类型

==!=, 还可以用于元组之间, 但两个元组的对应index的元素类型要相同, 且元组的所有元素都需要已经支持==!=

coalescing操作符#

coalescing操作符使用 ?? 表示, ??二元中缀操作符

coalescing操作符用于Option类型的解构

e1 ?? e2表达式, 在e1的值等于Option<T>.Some(v)时, e1 ?? e2的值等于v的值(此时, 不会再去对e2求值, 即满足 “短路求值”); 在e1的值等于Option<T>.None时, e1 ?? e2的值等于e2的值

coalescing表达式使用举例:

main(): Int64 {
let v1 = Option<Int64>.Some(100)
let v2 = Option<Int64>.None
let r1 = v1 ?? 0
let r2 = v2 ?? 0
print("${r1}") // 100
print("${r2}") // 0
return 0
}

仓颉中, 存在Option<T>类型, 是一种泛型枚举类型

主要作用是通过模式匹配, 来安全的处理空值

??是对Option<T>类型对象进行操作的操作符, 使用语法为: Option<T> ?? value, value类型需要为T

功能就是解构并判断Option<T>对象是否为空, 如果为空 则表达式值为value, 如果不为空 则表达式值为解构结果

就如文档中的例子:

let v1 = Option<Int64>.Some(100)
let v2 = Option<Int64>.None
let r1 = v1 ?? 0
let r2 = v2 ?? 0
/*
* v1 是 Some(100)
* 此时 v1 ?? 0, 因为 v1 实际是不为空的, 所以 会将实际的值解构出来, 得到 100
* 即, 表达式 v1 ?? 0 的值就是100
*
* v2 同理, 只不过 v2 实际就为 None, 所以结果为空, 就会用到 ?? 右边的值
* 即, 表达式 v2 ?? 0 的值就是0
*/

区间操作符#

区间操作符有两种: ....=, 分别用于创建 “左闭右开” 和 “左闭右闭” 的区间实例

关于它们的介绍, 请参见 [区间类型]

仓颉中存在区间类型, 主区间类型是连续的整型序列

....=, 两个操作符就可以用来快速创建连续的整型序列

举个例子:

let range1 = 0..10
let range2 = 0..=10
for (value in range1) {
print("${value} ")
}
println()
for (value in range2) {
print("${value} ")
}
println()

这段代码的执行结果为:

从输出结果可以看出来, ..可以创建[)左闭右开的区间实例, 而..=可以创建[]左闭右闭的区间实例

逻辑操作符#

仓颉编程语言支持三种逻辑操作符: 逻辑非(!)、逻辑与(&&)、逻辑或(||)

逻辑非(!)是一元操作符, 它的作用是对其操作数的布尔值取反: !false的值等于true, !true的值等于false

var a: Bool = true // a = true
var b: Bool = !a // b = false
var c: Bool = !false // c = true

逻辑与(&&)和逻辑或(||)均是二元操作符

对于表达式expr1 && expr2, 只有当expr1expr2的值均等于true时, 它的值才等于true

对于表达式expr1 || expr2, 只有当expr1expr2的值均等于false时, 它的值才等于false

var a: Bool = true && true // a = true
var b: Bool = true && false // b = false
var c: Bool = false && false // c = false
var d: Bool = false && true // d = false
a = true || true // a = true
b = true || false // b = true
c = false || false // c = false
d = false || true // d = true

逻辑与(&&)和逻辑或(||)采用短路求值策略:

计算expr1 && expr2时, 当expr1=false则无需对expr2求值, 整个表达式的值为false; 计算expr1 || expr2时, 当expr1=true则无需对expr2求值, 整个表达式的值为true

func isEven(a: Int64): Bool {
if((a % 2) == 0) {
println("${a} is an even number")
true
} else {
println("${a} is not an even number")
false
}
}
main() {
var a: Bool = isEven(2) && isEven(20)
var b: Bool = isEven(3) && isEven(30) // isEven(3)返回值是false, b 值为false, 无需对isEven(30)求值
a = isEven(4) || isEven(40) // isEven(4)返回值是true, a 值为true, 无需对isEven(40)求值
b = isEven(5) || isEven(50)
}
  1. &&, 逻辑与

    二元操作符, 操作类型为布尔类型的表达式

    操作的所有表达式中, 只要有一个表达式值为false, 整个被||连接的表达式值为false

    换句话说, 被&&连接的所有表达式均为true时, 整个表达式才为true

  2. ||, 逻辑或

    二元操作符, 操作类型为布尔类型的表达式

    操作的所有表达式中, 只要有一个表达式值为true, 整个被||连接的表达式值为true

  3. !, 逻辑非

    一元操作符, 操作类型为布尔类型的的表达式

    对布尔类型表达式的现有值逻辑取反, 即!truefalse, !falsetrue

如果你接触过其他编程语言, 那么大概率对这三个操作符不会陌生

&&||遵循短路取值, 从左到右计算每个被连接的布尔类型表达式

  1. &&, 遇到false, 则右边的所有表达式不再执行、计算, 整个表达式为false

  2. ||, 遇到true, 则右边的所有表达式不再执行、计算, 整个表达式为true

最容易忽略的不是表达式的值, 而是短路之后右侧表达式不再计算、执行

func cmp0(value: Int64): Bool {
println(value)
if (value == 0) {
return true
}
return false
}
main() {
var a = -1
var b = 0
var c = 1
if (cmp0(a) || cmp0(b) || cmp0(c) ) {
print("a: ${a}, b: ${b}, c: ${c}")
}
println()
}

这段代码, 很显然cmp0(a) 为 false``cmp0(b) 为 true``cmp0(c) 为 false

如果三次调用都执行, 应该会有输出:

-1
0
1

但实际的执行结果为:

只输出了:

-1
0

所以, ||从左向右的执行过程中, 如果遇到true, 右边的表达式将不再执行

&&同理, 这就是短路求值策略

位运算操作符#

仓颉编程语言支持:

一种一元前缀位运算操作符: 按位求反(!)

五种二元中缀位运算操作符: 左移(<<)、右移(>>)、按位与(&)、按位异或(^)和按位或(|)

位运算操作符的操作数只能为整数类型, 通过将操作数视为二进制序列, 然后在每一位上进行逻辑运算(0视为false, 1 视为true)或移位操作来实现位运算

对于移位操作符, 要求其操作数必须是整数类型(但两个操作数可以是不同的整数类型, 例如: 左操作数是Int8, 右操作数是Int16), 并且无论左移还是右移, 右操作数都不允许为负数(对于编译时可检查出的此类错误, 编译报错, 如果运行时发生此错误, 则抛出异常)

对于无符号数的移位操作, 移位和补齐规则是: 左移低位补0高位丢弃, 右移高位补0低位丢弃

对于有符号数的移位操作, 移位和补齐规则是:

  • 正数和无符号数的移位补齐规则一致

  • 负数左移低位补0高位丢弃

  • 负数右移高位补1低位丢弃

另外, 如果右移或左移的位数(右操作数)等于或者大于操作数的宽度, 则为移位越界, 如果编译时可以检测到则报错, 否则运行时抛出异常

var a = !10 // -11, 符合移位和补齐规则
a = !20 // -21, 符合移位和补齐规则
a = 10 << 1 // 20, 符合移位和补齐规则
// a = 1000 << -1 // 编译报错, 移位操作溢出(右操作数都不允许为负数)
// a = 1000 << 100000000000 // 编译报错, 移位操作溢出(移位越界)
a = 10 << 1 << 1 // 40, 符合移位和补齐规则
a = 10 >> 1 // 5, 符合移位和补齐规则
a = 10 & 15 // 10
a = 10 ^ 15 // 5
a = 10 | 15 // 15
a = (1 ^ (8 & 15)) | 24 // 25

如果已经非常了解C语言, 相信仓颉中关于位操作符的具体操作应该不会很陌生

仓颉对位运算操作符进行了很严格的安全性的检查

  1. 位移操作符右操作数禁止负数

  2. 位移操作符 位移位数禁止超过原数宽度, 即 如果左操作数是32位, 那么右操作数就不能大于等于32

按位与或非和异或, 就不需要过多介绍了吧

自增自减操作符#

自增(++)和自减(--)操作符实现对值的加1和减1操作, 且只能作为后缀操作符使用

自增(++)和自减(--)操作符是非结合

对于表达式expr++(或expr--), 规定如下:

  1. expr的类型必须是整数类型

  2. 因为expr++(或expr--)是expr += 1(或expr -= 1)的语法糖, 所以此expr同时必须也是可被赋值的

  3. expr++(或expr--)的类型为Unit

自增(自减)表达式举例:

var i: Int32 = 5
i++ // i = 6
i-- // i = 5
i--++ // 语法错误
var j = 0
j = i-- // 语义错误

仓颉中的++--, 与C/C++非常不一样

首先仓颉中的++--只能后置

其次, ++--表达式类型恒为Unit, 也就意味着++--表达式不能出现在Unit不能出现的表达式中

即, C/C++中的这些合法操作, 在仓颉中是非法的:

int i = 0;
int j = i++ + ++i;

即, 即使++只是后置, 也不能在仓颉中编写类似的代码:

var i: Int32 = 0
var j: Int32 = i++ + i++
error: invalid binary operator '+' on type 'Unit' and 'Unit'
错误: 类型'Unit'和'Unit'上的二元运算符`+`无效