对象入门第一阶

供新手入门和老手参考的教程和相关资料,包括中文帮助
User avatar
amnesiac
Posts: 186
Joined: 22 Nov 2013, 03:08
Location: Egret Island, China
Contact:

对象入门第一阶

21 Aug 2014, 23:53

注:本文为三年前所写,之后 AutoHotkey_L 中的对象特性有所调整和增强,且我当时对对象的理解也处于摸石头过河的阶段,所以仅供参考。若在学习中遇到问题或想分享使用心得,欢迎回贴。本文需更新和补充。

导言:在这里,我把在学习对象过程中的感受和大家分享,以期抛砖引玉。有些地方可能理解有所偏差,请不吝赐教,遇到问题也请回复说明,这样可以让教程越来越完善。
为了研究一种事物,而把这个事物抽象为对象:把事物的特性表示为对象的属性,把事物能执行的操作表示为对象的方法。在内部,可以把对象理解为一种抽象的数据结构,这种数据结构的特征是多个键值对系列,后面的说明中将通过这条线把各种功能统一在一起。例如获取属性就像查字典,指明一个对象后指定属性名作为键,这样就能得到它的值了。
AutoHotkey 中的对象是基于原型而不是基于类,即对象可以从其原型或 base 对象中继承属性和方法,但不需要有预定义结构。在任何时候,还可以添加属性和方法到(或从中移除)一个对象或它所派生自的任何对象。不过,AutoHotkey 通过把类定义转化为普通对象来模仿类。由于这里的类在本质上仍是对象,所以在这个教程中只会在最后才讨论类的一些特殊之处。
此外,这里对对象的介绍基于 AutoHotkey_L 目前的版本,在其他语言中的对象某些特性可能不尽相同,在以后的版本中某些形式或特性也可能会改变,所以建议在学习过程中不必过于纠结细节,应该着眼于对对象本质的理解。
注:这里所讨论的对象仅指由 AutoHotkey 创建的对象,不包括 COM 对象、文件对象等。

简单数组
这是对象最基本的功能,即使用对象来存储、操作一系列数据。
可以用 Array() 创建简单数组:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

MyFruits := Array("apple", "banana")

获取数组中的元素,需要通过索引,注意这里的索引从 1 开始:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

Fruit := MyFruits[1]

修改数组中的元素,也是通过索引进行操作:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

MyFruits[1] := "pear"

此外,通过 Insert() 和 Remove() 分别可以插入和移除数组中的元素,而 MinIndex() 和 MaxIndex() 则可以获取数组中最小的索引和最大的索引值。
要查看整个数组的内容呢?可能会首先想到 Loop 循环,先看看下面的代码:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

MyFruits := Array("apple", "banana")
AllFruits := ""
Loop, % MyFruits.MaxIndex() {
AllFruits .= A_Index ": " MyFruits[A_Index] "`n"
}
MsgBox, % AllFruits

这里对整个数组进行循环,按顺序根据索引取出每个数组对应的元素,但必须注意到,这样遍历的前提是所有的索引为从1开始的整数,所以最大的索引是数组的元素个数。然而实际上这里的数组并未限制一定如此,看看下面的代码:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

MyFruits := Array("apple", "banana")
MyFruits[6] := "pear" ; 如果数组中尚不存在这个索引,那么此时将添加新的元素。

这时使用 Loop 进行遍历,结果中会产生许多空的元素,这时 MaxIndex() 也不再是数组元素个数了。所以,建议使用 For 进行遍历:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

MyFruits := Array("apple", "banana")
MyFruits[6] := "pear"
AllFruits := ""
for Index, Fruit in MyFruits {
AllFruits .= Index ": " Fruit "`n"
}
MsgBox, % AllFruits

即使不存在这里所述的特殊情况,也建议使用 For 进行遍历,因为 Loop 循环只是对于前面所说的特殊的前提下才有效的,后面将逐渐明白这一点。
这里这么详细的解释是为了让大家抛开之前对伪数组的认识,理解为什么这里的数组实际上就是对象,索引是键,元素是值。在创建时索引由内部自动分配,取值、设置值等是由对象内部机制实现的,在后面元函数部分还会涉及这点。

关联数组
如果已经理解简单数组实际是对象,那么关联数组就好理解了,因为我们直观看到的就是一系列键值对:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

MyFruits := Object(1, "apple", 2,"banana")
AllFruits := ""
for Index, Fruit in MyFruits {
AllFruits .= Index ": " Fruit "`n"
}
MsgBox, % AllFruits

使用 Object() 创建关联数组时,它的参数是一系列的键值。与前面的 Array() 不同的是,这里的键由我们指定而不是自动分配。所以实际上,我们可以使用任何表达式作为键。下面的代码中使用字符串作为键:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

MyFruits := Object("a", "apple", "b","banana")

当这里使用从 1 开始的整数作为键时,它就成了简单数组。下面两行代码效果相同,即它们创建的数组没有区别:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

MyFruits := Array("apple", "banana")
MyFruits := Object(1, "apple", 2,"banana")

与简单数组一样,对关联数组中的值进行操作也是通过键进行:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

Fruit := MyFruits["a"]
MyFruits["p"] := "pear"

Array() 与 Object() 两个函数在创建数组时除了对参数的处理有差异外,它们所创建的数组没有区别,例如 Array() 创建的数组之后也可以通过 MyFruits["p"] := "pear" 添加使用非数字键的键值对到数组中(这么说明更多是为了理解,若在使用时这样操作会把简单数组复杂化了)。
帮助中说到关联数组可以是稀疏分布的,实际上是指对象中的键可以稀疏分布,简单数组也是如此。另外,需要注意到,在这里使用 Loop 循环遍历数组已经行不通了。

对象的本质
前面的数组部分说明了,数组是键值对系列,如果学了 Python,会发现这里的数组和它的字典几乎一样,都是无序的键值对系列。现在说说这里的对象:
获取及设置属性:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

Value := Object.PropertyName ; 获取属性
Object.PropertyName := Value ; 设置属性

这里很好理解,也是键值对,属性是键,而属性的值则是与键对应的值。与前面所说的数组剩下的区别则是形式上的,看看下面的演示:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

MyFruits := Object("a", "apple", "b","banana")
MsgBox, % MyFruits.a
MyFruits.a := "pear"
if (MyFruits["a"] = MyFruits.a)
MsgBox, 两个值相等。

这两种形式都是等同的,所以刚才的 Object.PropertyName 也可以表示为 Object["PropertyName"]。这里顺便提到,一般而言使用句点语法书写比较简单,但对于某些比较特殊的属性名称只能使用方括号语法,例如MyFruits["1.1"],这时使用句点语法就不会按期望的进行解释了。
对于对象,除了属性,还可以调用方法:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

ReturnValue := Object.MethodName.(Parameters)

这种形式与刚才的属性形式类似,区别只是在后面多了句点连接传递给函数的参数,所以类似的可以变换成方括号语法形式:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

ReturnValue := Object["MethodName"].(Parameters)

它们只是形式上的区别,这里的方法名是键,那么值呢?注意不是返回值,而是指向函数的引用(调用这个方法时会把后面的参数传递给它指向的函数执行),即帮助中所说的 Func 对象:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

FuncRef := Func("FunctionName")

使用 Func() 函数创建到某个函数的引用,有了函数引用后,可以添加新的方法:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

Object.MethodName := FuncRef ; 添加新的方法
ReturnValue := Object.MethodName.(Parameters) ; 调用方法,即获取与 MethodName 对应的值,但这里的值为函数引用,所以他会直接执行这个函数引用所指向的函数。

这里进行小结,
  • 简单数组是一系列键值对,这些键是从1开始的连续整数;
  • 关联数组是一系列键值对,这些键可以是任意字符串;
  • 对象是一系列键值对,键是属性或方法,方法的值为函数引用;
所以,之前的所有内容可以归纳为:这里的数组实际上是对象,而对象本质上则是由一系列键值对组成的抽象数据结构,键几乎可以为任意表达式,而值可以为任意表达式(包括 Func 对象),对象的内置方法(Insert() 等)适用于所有的这些对象。所以在创建(即方括号、Array()、大括号和 Object())、添加或修改及调用(句点语法或方括号语法)等方面的不同只是可选的语法形式。对象的创建、通过键添加或修改值、内置方法及 For 的遍历都是由对象的内置机制实现的,之后在类的部分将涉及如何覆盖对象的内置实现机制。
理解了刚才的概括之后,我想很容易理解帮助中的数组嵌套和函数数组了:数组嵌套是一个数组的元素本身为一个数组,而函数数组则是数组的元素为函数引用。看帮助时稍留意一下的的形式就行了。

自定义对象
基对象
之前所述的对象的实现机制实际上是在 AutoHotkey 内部实现的,所以可以把 AutoHotkey 内部对象原型看成我们刚才操作的那些对象的基对象(当我们创建空对象时得到的对象与内部对象完全相同)。这里的自定义是指在内部对象原型基础上增加键值对,并且把产生的对象作为我们之后需要的对象的基对象:
内部对象
├─对象A
│ ├─对象A1
│ └─对象A2
└─对象B

看到这个分支图后,我想很容易理解这些对象之间的关系:对象 A 和对象 B 的直接的基对象为内部对象,而对象 A1 和对象 A2 的直接的基对象为对象 A,并且内部对象是所有对象的基对象(包括直接和间接的)。而这里讨论的对象是指类似于对象 A 这样的中间对象,看看下面的演示代码:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

ObjectA := Object("a", "apple")
ObjectA1 := {base: ObjectA} ; 这里的 base 键具有特殊含义,它所对应的值表示基对象
MsgBox, % ObjectA1.a

这里的 ObjectA 除含有内部对象的特性外,还添加了一对键值对。对象 ObjectA1 的基对象为 ObjectA,所以它具有后者的所有特性,当前所有这些特性都是在内部对象的基础上继承而来。通过这个例子,我们需要理解几个词语的含义:
  • 对象 ObjectA1 的基对象为 ObjectA
  • 相对的,对象 ObjectA1ObjectA 的子对象;
  • 对象 ObjectA1 继承自 ObjectA
  • 对象 ObjectA1ObjectA 对象派生而来。
这几个句子的含义没有区别,其中的基对象与子对象是相对而言的,基对象与其他语言中的父对象是相同的含义,都是指上一级对象。

继承
在自定义对象时,除了继承基对象的特性(继承它的键值对)外,还可以覆盖基对象的特性:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

ObjectA := Object("a", "apple", "b", "banana")
ObjectA1 := {b : "pear", base: ObjectA}
MsgBox, % ObjectA1.a
MsgBox, % ObjectA1.b ; 这个键的值被覆盖了

在这里覆盖的工作原理可以这样理解:在 ObjectA1 对象中获取 b 键对应的值时,首先看这个对象中是否存在 b 键,若有则获取对应的值,如果没有则查找这个对象的基对象(ObjectA1.base 即 ObjectA)是否存在这个键,若有则获取对应的值,否则继续查找 ObjectA 的基对象、它的基对象的基对象……,一直循环到找到对应的值,或者在查找到最底层的对象(即内部对象)时依然没找到,则返回空。这是继承的原理,这个特点是对象真正强大的地方。
通过自定义,甚至还可以覆盖对象的内置方法:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

ObjectA := {Insert : Func("MyInsert")}
ObjectA.Insert() ; 这里调用方法。
return

MyInsert() {
MsgBox, % "这是自定义的Insert()函数。"
}

由于 ObjectA 对象中 Insert() 方法被覆盖了,所以在调用这个方法时会忽略对象的内置方法。
由于可以动态添加或修改对象中的键值对,即可以动态添加或修改属性和方法,同时继承是动态的,所以修改基对象中的键值对时也会影响它的子对象。

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

ObjectA := Object("a", "apple", "b", "banana")
ObjectA1 := {base: ObjectA}
ObjectA.a := "pear"
MsgBox, % ObjectA1.a

不过如果子对象中添加了与基对象中相同的键,之后子对象中的这个键值对则不受基对象影响(彼此独立):

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

ObjectA := Object("a", "apple", "b", "banana")
ObjectA1 := {base: ObjectA}
ObjectA1.a := "pear"
ObjectA.a := "watermelon"
MsgBox, % "ObjectA对象中a键对应的值:" ObjectA.a "`n"
. "ObjectA1对象中a键对应的值:" ObjectA1.a

对于对象的继承,对于初学的人并不太容易理解,为此我制作了下面这个图:
2014-08-22 12 51 15.png
对象继承原理图

元函数
与 Insert() 等其他内置方法一样,元函数也是内置对象实现的方法,它们的特殊之处在于一般是隐式调用(尽管可显式调用,但一般不必要),而像 Insert() 这些内置方法是只能显式调用。还有一点区别是元函数的名称以双下划线开头,这一方面是不容易与用户自定义的方法冲突,另一方面是从名称上区别于其他内置方法。其他的一些区别将在后面说明。
对象的内置实现机制是由元函数实现的,元函数决定了在创建和销毁时进行的操作及遇到未知键时对象的处理方式,即当对象中不存在指定的键时,那么在取值时调用 __Get(),在赋值时调用 __Set(),而在执行方法时调用 __Call(),这里的调用都是隐式调用。所以在这里将通过自定义函数覆盖元函数的例子来讲解在对象中的元函数是如何起作用的,这里以覆盖 __Set() 元函数作为例子(如果认真看过帮助请先尝试自己写一个),下面先介绍存在问题的一些实现方式:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

ObjectA := {__Set: Func("MySet")}
ObjectA.a := "apple"
MsgBox, % ObjectA.a
return

MySet() {
MsgBox, % "这是自定义的取值函数。"
}

在这个例子中,在 ObjectA 对象中不存在 a 键,所以为 ObjectA.a 赋值时显然它应该调用 __Set() 元函数,那么为什么没有执行 MySet() 呢(没有显示消息框)?实际上此时调用了__Set(),但调用的不是 ObjectA 对象的 __Set(),而是它的基对象(此时为内部对象)的元函数,所以可以理解在 MsgBox, % ObjectA.a 的消息框中显示“apple”。
现在改进一下代码,把代替元函数的自定义函数放在基对象中:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

ObjectA := {base: {__Set: Func("MySet")}}
ObjectA.a := "apple"
MsgBox, % ObjectA.a
return

MySet() {
MsgBox, % "这是自定义的取值函数。"
}

这里在赋值时调用了它的基对象的 __Set(),所以执行了 MySet()。再修改一下在这个函数中为 a键赋值:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

ObjectA := {base: {__Set: Func("MySet")}}
ObjectA.a := "apple"
MsgBox, % ObjectA.a
return

MySet(this, key, value) {
MsgBox, % key ":" value
this[key] := value
}

这里 MySet() 的参数分别对应 ObjectA.a := "apple" 中的对象、键及值(即 this 的值为 ObjectAkey 的值为 a,而 value 的值为 apple),看到这里时已经执行过代码了吧?哈,进入无限循环了,不过真的需要感谢我,在 MySet() 中显示了一个消息框,这时只要您没有对这个消息框狂按确定,那么您的系统还是好好的(如果想看看效果,只需把 MsgBox, % key ":" value 去除后重新执行,这样对对象的理解也许会更深~)。现在解释进入无限循环的原因,其实很简单,因为执行 this[key] := value 时,实际上等于执行 ObjectA.a := "apple",因而陷入了隐式调用 ObjectA 的基对象中的 __Set() 即执行 MySet() 的循环。
所以在 MySet() 中不能使用 this[key] := value 进行赋值,现在修改为使用内置方法进行赋值:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

ObjectA := {base: {__Set: Func("MySet")}}
ObjectA.a := "apple"
MsgBox, % ObjectA.a
return

MySet(this, key, value) {
this.Insert(key, "这是自定义函数赋的值。")
MsgBox, % this[key] ; this.key 表示错误,因为这里的 key 不是键名,是包含键名的变量。
}

注:这里的 Insert() 方法会绕过 __Set(),所以这里不会陷入循环。
这里 MySet() 中的消息框可以看到已经正确赋值了,然而为什么在 MsgBox, % ObjectA.a 中却依然显示“apple”呢?思考一下,再调整代码:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

ObjectA := {base: {__Set: Func("MySet")}}
ObjectA.a := "apple"
MsgBox, % ObjectA.a
return

MySet(this, key, value) {
this.Insert(key, "这是自定义函数赋的值。")
MsgBox, % this[key] ; this.key 表示错误,因为这里的 key 不是键名,是包含键名的变量。
return
}

与上一段代码比较,这里只是在自定义函数体的最后加上了 return。原来如果在自定义函数中没有返回时,会继续调用基函数的基函数的元函数,并最终会调用内部对象的元函数,此时会赋值为“apple”而覆盖了 MySet() 中的赋值。这里的 MySet() 函数中包含了 return,所以直接返回,在 MsgBox, % ObjectA.a 中将显示“这是自定义函数赋的值。”。
在前面覆盖元函数的代码示例中,可以了解元函数的一些实现机制,并且看到了它与其他内置方法的另一区别:可以直接在当前对象中覆盖内置方法,而元函数则必须在它的基对象、基对象的基对象等中才能覆盖。最后看一个例子,加深在执行时调用顺序的印象:

Code: [Select all] [Expand] [Download] (Script.ahk)GeSHi © Codebox Plus


ObjectA1 := {a: "apple in ObjectA1", base: ObjectA} 修改为 ObjectA1 := {base: ObjectA} 后再次执行,看看效果。在执行 ObjectA1.a := "apple" 时首先看 ObjectA1 对象中是否有“a”键,若有则重新赋值后返回,否则接着执行,但只调用基对象的 __Set() 方法而不首先看基对象中是否含有这个键。可以看到它依次执行了 MySetInObjectA()MySetInBase() 函数,假设在下面还有多级基对象,则继续调用它们的 __Set(),一直到最后调用内部对象的元函数才返回,除非在某个基对象的 __Set() 已经返回了才不继续往下执行。
对于 __Get__Call 类似,调用自定义函数时首个参数为目标对象。而 __New()__Delete() 是在创建及销毁对象时调用的,在下面的例子中在创建派生对象时为派生对象添加一个键值对:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

ObjectA := {__New: Func("MyNew")}
ObjectA1 := new ObjectA("a", "apple")
MsgBox, % ObjectA1.a
return

MyNew(this, param1, param2) {
this.Insert(param1, param2)
return, this
}

所以通过 __New() 可以进行一些初始化操作,而 __Delete() 可以在对象销毁时回收资源,不再赘述。

函数对象和枚举器对象
对象入门的内容基本说完了,这里补充关于函数对象和枚举器对象的一些说明。它们都简单,不过在具体使用时容易产生一些误解。
从某个角度看,这两种对象有点类似,函数对象的一个共同点是实现了 __Call() 方法,可以通过这个对象调用对应的函数(其实是隐式调用这个方法);而枚举器对象则含有 _NewEnum() 方法,所以可以被枚举。

函数对象
函数对象也被称为函数引用,可通过如下方法获取内置或自定义函数的引用:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

FuncRef := Func("FunctionName")

通过函数引用调用相应的函数时,语法为:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

FuncRef.(Parameters)

即在函数引用与包围参数的小括号之间需要一个句点,这是由于句点之前是函数引用,而不是原义的函数名。如果不添加句点,则需要把函数引用包围在百分号之间,例如:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

StrLenRef := Func("StrLen")
MsgBox, % StrLenRef.("Hello, world!")
MsgBox, % %StrLenRef%("Hello, world!")

这里两种通过函数引用调用函数的形式效果一样。
不过,如果把键和函数引用的值保存在对象中,通过这个键调用对应的函数时,需要区分下面两种调用形式:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

ObjectA := {MyMethod: Func("MyFunction")}
MsgBox, % ObjectA.MyMethod.("这是测试参数")
MsgBox, % ObjectA.MyMethod("这是测试参数")
return

MyFunction(param1, params*) {
return, IsObject(param1) ? "第一个参数是对象" : param1
}

在这个例子中,第一个 MsgBox 语句比第二个多一个句点。前者表示通过函数名调用函数,所以函数接收到的参数列表即为调用时提供的参数列表。后者表示通过函数引用调用函数,此时会自动把当前对象插入到参数列表的开始处,所以此时函数接收到的第一个参数为当前对象,后面的参数才是我们调用时提供的参数。

枚举器对象
类似于函数对象,枚举器对象是指实现了 _NewEnum 方法的对象,所以这种对象是可枚举的(据观察可知,这种对象具有键值对结构)。因此,AutoHotkey 创建的对象和某些 COM 对象都是枚举器对象。下面简单说说枚举这种对象的一些方法。

直接调用 _NewEnum 这里显式调用 _NewEnum 方法来枚举对象。

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

MyFruits := Object("a", "apple", "b","banana", "p", "pear")
EnumObj := MyFruits._NewEnum()
While, EnumObj[key, value]
AllFruits .= key ": " value "`n"
MsgBox, % AllFruits

这段代码我想不需要解释。这里提到其他的书写形式:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

EnumObj := MyFruits._NewEnum()
EnumObj := ObjNewEnum(MyFruits)

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

While, EnumObj[key, value]
While, EnumObj.Next(key, value)

生成枚举器对象的两个语句含义相同,同样的,两个 while 语句也是如此。

For 循环对于枚举器对象使用这种方法枚举比较方便,建议使用。

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

MyFruits := Object("a", "apple", "b","banana", "p", "pear")
For key, value in MyFruits
AllFruits .= key ": " value "`n"
MsgBox, % AllFruits

返回的键值对是由实现决定的顺序。即返回的键值对顺序可能与我们定义时的顺序不同。执行下面这段演示代码:

Code: [Select all] [Download] (Script.ahk)GeSHi © Codebox Plus

MyFruits := Object("p", "pear", "b","banana", "a", "apple")
For key, value in MyFruits
AllFruits .= key ": " value "`n"
MsgBox, % AllFruits


小结
Spoiler
AutoHotkey 学习指南(Beauty of AutoHotkey)
I do not make codes, and only a porter of AutoHotkey: from official to Chinese, from other languages to AutoHotkey, and show AutoHotkey to ordinary users sometimes.

Return to “教程资料”

Who is online

Users browsing this forum: No registered users and 2 guests