类可以动态创建数据结构相同的 table 对象。
定义类的语法如下:
//定义类
class className{
//可选的构造函数,必须放在最前面,不能有多个构造函数
ctor( parameter1,parameter2,... ){
//构造函数代码
}
//定义属性
property = "value";
//定义方法(成员函数),必须写为名值对格式。
method = function(parameter1,parameter2,... ){
}
}
也可以使用下面的格式定义类:
//定义类
className = class {
//可选的构造函数,必须放在最前面,不能有多个构造函数
ctor( parameter1,parameter2,... ){
//构造函数代码
}
//定义属性
property = "value";
//定义方法
method = function(parameter1,parameter2,... ){
}
}
类主体指的是整个 class {} 语句包括的部分。
类主体内除了用 ctor 关键字定义的构造函数以外,其他部分的语法与构造表成员的语法完全相同,请参考:构造表 。
示例如下:
class cls{
property = "value"; //遵守表格造器规则,以分号分隔元素
method = function(){
//类有自已的命名空间,访问全局对象要加上 .. 前缀
..console.log("我是对象内部的成员函数")
}
}
类主体内部的成员函数必须写为名值对格式,函数体必须是匿名函数,不能省略 function 关键字与等号。
使用 ctor 关键字定义构造函数,构造函数在调用类创建对象时被调用。构造函数可以接收参数,并返回对象。
要点:
示例:
class cls {
ctor( name ){
this.name = name;
};
//其他成员
method = function(){
};
}
调用类创建对象实际上等价于调用类的构造函数,其语法与调用函数完全一样,在类名后加 () 表示调用类创建对象,可以在 () 内指定构造参数。
请参考: 调用函数
示例:
//创建对象
var obj = cls("名字1");
var obj2 = cls("名字2");
//访问对象属性
var name = obj.name;
//调用对象的方法
obj.method();
类创建的对象都是 table 对象,使用方法与其他 table 对象一样。
请参考:命名空间
class 语句定义了一个新的“类命名空间”,类的命名空间就是类自身。
在“类命名空间”的所有函数体内部直接定义的名称默认为类命名空间的成员。
类命名空间中的成员也就是类的静态成员,为所有类的实例对象所共享。
在类内部访问外部命名空间需要使用完整路径。
在类外部可以通过类的名字访问类命名空间内的成员(也就是类的静态成员)。
示例:
class cls{
property = "实例属性值";
method = function(){
//类有自已的命名空间,访问全局对象要加上 .. 前缀
..console.log( "实例属性",this.property )
..console.log( "类的静态属性",staticProperty) // staticProperty 等价于 self.staticProperty
}
}
//类自身也是命名空间
namespace cls{
staticProperty = "类的静态属性值";
}
//访问类命名空间的公用静态成员。
cls.staticProperty = "类的静态属性值";
因为类有独立的命名空间,在类内部要使用 .. 前缀的全局命名空间路径才能访问外部命名空间的对象,例如上面的 ..console.log( 类的方法被调用了 )。
要特别注意:
请看下面的代码:
import my.cls.c //如果 my.cls 是一个普通的命名空间而不是一个类
import my.cls //如果 my.cls是一个类,因为类会创建新的命名空间,那么上面导入的 my.cls 会被覆盖。
一般我们应当避免将类名作为共享的父命名空间,以避免出现上面的错误。
如果必须这样做,就需要使用一些技巧。
例如标准库里的 process 就是一个类,而 process 作为父命名空间包含了大量的下级库。
如果我们像下面这样写是不会出问题的:
import process.popen;
import process;
这是因为:
import process,这就保证了 process 命名空间总是指向 procss 类。当然我们仍然应当避免用类名作为父命名空间,避免不必要地将简单的事搞复杂。
参考文档:
在 class 语句内部的 ctor 函数定义了一个新的“类作用域”。
“类作用域”内用 var 语句定义的局部变量在的作用域是整个 class 语句块,其生命周期与构造函数创建的当前实例绑定。
示例:
class className {
ctor(name, ...){
var privateVar = "私有变量";
};
property = privateVar; //引用私有变量
method = function(v){
//访问私有变量
if( v = privateVar ){
}
};
}
var privateVar = "私有变量" 不能移到 ctor 函数之外。privateVar 的作用域不仅仅局际于 ctor 函数内部,而是整个 class 语句访问的“类作用域”访问。请参考 var 语句
this 也是类作用域内的私有变量,用于在类内部表示当前构造的实例对象。
示例如下:
import console;
class cls{
a = 123;
func = function(){
//类有自已的命名空间,访问全局对象要加上..前缀
..console.log( this.a )
}
}
//创建对象
var obj = cls();
//调用对象的方法
obj.func();
当一个对象使用 object.method() 的格式调用成员函数时,默认会将 object 传递为成员函数的 owner 参数。
虽然对于类的实例来说调用 object.method() 时 owner 会指向 object,而类内部的 this 也会指向 object,但这两者的的机制是不同的,主要区别:
aardio 中的 this 是在声明 class 时静态绑定类的当前实例,并不是像 owner 那样在运行时动态绑定。而 owner 对象是会根据函数调用时拥有函数的对象而相应改变,也就是基于调用点绑定(Call-site binding)。使用 object.method 格式调用时 owner 就会绑定 object , 但如果使用 object["method"]先获取函数再调用 owner 绑定就会丢失(函数内 owner 将会是 null 值 )。
请参考:owner
示例如下:
import console;
class cls{
func = function(){
//类有自已的命名空间,访问全局对象要加上 .. 前缀
..console.log("owner", owner )
..console.log("this", this )
..console.log("owner == this", owner == this )
}
}
//创建对象
var obj = cls();
//调用对象方法
obj.func(); //默认 table 与 owner 是同一个对象
console.more();
var func = obj.func;
func();//这里 owner 为null空值,而 this 对象没有改变
console.more();
直接继承是指在子类的构造函数中调用基类的构造函数创建实例对象 - 并直接基于该实例对象继续构造出子类实例对象。这种方法可以向上调用基类的构造函数 - 保证按继承关系正确的初始化基类,并且可以使用构造函数创建的闭包变量方便地存储实例对象的私有数据。
示例:
//创建一个基类
class baseClass{
a = 123;
b = 456;
c = 789
}
namespace baseClass{
static = 123; //类的静态成员
}
class derivedClass{
//构造函数
ctor(...){
this = ..baseClass(...); //调用基类构造对象原型。
};
c = "覆盖基类成员";
d = "子类的新成员";
}
import console;
//从子类创建对象
var obj = derivedClass();
//输出对象的所有成员(包括直接继承的基类成员)
for(k,v in obj){
console.log(k,v);
}
console.pause();
通过对象元表的 _get 元方法指定一个原型表,也可以实现继承的功能。
间接继承的好处继承指向静态的公用模板,并不会实际生成新的对象(浅拷贝),可以占用较少的空间,缺点是访问速度不如直接继承快,并且占用了 _get 元方法,当我们需要使用 _get 元方法来做一些其他的事(例如实现属性方法)时就比较麻烦。
在间接继承时可以通过 owner 来访问当前对象(类似直接继承中的 this 对象)。
示例:
class baseClass{
a = 123;
b = 456;
c = 789
}
class derivedClass{
c = "覆盖基类成员";
d = "子类的新成员";
@_prototype;
}
//在类的命名空间内指定静态共享成员 _prototype
derivedClass._prototype = { _get = ..baseClass() };
import console;
var obj = derivedClass();//从子类创建对象
//与类的直接继承不同
//遍历只能输出子类的所有对象,(不会遍历元表中原型对象的成员)
for(k,v in obj){
console.log(k,v);
}
//通过元方法继承仅在使用成员操作符时生效
console.log("访问 baseClass 类的成员", obj.a );
console.pause();
如果不需要构造函数,我们也可以使用 table.create 函数基于原型继承的原理直接创建表对象。
直接继承、原型继承可以相互结合使用,
一个对象可以直接继承一个基类,并且通过原型继承另外一个类或对象。
而在原型继承中使用的原型对象自身,也可以直接继承、或原型继承基它的类或对象。
在 aardio 中最常见的是直接继承,原型继承很少被使用。
另外使用过于复杂的继承关系是非常不好的习惯,重度使用继承的开发模式在现代编程中已经不流行了。
优先使用组合而不是继承是良好的设计习惯,尤其在动态语言中更应该这样做。
在我们设计 winform 窗体时,会在上面放一些控件,当我们访问这些控件对象的 text 属性时,无论读或写 text 属性都会调用系统 API 函数,这种属性必须指向一个函数,因为除了简单的读写数据以外还需要执行一些其他的代码去操作界面上的控件窗口。
例如一个 winform 窗体上有一个 button2 控件,我们这样读它的 text 属性。
var text = winform.button2.text
为了拦截对象的属性读写操作,一般我们想到通过 _get、_set 元方法 将属性转换为函数。
但是我们会遇到一些麻烦:
所有的控件有一些共同的属性,例如大小、字体等等,如果都写在 _get 或 _set 元方法里会非常乱。属性字段越来越多,我们假设有二十个属性,我们的 _get 或 _set 元方法里的代码将会非常的多,然而我们每次需要访问的仅是其中一小部分代码。
aardio 的早期版本并没有属性表的概念,win.ui.ctrl 库的设计臃肿而复杂。为了解决所有的问题,让可变的代码与不变的代码合理分离,aardio 设计了 util.metaProperty 库,这个库的源码很简短,但是解决了所有的问题。win.ui.ctrl.metaProperty 则是util.metaProperty的一个扩展,提供了类似的功能。应用属性表以后,win.ui.ctrl 减少了很多代码并更为清晰。
我们看下面 win.ui.ctrl.picturebox 控件的部分代码:
namespace win.ui.ctrl;
class picturebox{
ctor(parent,tParam){
if(tParam){
tParam.style |= 0xE/*_SS_BITMAP*/;
}
}
@_metaProperty;
}
picturebox._metaProperty = metaProperty(
image = {
_get = function(){
return owner.getBitmap();
}
_set = function( v ){
if(type(v)==type.pointer)
return owner.setBitmap(h);
}
}
)
当我们在窗体 winform 上创建一个 picturebox 控件,我们可以直接通过 winform.picturebox.image 读写属性。当然图片在屏幕的控件上,而不是 picturebox 这个对象里面,无论是读或写都要从控件中实时读写,这就需要把它们映射为函数。
上面的属性字段 image 就被映射为了独立的 _get ,_set 函数。
_get 负责读属性,_set 负责写属性,当省略_get时可以创建一个只写属性,而省略_set时可以创建一个只读属性。
我们可以仅仅在需要属性表时加载 util.metaProperty,这样不会影响普通表存取的速度。无论是 util.metaProperty 的实现、还是 util.metaProperty 的使用都非常的简洁、直观并易于扩展。
属性表的意义在于扩展了_get ,_set元方法、使每一个属性字段都可以定义自已的_get ,_set方法, 方便地定义只读、只写属性。属性元表不但可以自定义对象的元方法,而且可以通过属性表定义对象的原型,该原型(属性表)是支持多级继承的。并且在多级继承中始终可以通过 owner 参数访问当前对象。
实际上经过数十年的发展,util.metaProperty 已经成为了 aardio 标准库中可能是被使用次数最多的一个库。
请参考: util.metaProperty
私有变量
在构造函数内可以用 var 语句定义在类作用域 范围内有效的私有变量,在类外部无法访问它们。利用此特性可以创建对象的私有数据,实现对象的信息隐藏。
成员保护
使用下划线作为首字符的类成员是只读成员,只读成员的数据只能读取不能修改。 可以使用只读成员保护对象内部不希望被修改的数据。
另外,使用元表也可以创建只读的成员,例如使用 util.metaProperty 创建的属性表可以指定 _get 方法而省略 _set 方法实现只读成员。
在 aardio 中可以用类定义结构体,只要在类构造器的成员键名前添加原生类型声明就可以了。
//定义结构体
class Point{
int x = 0;
int y = 0;
}
//创建一个结构体实例对象
var pt = Point();
aardio 默认已经定义了所有字母大写的保留常量
::POINT用于表示坐标。
参考:原生数据类型 - 结构体
类有独立的命名空间,默认访问外部命名空间(包括父命名空间)的对象都要写完整的命名空间路径(必须是由 .. 操作符引导的全局路径)。
类内部也无法通过类名访问自身,只能通过 self 访问类的命名空间。虽然类的静态成员也就是类命名空间的成员,但 self 并不能作为类构造器使用。
所以如果要在类内部访问类自身的构造器仍然必须使用由 .. 操作符引导的全局路径。
如果想直接使用类名而不是写完整路径,有 3 种方法:
最常见的解决方案是在定义类以后,定义一个同名的局部变量,局部变量在作用域内可以跨命名空间使用。
在类内部使用特殊的 import global 语句,可允许在类内部直接访问全局命名空间成员。
使用 table.linkClasses 。
下面具体说明 table.linkClasses 的用法。
table.linkClasses 是一个扩展类库,需要导入才能使用。table.linkClasses 将参数 @1 指定命名空间中的所有子类名都导入到该命名空间的所有子类,使同于命名空间的子类可以相互访问。
举例,运行以下代码什么也不会输出:
namespace namspace1{
class cls1{
ctor(){
//类内部 cls1,cls2 为 null
..print(cls1,cls2);
};
}
class cls2{
ctor(){
//类内部 cls1,cls2 为 null
..print(cls1,cls2);
};
}
cls1();
cls2();
}
但如果加上 table.linkClasses 就可以使用父命名空间的类名了,例如:
namespace namspace1{
class cls1{
ctor(){
..print(cls1,cls2); //等价于 ..print(..namspace1.cls1,..namspace1.cls2)
};
}
class cls2{
ctor(){
..print(cls1,cls2); //等价于 ..print(..namspace1.cls1,..namspace1.cls2)
};
}
import table.linkClasses;
table.linkClasses(self);
cls1();
cls2();
}