1 概述
1.1 背景介绍
仓颉编程语言作为一款面向全场景应用开发的现代编程语言,通过现代语言特性的集成、全方位的编译优化和运行时实现、以及开箱即用的 IDE工具链支持,为开发者打造友好开发体验和卓越程序性能。
案例结合代码体验,让大家更直观的了解仓颉语言中的结构体、类和接口。
1.2 适用对象
- 个人开发者
- 高校学生
1.3 案例时间
本案例总时长预计60分钟。
1.4 案例流程
说明:
① 进入华为开发者空间,登录云主机; ② 使用CodeArts IDE for Cangjie编程和运行仓颉代码。
1.5 资源总览
资源名称 | 规格 | 单价(元) | 时长(分钟) |
开发者空间 - 云主机 | 鲲鹏通用计算增强型 kc2 | 4vCPUs | 8G | Ubuntu | 免费 | 60 |
最新案例动态,请查阅 《仓颉之结构体、类与接口的奇幻乐园》。小伙伴快来领取华为开发者空间进行实操体验吧!
2 运行测试环境准备
2.1 开发者空间配置
面向广大开发者群体,华为开发者空间提供一个随时访问的“开发桌面云主机”、丰富的“预配置工具集合”和灵活使用的“场景化资源池”,开发者开箱即用,快速体验华为根技术和资源。
领取云主机后可以直接进入华为开发者空间工作台界面,点击打开云主机 \> 进入桌面连接云主机。没有领取在开发者空间根据指引领取配置云主机即可,云主机配置参考1.5资源总览。
2.2 创建仓颉程序
点击桌面CodeArts IDE for Cangjie,打开编辑器,点击新建工程,保持默认配置,点击创建。
产物类型说明:
- executable,可执行文件;
- static,静态库,是一组预先编译好的目标文件的集合;
- dynamic,动态库,是一种在程序运行时才被加载到内存中的库文件,多个程序共享一个动态库副本,而不是像静态库那样每个程序都包含一份完整的副本。
2.3 运行仓颉工程
创建完成后,打开src/main.cj,参考下面代码简单修改后,点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
package demo
// 第一个仓颉程序
main(): Int64 {println("hello world")println("你好,仓颉!")return 0
}
(\* 注意:后续替换main.cj文件代码时,package demo保留)
(\* 仓颉注释语法:// 符号之后写单行注释,也可以在一对 /\* 和 \*/ 符号之间写多行注释)
到这里,我们第一个仓颉程序就运行成功啦!后面案例中的示例代码都可以放到main.cj文件中进行执行,接下来我们继续探索仓颉语言。
3 仓颉语言结构类型
3.1 定义struct类型
仓颉编程语言中定义结构类型使用struct关键字,后跟结构体的名字,接着是定义在一对花括号中的结构体。struct结构体中可以定义一系列成员变量、成员属性、静态初始化器、构造函数和成员函数。
结构类型初识,具体代码如下:
// 定义名为Rectangle的struct
struct Rectangle {// 定义成员变量width,类型为Int64let width: Int64// 定义成员变量height,类型为Int64let height: Int64// 构造函数使用关键字init,在构造函数中对成员变量初始化public init(width: Int64, height: Int64) {this.width = widththis.height = height}// 定义成员函数area,返回width和height的乘积public func area() {width * height}
}
注意:struct只能定义在源文件的顶层作用域。
struct成员变量:
struct成员变量分为实例成员变量和静态成员变量(使用static修饰符修饰)。
两者访问区别:
实例成员变量只能通过struct实例访问;静态成员变量只能通过struct类型名访问。
定义struct类型Rectangle,通过struct访问实例成员变量和静态成员变量。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
// 定义名为Rectangle的struct
struct Rectangle {// 静态成员变量static let width: Int64 = 10// 实例成员变量let height: Int64public init(height: Int64) {this.height = height}// 定义成员函数area,返回width和height的乘积public func area() {width * height}
}
main() {// 创建struct实例let r = Rectangle(20)// 实例成员变量通过实例访问let height = r.height// 静态成员变量通过struct类型名访问let width = Rectangle.widthprintln("矩形的高度为:${height}")println("矩形的宽度为:${width}")
}
Step2:点击右上角“运行”按钮,终端控制台打印出内容。
struct静态初始化器
struct 支持定义静态初始化器,并在静态初始化器中通过赋值表达式来对静态成员变量进行初始化。
struct静态初始化器要点:
- 静态初始化器以关键字组合 static init 开头,后跟无参参数列表和函数体;
- 关键字组合 static init不能被访问修饰符访问;
- 函数体中必须完成对所有未初始化的静态成员变量的初始化,否则编译报错;
- 一个 struct 中最多允许定义一个静态初始化器,否则报重定义错误。
关于struct静态初始化器使用,具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
struct Rectangle {static let degree: Int64static init() {degree = 180}static init() { // 报错,只能定义一个静态初始化器degree = 180}
}
Step2:代码复制完成后,编译器直接提示报错。
struct构造函数:
struct构造函数分为普通构造函数和主构造函数。
普通构造函数:普通构造函数以关键字 init 开头,后跟参数列表和函数体,函数体中必须完成对所有未初始化的实例成员变量的初始化。
主构造函数:struct内最多可以定义一个主构造函数。主构造函数的名字和 struct 类型名相同,它的参数列表中可以有两种形式的形参:普通形参和成员变量形参(需要在参数名前加上 let 或 var),成员变量形参同时扮演定义成员变量和构造函数参数的功能。
关于struct普通构造函数使用,具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
struct Rectangle {let width: Int64let height: Int64//普通构造函数,函数体中必须完成对所有未初始化的实例成员变量的初始化public init(width: Int64) {this.width = width}// 与第一个普通构造函数构成重载public init(width: Int64, height: Int64) { this.width = widththis.height = height}// 报错,重定义错误public init(height: Int64) { this.width = heightthis.height = height}
}
Step2:代码复制完成后,编译器直接提示报错。
关于struct主构造函数使用,具体代码如下:
struct Rectangle {public Rectangle(let width: Int64, let height: Int64) {}
}
struct成员函数:
struct 成员函数分为实例成员函数和静态成员函数(使用 static 修饰符修饰)
二者区别:
实例成员函数只能通过 struct 实例访问,静态成员函数只能通过 struct 类型名访问;
静态成员函数中不能访问实例成员变量,也不能调用实例成员函数,但在实例成员函数中可以访问静态成员变量以及静态成员函数。
定义struct类型Rectangle,通过struct访问实例成员函数和静态成员函数。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
// 定义名为Rectangle的struct
struct Rectangle {let width: Int64let height: Int64// 构造函数public init(width: Int64, height: Int64) { this.width = widththis.height = height}// 实例成员函数public func area() {this.width * this.height}// 静态成员函数public static func typeName(): String {"Rectangle"}
}main() {// 创建struct实例let r = Rectangle(20,10)// 实例成员函数通过实例访问let rectangle_area = r.area()// 静态成员函数通过struct类型名访问let type_name = Rectangle.typeName()println("矩形的面积为:${rectangle_area}")println("矩形的typeName为:${type_name}")
}
Step2:点击右上角“运行”按钮,终端控制台打印出内容。
struct成员访问修饰符
访问修饰符修饰:private、internal、protected 和 public。
- private:在 struct 定义内可见。
- internal:当前包及子包内可见。
- protected:当前模块可见。
- public:模块内外均可见。
3.2 创建struct实例
定义了 struct 类型后,即可通过调用 struct 的构造函数来创建 struct 实例。在 struct 定义之外,通过 struct 类型名调用构造函数。
创建struct实例,具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
// 定义名为Rectangle的struct
struct Rectangle {let width: Int64let height: Int64public init(height: Int64,width: Int64) {this.height = heightthis.width = width}public func area() {width * height}
}main() {// 创建struct实例let r = Rectangle(30,20)let width = r.width let height = r.height let a = r.area() println("矩形的高度为:${height}")println("矩形的宽度为:${width}")println("矩形的面积为:${a}")
}
Step2:点击右上角“运行”按钮,终端控制台打印出内容。
3.3 mut函数
mut 函数是一种可以修改 struct 实例本身的特殊的实例成员函数。mut 函数与普通的实例成员函数相比,多一个 mut 关键字来修饰。
mut函数使用,具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
struct Foo {var i = 9public mut func g() {i += 1 println("成员变量i被修改了,i = ${i}")}
}
main() {var f = Foo()f.g()
}
Step2:点击右上角“运行”按钮,终端控制台打印出内容。
4 仓颉语言中的类
class 类型是面向对象编程中的经典概念,仓颉中同样支持使用 class 来实现面向对象编程。class 与 struct 的主要区别:
- class 是引用类型,struct 是值类型;
- class 之间可以继承,但 struct 之间不能继承;
4.1 Class定义
class 类型的定义以关键字 class 开头,后跟 class 的名字,接着是定义在一对花括号中的 class 定义体。class 定义体中可以定义一系列的成员变量、成员属性、静态初始化器、构造函数、成员函数等。
class类型初识,具体代码如下:
// class定义类
class Rectangle {// 成员变量let width: Int64let height: Int64// 构造函数public init(width: Int64, height: Int64) {this.width = widththis.height = height}// 成员函数areapublic func area() {width * height}
}
抽象类定义,具体代码如下:
abstract class AbRectangle {public func foo(): Unit
}
注意:
- 抽象类中禁止定义 private 的抽象函数;
- 不能为抽象类创建实例;
- 抽象类的非抽象子类必须实现父类中的所有抽象函数。
成员变量:
class 成员变量分为实例成员变量和静态成员变量,静态成员变量使用 static 修饰符修饰,没有静态初始化器时必须有初值,只能通过类型名访问。
静态成员变量通过类名访问,具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
class Rectangle {let width = 10static let height = 20
}
main() {// 静态成员变量通过类名访问let height = Rectangle.height println("矩形的高度为:${height}")
}
Step2:点击右上角“运行”按钮,终端控制台打印出内容。
实例成员变量定义时可以不设置初值,也可以设置初值,只能类的实例访问。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
class Rectangle {let width = 10let height: Int64init(h: Int64){height = h}
}
main() {// 实例成员变量通过类的实例访问let rectangle = Rectangle(30)let height = rectangle.height println("矩形的高度为:${height}")
}
Step2:点击右上角“运行”按钮,终端控制台打印出内容。
class静态初始化器:
静态初始化器以关键字组合 static init 开头,后跟无参参数列表和函数体,且不能被访问修饰符修饰。函数体中必须完成对所有未初始化的静态成员变量的初始化,否则编译报错。
一个 class 中最多允许定义一个静态初始化器,否则报重定义错误。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
class Rectangle {static let degree: Int64static init() {// 初始化静态成员变量degree = 180}// 报错,重定义错误static init() { degree = 180}
}
Step2:代码复制完成后,编译器直接提示报错。
class构造函数:
和 struct 一样,class 中也支持定义普通构造函数和主构造函数。
普通构造函数以关键字 init 开头,后跟参数列表和函数体,函数体中必须完成所有未初始化实例成员变量的初始化,否则编译报错。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
class Rectangle {let width: Int64let height: Int64// 报错,成员变量height未初始化public init(width: Int64, height: Int64) { this.width = width}
}
Step2:代码复制完成后,编译器直接提示报错。
主构造函数的名字和 class 类型名相同,它的参数列表中可以有两种形式的形参:普通形参和成员变量形参(需要在参数名前加上 let 或 var),成员变量形参同时具有定义成员变量和构造函数参数的功能。class 内最多可定义一个主构造函数。
主构造函数定义如下:
class Rectangle {public Rectangle(let width: Int64, let height: Int64) {}
}
class成员函数:
class 成员函数同样分为实例成员函数和静态成员函数(使用 static 修饰符修饰),实例成员函数只能通过对象访问,静态成员函数只能通过 class 类型名访问。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
class Rectangle {let width: Int64 = 15let height: Int64 = 28// 实例成员函数public func area() {this.width * this.height}// 静态成员函数public static func typeName(): String {"Rectangle"}
}
main() {let rectangle = Rectangle()// 实例成员函数只能通过对象访问let a = rectangle.area()println("矩形的面积为:${a}")// 静态成员函数只能通过类名访问let type_name = Rectangle.typeName()println("矩形typeName:${type_name}")
}
Step2:点击右上角“运行”按钮,终端控制台打印出内容。
class成员的访问修饰符:
对于 class 的成员(包括成员变量、成员属性、构造函数、成员函数),可以使用的访问修饰符有 4 种访问修饰符修饰:private、internal、protected 和 public。
- private 表示在 class 定义内可见。
- internal 表示仅当前包及子包(包括子包的子包)内可见。
- protected 表示当前模块及当前类的子类可见。
- public 表示模块内外均可见。
4.2 创建对象
定义了 class 类型后,即可通过调用其构造函数来创建对象。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
class Rectangle {let width: Int64let height: Int64public init(width: Int64, height: Int64) {this.width = widththis.height = height}public func area() {width * height}
}
main() {//创建对象let rectangle = Rectangle(8,12)// 访问成员实例let height = rectangle.height println("矩形的高度为:${height}")let width = rectangle.width println("矩形的宽度为:${width}")// 访问成员函数let area = rectangle.area()println("矩形的面积为:${area}")
}
Step2:点击右上角“运行”按钮,终端控制台打印出内容。
4.3 Class继承
像大多数支持 class 的编程语言一样,仓颉中的 class 同样支持继承。如果类 B 继承类 A,则称 A 为父类,B 为子类。子类将继承父类中除 private 成员和构造函数以外的所有成员。
open修饰符修饰的类是可被继承;
class类仅支持单继承;
抽象类总是可被继承的,故抽象类定义时的 open 修饰符是可选的;
可以使用 sealed修饰符修饰抽象类,表示该抽象类只能在本包被继承;
可以在子类定义处通过 \<: 指定其继承的父类,但要求父类必须是可继承的。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
open class A {let a: Int64 = 10
}
open class B {let b: Int64 = 10
}
// Ok,open修饰符修饰的类是可被继承
class C <: A { let c: Int64 = 20
}
// Error,在子类定义处通过 <: 指定其继承的父类,但要求父类必须是可继承的。
class D <: C { let d: Int64 = 30
}
// Error, 类仅仅支持单继承
class E <: A & B { let e: Int64 = 30
}
Step2:代码复制完成后,编译器直接提示报错。
父类构造函数调用:
子类的 init 构造函数可以使用 super(args) 的形式调用父类构造函数,或使用 this(args) 的形式调用本类其它构造函数,但两者之间只能调用一个。如果调用,必须在构造函数体内的第一个表达式处。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
open class A {A(let a: Int64) {}
}
class B <: A {let b: Int64init(b: Int64) {super(30)println("super:调用父类构造函数")this.b = b}init() {this(20)println("this:调用本类其它构造函数")}
}
main() {let b = B()
}
Step2:点击右上角“运行”按钮,终端控制台打印出内容。
覆盖和重定义:
子类中可以覆盖(override)父类中的同名非抽象实例成员函数,即在子类中为父类中的某个实例成员函数定义新的实现。
覆盖时,要求父类中的成员函数使用 open 修饰,子类中的同名函数使用 override 修饰,其中 override 是可选的。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
open class A {public open func f(): Unit {println("I am superclass")}
}
class B <: A {public override func f(): Unit {println("I am subclass")}
}
main() {let a: A = A()let b: A = B()a.f()b.f()
}
Step2:点击右上角“运行”按钮,终端控制台打印出内容。
5 仓颉语言中的接口
接口用来定义一个抽象类型,它不包含数据,但可以定义类型的行为。一个类型如果声明实现某接口,并且实现了该接口中所有的成员,就被称为实现了该接口。
接口的成员可以包含:
- 成员函数;
- 操作符重载函数;
- 成员属性。
这些成员都是抽象的,要求实现类型必须拥有对应的成员实现。
5.1 接口定义
接口定义:
interface I { func f(): Unit
}
接口使用关键字 interface 声明,其后是接口的标识符 I 和接口的成员。接口成员可被 open 修饰符修饰,并且 open 修饰符是可选的。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
// 定义接口
interface I { func f(): Unit
}
// 类Foo实现接口
class Foo <: I {public func f(): Unit {println("Foo")}
}
main() {let a = Foo()let b: I = ab.f()
}
Step2:点击右上角“运行”按钮,终端控制台打印出内容。
静态成员函数和实例成员函数类似,都要求实现类型提供实现。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
// 接口NamedType
interface NamedType {// 静态成员函数static func typename(): String
}// class A实现接口NamedType
class A <: NamedType {public static func typename(): String {"A"}
}
// class B实现接口NamedType
class B <: NamedType {public static func typename(): String {"B"}
}
main() {println("the type is ${ A.typename() }")println("the type is ${ B.typename() }")
}
Step2:点击右上角“运行”按钮,终端控制台打印出内容。
5.2 接口继承
接口可以继承一个或多个接口,多个接口使用 & 分隔,多个接口之间没有顺序要求。
接口继承时可以添加新的接口成员。
具体代码如下:
// 接口 Addable
interface Addable {func add(other: Int64): Int64
}
// 接口 Subtractable
interface Subtractable {func sub(other: Int64): Int64
}
// 一个接口继承多个接口,并添加新的接口成员
interface Calculable <: Addable & Subtractable {func mul(other: Int64): Int64func div(other: Int64): Int64
}
5.3 接口实现
仓颉除 Tuple、VArray 和函数外的其它类型都可以实现接口。
如果接口中的成员函数或操作符重载函数的返回值类型是 class 类型,那么允许实现函数的返回类型是其子类型。
例如下面这个例子,I 中的 f 返回类型是一个 class 类型 Base,因此 C 中实现的 f 返回类型可以是 Base 的子类型 Sub。
具体代码如下:
open class Base {}
class Sub <: Base {}
interface I {// 返回class类型的Basefunc f(): Base
}
class C <: I {// C 中实现的 f 返回类型可以是 Base 的子类型 Sub。public func f(): Sub {Sub()}
}
接口的成员可以提供默认实现。
例如下面的代码中,SayHi 中的 say 拥有默认实现,因此 A 实现 SayHi 时可以继承 say 的实现,而 B 也可以选择提供自己的 say 实现。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
interface SayHi {func say() {"hi"}
}
class A <: SayHi {}
class B <: SayHi {public func say() {"hi, B"}
}
main() {let a = A()println(a.say())let b = B()println(b.say())
}
Step2:点击右上角“运行”按钮,终端控制台打印出内容。
如果一个类型在实现多个接口时,多个接口中包含同一个成员的默认实现,这时会发生多重继承的冲突,语言无法选择最适合的实现,因此这时接口中的默认实现也会失效,需要实现类型提供自己的实现。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
// 接口 SayHi,默认实现say方法
interface SayHi {func say() {"hi"}
}
// 接口 SayHello,默认实现say方法
interface SayHello {func say() {"hello"}
}
// 类 Foo实现多个接口,多个接口中包含同一成员的默认实现,要求类Foo提供自己的实现
class Foo <: SayHi & SayHello {public func say() {"Foo"}
}
main() {let f = Foo()println(f.say())
}
Step2:点击右上角“运行”按钮,终端控制台打印出内容。
struct、enum 和 class 在实现接口时,函数或属性定义前的 override 修饰符是可选的,无论接口中的函数或属性是否存在默认实现。
具体代码操作如下:
Step1:复制以下代码,替换main.cj文件中的代码。(保留package)
interface I {func foo(): Int64 {return 0}
}
struct S <: I {public override func foo(): Int64 {return 1}
}
main() {let s = S()println(s.foo())}
Step2:点击右上角“运行”按钮,终端控制台打印出内容。
5.4 Any类型
Any 类型是一个内置的接口,定义如下:
interface Any {}
仓颉中所有接口都默认继承它,所有非接口类型都默认实现它,因此所有类型都可以作为 Any 类型的子类型使用。
具体代码如下:
main() {var any: Any = 1any = 2.0any = "hello, world!"
}
至此,仓颉之结构体、类与接口的奇幻乐园案例内容已全部完成。
如果想了解更多仓颉编程语言知识可以访问:https://cangjie-lang.cn/