html怎么做网站后台,wordpress与淘宝,网站设计就业前景,wordpress会员地址官网 文档 原生智能化 内嵌AgentDSL的编程框架#xff0c;自然语言编程语言有机融合#xff1b;多Agent协同#xff0c;简化符号表达#xff0c;模式自由组合#xff0c;支持各类智能应用开发。 天生全场景 轻量化可缩放运行时#xff0c;模块化分层设计#xf…官网 文档 原生智能化 内嵌AgentDSL的编程框架自然语言编程语言有机融合多Agent协同简化符号表达模式自由组合支持各类智能应用开发。 天生全场景 轻量化可缩放运行时模块化分层设计内存再小也能装得下全场景领域扩展元编程和eDSL技术支持面向领域声明式开发。 高性能 终端场景首款全并发 GC 应用线程更流畅响应更快。轻量化线程并发性能更好开销更少。 强安全 安全DNA融入语言设计帮助开发者专注于业务逻辑免于将太多精力投入到防御性编程中编码即安全漏洞无处藏。
仓颉编程语言作为一款面向全场景应用开发的现代编程语言通过现代语言特性的集成、全方位的编译优化和运行时实现、以及开箱即用的IDE工具链支持为开发者打造友好开发体验和卓越程序性能。其具体特性表现为 高效编程面向应用开发我们希望语言能够易学易用降低开发者入门门槛和开发过程中的心智负担支持各种常见的开发范式和编程模式让开发者简洁高效地表达各种业务逻辑。仓颉是一门多范式编程语言支持函数式、命令式和面向对象等多种范式包括值类型、类和接口、泛型、代数数据类型、模式匹配、以及高阶函数等特性。此外仓颉还支持类型推断能够减轻开发者类型标注的负担通过一系列简明高效的语法能够减少冗余书写、提升开发效率语言内置的各种语法糖和宏macro的能力支持开发者基于仓颉快速开发领域专用语言DSL构建领域抽象。 安全可靠作为现代编程语言仓颉追求编码即安全通过静态类型系统和自动内存管理确保程序的类型安全和null safety等内存安全同时仓颉还提供各种运行时检查包括数组下标越界检查、类型转换检查、数值计算溢出检查、以及字符串编码合法性检查等能够及时发现程序运行中的错误此外还通过代码扫描工具、混淆工具以及消毒器进一步提供跨语言互操作安全和代码资产保护等支持。 轻松并发并发和异步编程能够有效提高处理器利用率并在交互式应用中确保程序的响应速度是应用开发中必不可少的能力。仓颉语言实现了轻量化用户态线程和并发对象库让高效并发变得轻松。 仓颉语言采用用户态线程模型每个仓颉线程都是极其轻量级的执行实体拥有独立的执行上下文但共享内存。对开发者来说用户态线程的使用和传统的系统线程的使用方式保持一致没有带来额外负担而从运行态视角看线程的管理由运行时完成不依赖操作系统的线程管理因此线程的创建、调度和销毁等操作更加高效且资源占用比系统线程更少。为了避免数据竞争仓颉语言提供了并发对象库并发对象的方法是线程安全的因此在多线程中调用这些方法和串行编程没有区别应用逻辑的开发者无需额外关心并发管理。对于一些核心库仓颉还提供了无锁或者细粒度锁的算法实现能够进一步减少线程的阻塞提升并发度。 卓越性能仓颉编译器及运行时从全栈对编译进行优化包括编译器前端基于CHIRCangjie HighLevel IR高层编译优化比如语义感知的循环优化、语义感知的后端协同优化等基于后端的编译优化比如SLP向量化、Intrinsic优化、InlineCache、过程间指针优化、Barrier优化等基于运行时的优化比如轻量锁、分布式标记、并发Tracing优化等一系列的优化让仓颉充分发挥处理器能力为应用提供卓越的性能支持。另外仓颉语言对运行时进行原生的轻量化设计通过对运行时模块化分层设计定义仓颉公共对象模型和运行时公共基础组件基于公共对象模型实现运行时的内存管理、回栈、异常处理、跨语言调用等基础能力大幅减少多个能力间的冗余对象设计精简运行时体积。同时通过包的按需加载技术减少仓颉应用启动的冗余包内存开销因此对于资源敏感设备占用资源更少支持更友好。
除此之外仓颉还支持面向应用开发的一系列工具链包括语言服务高亮、联想、调试跨语言调试、线程级可视化调试、静态检查、性能分析、包管理、文档生成、Mock工具、测试框架、覆盖率工具、Fuzz工具以及智能辅助编程工具进一步提升软件开发体验以及效率。以下我们将围绕上述几个方面介绍仓颉语言的主要特性让读者能够快速了解仓颉语言的定位和主要技术特色。
1、高效编程
1.1 多范式
仓颉是一个典型的多范式编程语言对过程式编程、面向对象编程和函数式编程都提供了良好的支持包括值类型、类和接口、泛型、代数数据类型和模式匹配以及函数作为一等公民等特性支持。
1.1.1 类和接口
仓颉支持使用传统的类class和接口interface来实现面向对象范式编程。仓颉语言只允许单继承每个类只能有一个父类但可以实现多个接口。每个类都是Object的子类直接子类或者间接子类。此外所有的仓颉类型包括Object都隐式的实现Any接口。
仓颉提供open修饰符来控制一个类能不能被继承或者一个对象成员函数能不能被子类重写override。
在下面的例子中类B继承了类A且同时实现了接口I1和I2。为了让A能够被继承它的声明需要被open修饰。类A中的函数f也被open修饰因此可以在B中被重写。对函数f的调用会根据对象具体的类型来决定执行哪个版本即动态派遣。
open class A {let x: Int 1var y: Int 2open func f(){println(function f in A)}func g(){println(function g in A)}
}interface I1 {func h1()
}interface I2 {func h2()
}class B : A I1 I2 {override func f(){println(function f in B)}func h1(){println(function h1 in B)}func h2(){println(function h2 in B)}
}main() {let o1: I1 B()let o2: A A()let o3: A B()o1.h1() // function h1 in Bo2.f() // function f in Ao3.f() // 动态派遣function f in Bo3.g() // function g in A
}仓颉的interface之间也可以继承并且不受单继承的约束即一个interface也可以继承多个父 interface。如下示例I3可以同时继承I1和I2。因此若要实现I3需要提供对f、g和h三个函数的实现。
interface I1 {func f(x: Int): Unit
}interface I2 {func g(x: Int): Int
}interface I3 : I1 I2 {func h(): Unit
}1.1.2 函数作为一等公民
仓颉中函数可以作为普通表达式使用可以作为参数传递作为函数返回值被保存在其他数据结构中或者赋值给一个变量使用。
func f(x: Int) {return x
}let a flet square {x: Int x * x} // lambda 表达式// 函数嵌套定义以及函数作为返回值
func g(x: Int) {func h(){return f(square(x))}return h
}func h(f: ()-Unit) {f()
}let b h(g(100))除了上面例子中的全局函数对象或结构体等数据类型的成员函数同样也可以作为一等公民使用。下面的例子中对象o的成员函数resetX作为普通表达式被赋值给变量f对f的调用则会改变对象o中成员变量x的值。
class C{var x 100func resetX(n: Int){x nreturn x}
}main(){let o C()let f o.resetX // 成员函数作为一等公民f(200)print(o.x) // 200
}1.1.3 代数数据类型和模式匹配
代数数据类型是一种复合类型指由其它数据类型组合而成的类型。两类常见的代数类型是积类型如struct、tuple等与和类型如tagged union。
在此我们着重介绍仓颉的和类型enum以及对应的模式匹配能力。
在下面的例子中enum类型BinaryTree具有两个构造器Node和Empty。其中Empty不带参数对应于只有一个空节点的二叉树而Node需要三个参数来构造出一个具有一个值和左右子树的二叉树。
enum BinaryTree {| Node(value: Int, left: BinaryTree, right: BinaryTree)| Empty
}访问这些enum实例的值需要使用模式匹配进行解析。模式匹配是一种测试表达式是否具有特定特征的方法在仓颉中主要提供了match表达式来完成这个目标。对于给定的enum类型的表达式我们使用match表达式来判断它是用哪个构造器构造的并提取相应构造器的参数。下面的例子中递归函数sumBinaryTree实现对二叉树节点中保存的整数求和。
func sumBinaryTree(bt: BinaryTree) {match (bt) {case Node(v, l, r) v sumBinaryTree(l) sumBinaryTree(r)case Empty 0}
}除此enum模式以外仓颉也提供了其它各种模式如常量模式、绑定模式、类型模式等以及各种模式的嵌套使用。在下面的例子中我们给出了对应模式的使用
常量模式可以使用多种字面量值进行判等比较如整数、字符串等。绑定模式可以将指定位置的成员绑定到新的变量多用于解构 enum 或 tuple。上面的sumBinaryTree例子中就用到了绑定模式将Node节点中实际的参数与三个新声明的变量v、l和r分别绑定。类型模式可以用于匹配是否目标类型多用于向下转型。tuple模式用于比较或者解构tuple。通配符模式用于匹配任何值。
未来仓颉还计划引入更加丰富的模式如序列sequence模式、record模式等。
// 常量模式-字符串字面量
func f1(x: String) {match (x) {case abc ()case def ()case _ () // 通配符模式}
}// tuple 模式
func f2(x: (Int, Int)) {match (x) {case (_, 0) 0 // 通配符模式和常量模式case (i, j) i / j // 绑定模式将 x 的元素绑定到 i 和 j 两个变量}
}// 类型模式
func f3(x: ParentClass) {match (x) {case y: ChildClass1 ... case y: ChildClass2 ...case _ ...}
}1.1.4 泛型
在现代软件开发中泛型编程已成为提高代码质量、复用性和灵活性的关键技术。泛型作为一种参数化多态技术允许开发者在定义类型或函数时使用类型作为参数从而创建可适用于多种数据类型的通用代码结构。泛型带来的好处包括
代码复用能够定义可操作多种类型的通用算法和数据结构减少代码冗余。类型安全支持更多的编译时的类型检查避免了运行时类型错误增强了程序的稳定性。性能提升由于避免了不必要的类型转换泛型还可以提高程序执行效率。 仓颉支持泛型编程诸如函数、struct、class、interface、extend 都可以引入泛型变元以实现功能的泛型化。数组类型在仓颉中就是典型的泛型类型应用其语法表示为 ArrayT其中 T 表示了元素的类型可以被实例化为任何一个具体的类型例如 ArrayInt 或 ArrayString甚至可以是嵌套数组 ArrayArrayInt从而可以轻易地构造各种不同元素类型的数组。
除了类型外我们还可以定义泛型函数。例如我们可以为使用泛型函数来实现任意两个同类型数组的 concat 操作。如下代码所示我们定义了一个泛型函数 concat并且它支持任意两个 Array 类型的数组参数经过处理后返回了一个拼接后的新数组。这样定义的 concat 函数可以应用在 Array、Array、ArrayArray 以及其它任意类型的数组上实现了功能的通用化。
func concatT(lhs: ArrayT, rhs: ArrayT): ArrayT {let defaultValue if (lhs.size 0) {lhs[0]} else if (rhs.size 0) {rhs[0]} else {return []}let newArr ArrayT(lhs.size rhs.size, item: defaultValue)// 使用数组切片进行整段拷贝newArr[0..lhs.size] lhsnewArr[lhs.size..rhs.size] rhsreturn newArr
}泛型和接口以及子类型结合使用还可以让我们对泛型中的类型变元给出具体的约束从而对可以实例化该类型变元的实际类型做出限制。下面的例子中我们希望在数组arr查找元素element。虽然我们并不关心数组及其元素的具体类型但元素类型T必须能够支持判等操作让我们能够比较数组中的元素与给定元素是否相等。因此在lookup函数中的where子句中我们要求T : EquatableT即类型T必须实现了接口EquatableT。
func lookupT(element: T, arr: ArrayT): Bool where T : EquatableT {for (e in arr){if (element e){return true}}return false
}仓颉的泛型类型不支持协变。以数组为例不同元素类型的数组是完全不相同的类型它们之间不能互相赋值哪怕元素类型之间具有父子类型关系也是禁止的。这避免了数组协变导致的类型不安全问题。
如下示例所示Apple 是 Fruit 的子类但是变量 a 和变量 b 之间是不能互相赋值的Array 和 Array 之间没有子类型关系。
如下示例所示Apple 是 Fruit 的子类但是变量 a 和变量 b 之间是不能互相赋值的Array 和 Array 之间没有子类型关系。
open class Fruit {}
class Apple : Fruit {}main() {var a: ArrayFruit []var b: ArrayApple []a b // 编译报错b a // 编译报错
}1.2 类型扩展
仓颉支持类型扩展特性允许我们在不改变原有类型定义代码的情况下为类型增加成员函数等功能。具体来说
仓颉的类型扩展可以对已有的类型做如下几类扩展
添加函数添加属性添加操作符重载实现接口
下面的例子中我们为String类型增加了printSize成员函数因此在下面的代码中就可以像调用其他预定义的成员函数一样来调用printSize。
extend String {func printSize() {print(this.size)}
}123.printSize() // 3而当扩展和接口搭配使用的时候它更能大幅提升语言的表达能力我们甚至可以给已有的类型添加新的继承体系。
在下面的例子中我们可以定义一个新接口Integer然后用extend给已有的整数类型实现 Integer 接口这样已有的整数类型就自然成为了 Integer 的子类型。其中sealed修饰符表示该接口只能在当前包中被实现或扩展。
sealed interface Integer {}extend Int8 : Integer {}
extend Int16 : Integer {}
extend Int32 : Integer {}
extend Int64 : Integer {}let a: Integer 123 // ok1.3 类型推断
类型推断是指由编译器根据程序上下文自动推断变量或表达式的类型而无需开发者显式写出。
仓颉作为现代编程语言对类型推断也提供了良好的支持。
在仓颉中变量的定义可以根据初始化表达式的类型来推断其类型。除了变量以外仓颉还额外支持了函数定义返回值类型的推断。在仓颉中函数体的最后一个表达式会被视为这个函数的返回值。像变量一样当函数定义省略了返回类型函数就会通过返回值来推断返回类型。
var foo 123 // foo 是 Int64
var bar hello // bar 是 Stringfunc add(a: Int, b: Int) { // add 返回 Inta b
}仓颉还支持在泛型函数调用中推断类型参数包括对柯里化函数里泛型参数的推断如下面的例子所示
func mapT, R(f: (T)-R): (ArrayT)-ArrayR {...
}map({ i i.toString() })([1, 2, 3]) // 支持推断泛型柯里化函数
// 推断结果为mapInt, String({ i i.toString() })([1, 2, 3])注意lambda表达式作为map的第一个参数它的参数类型T和返回值类型R都可以被推断出来尽管参数类型T的推断还反过来依赖对map的第二个参数的类型Array的推断。
柯里化函数Currying什么是柯里化为什么要进行柯里化高级柯里化函数的实现 柯里化是一种函数的转换它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)。 1.4 其他现代特性及语法糖
1.4.1 函数重载
仓颉允许在同一作用域内定义多个同名函数。编译器根据参数的个数和类型来决定函数调用实际执行的是哪个函数。例如下面的绝对值函数为每种数值类型都提供了对应的实现但这些实现都具有相同的函数名abs从而让函数调用更加简单。
func abs(x: Int64): Int64 { ... }
func abs(x: Int32): Int32 { ... }
func abs(x: Int16): Int16 { ... }
...1.4.2 命名参数
命名参数是指在调用函数时提供实参表达式的同时还需要同时提供对应形参的名字。使用命名参数可以提升程序的可读性减少参数的顺序依赖性让程序更加易于扩展和维护。
在仓颉中函数定义时通过在形参名后添加 ! 来定义命名参数。当形参被定义为命名参数后调用这个函数时就必须在实参值前指定参数名如下面的例子所示
func dateOf(year!: Int, month!: Int, dayOfMonth!: Int) {...}dateOf(year: 2024, month: 6, dayOfMonth: 21)1.4.3 参数默认值
仓颉的函数定义中可以为特定形参提供默认值。函数调用时如果选择使用该默认值做实参则可以省略该参数。
这个特性可以减少很多函数重载或者引入建造者模式的需求降低代码复杂度。
func dateOf(year!: Int64, month!: Int64, dayOfMonth!: Int64, timeZone!: TimeZone TimeZone.Local) {...
}dateOf(year: 2024, month: 6, dayOfMonth: 21) // ok
dateOf(year: 2024, month: 6, dayOfMonth: 21, timeZone: TimeZone.UTC) // ok1.4.4 尾随lambdatrailing lambda
仓颉支持尾随lambda语法糖从而更易于DSL中实现特定语法。具体来说很多语言中都内置提供了如下经典的条件判断或者循环代码块
if(x 0){x -x
}while(x 0){x--
}尾随lambda则能够让DSL开发者定制出类似的代码块语法而无需在宿主语言中内置。例如在仓颉中我们支持下面这种方式的函数调用
func unless(condition: Bool, f: ()-Unit) {if(!condition) {f()}
}int a f(...)
unless(a 0) {print(no greater than 0)
}这里对unless函数的调用看上去像是一种特殊的if表达式这种语法效果是通过尾随lambda语法实现 —— 如果函数的最后一个形参是函数类型那么实际调用这个函数时我们可以提供一个lambda表达式作为实参并且把它写在函数调用括号的外面。尤其当这个lambda表达式为无参函数时我们允许省略lambda表达式中的双箭头将其表示为代码块的形式从而进一步减少对应DSL中的语法噪音。因此在上面的例子中unless调用的第二个实参就变成了这样的lambda表达式
{ print(no greater than 0) }如果函数定义只有一个参数并且该参数是函数类型我们使用尾随lambda调用该函数时还可以进一步省略函数调用的括号从而让代码看上去更简洁自然。
func runLater(fn:()-Unit) {sleep(5 * Duration.Second)fn()
}runLater() { // okprintln(I am later)
}runLater { // 可以进一步省略括号println(I am later)
}1.4.5 管道Pipeline操作符
仓颉中引入管道Pipeline操作符来简化嵌套函数调用的语法更直观的表达数据流向。下面的例子中给出了嵌套函数调用和与之等效的基于管道操作符|的表达式。后者更加直观的反映了数据的流向|左侧的表达式的值被作为参数传递给右侧的函数。
func double(a: Int) {a * 2
}func increment(a: Int) {a 1
}double(increment(double(double(5)))) // 425 | double | double | increment | double // 421.4.6 操作符重载
仓颉中定义了一系列使用特殊符号表示的操作符其中大多数操作符都允许被重载从而可以作用在开发者自己定义的类型上为自定义类型的操作提供更加简洁直观的语法表达。
在仓颉中只需要定义操作符重载函数就能实现操作符重载。在下面的例子中我们首先定义一个类型Point表示二维平面中的点然后我们通过重载操作符来定义两个点上的加法操作。
struct Point {let x: Intlet y: Intinit(x: Int, y: Int) {...}operator func (rhs: Point): Point {return Point(this.x rhs.x,this.y rhs.y)}
}let a: Point ...
let b: Point ...
let c a b1.4.7 属性property
在面向对象范式中我们常常会将成员变量设计为private的而将成员变量的访问封装成getter和setter两种public方法。
这样可以隐藏数据访问的细节从而更容易实现访问控制、数据监控、跟踪调试、数据绑定等业务策略。
仓颉中直接提供了属性这一种特殊的语法它使用起来就像成员变量一样可以访问和赋值但内部提供了getter和setter来实现更丰富的数据操作。对成员变量的访问和赋值会被编译器翻译为对相应getter和setter成员函数的调用。
具体来说prop 用于声明只读属性只读属性只具有 getter 的能力必须提供 get 实现mut prop 用于声明可变属性。可变属性同时具备 getter 和 setter 的能力必须提供 get 和 set 实现。
如下示例所示开发者希望对 Point 类型的各数据成员的访问进行记录则可以在内部声明 private 修饰的成员变量通过声明对应的属性来对外暴露访问能力并在访问的时候使用日志系统Logger记录它们的访问信息。对使用者来说使用对象p的属性与访问它的成员变量一样但内部却实现了记录的功能。
注意这里x和y是只读的只有get实现而color则是可变的用mut prop修饰同时具有get和set实现。
class Point {private let _x: Intprivate let _y: Intprivate var _color: Stringinit(x: Int, y: Int, color: String) {...}prop x: Int {get() {Logger.log(level: Debug, access x)return _x}}prop y: Int {get() {Logger.log(level: Debug, access y)return _y}}mut prop color: String {get() {Logger.log(level: Debug, access color)return _color}set(c) {Logger.log(level: Debug, reset color to ${c})color c}}
}main() {let p Point(0, 0, red)let x p.x // access xlet y p.y // access yp.color green // reset color to green
}2、安全可靠
2.1 静态类型和垃圾收集
仓颉是静态类型语言程序中所有变量和表达式的类型都是在编译期确定的并且在程序运行过程中不会发生改变。相比动态类型系统静态类型系统对开发者有更多的约束但能够在编译期尽量早的发现程序中的错误提高程序的安全性同时也让程序的行为更加容易预测为编译优化提供了更多信息使能更多的编译优化提升程序的性能。
垃圾收集GC是一种自动内存管理机制它能够自动识别和回收不再需要使用的对象将开发者从手工释放内存中解放出来不仅可以提高开发效率还能有效避免各种常见内存错误提升程序的安全性。常用的垃圾收集技术包括tracing和引用计数reference counting即RC。仓颉采用tracing GC技术通过在运行时跟踪对象之间的引用关系来识别活动对象和垃圾对象。
2.2 空引用安全
空引用是指引用类型的值可以为 null。代码存在空引用会引发各种各样潜在的风险空引用被图灵奖得主Tony Hoare称为“价值十亿美元的错误”。
在许多编程语言中空引用都是最常见的陷阱之一开发者很容易在未确保非空的情况下访问引用类型的成员从而引发错误或异常。因为语言类型系统并未给非空引用类型提供任何保障。
空引用安全就是旨在消除代码空引用危险。
仓颉是实现了空引用安全的语言之一。在仓颉中没有提供 null 值换句话说仓颉的引用类型永远是非空的。从而在类型上杜绝了空引用的发生。
值得注意的是表示一个空值在语义中是十分有用的。在仓颉中对于任意类型T都可以有对应的可选类型OptionT。具有OptionT类型的变量要么对应一个实际的具有T类型的值v因此取值为Some(v)要么具有空值取值为None。
可选类型OptionT是一种 enum 类型是一个经典的代数数据类型表示有值或空值两种状态。
enum OptionT {Some(T) | None
}var a: OptionInt Some(123)
a None注意OptionT和T是两个不同的类型具有两种类型的值之间不能互相转换 —— 给定一个OptionT类型的表达式e我们只有通过模式匹配确定其值为Some(v)时也就是说其值非空才可以得到一个T类型的值v。因此对表达式e的任意有意义的处理必需伴随着模式匹配和对应的判空操作从而不可能直接对空值None做解引用避免了空引用异常。
基于可选类型使用的广泛性仓颉还为可选类型提供了丰富的语法糖支持。例如可以使用 ?T 来代替 OptionT也提供了可选链操作符?.来简化成员访问以及空合并操作符??来合并有效值。
var a: ?Int None
a?.toString() // None
a ?? 123 // 123
a Some(321)
a?.toString() // Some(321)
a ?? 123 // 3212.3 值类型
值类型是一种具有传递即复制的语义行为的类型具有值类型的变量其中保存的是数据自身而不是指向数据的引用。由于值类型的这种特性开发者选择性地使用值类型可以使得程序显著减少修改语义从而让程序变得更可预测、更可靠。
例如最典型的并发安全问题就是在程序不同的线程中传递了同一个可变对象此时访问这个对象的字段将会造成不可预测的 data race 问题。如果这个对象具备值语义那么在传递的过程中我们就可以保证它经过了完整的复制让每个线程对该值的访问都是彼此独立的从而保证了并发安全。
仓颉原生支持了值类型除了常见的 Int 类型以外仓颉也可以使用 struct 来实现用户自定义的值类型。
如下面的例子Point 正是一个值类型因此在经过赋值后a 和 b 已经是两个彼此独立的变量对 a 的修改不会影响到 b。
struct Point {var x: Intvar y: Intinit(x: Int, y: Int) { ... }...
}var a Point(0, 0)
var b a
a.x 1
print(b.x) // 02.4 “不可变”优先
不可变Immutable指的是在变量赋值或对象创建结束之后使用者就不能再改变它的值或状态。不可变意味着只读不写因此不可变对象天然地具备线程安全的特性即如无其它特殊限制的话可以在任何线程上自由调用。此外相较于可变对象不可变对象的访问没有副作用因此在一些场合下也会让程序更易于了解而且提供较高的安全性。
不可变通常可以分为两种一种是不可变变量不可变变量是指经初始化后其值就不可被修改的变量另一种是不可变类型不可变类型是指在构造完成后实际数据对象的内容无法被改变。
在仓颉中let定义的变量是不可变变量而像 String、enum 等类型是不可变类型这些都是不可变思想在仓颉中的应用。更多地使用不可变特性可以让程序更安全也更利于理解和维护。
在仓颉中let定义的变量是不可变变量而像 String、enum 等类型是不可变类型这些都是不可变思想在仓颉中的应用。更多地使用不可变特性可以让程序更安全也更利于理解和维护。
2.4.1 函数参数不可变
在仓颉中所有函数形参都是不可变的这意味着我们无法对形参赋值如果形参是值类型也无法修改形参的成员。
struct Point {var x: Intvar y: Intinit(x: Int, y: Int) { ... }...
}func f(a: Point) { // a 不可变a Point(0, 0) // errora.x 2 // error
}2.4.2 模式匹配引入的新变量不可变
在仓颉中模式匹配支持变量绑定模式我们可以将目标值解析到新绑定的变量中但这个变量仍然是不可变的。这意味着我们无法对绑定的变量赋值如果变量是值类型也无法修改变量的成员。
func f(a: ?Point) {match (a) {case Some(b) //b 不可变b Point(0, 0) // errorb.x 2 // errorcase None ()}
}2.4.3 闭包捕获可变变量不允许逃逸
在仓颉中闭包指的自包含的函数或 lambda闭包可以从定义它的静态作用域中捕获变量即使对闭包调用不在定义的作用域仍可以访问其捕获的变量。
仓颉中允许闭包捕获可变变量但不允许该闭包继续逃逸这避免了对可变变量修改可能导致的意外行为。
func f() {let a 1var b 2func g() {print(a) // okprint(b) // ok}return g // error, g 捕获了可变变量 bg 不允许作为表达式使用。
}2.4.4 默认封闭
仓颉虽然支持了完整的面向对象范式支持了类继承的特性但仓颉并不鼓励滥用继承尤其是默认可继承可覆盖。默认可继承语义会使得开发者设计的库无意间被使用者增加了抽象层次提升了不必要的复杂性从而引起一系列工程维护问题。
出于工程友好性的考虑仓颉采取了默认封闭的设计选择即类默认不可被继承方法默认也不可被覆盖override。开发者需要主动考虑是否需要自己的类型提供子类的能力通过这样的约束减少了滥用继承的现象。
类默认不可继承 在仓颉中开发者定义class时默认是不可继承的。如果希望该class有子类必须显式使用open、abstract、sealed其中一个修饰这些修饰符的语义有细微差别但都允许class被继承。
class A {}
class B : A {} // errorA 不允许被继承
open class C {}
class D : C {} // ok成员方法默认不可覆盖 在仓颉中开发者定义成员方法默认是不可覆盖override的。这意味着即使该类拥有子类子类也无法修改该成员方法。如果希望一个成员方法可以被覆盖必须显式使用 open 修饰。
open class A {func f() {}open func g() {}
}class B : A {override func f() {} // errorf 不允许被覆盖override func g() {} // ok
}2.5 try-with-resources
仓颉使用try-catch-finally表达式来实现异常处理该机制和传统语言的异常处理机制十分相似但仓颉额外提供了try-with-resources表达式语法来自动释放非内存资源。
不同于普通try表达式try-with-resources表达式中的catch块和finally块均是可选的并且try关键字其后的块之间可以插入一个或者多个变量定义用来申请一系列的资源对象这些资源对象在try-with-resources表达式中会被自动管理起来当某个资源发生异常或表达式结束后都会自动释放达到安全管理资源的目的。
如下实例所示input和output变量会在try-with-resources表达式过程中自动管理开发者不需要关注当中各种情况的资源释放问题。
try (input MyResource(),output MyResource()) {while (input.hasNextLine()) {let lineString input.readLine()output.writeLine(lineString)}
}这里资源对象的类型上面例子中的MyResource必须已经实现了Resource接口特别是已经实现了Resource接口中要求的isClosed和close函数能够判别资源是否已经被释放以及做对应的释放操作。编译器将会在发生异常时或者try代码块正常结束时插入对相应函数的调用自动释放资源。
2.6 动态安全检查
除了静态类型给我们提供的安全保证以外仓颉同时也非常重视运行时的安全检查对于一些不适合使用静态类型的场景仓颉也提供了运行时检查的安全保证。
2.6.1 溢出检查
不同于大多数传统语言在仓颉中的整数运算默认会进行溢出检查而不是任由其 wrapping。
当上下文足以静态分析的时候整数溢出可提前在编译期检测出来编译器会直接给出报错当上下文不足以静态分析的时候整数溢出会在运行时做一个检查如果溢出会抛出运行时异常。
这个机制使得大多数时候整数溢出都会及时被感知避免造成业务隐患。
运行时检查会增加额外的计算开销但经过仓颉编译器优化后可以将计算开销控制在一个较小的水平。
如果一些敏感场景希望通过接受 wrapping 的代价来换取更好的极限性能也可以手动指定溢出策略来实现传统语言的行为。
OverflowWrapping
func test(x: Int8, y: Int8) { // if x equals to 127 and y equals to 3let z x y // z equals to -126
}2.6.2 数组越界检查
同样的在仓颉数组的下标访问中对数组的下标越界访问也有安全检查。当上下文足以静态分析的时候下标访问可提前在编译期检测出来编译器会直接给出报错当上下文不足以静态分析的时候下标访问会在运行时做一个检查如果溢出会抛出运行时异常。
func f(index: Int) {let a [1, 2, 3]let b a[-1] // 编译期报错let c a[index] // 运行时检查
}2.7 混淆
仓颉语言提供了多种混淆技术用于保护开发者的软件资产提升攻击者逆向攻击仓颉软件的难度。攻击者可采用逆向工程技术对程序进行攻击并获取程序的符号名、路径信息和行号信息、特征字符串和特征常数以及控制流信息。仓颉混淆技术可以对这些信息进行混淆和隐藏让攻击者难以借用这些信息辅助理解程序的运行逻辑。 外形混淆外形混淆可以混淆仓颉应用的符号名、路径信息和行号信息并且对仓颉二进制中的函数进行重排。混淆后攻击者无法再利用这些信息辅助理解程序的运行逻辑。使能外形混淆后函数名和变量名被随机字符串替换、路径名被字符串“SOURCE”替换、行号被修改为0。 数据混淆仓颉编译器支持字符串混淆和常量混淆。字符串混淆功能会识别代码中的明文字符串将其加密保存。程序在初始化时会先解密字符串再执行程序逻辑。因此外部攻击者无法直接从程序文件中获取明文字符串只能看到加密后的数据因此无法根据字符串信息猜测代码逻辑。对于程序中使用的已知常量仓颉编译器支持使用常量混淆功能将使用这些常量的代码片段转化为等价的、更加难以理解的代码片段。 控制流混淆仓颉编译器支持虚假控制流和控制流平坦化两种控制流混淆功能在不影响程序正常执行的前提下打乱、重排基本块之间的跳转关系从而提升分析理解程序控制流的难度。虚假控制流的原理是在程序中随机添加大量虚假的条件跳转分支并且这些条件跳转分支的条件变量都是由不透明谓词组成控制流平坦化的主要目的是隐藏基本块之间的跳转关系并确保在实际执行时基本块的执行顺序和混淆前一致保证程序的正常功能不被影响但攻击者无法静态根据控制流信息得到基本块之间的先后执行关系以及跳转关系。
2.8 消毒器
消毒器是一种程序测试工具通过插入检测代码来检测未定义或可疑行为形式的错误。仓颉支持多种类型的消毒器仓颉在移动应用开发语言中率先支持基于硬件特性的地址消毒器。例如使用直接映射的影子内存来检测内存损坏、缓冲区溢出或访问悬空指针use-after-free。
基于软件算法的仓颉CFFI内存安全检测机制地址消毒器仓颉语言的C语言互操作能力CFFI在与C/C代码进行交互的过程中由于其可以与C/C代码进行不受限制的内存互访C/C侧安全漏洞可能影响整体安全。仓颉提供基于软件实现的CFFI内存安全检测机制提供仓颉代码与C/C代码交互过程中的内存安全检测能力可以检测常见的空间内存安全问题如堆、栈、全局变量溢出和时间内存安全问题如释放后使用、双重释放等。基于硬件特性的仓颉CFFI内存安全检测机制硬件辅助的地址消毒器基于硬件实现的仓颉CFFI内存安全检测利用处理器能力实现更高效地检测仓颉代码与C/C代码交互过程中的内存安全问题。相对于前述的软件CFFI内存安全检测机制该机制可以检测更多内存安全问题并且运行开销更低。仓颉数据竞争安全检测线程消毒器仓颉定义的数据竞争指两个协程对同一个数据访问其中至少有一个是写操作而且这两个操作之间没有happens-before关系。仓颉数据竞争安全检测使用happens-before和lock-set算法检测数据竞争问题。开发者可以通过仓颉编译器提供的sanitizethread选项使能该能力。
3、轻松并发
仓颉语言为并发编程提供了一种简单灵活的方式通过轻量化线程模型和高效易用的无锁并发对象让并发编程变得轻松将高效并发处理的能力直接置于开发者的手中。这一节将详细介绍仓颉并发编程两大关键技术的核心思想、设计、以及带来的显著优势揭示仓颉语言如何实现“轻松并发”。
3.1 轻量化线程模型
仓颉语言采用用户态线程模型在该模型下每个仓颉线程都是极其轻量级的执行实体拥有独立的执行上下文但共享内存。该模型不仅简化了开发者编写并发代码的过程还带来了显著的性能优势。
开发态仓颉语言的线程模型使开发者能够像编写普通代码一样轻松地实现并发编程。通常用户态线程模型可分为“无栈”和“有栈”两种实现方案。尽管“无栈”模型可以将内存占用降到极低但其实现通常需要在语言中引入新语法最常见的就是 async/await 关键字。然而这种新语法会显著增加开发者编写并发代码的复杂度。开发者不仅需要在编程过程中手动标记如用 async 标记异步函数并用 await 标记其调用点而且这种标记具有“传染性”包含await的函数必须标记为 async导致经典的“函数染色”问题。仓颉线程拥有独立的执行上下文因此能够自由切换开发者无需为标记操心从而彻底消除这一复杂性。运行态与传统的操作系统线程相比轻量化线程模型在性能上具有明显优势。由于其实现完全在用户空间进行不依赖操作系统的线程管理这从根本上减少了线程创建和销毁的开销同时简化了线程调度流程。仓颉语言通过这种设计实现了更高效的资源利用和更快的执行速度尤其是在高并发场景下这种优势更为显著。在一台常见的 x86 服务器上仓颉线程创建的平均耗时为 700ns这远小于操作系统线程的创建开销操作系统线程的创建耗时量级一般为百微妙。此外一个仓颉线程仅占用 8Kb 内存资源因此开发者可以在一个程序中同时创建十万级数量的仓颉线程大大超出操作系统线程的限制。
整体而言仓颉语言的轻量化线程设计不仅降低系统的负担而且使得开发者能够在不增加编程复杂度的前提下轻松实现数千甚至数万个并发任务。其核心优势包括
简单的并发编程不对开发者编写并发代码做过多语法约束使其方便地使用仓颉线程并专注业务处理。轻量级的开销由于创建和切换用户态线程的开销远远小于传统的内核线程仓颉语言可以快速地创建和销毁大量用户态线程使得开发高并发应用变得轻而易举。更高的并发能力仓颉语言通过用户态线程模型可以实现非常高的并发数这使得它特别适合于I/O密集型和高并发的网络服务场景。减少上下文切换成本在轻量化线程模型中上下文切换发生在用户空间避免了传统线程切换需要经过内核态和用户态之间频繁转换的高成本。
基于这样的设计在仓颉语言中实现高效并发不再是一项复杂且耗时的任务。开发者可以通过简单的语法构造大量的用户态线程无需担心传统并发模型中常见的性能瓶颈。假设我们有一个需求需要同时处理多个网络请求。在仓颉语言中这可以轻松实现如下代码所示
func fetch_data(url: String) {let response http_get(url)process(response)
}main() {let urls [https://example.com/data1, https://example.com/data2, ...]let futures ArrayListFutureUnit()for (url in urls) {let fut spawn { fetch_data(url) } // 创建仓颉线程进行网络请求futures.append(fut)}for (fut in futures) { // 等待所有仓颉线程完成fut.get()}
}在上述例子中spawn 关键字用于创建一个新的仓颉线程每个线程独立地执行 fetch_data 函数。仓颉语言的运行时环境会自动调度这些线程而开发者只需关注业务逻辑的实现。最后通过获取线程结果来等待所有的仓颉线程完成确保主线程能够同步地获取所有结果。
仓颉语言的用户态线程模型以其显著的性能优势和轻量级的设计理念为并发编程提供了一个颇具吸引力的新选择。它使得编写高并发应用变得更加直接和高效不仅适用于高负载的网络服务还能满足各种计算密集型任务的需要。通过这种新型并发模型仓颉语言降低了并发编程的复杂性同时还充分利用了现代多核处理器的强大能力。
3.2 无锁并发对象
在多线程共享内存并发场景程序员需要注意控制不同线程访问同一内存单元的顺序否则可能产生“数据竞争”。一般语言通过提供互斥锁等特性来支持进程并发的访问共享内存。
然而让程序员自己来控制线程访问共享内存仍然是一件复杂且并发性能不高的事情 例如上图一块内存 M 多线程共享用一个互斥锁同步对内存块 M 并发访问开发效率、运行性能都不是最优。
那么是否能对内存 M 进行切分以更细粒度的方式来加锁以提升性能并且语言自动实现锁的保护而让开发者像单线程一样编写程序呢
如下图展示了细粒度并发控制将内存块 M 划分为多个区域不同线程可以并发访问不同区域。但细粒度并发算法复杂并且在实际场景中M 可能代表一个数据结构对本就复杂的数据结构做细粒度的并发控制并不容易很容易产生“数据竞争”或“不具有并发原子性”等并发问题。 为了解决该问题仓颉提供了基于细粒度并发算法实现的并发对象而用户通过调用并发对象的接口来操作多线程共享内存从而实现
为用户提供无锁编程体验用户通过接口调用实现高效的共享内存并发访问。为用户提供并发安全保障仓颉并发对象的接口可保证无数据竞争核心接口具有并发原子性。提升性能仓颉并发对象的设计使用细粒度并发算法。保证并发原子性仓颉并发对象的核心方法具有并发“原子性”即从用户视角来看该方法调用执行不会被其它线程打断。 下表展示了仓颉提供的多线程共享并发对象并提供并发安全和并发性能的保障 1.并发安全
用户在并发场景调用原子类型和并发数据结构接口操作多线程共享对象不会产生“数据竞争”。原子类型为用户提供了并发场景下整型、布尔型和引用类型的原子操作。并发数据结构的核心方法具有并发原子性例如ConcurrentHashMap 中的插入键值对 put删除键值对 remove 和替换键值对 replace 等方法。并发场景下用户可以将这些操作的调用执行视为原子的不会被其它线程打断。
2.提升并发性能
并发哈希表和并发队列基于上述介绍的细粒度并发算法实现下图展示了仓颉并发哈希表 ConcurrentHashMap 与使用一把互斥锁控制多线程并发访问 HashMap粗粒度并发控制的性能对比其中横坐标为线程数纵坐标为每毫秒完成的并发哈希表操作数并发哈希表操作中put、remove 和 get 方法分别占 10%、10%、80%。黄色线条为仓颉 ConcurrentHashMap 的测试数据而蓝色线条则为粗粒度方法的测试数据可见使用细粒度并发算法的仓颉并发哈希表性能相比粗粒度方法优势明显且性能随着线程数的增加而提升。 下图是仓颉并发队列 BlockingQueue 在 single-producer single-consumer 场景下与使用一把互斥锁控制多线程并发访问队列粗粒度并发控制的性能对比我们分别测试了队列容量为 128 和 4096 的场景纵坐标为每毫秒出入队元素的个数仓颉 BlockingQueue 性能相比粗粒度方法优势明显。