一、 前言
> 仓颉在力扣上已经能用了,试了下速度,很快啊。
> 于是打算开个坑存一下仓颉模板。
> 本文记录学习仓颉过程中遇到的坑和吐槽点,以及优秀点
安全声明1:本人是Python吹,以下大多与Python对比
安全声明2:优先以敲算法代码(刷题)的角度开喷,工程角度的后续再补(也可能不补)
另外,本吐槽纯主观,基于个人喜好
二、 槽点
1) 元组不好用
和python相比,CJ的元组
- 不能for
- 不能比大小
- 没有size
- 不能toString
- 不能toArray
- 泛型类传了元组就不能toString了
class Deque<T> <: Collection<T> {
// 其他方法和属性
public init() { }
public init(items: Collection<T>) {
// 初始化
}
public func toArray() {
return Array<T>(Size, {i=>_arr[(head+i)%_arr.size].getOrThrow()})
}
// 其他方法
}
extend<T> Deque<T> <: ToString where T <: ToString {
public func toString() {
return toArray().toString()
}
}
以下可以正常打印
main():Int64 {
let q = Deque([1,2,3,4])
println(q) // [1, 2, 3, 4]
return 0
}
以下传元组则无法打印,其他功能正常使用
main():Int64 {
let q = Deque([(1,2),(3,4)])
println(q) // Error: ^ expected 'Struct-String', found 'Class-Deque<Tuple<Int64, Int64>>'
return 0
}
2) 数组不能解包
遇到传edges=[[1,2],[3,4]]的题目:
python可以
g = [[] for _ in range(n)]
for u, v in edges: # Python 优雅!
g[u].append(v)
而仓颉需要
g = Array<ArrayList<Int64>>(n,{_=>ArrayList()})
for (e in edges) {
let (u, v) = (e[0], e[1]) // CJ emm...至少比c++强
g[u].append(v)
}
3) print不能多参数,并且和println是两个方法
这一点见仁见智吧,对py选手来说确实很不方便
不能多参(逗号)
print和println是两个方法
print(a,b) # Python 优雅!
print(*arr)
print(*arr, sep='\n')
println(a,b) // CJ Error
println("${a} ${b}") // Ok
4) 插值字符串
python里叫f-string
a, b = 3, 4
print(f"{a}+{b}={a+b}")
CJ里叫插值字符串, 实在不想多写个$啊,看得眼花
let (a, b) = (3, 4)
print("${a}+${b}=${a+b}")
5) 自定义排序很麻烦
目前主流的自定义排序方案基本有三种
- Python的排序值值类型返回值
key=
,如a.sort(key=lambda x:x*2
;
这个最简洁直观好写,但是存在局限性,很多场景不适用(比如排序key是触发存在精度问题、除0问题),比如每个元素给(sum,cnt),用平均值排序,如果写key=lambda x:(x[0]/x[1])
,就会有精度和除0问题。
此时可以用方法2或者3代替- 大多语言都支持的正负0返回值,以平均值场景举例:
s1/c1 < s2/c2 => s1 * c2 < s2 * c1
那么可以写a.sort(cmp_to_key(lambda x,y:x[0]*y[1]-y[0]*x[1]))
# 返回值是正负0- 大多数语言都支持布尔返回值 ,以c++为例
sort(begin,end,[&](int a,int b)=>{return a<b;});
仓颉类似于上边的方案二,但是不能自动识别正负0,要明确返回三个枚举值,这样把一行减法的事情变成了手写五六行
当然,也不是完全没意义,一定程度上避免了减法带来的溢出问题。
let a = [1,3,2,3]
a.sortBy(stable:false, comparator:{x,y=>
if (x < y) {
return Ordering.LT
} else if (x > y) {
return Ordering.GT
}
return Ordering.EQ
})
println(a) // [1, 2, 3, 3]
6) 切片为什么是start … end:step
好难受啊,总是写错成start:end,然后飘一片红。要么你step前边也统一呗,矛盾的一批
7) Int64写起来难受
官方是生怕仓颉开发者敲代码敲得太快吗?首字母大写就罢了,后边还要加64,
Int64
这个特别常敲的类型,搞的这么复杂,敲键盘的时候非常影响手速!!这几个按键敲击节奏很卡顿!!
这么垃圾的命名也要硬抄swift。但是已经这么设计了,估计也不会改了
对比一下:
c++/java: long/int
rust:i64/i32
go:int64/int
py:int
8) range竟然不是collection
场景:创建一个[l,r]的闭区间数组
直观的写法:
let (l, r) = (10, 20)
println(Array(l..=r)) // 编译报错
实际能编过的写法,这有必要吗,多恶心啊:
println(Array(r - l + 1, {i => i + l})) // [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
println((l..=r).iterator() |> collectArray<Int64>) // [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] 注意本行需要import std.collection.*
三、 优点
- 工程里可以有多个main,方便单文件管理调试:在当前文件右键 - build and debug file
- 同时主入口可以一键运行:vscode右上角的三角形
- extend扩展语法,类似于python的猴子补丁,非常方便。
- 大数和位图操作可以用std.math.BigInt,好评
- 嵌套函数好评
有时候遇到那种可以抽出来函数的逻辑,但又依赖多个局部变量,用嵌套函数真的很方便啊,普通函数要传一大堆参数,真不想写。
比如这个并查集代码:
let fa = Array<Int64>(n,{i=>i})
func find(x:Int64):Int64 {
if (fa[x] != x) {
fa[x] = find(fa[x])
}
return fa[x]
}
四、 见仁见智
1) 继承语法 <: 这什么emoji,为啥不直接用extend或冒号之类的更不让人眼花的
// 不是哥们,这真眼花,我老觉得尖括号没关闭
public class HashSet<T> <: Set<T> where T <: Hashable & Equatable<T>
2) 函数缺省实参,以及命名实参:我更喜欢python那种定义方式,因为冒号是有别的语义的(类型指定),而放到这不如等于号直观
func add(a!: Int64, b!: Int64) {
return a + b
}
main() {
let x = 1
let y = 2
let r = add(b: y, a: x)
println("The sum of x and y is ${r}")
}
3) 尾随lambda:我会主动禁用这个不利于阅读的语法
官方的说法是:“尾随 lambda 可以使函数的调用看起来像是语言内置的语法一样,增加语言的可扩展性。”
我的说法:把本应写在小括号(参数列表)里的东西抽出来,外边包的是大括号。这写法看起来像函数定义而不是调用,反而容易造成开发者代码检视上的失误;如果让我出代码规范,我就强制大家禁用这个写法。
以下是官方示例:
// 示例1
func myIf(a: Bool, fn: () -> Int64) {
if(a) {
fn()
} else {
0
}
}
func test() {
myIf(true, { => 100 }) // General function call
myIf(true) { // Trailing closure call
100
}
}
// 示例2
func f(fn: (Int64) -> Int64) { fn(1) }
func test() {
f { i => i * i }
}
4) ArrayList构造方法传capacity
- 在C语言里,int a[10]代表创建一个size为10的数组。
- 在C++里
new Vector(10)
代表创建一个size为10的向量。 - 在JS里
new Array(10)
代表创建一个size为10的数组。 - 在CJ里,
new ArrayList(10)
代表创建一个capacity=10
(而不是size
)的列表。这和Java的表现是一致的- 我认为这个构造方法如果非要设置capacity这个不常用的操作,大可以把参数修改成命名参数,这样开发者使用的时候可以明确感知到。而不是一味的对齐Java。
- 当然从工程的角度来看,capacity事先设置的考量是有意义的,见仁见智。我更喜欢reserve用法。
5) 构造方法看起来可以推导,但确实没有列表推导
先来看py优雅的列表推导
# 从a计算出b(在a的基础上每个元素加十)和c(加10并且筛选)
a = [1, 2, 4, 5, 6]
b = [v + 10 for v in a] # [11, 12, 14, 15, 16]
c = [v + 10 for v in a if v % 2] # [11, 15]
仓颉主推链式调用
// import std.collection.*
let a = [1, 2, 4, 5, 6]
let b = Array<Int64>(a.size, {i=>a[i]+10}) // 很像列表推导,但并不是
let c = a |> filter{v=>v%2>0} |> map{v=>v+10} |> collectArray // 链式调用(PipeLine)
五、 小结
- 尽管吐槽了很多,但瑕不掩瑜,仓颉至少是国产编程语言迈出的重要一步。
- 它吸收了很多小现代语言的优点,做到了兼顾:书写简洁、运行性能、调试便利、已于扩展等,这对开发者的吸引力是很强的。
- 仓颉的出现,代表着国产编程语言已经做到跻身第一梯队。