aardio教程三) 元表、元方法

前言

还有个迭代器,基础语法基本已经说完了,后面想到啥再补充,之后的教程会从以下方面来讲:

  • 基础库的使用,比如string、table等
  • 基础控件的使用,比如listview、tab等
  • aardio和Python交互,比如给Python写个界面
  • 自带的范例程序
  • 我写的一些小程序

当然,我的理解也是很基础的,特别是在界面设计上,我都是用的默认控件的默认设置,不会去自定义控件内容。要想做出特别炫酷的程序,你还得依赖其他语言和工具的基础。例如用HTML和CSS来实现界面。

元表、元方法

参考文档:

  • https://bbs.aardio.com/doc/reference/libraries/kernel/table/meta.html
  • https://bbs.aardio.com/doc/reference/the%20language/operator/overloading.html

主要是用于重载运算符和内置函数的行为。

表可以定义另一个表作为元表,然后在元表里定义元方法来定义操作符或内置函数操作表的一些行为。这种类似于Python的魔法方法,在Python中使用__eq__定义==的行为,而在aardio中用_eq元方法来定义==的行为。

初级使用例子

举个例子,python中print会调用对象的__str____repr__来打印一个对象,而aardio也是调用tostring来打印一个对象,但表默认并没有定义_tostring元方法,导致打印出来的内容是table: 03B2E3A8的格式

我们可以通过给表定义_tostring元方法,来使io.print或者console.log正常显示表

import console; 
io.open()
var tab = {
	a=1;
	b=2;
	@{
		_tostring = function(...) {
		    // 元方法里不能调用触发元方法的函数,比如_tostring里不能调用tostring
		    // _get元方法可以通过[[k]]运算符来避开元方法,通过.和[]会触发_get,而[[]]不会
			return table.tostring(owner);
		}
	}
}
io.print("没有定义元方法" , {});
io.print("定义了元方法" , tab);
console.pause(true);

输出如下:

没有定义元方法  table: 03A8E2F8
定义了元方法    {
a=1;
b=2
}

运算符重载

这个就不细说了,应该很容易理解。

io.open(); tab = { x=10 ; y=20 };
tab2 = { x=12 ; y=22 }
//c = tab + tab2; //这样肯定会出错,因为 table默认是不能相加的

//创建一个元素,元表中的__add函数重载加运算符。
tab@ = {
	_add = function(b) { 
		return owner.x + b.x
	};
}

c = tab + tab2; //这时候会调用重载的操作符 tab@._add(tab2)
io.print( c ) //显示22

入门使用例子

还有一个很常用的元方法是_get_set,是定义访问对象属性时触发的。利用这个可以让代码量少很多,看起来逻辑也更清晰。

这里举个实际例子,我在封装sunny的时候,遇到个很累人的事。sunny的dll导出函数,返回值有些是指针,你需要手动给他转成字符串,而且还需要手动释放这个指针指向的内存,也就是说你调用一次导出函数,就得写至少三行代码(调用、转字符串和释放)。

那么,有没有一种方法,定义完这个导出函数,在使用的时候就调用函数释放内存,并转成字符串返回,而不用我每次都手动释放和转字符串。

先定义request类,现在只需要给它定义一个messageId属性和_meta元方法:

namespace sunny;

class request{
	ctor(messageId){
		this.messageId = messageId;
	}
	@_meta;
	
}

@后面跟的是元表的名称,你可以将元表定义在名字空间里,这样看起来代码更舒服。下面在类的名字空间里定义dll方法和元表.

namespace request{
    //释放指针的函数
	Free = ::SunnyDLL.api("Free","void(pointer p)");
	// 下面的函数第一个参数都是messageId
    DelRequestHeader = ::SunnyDLL.api("DelRequestHeader","void(int id,str h)");
    GetRequestBodyLen = ::SunnyDLL.api("GetRequestBodyLen","int(int id)");
	GetRequestBody = ::SunnyDLL.api("GetRequestBody","pointer(int id)");
	// 定义一个中间方法
    // name是要调用的导出函数,messageId则是导出函数的第一个参数
	xcall = function(name, messageId, len){
		var func = self[name];
		if(!func) error("不支持的函数!");
		function proxyFunc(...){
			var v = func(messageId, ...);
			var result;
			if(type(v) == type.pointer){
				if(len) result = ..raw.tostring(v,1,len);
				else result = ..raw.tostring(v);
				Free(v);
			}else{
				result = v;
			}
			return result;
		}
		
		return proxyFunc;
	}
	// 定义元表
	_meta = {
		_get = function(k){
			return xcall(k, owner.messageId);
		}
		
	}
}

这个代码初看可能有点费劲,我们拆解着来看。

首先前面几行只是定义了四个dll的导出函数,然后下面定义了_meta这个表。

而_meta里只定义了一个元方法_get,它的作用是当你访问对象的属性时会触发这个方法,然后给你返回值。比如我先实例化一个request对象

r = request(111111);
// 当访问r.GetRequestBody时,这个对象没有GetRequestBody属性,所以会触发_get元方法
// 得到的返回值就是 返回它的返回值也就是`xcall("GetRequestBody", owner.messageId)`.
console.log(r.GetRequestBody)

这里的owner就是指r这个对象。然后定义了xcall这个函数,它里面又定义了一个函数proxyFunc,并将它作为返回值,这种被称为闭包。先分析下xcall方法

// 这里的self指的是当前名字空间,也就是request,name则是需要调用的方法名,例如是GetRequestBody
// 这里func的值就等于GetRequestBody,也就是::SunnyDLL.api("GetRequestBody","pointer(int id)");
var func = self[name];
// 如果func是null的话,说明当前名字空间下没有这个函数,也就不是我们定义的sunny导出函数
if(!func) error("不支持的函数!");
// 定义了proxyFunc函数,`xcall(k, owner.messageId)`返回的值就是proxyFunc函数,这里的三个点表示传入任意个参数,类似于Python中的*args
function proxyFunc(...){
    // 调用GetRequestBody(messageId, ...)
	var v = func(messageId, ...);
	// 定义返回结果
	var result;
	// 如果结果是指针的话
	if(type(v) == type.pointer){
	    // 就把它转为字符串,二进制数据需要指定长度,不然就是到\0结束
		if(len) result = ..raw.tostring(v,1,len);
		else result = ..raw.tostring(v);
		// 调用导出函数释放内存
		Free(v);
	}else{
	    // 如果是其他类型数据就直接返回,比如数值或null
		result = v;
	}
	return result;
}

这样一番折腾,起了什么效果呢,看一下下面两段代码,如果不利用元方法的话,你使用dll导出函数得这么写

// 导入request名字空间
improt request;
// 调用名字空间下的函数
var messageId = 111111
var pResult = request.GetRequestBody(messageId);
// 将指针转为字符串
var result = raw.tostring(pResult,1);
// 释放内存
request.Free(pResult);
// 再使用其他导出函数也需要重复写这几行代码

看着就几行代码,但是你想想调用一个函数都得写好几行,如果调用多次呢。而定义了xcall和_meta之后,只需要这样写代码:

improt request;
var messageId = 111111;
var req = request(messageId);
var result = req.GetRequestBody();
// 后面调用都只需要用req.方法名()调用,不需要管raw.tostring和Free了

因为req是可以复用的,所以我调用任何导出函数都只需要写一行代码,使用sunny库的代码也变得更简洁易懂了。

官方例子

给表创建一个代理,监控表属性的访问和设置:

// 创建一个代理,为另一个table对象创建一个替身以监控对这个对象的访问
function table.createProxy(tab) {
    var real = tab;//在闭包中保存被代理的数据表tab
    var _meta = {
        _get = function(k){
            io.print(k+"被读了");
            return real[k];
        };
        _set = function (k,v){
            io.print(k+"被修改值为"+v)
            real[k]=v; //删除这句代码就创建了一个只读表
        }
    }
    var proxy = {@_meta};//创建一个代理表
    
    return proxy; //你要访问真正的表?先问过我吧,我是他的经纪人!!!
}

//下面是使用示例

tab = {x=12;y=15};
proxy = table.createProxy(tab);//创建一个代理表,以管理对tab的存取访问

io.open();
c = proxy.x; //显示 "x被读了"
proxy.y = 19; //显示 "y被修改值为19"
io.print(proxy.y); //显示 "y被读了" 然后显示19

所有的元方法

元方法/属性 函数定义 Python中的魔法方法 说明
_weak 用不到
_type 属性 type(obj)函数的行为
_readonly 属性 等于false,_开头的成员也不是只读属性
_defined 感觉没啥用
_keys 属性 可用于table.keys等函数动态获取对象的键名列表(例如动态生成键值对的外部JS对象可使用这个元方法返回成员名字列表
_startIndex 属性 用于table.eachIndex等函数动态指定数组的开始下标。
_get function(k,ownerCall) __getattr____getitem__ 如果读取表中不存在的键会触发_get元方法并返回值
_set function(k,v) __setattr____setitem__ 当你给表的一个缺少的键赋值时会触发_set元方法
_tostring function(...) __str____repr__ tostring(obj, ...)
_tonumber function() tonumber(obj)
_json function() web.json.stringify(obj),可返回一个可被转化为json的值。或者返回一个字符串和true
_toComObject 用于自定义一个表对象如何转换为 COM 对象,可定义为函数,也可以直接定义为对象
_eq function(b) __eq____ne__ ==!=,比较对象时,两个对象的_eq必须是同一个
_le function(b) __le____ge__ <=>=
_lt function(b) __lt____gt__ <>
_add function(b) __add__ +
_sub function(b) __sub__ -
_mul function(b) __mul__ *
_div function(b) __truediv__ /
_lshift function(b) __lshift__ << 左移
_rshift function(b) __rshift__ >> 右移
_mod function(b) __mod__ % 取模
_pow function(b) __pow__ **幂运算
_unm function() __neg__ - 负号
_len function() __len__ #取长运算符,Python中则为len函数
_concat function(b) ++ 连接运算符
_call function(...) __call__ 对象当函数来调用

属性元表

不仅可以给对象定义元表,也可以给对象的属性定义一个元表,有点类似于Python中的property,可以控制属性修改和获取的行为。

如果要看例子的话,可以在aardio的目录全局搜下@_metaProperty

以使用最多的属性text为例,基本每个控件都有一个text属性,你可以很方便的通过.text获取和修改空间显示的文字。

其实不用属性元表也能实现这个效果,代码如下:

import console; 
class staticText{
	getText = function(){
		..console.log("获取到界面文本内容")
	};
	setText = function(v){
		..console.log("将文本("+v+")显示到界面控件上")
	}
	@_meta;
}

namespace staticText{
    _meta = {
        _get = function(k){
        	if(k == "text"){
        	    return owner.getText();
        	}
        };
        _set = function(k,v){
        	if(k == "text"){
        	    return owner.setText(v);
        	}
        }    
    }
}

s = staticText()
console.log(s.text);
s.text = "修改文本";
console.pause(true);

但是如果属性多了的话,就需要一堆的if来判断属性,所以aardio作者就引入了metaProperty这个功能。这样写的代码看起来更简洁和清晰,用法如下:

import console; 
import util.metaProperty;

class staticText{
	getText = function(){
		..console.log("获取到界面文本内容")
	};
	setText = function(v){
		..console.log("将文本("+v+")显示到界面控件上")
	}
	@_metaProperty;
}

namespace staticText{
    _metaProperty = ..util.metaProperty(
        text = {
            _get = function(){
            	return owner.getText();
            };
            _set = function(v){
            	return owner.setText(v);
            }
        };
        // 可以写其他属性
    );
    // 可以打印下_metaProperty看看
    ..console.dump(_metaProperty);
}

s = staticText()
console.log(s.text);
s.text = "修改文本";
console.pause(true);

本文由博客一文多发平台 OpenWrite 发布!

热门相关:强宠头号鲜妻:陆少,滚!   翻天   一级BOSS:你结婚,我劫婚   神医娘亲之腹黑小萌宝   神医娘亲:腹黑萌宝赖上门