提示:本文为入门讲解(下)篇
2024年6月21日下午,华为终端BG软件部总裁龚体先生在华为开发者大会主题演讲《鸿蒙原生应用,全新出发!》中向全球开发者介绍了华为自研仓颉编程语言,并发布了HarmonyOS NEXT仓颉语言开发者预览版。这是华为首次公开发布仓颉编程语言。
2019年,仓颉编程语言项目在华为诞生,历经5年研发沉淀,大量研发投入,今日终于和全球开发者见面。仓颉编程语言通过现代语言特性的集成、全方位的编译优化和运行时实现、以及开箱即用的IDE工具链支持,为开发者打造友好开发体验和卓越程序性能。
仓颉编程语言是一款面向全场景智能的新一代编程语言,主打原生智能化、天生全场景、高性能、强安全。
话不多说,直接书接上文,接着讲解仓颉的入门指南:
枚举(enum)类型是仓颉编程语言中极具特色的数据类型,它提供了一种简洁且严谨的方式来定义一组有限的离散值。
当我们处理一些特定场景,如表示星期、季节、状态等具有明确取值范围的变量时,枚举类型便能大显身手。
定义枚举类型时,以关键字enum开头,接着是枚举类型的名字,随后在一对花括号内列举出所有可能的取值,这些取值被称为构造器,多个构造器之间用|分隔。
例如:
enum Season {
Spring | Summer | Autumn | Winter
}
这里定义了一个名为Season的枚举类型,它清晰地限定了季节只有春夏秋冬这四种可能取值。
再看另一个例子,定义表示星期的枚举:
enum DayOfWeek {
Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday
}
这使得星期变量只能在这七个明确的取值中选择,避免了因取值错误而导致的程序逻辑问题。
枚举类型的构造器还能携带参数,以满足更复杂的场景需求。
例如,若要表示不同颜色及其对应的 RGB 值:
enum Color {
Red(UInt8, UInt8, UInt8) | Green(UInt8, UInt8, UInt8) | Blue(UInt8, UInt8, UInt8)
}
通过这样的定义,每个颜色构造器都可以携带三个UInt8类型的参数,用于精确指定颜色的 RGB 分量值,让颜色表示更加精准细致。
在使用枚举类型时,创建枚举值只能从定义的构造器中选取。
如对于上述Season枚举,若要定义一个表示当前季节的变量:
let currentSeason: Season = Season.Summer
这里明确将currentSeason变量赋值为Season.Summer,保证了取值的合法性与规范性。若尝试赋值一个不在枚举定义中的值,编译器会立即报错,有效防止了因错误赋值引发的潜在隐患,让程序在处理这类有限取值场景时更加健壮、可靠,极大提升了代码的可维护性与可读性。
struct(结构体)与class(类)是构建复杂数据结构、实现面向对象编程的关键要素,二者既有相似之处,又存在显著差异,各自适用于不同的编程场景需求。
struct是值类型,这意味着在赋值或传递参数时,会对其进行复制操作,生成全新的副本,各个副本之间相互独立,对其中一个副本的修改不会影响到其他副本。它的定义以关键字struct开启,紧跟结构体名称,然后在花括号内定义成员变量、构造函数、成员函数等。
例如:
struct Point {
let x: Int64
let y: Int64
public init(x: Int64, y: Int64) {
this.x = x
this.y = y
}
public func distanceToOrigin() -> Float64 {
return Float64(x * x + y * y).sqrt()
}
}
这里定义了一个名为Point的结构体,包含表示坐标的成员变量x和y,以及一个用于计算该点到原点距离的成员函数distanceToOrigin。通过let r1 = Point(x: 3, y: 4)这样的语句就能创建结构体实例,并使用r1.distanceToOrigin()调用成员函数。
class则是引用类型,多个变量可以引用同一个对象实例,当通过一个引用修改对象内容时,其他引用所指向的对象也会随之改变。class的定义使用关键字class,同样包含成员变量、构造函数、成员函数,还支持继承等特性。
比如:
class Animal {
var name: String
public init(name: String) {
this.name = name
}
public func makeSound() {
println("Animal makes a sound")
}
}
class Dog: Animal {
override public func makeSound() {
println("Woof!")
}
}
在这个例子中,Dog类继承自Animal类,并重写了makeSound函数,体现了class强大的继承与多态特性。通过let dog = Dog(name: "Buddy")创建实例,用dog.makeSound()就能触发对应类的行为。
在构造函数方面:
成员变量与函数的访问权限上:
例如,在struct或class中,若将某个成员变量设为private,则该变量只能在定义它的结构体或类内部访问,外部代码无法直接触及,保障了数据的封装性与安全性。
总的来说:
当需要轻量级、数据存储为主且不涉及复杂继承关系的数据结构时,struct是不错的选择,它在性能和内存使用上更具优势; 而对于构建复杂的对象层次、需要多态与继承特性来实现灵活扩展的场景,class则能更好地满足需求,助力开发者打造出结构清晰、功能强大的面向对象程序。
在仓颉编程语言中,接口是一种强大的抽象工具,它定义了一组方法签名,为不同类型提供了统一的行为规范。
接口就像是一份契约,实现接口的类型必须严格遵守契约中的规定,实现接口所声明的方法。
例如,定义一个简单的绘图接口:
interface Drawable {
func draw();
}
这里的Drawable接口规定了任何实现它的类型都需要具备draw方法。
接着,假设有一个Circle类,它可以实现这个接口:
class Circle: Drawable {
func draw() {
println("绘制圆形")
}
}
通过这种方式,Circle类遵循了Drawable接口的约定,保证了在需要绘图操作的场景下,它能以统一的方式响应。而且,一个类还可以实现多个接口,满足多样化的功能需求,使得代码的架构更加灵活、可维护。
扩展特性则允许开发者在不修改原有类型定义的基础上,为类型添加新功能。
比如,对于内置的String类型,如果想要快速获取字符串长度并打印,就可以使用扩展:
extend String {
public func printSize() {
println("字符串长度为: ${this.size}")
}
}
之后,对于任意字符串实例,都能直接调用printSize方法,如同这个方法原本就属于String类型一样,极大地增强了语言的表达能力,让代码编写更加便捷高效,能够根据实际需求灵活地对已有类型进行功能增强,避免了频繁修改基础类型定义带来的风险。
异常处理是保障程序健壮性的关键防线。
在仓颉编程语言中,采用了try-catch-finally语句来优雅地应对程序运行时可能出现的意外情况。
当一段代码可能抛出异常时,将其包裹在try块中,就像是给这段危险区域围上了防护栏。一旦try块中的代码抛出异常,程序流程会立即跳转到对应的catch块中进行处理。
例如:
try {
let num = 10 / 0; // 这里会抛出除零异常
} catch (e: DivisionByZeroError) {
println("捕获到除零异常: ${e.getMessage()}")
} finally {
println("无论是否发生异常,此代码块都会执行")
}
在这个例子中,尝试进行除以零的操作,必然会触发DivisionByZeroError异常,程序瞬间转移到catch块,精准捕获并处理异常,输出错误信息,同时,finally块中的代码也会如期执行,通常用于释放资源等收尾工作,确保程序状态的完整性,避免因异常导致资源泄露或程序崩溃,让程序在面对突发状况时依然能稳定运行,为用户提供可靠的服务。
随着多核处理器的普及,并发编程成为提升程序性能的必备技能
仓颉编程语言在这方面提供了简洁而强大的支持。
其中,Future是并发编程的得力助手,它代表一个可能尚未完成的异步计算结果。
以一个多任务场景为例,假设有多个网络请求任务需要同时发起,以节省总的时间开销:
import std.sync.*
import std.time.*
func fetchDataFromUrl(url: String): Future<String> {
return spawn {
// 模拟网络请求耗时操作,这里简单睡眠一段时间
sleep(2 * Duration.seconds)
return "从${url}获取的数据"
}
}
main() {
let url1 = "https://example.com/api1"
let url2 = "https://example.com/api2"
let future1 = fetchDataFromUrl(url1)
let future2 = fetchDataFromUrl(url2)
// 在此期间可以执行其他非阻塞代码
//...
let result1 = future1.get()
let result2 = future2.get()
println(result1)
println(result2)
}
在这段代码中,通过spawn关键字创建多个仓颉线程,每个线程负责一个网络请求任务,它们并行执行,互不干扰。fetchDataFromUrl函数返回Future对象,主线程继续往下执行其他非阻塞操作,当需要获取结果时,调用Future的get方法,此时若对应线程任务还未完成,主线程会阻塞等待,直到获取到最终结果。
这样,充分利用了系统资源,将原本串行的多个长时间任务转化为并行执行,大大缩短了整体运行时间,提升了程序的响应速度和执行效率,使得程序在处理复杂的多任务场景时游刃有余,满足现代高性能应用的需求。
在实际的软件开发中,很少有编程语言是孤立存在的。
仓颉编程语言深知这一点,为了兼容已有的庞大生态,它提供了强大的跨语言互操作能力,尤其是与 C 语言的交互,宛如搭建了一座沟通的桥梁,让开发者能够充分利用 C 语言丰富的库资源。
接下来,就让我们深入探讨仓颉与 C 语言互操作的精彩世界。
在仓颉中调用 C 语言函数时,需要使用foreign关键字来声明外部函数
例如,若要调用 C 语言中的rand和printf函数,它们在 C 语言中的函数签名如下:
// stdlib.h
int rand();
// stdio.h
int printf(const char *fmt,...);
那么在仓颉中调用这两个函数,需进行如下声明:
// 声明外部函数,@C可省略
foreign func rand(): Int32
foreign func printf(fmt: CString,...): Int32
这里要注意,foreign修饰的函数只能有声明,不能有函数实现,它就像是一个函数的 “名片”,告诉仓颉这个函数来自外部。而且,被foreign修饰的函数,其参数和返回类型必须严格符合 C 和仓颉数据类型之间的映射关系,这是跨语言交流的 “语法规则”,稍有差错就可能导致沟通不畅。
调用这些被foreign声明的函数时,要用unsafe块包裹,这是因为 C 侧函数很可能存在一些诸如指针操作、内存管理等不安全操作,unsafe关键字就像是给这趟跨语言调用之旅系上了 “安全带”。
例如:
main() {
// 在unsafe块中调用rand函数
let r = unsafe { rand() }
println("random number ${r}")
unsafe {
var fmt = LibC.mallocCString("Hello, No.%d\n")
printf(fmt, 1)
LibC.free(fmt)
}
}
在这个例子中,先通过unsafe块调用rand函数获取一个随机数并打印,接着又使用unsafe块调用printf函数输出一段格式化文本,同时注意到在使用printf函数前,需要使用LibC.mallocCString分配内存,使用完后要用LibC.free释放内存,这一系列操作都在unsafe的 “保驾护航” 下进行,确保程序不会因跨语言调用的不安全因素而 “翻车”。
此外,还有一些要点务必牢记:
宏(Macro)在仓颉编程语言里,犹如一位拥有神奇魔法的代码工匠,它能够在编译时期对代码进行精妙的变换,极大地提升编程效率与灵活性。
宏的定义遵循一套严谨规则。
假设我们在开发过程中,常常需要对代码中的变量或表达式进行调试打印,若采用传统的编写大量print语句的方式,不仅繁琐,还极易出错。此时,宏就能大显身手,助我们轻松解决难题。
下面展示如何定义一个用于调试打印的dprint宏:
// 在macros/dprint.cj文件中
macro package define
import std.ast.*
// 定义dprint宏,接受Tokens类型输入,返回Tokens类型输出
public macro dprint(input: Tokens): Tokens {
// 将输入Tokens转换为字符串,获取原始代码片段
let inputStr = input.toString()
// 利用quote表达式构建新Tokens,生成打印语句
let result = quote {
print($(inputStr) + " = ")
println($(input))
}
return result
}
在这段代码中,先通过input.toString()把输入的代码片段转换成字符串,以便后续构建打印语句。接着,使用quote表达式精心构造出包含原始表达式及其计算结果的新Tokens,最终返回处理后的代码片段。
宏的调用方式也别具一格,需使用@前缀加以区分。
例如:
// 在main.cj文件中
import define.*
main() {
let x = 3
let y = 2
// 调用dprint宏,打印变量x的值与表达式
@dprint(x)
// 调用dprint宏,打印表达式x + y的值与表达式
@dprint(x + y)
}
如此一来,程序在编译时,@dprint宏就会按照定义展开,自动生成对应的打印代码,让调试信息一目了然,仿佛为代码调试开启了一盏明灯。
再看一个实际应用场景,若要实现一个数学运算函数库,针对不同数据类型(如Int64、Float64等)都需要重复编写相似的运算函数,代码冗长且维护困难。
借助宏的强大功能,我们可以定义一个通用的模板宏来自动生成这些函数:
// 在macros/math_ops.cj文件中
macro package define
import std.ast.*
public macro generateMathOp(op: TokenKind, resultType: Type)(input: Tokens): Tokens {
let funcName = input.toString()
let args = input.parseArgs()
let argTypes = args.map { a => a.getType() }
let body = quote {
return ($(args[0]) $(op.name) $(args[1])).$(resultType.name)
}
return quote {
public func $(funcName)($(argTypes[0]) a, $(argTypes[1]) b): $(resultType) {
$(body)
}
}
}
这个宏接受操作符类型(如加法对应的TokenKind.ADD)和结果数据类型作为参数,根据输入的函数名和参数类型,自动生成相应的数学运算函数代码。
使用时,只需简单调用:
// 在main.cj文件中
import define.*
import math_ops.*
@generateMathOp(TokenKind.ADD, Int64)("addInt64")
@generateMathOp(TokenKind.MULTIPLY, Float64)("multiplyFloat64")
main() {
let a: Int64 = 5
let b: Int64 = 3
println(addInt64(a, b))
let c: Float64 = 2.5
let d: Float64 = 4.0
println(multiplyFloat64(c, d))
}
通过这一巧妙运用,宏为不同数据类型快速生成了特定的运算函数,大大减少了代码重复编写量,提升了代码的可读性与可维护性,让程序开发更加高效、便捷,仿佛拥有了一位智能的代码助手,随时响应开发者的需求。
至此,我们已一同踏入仓颉编程语言的奇妙世界,从搭建编程环境的细致入微,到变量、函数、复合数据类型的精巧运用;从接口、异常、并发编程的强大功能,到跨语言互操作、宏的独特魅力,每一处都彰显着仓颉编程语言的创新与实用。
仓颉编程语言不仅为开发者提供了一种全新的编程工具,更是为智能时代的软件开发生态注入了新活力。
它凭借原生智能化、天生全场景、高性能、强安全等特性,在物联网、人工智能、移动开发等诸多领域展现出巨大潜力,有望打破现有编程格局,助力开发者创造出更加智能、高效、安全的应用程序。
当下,仓颉编程语言正处于蓬勃发展的初期,虽已初露锋芒,但仍需广大开发者携手共进,深入探索其无限可能。
希望大家以此为起点,在后续的学习与实践中不断挖掘仓颉编程语言的深度,为中国软件产业自主创新添砖加瓦,共同迈向智能编程的新未来。
本文完。