前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >一文带你入门仓颉编程语言(下)

一文带你入门仓颉编程语言(下)

作者头像
用户11396077
发布2025-01-02 08:48:08
发布2025-01-02 08:48:08
14300
代码可运行
举报
文章被收录于专栏:C/C++指南C/C++指南
运行总次数:0
代码可运行

提示:本文为入门讲解(下)篇

ce55c65a744a4fd6aec86a2f866d38dc.png
ce55c65a744a4fd6aec86a2f866d38dc.png

一、引言:仓颉编程的崛起

2024年6月21日下午,华为终端BG软件部总裁龚体先生在华为开发者大会主题演讲《鸿蒙原生应用,全新出发!》中向全球开发者介绍了华为自研仓颉编程语言,并发布了HarmonyOS NEXT仓颉语言开发者预览版。这是华为首次公开发布仓颉编程语言

e60fae52783d4a0c8bbcfffcef5278e5.png
e60fae52783d4a0c8bbcfffcef5278e5.png

2019年,仓颉编程语言项目在华为诞生,历经5年研发沉淀,大量研发投入,今日终于和全球开发者见面。仓颉编程语言通过现代语言特性的集成、全方位的编译优化和运行时实现、以及开箱即用的IDE工具链支持,为开发者打造友好开发体验和卓越程序性能。

仓颉编程语言是一款面向全场景智能的新一代编程语言,主打原生智能化、天生全场景、高性能、强安全。

话不多说,直接书接上文,接着讲解仓颉的入门指南:

二、复合数据类型探索

(一)枚举

枚举(enum)类型是仓颉编程语言中极具特色的数据类型,它提供了一种简洁且严谨的方式来定义一组有限的离散值。

当我们处理一些特定场景,如表示星期、季节、状态等具有明确取值范围的变量时,枚举类型便能大显身手。

定义枚举类型时,以关键字enum开头,接着是枚举类型的名字,随后在一对花括号内列举出所有可能的取值,这些取值被称为构造器,多个构造器之间用|分隔。

例如:

代码语言:javascript
代码运行次数:0
复制
enum Season {

Spring | Summer | Autumn | Winter

}

这里定义了一个名为Season的枚举类型,它清晰地限定了季节只有春夏秋冬这四种可能取值。

再看另一个例子,定义表示星期的枚举:

代码语言:javascript
代码运行次数:0
复制
enum DayOfWeek {

Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday

}

这使得星期变量只能在这七个明确的取值中选择,避免了因取值错误而导致的程序逻辑问题。

枚举类型的构造器还能携带参数,以满足更复杂的场景需求。

例如,若要表示不同颜色及其对应的 RGB 值:

代码语言:javascript
代码运行次数:0
复制
enum Color {

Red(UInt8, UInt8, UInt8) | Green(UInt8, UInt8, UInt8) | Blue(UInt8, UInt8, UInt8)

}

通过这样的定义,每个颜色构造器都可以携带三个UInt8类型的参数,用于精确指定颜色的 RGB 分量值,让颜色表示更加精准细致。

在使用枚举类型时,创建枚举值只能从定义的构造器中选取。

如对于上述Season枚举,若要定义一个表示当前季节的变量:

代码语言:javascript
代码运行次数:0
复制
let currentSeason: Season = Season.Summer

这里明确将currentSeason变量赋值为Season.Summer,保证了取值的合法性与规范性。若尝试赋值一个不在枚举定义中的值,编译器会立即报错,有效防止了因错误赋值引发的潜在隐患,让程序在处理这类有限取值场景时更加健壮、可靠,极大提升了代码的可维护性与可读性。

(二)Struct 与 Class

struct(结构体)与class(类)是构建复杂数据结构、实现面向对象编程的关键要素,二者既有相似之处,又存在显著差异,各自适用于不同的编程场景需求。

struct是值类型,这意味着在赋值或传递参数时,会对其进行复制操作,生成全新的副本,各个副本之间相互独立,对其中一个副本的修改不会影响到其他副本。它的定义以关键字struct开启,紧跟结构体名称,然后在花括号内定义成员变量、构造函数、成员函数等。

例如:

代码语言:javascript
代码运行次数:0
复制
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,同样包含成员变量、构造函数、成员函数,还支持继承等特性。

比如:

代码语言:javascript
代码运行次数:0
复制
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可以有多种形式,包括普通构造函数(以init开头)和主构造函数(与结构体名相同),用于初始化成员变量;
  • class的构造函数同样用init定义,并且在继承体系中,子类构造函数还需注意调用父类构造函数以完成初始化流程。

成员变量与函数的访问权限上:

  • 二者都支持private、internal、protected和public等修饰符,用于精细控制成员的可见范围。

例如,在struct或class中,若将某个成员变量设为private,则该变量只能在定义它的结构体或类内部访问,外部代码无法直接触及,保障了数据的封装性与安全性。

总的来说:

当需要轻量级、数据存储为主且不涉及复杂继承关系的数据结构时,struct是不错的选择,它在性能和内存使用上更具优势; 而对于构建复杂的对象层次、需要多态与继承特性来实现灵活扩展的场景,class则能更好地满足需求,助力开发者打造出结构清晰、功能强大的面向对象程序。

三、进阶特性揭秘

(一)接口与扩展

在仓颉编程语言中,接口是一种强大的抽象工具,它定义了一组方法签名,为不同类型提供了统一的行为规范

接口就像是一份契约,实现接口的类型必须严格遵守契约中的规定,实现接口所声明的方法。

例如,定义一个简单的绘图接口:

代码语言:javascript
代码运行次数:0
复制
interface Drawable {

func draw();

}

这里的Drawable接口规定了任何实现它的类型都需要具备draw方法。

接着,假设有一个Circle类,它可以实现这个接口:

代码语言:javascript
代码运行次数:0
复制
class Circle: Drawable {

func draw() {

println("绘制圆形")

}

}

通过这种方式,Circle类遵循了Drawable接口的约定,保证了在需要绘图操作的场景下,它能以统一的方式响应。而且,一个类还可以实现多个接口,满足多样化的功能需求,使得代码的架构更加灵活、可维护。

扩展特性则允许开发者在不修改原有类型定义的基础上,为类型添加新功能。

比如,对于内置的String类型,如果想要快速获取字符串长度并打印,就可以使用扩展:

代码语言:javascript
代码运行次数:0
复制
extend String {

public func printSize() {

println("字符串长度为: ${this.size}")

}

}

之后,对于任意字符串实例,都能直接调用printSize方法,如同这个方法原本就属于String类型一样,极大地增强了语言的表达能力,让代码编写更加便捷高效,能够根据实际需求灵活地对已有类型进行功能增强,避免了频繁修改基础类型定义带来的风险。

(二)异常处理

异常处理是保障程序健壮性的关键防线。

在仓颉编程语言中,采用了try-catch-finally语句来优雅地应对程序运行时可能出现的意外情况。

当一段代码可能抛出异常时,将其包裹在try块中,就像是给这段危险区域围上了防护栏。一旦try块中的代码抛出异常,程序流程会立即跳转到对应的catch块中进行处理。

例如:

代码语言:javascript
代码运行次数:0
复制
try {

let num = 10 / 0; // 这里会抛出除零异常

} catch (e: DivisionByZeroError) {

println("捕获到除零异常: ${e.getMessage()}")

} finally {

println("无论是否发生异常,此代码块都会执行")

}

在这个例子中,尝试进行除以零的操作,必然会触发DivisionByZeroError异常,程序瞬间转移到catch块,精准捕获并处理异常,输出错误信息,同时,finally块中的代码也会如期执行,通常用于释放资源等收尾工作,确保程序状态的完整性,避免因异常导致资源泄露或程序崩溃,让程序在面对突发状况时依然能稳定运行,为用户提供可靠的服务。

(三)并发编程

随着多核处理器的普及,并发编程成为提升程序性能的必备技能

仓颉编程语言在这方面提供了简洁而强大的支持。

其中,Future是并发编程的得力助手,它代表一个可能尚未完成的异步计算结果。

以一个多任务场景为例,假设有多个网络请求任务需要同时发起,以节省总的时间开销:

代码语言:javascript
代码运行次数:0
复制
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 语言中的函数签名如下:

代码语言:javascript
代码运行次数:0
复制
// stdlib.h

int rand();

// stdio.h

int printf(const char *fmt,...);

那么在仓颉中调用这两个函数,需进行如下声明:

代码语言:javascript
代码运行次数:0
复制
// 声明外部函数,@C可省略

foreign func rand(): Int32

foreign func printf(fmt: CString,...): Int32

这里要注意,foreign修饰的函数只能有声明,不能有函数实现,它就像是一个函数的 “名片”,告诉仓颉这个函数来自外部。而且,被foreign修饰的函数,其参数和返回类型必须严格符合 C 和仓颉数据类型之间的映射关系,这是跨语言交流的 “语法规则”,稍有差错就可能导致沟通不畅。

调用这些被foreign声明的函数时,要用unsafe块包裹,这是因为 C 侧函数很可能存在一些诸如指针操作、内存管理等不安全操作,unsafe关键字就像是给这趟跨语言调用之旅系上了 “安全带”。

例如:

代码语言:javascript
代码运行次数:0
复制
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的 “保驾护航” 下进行,确保程序不会因跨语言调用的不安全因素而 “翻车”。

此外,还有一些要点务必牢记:

  • @C修饰的foreign关键字只能用来修饰函数声明,不可用于修饰其他声明,否则仓颉编译器会 “报错抗议”;
  • foreign函数不支持命名参数和参数默认值,但允许变长参数,使用...表达,且只能用于参数列表的最后,变长参数均需要满足CType约束,但不必是同一类型;
  • 虽然仓颉(CJNative 后端)提供了栈扩容能力,可由于 C 侧函数实际使用栈大小仓颉无法感知,所以在进行 FFI(Foreign Function Interface)调用进入 C 函数后,仍存在栈溢出的风险,开发者需要根据实际情况,谨慎修改cjStackSize的配置,就像驾驭一匹烈马,需要精准掌控缰绳,才能安全驰骋。

五、宏:代码的神奇变换

宏(Macro)在仓颉编程语言里,犹如一位拥有神奇魔法的代码工匠,它能够在编译时期对代码进行精妙的变换,极大地提升编程效率与灵活性。

宏的定义遵循一套严谨规则。

  • 首先,它必须声明在以macro package define创建的独立宏包之中,使其与普通函数、变量隔离开来,有效避免潜在冲突。
  • 定义宏时,要使用关键字macro,其输入和输出类型均为Tokens,这里的Tokens是对代码片段的抽象表示

假设我们在开发过程中,常常需要对代码中的变量或表达式进行调试打印,若采用传统的编写大量print语句的方式,不仅繁琐,还极易出错。此时,宏就能大显身手,助我们轻松解决难题。

下面展示如何定义一个用于调试打印的dprint宏:

代码语言:javascript
代码运行次数:0
复制
// 在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,最终返回处理后的代码片段。

宏的调用方式也别具一格,需使用@前缀加以区分。

例如:

代码语言:javascript
代码运行次数:0
复制
// 在main.cj文件中

import define.*

main() {

let x = 3

let y = 2

// 调用dprint宏,打印变量x的值与表达式

@dprint(x)

// 调用dprint宏,打印表达式x + y的值与表达式

@dprint(x + y)

}

如此一来,程序在编译时,@dprint宏就会按照定义展开,自动生成对应的打印代码,让调试信息一目了然,仿佛为代码调试开启了一盏明灯。

再看一个实际应用场景,若要实现一个数学运算函数库,针对不同数据类型(如Int64、Float64等)都需要重复编写相似的运算函数,代码冗长且维护困难。

借助宏的强大功能,我们可以定义一个通用的模板宏来自动生成这些函数:

代码语言:javascript
代码运行次数:0
复制
// 在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)和结果数据类型作为参数,根据输入的函数名和参数类型,自动生成相应的数学运算函数代码。

使用时,只需简单调用:

代码语言:javascript
代码运行次数:0
复制
// 在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))

}

通过这一巧妙运用,宏为不同数据类型快速生成了特定的运算函数,大大减少了代码重复编写量,提升了代码的可读性与可维护性,让程序开发更加高效、便捷,仿佛拥有了一位智能的代码助手,随时响应开发者的需求。

六、结尾总结

至此,我们已一同踏入仓颉编程语言的奇妙世界,从搭建编程环境的细致入微,到变量、函数、复合数据类型的精巧运用;从接口、异常、并发编程的强大功能,到跨语言互操作、宏的独特魅力,每一处都彰显着仓颉编程语言的创新与实用。

仓颉编程语言不仅为开发者提供了一种全新的编程工具,更是为智能时代的软件开发生态注入了新活力。

cf18d37223a0419b90c48028e9e5be9c.png
cf18d37223a0419b90c48028e9e5be9c.png

它凭借原生智能化、天生全场景、高性能、强安全等特性,在物联网、人工智能、移动开发等诸多领域展现出巨大潜力,有望打破现有编程格局,助力开发者创造出更加智能、高效、安全的应用程序。

当下,仓颉编程语言正处于蓬勃发展的初期,虽已初露锋芒,但仍需广大开发者携手共进,深入探索其无限可能。

希望大家以此为起点,在后续的学习与实践中不断挖掘仓颉编程语言的深度,为中国软件产业自主创新添砖加瓦,共同迈向智能编程的新未来。

本文完。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-12-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、引言:仓颉编程的崛起
  • 二、复合数据类型探索
    • (一)枚举
    • (二)Struct 与 Class
  • 三、进阶特性揭秘
    • (一)接口与扩展
    • (二)异常处理
    • (三)并发编程
  • 四、跨语言互操作实战
  • 五、宏:代码的神奇变换
  • 六、结尾总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档