1 概述
1.1 案例介绍
仓颉编程语言作为一款面向全场景应用开发的现代编程语言,通过现代语言特性的集成、全方位的编译优化和运行时实现、以及开箱即用的 IDE 工具链支持,为开发者打造友好开发体验和卓越程序性能。
本案例将介绍仓颉中的反射和注解,并结合简单的编程代码进行知识体验,仓颉开发环境我们使用了开发者空间提供的云主机环境,环境已经预装了仓颉工具链和 CodeArts IDE for Cangjie,即开即用,非常便捷。
案例结合代码体验,让大家更直观的了解仓颉语言中的反射和注解。
1.2 适用对象
1.3 案例时间
本案例总时长预计 40 分钟。
1.4 案例流程
说明:
进入华为开发者空间,登录云主机;
使用 CodeArts IDE for Cangjie 编程和运行仓颉代码。
2 运行测试环境准备
2.1 开发者空间配置
面向广大开发者群体,华为开发者空间提供一个随时访问的“开发桌面云主机”、丰富的“预配置工具集合”和灵活使用的“场景化资源池”,开发者开箱即用,快速体验华为根技术和资源。
领取云主机后可以直接进入华为开发者空间工作台界面,点击打开云主机 > 进入桌面连接云主机。没有领取在开发者空间根据指引领取配置云主机即可,云主机配置参考 1.5 资源总览。
2.2 创建仓颉程序
点击桌面 CodeArts IDE for Cangjie,打开编辑器,点击新建工程,保持默认配置,点击创建。
产物类型说明:
2.3 运行仓颉工程
创建完成后,打开 src/main.cj,参考下面代码简单修改后,点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
package demo// 第一个仓颉程序main(): Int64 { println("hello world") println("你好,仓颉!") return 0}
复制代码
(* 注意:后续替换 main.cj 文件代码时,package demo 保留)
(* 仓颉注释语法:// 符号之后写单行注释,也可以在一对 /* 和 */ 符号之间写多行注释)
到这里,我们第一个仓颉程序就运行成功啦!后面案例中的示例代码都可以放到 main.cj 文件中进行执行,接下来我们继续探索仓颉语言。
3 反射和注解
3.1 动态特性
仓颉的动态特性主要包含反射、动态加载。
3.1.1 仓颉反射基本介绍
反射指程序可以访问、检测和修改它本身状态或行为的一种机制。
反射这一动态特性有以下的优点:
但使用反射调用,其性能通常低于直接调用,因此反射机制主要应用于对灵活性和拓展性要求很高的系统框架上。
3.1.2 如何获得 TypeInfo
什么是 TypeInfo?
TypeInfo 是一个类型,这个核心类型中记录任意类型的类型信息,并且定义了方法用于获取类型信息、设置值等。
可以使用三种静态的 of 方法来生成 TypeInfo 信息类。
例如可以用反射来获取一个自定义类型的类型信息,具体代码操作如下:
Step1:复制以下代码,替换 main.cj 文件中的代码。(保留 package)
import std.reflect.*class Foo {}main() { let a: Foo = Foo() let info1: TypeInfo = TypeInfo.of(a) let info2: TypeInfo = TypeInfo.of<Foo>() println(info1) println(info2)}
复制代码
Step2:点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
此外,为配合动态加载使用,TypeInfo 还提供了静态函数 get,该接口可通过传入的类型名称获取 TypeInfo。
具体代码操作如下:
注意:传入的类型名称和您的工程名称是对应的。
Step1:复制以下代码,替换 main.cj 文件中的代码。(保留 package)
import std.reflect.*class Foo {}main() { // get方法,通过传入类型名称获取TypeInfo // 类型名称是上个步骤中使用of方法生成的 let info: TypeInfo = TypeInfo.get("demo.Foo") println(info)}
复制代码
Step2:点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
采用 get 方法无法获取一个未实例化的泛型类型。
注意:传入的类型名称和您的工程名称是对应的。
具体代码操作如下:
Step1:复制以下代码,替换 main.cj 文件中的代码。(保留 package)
import std.collection.*import std.reflect.*class A<T> { A(public let t: T) {}}
class B<T> { B(public let t: T) {}}main() {
let b: B<Int64> = B<Int64>(1) // Ok `default.B<Int64>` has been instantiated. let bInfo: TypeInfo = TypeInfo.get("demo.B<Int64>") println(bInfo)
// Error,`default.A<Int64>` is not instantiated,will throw InfoNotFoundException let aInfo: TypeInfo = TypeInfo.get("demo.A<Int64>") println(aInfo)}
复制代码
Step2:点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
3.1.3 如何使用反射访问成员
在获取到对应的类型信息类即 TypeInfo 后,便可以通过其相应接口访问对应类的实例成员以及静态成员。
注意:仓颉的反射被设计为只能访问到类型内 public 的成员。
例如在运行时对类的某一实例成员变量进行获取与修改。
具体代码操作如下:
Step1:复制以下代码,替换 main.cj 文件中的代码。(保留 package)
import std.reflect.*public class Foo { public static var param1 = 20 public var param2 = 10}main(){ let obj = Foo() let info = TypeInfo.of(obj) // 获取成员变量 let staticVarInfo = info.getStaticVariable("param1") // 获取静态成员变量 let instanceVarInfo = info.getInstanceVariable("param2") println("成员变量初始值") print("Foo 的静态成员变量 ${staticVarInfo} = ") println((staticVarInfo.getValue() as Int64).getOrThrow()) print("obj 的实例成员变量 ${instanceVarInfo} = ") println((instanceVarInfo.getValue(obj) as Int64).getOrThrow()) println("--------------------------------------------") println("更改成员变量") // 更改静态成员变量 staticVarInfo.setValue(8) // 更改实例成员变量 instanceVarInfo.setValue(obj, 25) print("Foo 的静态成员变量 ${staticVarInfo} = ") println((staticVarInfo.getValue() as Int64).getOrThrow()) print("obj 的实例成员变量 ${instanceVarInfo} = ") println((instanceVarInfo.getValue(obj) as Int64).getOrThrow())}
复制代码
Step2:点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
通过反射对属性进行检查以及修改。
具体代码操作如下:
Step1:复制以下代码,替换 main.cj 文件中的代码。(保留 package)
import std.reflect.*public class Foo { public let _p1: Int64 = 1 // 属性p1没有使用mut修饰符,只能定义getter public prop p1: Int64 { get() { _p1 } } public var _p2: Int64 = 2 // 属性p2使用mut修饰符,必须分别定义getter和setter实现 public mut prop p2: Int64 { get() { _p2 } set(v) { _p2 = v } }}
main(): Unit{ let obj = Foo() let info = TypeInfo.of(obj) let instanceProps = info.instanceProperties.toArray() println("obj的实例成员属性包含${instanceProps}") let PropInfo1 = info.getInstanceProperty("p1") let PropInfo2 = info.getInstanceProperty("p2") print("获取原有属性值:p1=") println((PropInfo1.getValue(obj) as Int64).getOrThrow()) print("获取原有属性值:p2=") println((PropInfo2.getValue(obj) as Int64).getOrThrow())
println("-------------------")
// 属性p1没有使用mut修饰符,只能定义getter事项,不能定义setter实现,不能修改 if (PropInfo1.isMutable()) { PropInfo1.setValue(obj, 10) } if (PropInfo2.isMutable()) { PropInfo2.setValue(obj, 20) } print("修改后的属性值:p1=") println((PropInfo1.getValue(obj) as Int64).getOrThrow()) print("修改后的属性值:p2=") println((PropInfo2.getValue(obj) as Int64).getOrThrow()) return}
复制代码
Step2:点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
通过反射机制进行函数调用,具体代码操作如下:
Step1:复制以下代码,替换 main.cj 文件中的代码。(保留 package)
import std.reflect.*public class Foo { public static func add(a: Int64, b: Int64): Int64 { return a + b }}main(): Unit { var num = 0 // 获取参数的TypeInfo let intInfo = TypeInfo.of<Int64>() // 获取类Foo的TypeInfo let classInfo = TypeInfo.of<Foo>() // 获取静态函数的StaticFunctionInfo let funcInfo = classInfo.getStaticFunction("add", intInfo, intInfo) // 调用StaticFunctionInfo的apply方法,传入Array num = (funcInfo.apply([1, 1]) as Int64).getOrThrow() println(num)}
复制代码
Step2:点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
3.2 注解
仓颉中提供了一些属性宏用来支持一些特殊情况的处理。
3.2.1 正确使用整数运算溢出的注解
仓颉中提供三种属性宏来控制整数溢出的处理策略,即 @OverflowThrowing,**@OverflowWrapping** 和 @OverflowSaturating ,这些属性宏当前只能标记于函数声明之上,作用于函数内的整数运算和整型转换。
具体代码如下:
@OverflowThrowingfunc add(a: Int8, b: Int8){ return a + b}main() { add(100,29) /* 100 + 29 在数学上等于 129, * 在 Int8 的表示范围上发生了上溢出, * 程序抛出异常 */ 0}
复制代码
具体代码如下:
@OverflowWrappingmain() { let res: Int8 = Int8(105) * Int8(4) /* 105 * 4 在数学上等于 420, * 对应的二进制为 1 1010 0100, * 超过了用于接收该结果的 8 位内存空间, * 截断后的结果在二进制上表示为 1010 0100, * 对应为有符号整数 -92 */ let temp: Int16 = Int16(-132) let con: UInt8 = UInt8(temp) /* -132 对应的二进制为 1111 1111 0111 1100, * 超过了用于接收该结果的 8 位内存空间, * 截断后的结果在二进制上表示为 0111 1100 * 对应为有符号整数 124 */ 0}
复制代码
具体代码如下:
@OverflowSaturatingmain() { let res: Int8 = Int8(-100) - Int8(45) /* -100 - 45 在数学上等于 -145, * 在 Int8 的表示范围上发生了下溢出, * 选择 Int8 的最小值 -128 作为结果 */ let con: Int8 = Int8(1024) /* 1024 在 Int8 的表示范围上发生了上溢出, * 选择 Int8 的最大值 127 作为结果 */ 0}
复制代码
3.2.2 性能优化注解
为了提升与 C 语言互操作的性能,仓颉提供属性宏 @FastNative 控制 cjnative 后端优化对于 C 函数的调用。值得注意的是,属性宏 @FastNative 只能用于 foreign 声明的函数。
@FastNative 使用限制:
开发者在使用 @FastNative 修饰 foreign 函数时,应确保对应的 C 函数满足以下两点要求。
首先,函数的整体执行时间不宜太长;
其次,函数内部不能调用仓颉方法。
3.2.3 自定义注解
自定义注解机制用来让反射获取标注内容,目的是在类型元数据之外提供更多的有用信息,以支持更复杂的逻辑。
开发者可以通过自定义类型标注 @Annotation 方式创建自己的自定义注解。@Annotation 只能修饰 class,并且不能是 abstract 或 open 或 sealed 修饰的 class。当一个 class 声明它标注了 @Annotation,那么它必须要提供至少一个 const init 函数,否则编译器会报错。
下面的例子定义了一个自定义注解 @Version,并用其修饰 A, B 和 C。在 main 中,通过反射获取到类上的 @Version 注解信息,并将其打印出来。
具体代码操作如下:
Step1:复制以下代码,替换 main.cj 文件中的代码。(保留 package)
import std.reflect.TypeInfo@Annotationpublic class Version { let code: String const init(code: String) { this.code = code }}
@Version["1.0"]class A {}
@Version["1.1"]class B {}
main() { let objects = [A(), B()] for (obj in objects) { let annOpt = TypeInfo.of(obj).findAnnotation<Version>() if (let Some(ann) <- annOpt) { if (let Some(version) <- ann as Version) { println(version.code) } } }}
复制代码
Step2:点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
自定义注解在使用时必须使用 const init 构建出合法的实例。注解声明语法与声明宏语法一致,后面的[ ]括号中需要按顺序或命名参数规则传入参数,且参数必须是 const 表达式。
下面的例子中定义了一个拥有无参 const init 的自定义注解 @Marked,使用时 @Marked 和 @Marked[] 这两种写法均可。
具体代码操作如下:
Step1:复制以下代码,替换 main.cj 文件中的代码。(保留 package)
import std.reflect.TypeInfo@Annotationpublic class Marked { const init() {}}
@Markedclass A {}
@Marked[]class B {}main() { if (TypeInfo.of(A()).findAnnotation<Marked>().isSome()) { println("A is Marked") } if (TypeInfo.of(B()).findAnnotation<Marked>().isSome()) { println("B is Marked") }}
复制代码
Step2:点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
Annotation 不会被继承,因此一个类型的注解元数据只会来自它定义时声明的注解。如果需要父类型的注解元数据信息,需要开发者自己用反射接口查询。
下面的例子中,A 被 @Marked 注解修饰,B 继承 A,但是 B 没有 A 的注解。
具体代码操作如下:
Step1:复制以下代码,替换 main.cj 文件中的代码。(保留 package)
import std.reflect.TypeInfo@Annotationpublic class Marked { const init() {}}
@Markedopen class A {}
class B <: A {}
main() { if (TypeInfo.of(A()).findAnnotation<Marked>().isSome()) { println("A is Marked") } if (TypeInfo.of(B()).findAnnotation<Marked>().isSome()) { println("B is Marked") }}
复制代码
Step2:点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。
至此,仓颉之反射和注解的神秘力量案例内容已全部完成。
如果想了解更多仓颉编程语言知识可以访问:https://cangjie-lang.cn/
评论