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 發佈!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章