aardio教程三) 元表、元方法

Python成长路發表於2024-03-20

前言

還有個迭代器,基礎語法基本已經說完了,後面想到啥再補充,之後的教程會從以下方面來講:

  • 基礎庫的使用,比如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 釋出!

相關文章