表( table )是 aardio 中唯一的复合数据类型。除了非复合的基础数据类型以外,aardio 中几乎所有的复合对象都是表,即使是变量的命名空间也是表。
表的本质是一个集合(collection),可以用于容纳其他的数据成员,并且表也可以嵌套的包含其他的表(table),在aardio里表几乎可以容纳一切其他对象。
如果我们把字符串、buffer、数值、函数、指针.....这些基础数据类型比喻为盘子中的菜,那么表(table)这样的复合数据类型就是装菜的盘子,如果没有了盘子,所以就没有办法愉快的吃菜了。
aardio 中的表可以包含不定个数的成员,每个成员都由一个键值对组成(键用来指定成员的名称,值用来指定成员的值)。“键”可以是任何除 null 以外的数据类型。“值”也可以是任意数据类型,当值为 null 时表示删除该成员。table 也允许嵌套,可以在 table 元素中包含 table 。
通常把“键”放在索引操作符[]中来索引一个元素的值,这时候键又称为“下标”或“索引”。例如 tab["键"] 或 tab[1],[]则被称为下标操作符。也可以把一个符合标识符命名规则的键名放在成员操作符.后面,例如 tab.key , tab.key2 。
表包含的成员类型主要分为哈希表与有序数组,表可以同时包含这两种类型的成员。
哈希表(无序集合)
哈希表用于包含不定个数的键值对成员,使用哈希算法建议键与值之间的映射以实现高效的元素查询。
哈希表描述的数据结构通常也可以称为字典(dictionaries)、映射(map)、关联数组(associative arrays)、对象(object)...... 等等。当然这些数据结构在不同语言中存在不同实现,也可能不会用到哈希算法,但描述的数据结构相似。
哈希表使用哈希算法存取,不会按其他特定顺序排序(在代码中添加成员的顺序将被忽略)。
在遍历哈希表时的顺序并不一定会保持代码中书写、添加表成员的顺序,因此我们把这种集合称为无序集合。哈希表的优势是查找速度非常快,即使表包含的成员非常多,仍然可以快速地访问指定的成员。
下面是一个创建哈希表的例子:
var tab = {
a = 123;
str = "字符串";
[123] = "不符合变量命名规则的键名应放在下标内。";
["键 名"] = "不符合变量命名规则的键名应放在下标内。";
键名 = {
test = "表也可以包含表";
}
}
用 {} 表示创建一个表,表成员写在{} 内部, 使用
aardio 允许用 , 号代替 ; 号分隔表成员 ,并且允许用 : 替代 = 分隔键值对,也允许在表中用引号(双引号、单引号、反引号 )包含的字符串表示键名。因此可以使用类 JSON 的语法定义表对象,例如:
var tag ={"name1":123,"name2":456}
数组(有序集合) #
如果表中不定个数的成员的“键”是从 1 开始、有序、连续的数值,那么这些成员构成一个有序的 稠密数组( dense array )。 在 aardio 中未加特别说明时“数组”都是指这种稠密数组。
创建数组的示例:
var array = {
[1] = 123;
[2] = "数组的值可以是任何其他对象";
[3] = { "也可以嵌套包含其他表或数组"}
}
上面我们将数值键放在下标[]中,用分号;分隔数组成员(也可以使用逗号,分隔数组成员),用等号=分隔键值对。
表构造器中稠密数组的数值键可以省略不写,直接用出现的位置顺序作为数值索引,示例:
var array = {
123,456,789,"其他数组值"
}
表对象可以同时包含稠密数组成员与其他类型的键值对,即使不包含数组成员,我们也可以将表作为空数组处理。
如果用 [] 包围数组则可以创建纯数组对象,例如 [1,2,3]。纯数组仍然是一个表,但纯数组构造器 [] 内不能再用 [] 包围一个表达式作为元素的键。详细说明请参考 纯数组
如果目的只是创建稠密数组,则应当总是使用
[]构造纯数组,用{}去创建一个单纯的稠密数组是没有必要的。
稠密数组可以使用 for 循环( 或者 table.eachIndex )有序地遍历成员,示例:
import console;
var array = [
123,
"数组的值可以是任何其他对象",
[ "也可以嵌套包含其他表或数组"]
]
//遍历数组成员, 用 #array 取数组长度
for(i=1;#array;1){
console.dump(i,array[ i ]);
}
//使用 table.eachIndex 也可以遍历数组
for i,v in table.eachIndex(array){
console.dump(i,v);
}
console.pause();
表的构造器是用一对大括号 {} 包含零个或多个用分号;分隔的元素(键值对或数组成员),使用等号=分隔键值对。
import console;
//空表
var tab = {}
//哈希表、对象
var object = {
key1 = "字符串";
key2 = 123;
[123] = "不符合变量命名规则的键名应放在下标内。";
["键 名"] = "不符合变量命名规则的键名应放在下标内。";
}
//稠密数组成员可以省略数值索引,以出现位置作为索引(起始索引为 1 )
var array = {
123;456;789;"其他值"
}
console.pause(true);
表可以包含不同类型的数据,可以同时包含键值对与数组成员。
如果表的键名不符合标识符命名规则或者不是字符串,则必须写在下标内,例如:
var object = {
[123] = "不符合变量命名规则的键名应放在下标内。";
["键 名"] = "不符合变量命名规则的键名应放在下标内。";
}
```a
aardio 中的表允许用 `,` 号代替 `;` 号分隔表成员 ,并且允许用 `:` 替代 `=` 分隔键值对,也允许在表中用引号(双引号、单引号、反引号 )包含的字符串字面量表示键名。因此可以使用类 JSON 的语法定义表对象,例如:
```aardio
var tag ={"name1":123,"name2":456}
表中如果直接用数值字面量表示索引,则后面必须紧跟一个冒号(不能用等号替代), 示例:
var tag ={999:"稀疏数组成员";1;2;3}
表构造器 {} 内表示键值对的不同方法总结:
{1;2;3} ,可用分号或逗号分隔数组成员。{name="value";name2="value2"}, 可使用 : 号代替 = 号分隔键值对。{"name":123;"name2":789}, 也可以使用 : 号或 = 号分隔键值对。{1;2;3;999:"稀疏数组成员"}, 这种方式不能用 = 号分隔键值对。[] 作为键,例如:{ ["name"]="value";["name"]="value2"}。如果表的成员是一个函数,且函数体是一个语法块,则可以省略表分隔符,例如:
var object = {
func1 = function(){}
func2 = function(){}
}
不能在表构造器后直接使用成员操作符或下标,例如 {}.member 这样写是错的。
使用 [] 构造纯数组示例:
var array = [
123,456,789,"其他数组值" //可直接包含省略数值索引的稠密数组成员
]
纯数组( pure-array ) 是一种特殊的表对象(pure-array table ) 。 纯数组的数据类型仍然是表( table ), 各种特性与表相同,同样可以包含稠密数组成员、稀疏数组成员、以及其他类型的键值对成员。
纯数组与普通表的主要区别在于构造方式以及序列化行为。
用 [] 作为数组构造器时,内部的语法与 {} 构造器基本相同,主要区别是:
[] 构造器内部将 [] 包围的成员也识别为纯数组,但 {} 构造器内部 [] 包围的表达式表示元素的 键。
[ 1,2,3,["嵌套数组",4,5,6] ] ,纯数组错误写法: [ 1,2,3,["键"]="值"] , 在 [] 构造器内 [] 包围的成员不是键。{ 1,2,3,["键"]=["数组值"] },表的错误写法: { 1,2,3,["嵌套数组",4,5,6] } , 在 {} 构造器内 [] 包围的成员不是数组。[] 构造器内部允许包含名值对,但不允许包含原生静态类型成员,也就是说 [] 不能用于构造结构体(但结构体中的原生数组字段值可以是纯数组)。纯数组虽然也是表,也能包含非稠密数组类型的成员,但因为纯数组里不能用 [] 包围一个表达式作为成员的键,可以直接写在 [] 构造器内的是键的类型是有限的。纯数组构造器内可以包含下面三种键值对:
[name="value",name2="value2"], 可使用 : 号代替 = 号分隔键值对。["name":123,"name2":789], 也可以使用 : 号或 = 号分隔键值对。[1,2,3,999:"稀疏数组成员"], 这种方式不能用 = 号分隔键值对。纯数组的序列化行为与普通表也略有不同:
[] 包围键,而普通表则用 [] 包围字符串键。: 号分隔键值对,,不用 [] 包围键,而普通表则用 [] 包围稀疏数组的数值键。: 号分隔非稠密数组成员的键与值,并使用逗号分隔所有元素。而普通表在序列化时使用 = 号分隔键值对,并使用分号分隔所有元素。其他创建纯数组的方式:
table.isArray 函数用于检查参数是否纯数组 ,table.isArrayLike 函数的检测条件则更宽松,参数是纯数组或包含稠密数组成员、或者使用元属性声明为数组、来自外部接口可识别处理为稠密数组的表都会返回 true 。
使用 JSON.stringify 序列化纯数组,即使纯数组为空也会返回 []。
而其他普通表对象,则需要使用 table.isArrayLike 判断如果是类稠密数组对象则序列化为 JSON 数组。
如果目的只是创建稠密数组,则应当总是使用
[]构造纯数组,用{}去创建一个单纯的稠密数组是通常是没有必要的。
稀疏数组可以理解为无序集合(哈希表)的一个特例。
如果表中包含的成员使用了数值作为键,但是多个成员的键并不是从1开始的连续数值 - 则构成稀疏数组。
如果表中包含了稀疏数组 - 也就是说成员的数值键(索引)包含不连续的、中断的数值,那么不应当作为有序数组使用。
在 aardio 一般我们提到的数组 - 如果未加特别说明则是特指稠密数组(不包含稀疏数组)。aardio 中几乎所有针对数组操作的函数或操作符 - 如果未加特别说明都要求参数是有序的稠密数组。
至于稀疏数组我们将其作为一般的哈希表操作就可以了,不要用普通的数组函数去操作稀疏数组。
下面的数组就包含了 null 值,属于数值键(索引)不连续的稀疏数组:
var sparseArray = { "值:1", null, "值:2", "值:4", null, null }
null 值就是没有值,所以在表的尾部写 null 值是无意义的,正确写法如下:
var sparseArray = { "值:1", null, "值:2", "值:4" }
表中间的 null 值仅仅是占位作用,因为 null 仍然是表示不存在的值,以上的表等价写法如下:
var sparseArray = {
[1] = "值:1";
[2] = "值:2";
[4] = "值:4";
}
这种稀疏数组不应当作为有序的稠密数组使用,也不应当使用 # 操作符去获取稀疏数组的长度。
稀疏数组可以用 table.range() 获取最小索引、最大索引。
也可以使用 table.eachArgs() 遍历稀疏数组。
示例: #
import console.int;
var sparseArray = { "值:1", null, "值:2", "值:4" }
//获取稀疏数组长度
var min,max = table.range(sparseArray);
//遍历稀疏数组(可用于遍历函数的不定参数)
for i,v in table.eachArgs(sparseArray) {
console.log(i,v)
}
使用 [] 操作符构造的数组称为纯数组,是 aardio 中专用于操作稠密数组的类型。
aardio 中所有内置函数与标准库函数如果返回新的稠密数组,在没有特殊原因或特殊说明时返回的一定是纯数组类型。
类数组(array-like table) 💡
如果对象是一个纯数组,或者是包含稠密数组成员的表则统称为 类数组。
table.isArrayLike 函数用于检测对象是否类数组,检测步骤如下:
# 操作符获取表中稠密数组元素个数得到非零值则返回 true 。类数组适合使用基于数值范围的 for 循环直接遍历元素,也可以用 # 操作符直接获取稠密数组元素个数。
if( table.isArrayLike(tab) ){
print("tab 包含稠密数组成员");
//循环输出所有稠密数组成员
for(i=1;#tab;1){
print(tab[i]);
}
}
可以使用 table.type 函数判定表对象是否伪数组,
table.type(tab) 用于检测参数指定的表对象的首选操作类型(preferred type)。
如果参数 @tab 不是表(table)则返回 null,否则返回字符串 "array" 或者 "object"。
如果 table.type 函数返回 "array" 则表明表对象期适合作为伪数组使用 table.len, table.eachIndex` 等函数操作。
table.parseValue, table.strip, JSON.stringify 等函数会使用 table.type 判定是否需要将传入对象转换为数组。
table.type 的检测顺序为:
# 操作符取对象的稠密数组元素个数返回非零值则返回 "array"。🅰 示例 1:
var arr = ["a","b",length:3]
for i,v in table.eachIndex(arr){
print(i,arr[i]);
}
上面的代码控制台输出了:
1 a
2 b
3 null
可以看到 table.eachIndex 或 table.len 可以识别自定义的数组长度(即使对象是一个真正的纯数组)。
🅰 示例 2:
//伪数组
var pseudoArray = {
@{
length = 20;
_startIndex = 10;
_get = lambda (i) i * 2;
_set = function(i,v){ error("只读数组")}
}
}
//遍历数组
for i,v in table.eachIndex(pseudoArray){
print(i,v);
}
//JSON 库兼容伪数组
import JSON;
print( JSON.stringify(pseudoArray) );
//将伪数组转换为纯数组
var pureArray = table.parseValue(pseudoArray);
print(pureArray);
函数调用参数有且只有一个使用 {} 构造器构建的 table 参数(并且表不是结构体)时,并且第一个出现的成员是用 = 分隔的名值对(并且键名不在 [] 内),就可以省略外层的 {} 。
{}。{} [] 内 则不能省略外层的 {} 。例如以下 aardio 代码:
func( { k = 123, k2=456, 123 } )
可以省略外层的 {} 写成如下格式:
func( k = 123, k2 = 456, 123 )
注意上面的参数是一个表对象,虽然这样看起来很像其他编程语言里的"命名参数",但实际上 aardio 里没有"命名参数"。
但 aardio 确实可以用这种语法模拟其他语言的命名参数,例如 aardio 调用 Python 的示例:
import py3;
var requests = py3.import("requests");
var ses = requests.Session();
var res = ses.$get(verify=false,"https://www.aardio.com");
上面的代码 ses.$get(verify=false,"https://www.aardio.com") 里面就用 verify=fals 指定了命名参数。
aardio 实际上是执行了 ses.$get( { verify=false,"https://www.aardio.com" } ) ,传过去的调用参数实际是一个表对象,然后由 aardio 的 py3 扩展库内部转换为了 Python 的命名参数。
print 函数默认会将第一个参数指定的纯数组序列化后输出,前提是 print 函数未被重写且未进入 HTML 模板输出模式。其他参数或其表对象仅调用 tostring 转换为字符串后输出。
示例:
//在控制台输出 [1,2,3]
print([1,2,3]);
//在控制台输出类似 table: 041405C8 的字符串
print({k=1;k2=2;k3=3});
console.log 函数会将所有参数中的纯数组序列化后输出,其他参数仅调用 tostring 转换为字符串后输出。
示例:
import console;
console.log([1,2,3]); //输出 [1,2,3]
console.log({k=1;k2=2;k3=3}); //仅输出类型与地址
console.pause();
console.dump 函数默认会序列化并输出所有表参数, 参数为 .NET 对象时或 COM 对象时则会调用 com.DumpTypeInfo 获取并输出对象的 COM 类型信息。
示例:
import console;
console.dump([1,2,3]); //输出 [1,2,3]
console.dump({k=1;k2=2;k3=3}); //序列化后输出,列出所有键值
console.pause();
console.dumpTable() 函数则会调用 util.table.stringify 序列化并且添加格式化缩进后输出表的值,util.table.stringify 仅转换表或嵌套表中的文本、数值、布尔值等纯值对象。
console.dumpJson() 函数则会将表转换为带有格式化缩进的 JSON 然后再输出到控制台。这个函数的输出的缩进格式可读性更好一些,但仅输出 JSON 支持的数据类型,JSON 区分数组与对象,对于 table.isArrayLike() 函数判断为数组、纯数组、类数组的对象则转换为 JSON 数组。
console.varDump() 函数则会调用 tostring 函数将表的所有键值对转换为字符串输出,这个函数不会输出嵌套表包含的内容,也不会调用序列化函数。但 console.varDump() 会输出表的全部键值,不会有任何遗漏,并且 console.varDump() 会输出结构体的大小 。
在访问表中的元素时,可以用元素的键作为下标查询元素的值,例如:
//创建表
var tab = {};
//下标为字符串"x",键名为字符串"x",值为数值200。
tab["x"] = 200;
/下标为数值1,值为字符串 "Hello, World!"
tab[1] = "Hello, World!";
如果键名是合法的标志符,则可以用"."成员符访问。例如 tab["x"] = 200 也可以写为 tab.x = 200。
也可以使用直接下标 [[]] 读写表的成员,直接下标使用两对中括号包含要查询的索引表达式。对于一般的表直接下标与下标的作用是一样的,区别在于普通下标( [] )或成员操作符( . )会触发表的元方法(如果定义了元方法),而直接下标( [[]] )不会触发表的元方法。
示例:
//创建表
var tab = {
//定义元表
@{
_get = function(k){
return k + "的值"
}
}
};
import console
//输出了 "xyz的值"
console.log( tab["xyz"] );
//无输出值,直接下标不触发元方法
console.log( tab[["xyz"]] )
console.pause()
当我们将表元素赋值为 null 会删除这个元素。例如:
tab.x = null; //删除tab.x
aardio 允许在下标操作符中用逗号分隔多个索引值。
示例:
multiDimensionalArray[0,0] = "值"
上面的代码等价于下面的代码:
//注意方括号中间有一个空格以避免包含数组的双层 [] 被识别为直接下标操作符。
multiDimensionalArray[ [0,0] ] = "值"
aardio 的多项索引实际等价于构造一个纯数组( pure-array )作为索引。
下标包含多项索引时第一个索引必须是单个标识符或字面值而不能是复合表达式,后面的其他索引则无限制。例如
obj[row,col]语法正确,而obj[pos.row,pos.col]存在语法错误,改为obj[ [pos.row,pos.col] ]就不会报错了。
目标对象可以在 _get 或 _set 元方法自定义对象如何处理这样的多项索引( 也就是以数组为索引 )。
aardio 中的 COM 接口、.NET 接口已经自动支持上述的多项索引,当索引为数组时 COM 与 .NET 接口都可以自动将其展开为多维索引。
更多关于下标操作符与多项索引的内容请参考:下标操作符,多项索引
表对象( table )的成员名称如果首字符为下划线,并且长度大于 1 个字节并小于 256 个字节,则是一个只读成员( readonly member )。
示例:
var tab = {
_readonlyMember = "值不可修改";
member = "值可以修改";
}
//没有修改值,被忽略
tab._readonlyMember = "值不可修改";
//不能修改只读成员,报错。
tab._readonlyMember = "新的值";
要特别注意,表的只读成员并不要求是合法标识符,例如:
tab["_ (不是合法标识符)"] = 1;
tab["_ (不是合法标识符)"] = 1234;//报错,不能修改常量
在对象的元表中指定元属性 _readonly = false 可禁用读成员保护,这样就可以自由修改名字以下划线开头的表成员。如果在表对象的元表中不设置 _readonly 则默认启用只读成员保护(所有名字以下划线开头的成员禁止修改非 null 值)。
如果元表中设置 _readonly 为任何非 null 值都会被强制转换为 false, 该值一旦设置以后即不可修改,如果希望启用只读成员保护,唯一的方法就是不设置该属性。
全局表( global )将元属性 _readonly 设置为任何值都会被忽略,global 的只读成员保护总是启用状态。
请参考:
遍历表中的全部元素
import console;
var tab = { a111="字符串"; b2=123; c="字符串2"; d=23; e=56; 78; 99; 123; 0 }
for k,v in tab {
/*
k为键,v 是匹配的值,
在这里键值并不会按上面创建表的键值顺序输出
*/
console.log(k,v);
};
console.pause(true);
如果表只包含有序数组(“键”是从1开始、有序、连续的数值),则 for in 语句会按数组顺序循环。
如果表还包含非数组成员,则需要改用计数循环才能顺序遍历数组成员。示例:
import console;
var array = {
a="字符串";b=123;c="字符串2";d=23;e=56;
78; 99; 123; 0
}
//使用 #array 取数组长度
for( i=1; #array;1){
//i为当前数值索引,tab[ i ]为当前值
console.log( i ,array[ i ] );
}
//也可以用 while var 语句模拟 for 循环语句遍历数组
while( var i = 0; i++ ; i <= #array ) {
console.log( i ,array[ i ] );
}
console.pause(true);
遍历稀疏数组的方法请参考: table.eachArgs() 示例 。
使用 table.sort 可以重新排序有序数组(不适用于稀疏数组)。
函数原型:
table.sort(array[, compareProc])
说明:
排序 array 参数指定的数组,此函数无返回值。
默认较小的值排在前面,可选用 compareProc 参数自定义比较函数。
比较函数应使用 < 或 >操作符比较元素的大小。两元素相等时不能返回 true,因此应避免使用 <= 或 >= 操作符比较大小。
compareProc 需要比较的是 owner 参数与第一个参数,返回 true 表示 owner 应排序在前。
示例:
var array = {1,2,3}
//排序
table.sort(array,function(next){
//较大的值应排序在前
return owner > next
})
上面的自定义排序结果为 {3,2,1} , 较大的值排序在前。
哈希表的用途是快速查找值,无序正是他的最大优点(索引快如闪电)。
数组的排序功能与表的快速查找值的优势是可以相互结合的。
示例:
import console;
//无序的哈希表
var tab = {
c = "3";
b = "2";
a = "1";
}
//有序的数组
var keys = {
"c","b","a"
}
//用数组排序键名
table.sort(keys)
//顺序遍历键名数组
for(i,k in keys){
//再到哈希表中去查找值
console.log(k,tab[k])
}
console.pause(true);
aardio 提供了一个原理类似的 table.eachName 函数可以对表的键名排序然后遍历表的所有元素。
示例:
import console;
//无序的哈希表
var tab = {
c = "3";
b = "2";
a = "1";
}
//排序哈希表
for k,v in table.eachName(tab){
console.log(k,v)
}
console.pause(true);
table.eachName 函数可以指定排序函数与键名数组,具体请参考该函数参考文档。
我们可以把多个变量用一对花括号包含起来以转换为数组,也可以使用 table.unpack 函数将数组转换为多个变量。
示例:
import console;
var func = function(a,b,...){
//将多个变量与不定参数合并为数组,返回多个值的 ... 必须放在最后面
var args = {a,b,...}
//展开数组
var a,b,c,d = table.unpack(args);
//合并返回值,返回多个值的 table.unpack(args) 必须放在最后面
var args2 = {a,b,table.unpack(args)}
//返回多个值
return table.unpack(args2)
}
//合并函数的多个返回值到数组
var arr = { func(1,2,3,4,5) }
//输出数组
console.dumpTable(arr);
console.pause();