对象

AutoHotkey 中的对象是抽象的数据类型,它提供了三种基本功能:

对象 引用 是指向特殊对象的指针或 "句柄". 和字符串和数字一样, 对象引用可以存储到变量中, 传递给函数或从函数返回以及保存到对象中. 在从一个变量复制引用到另一个后, 例如 x := y , 两个变量都引用相同的对象.

IsObject 可以用来确定一个值是否为对象:

Result := IsObject(expression)

对象类型包括:

目录

基本用法

简单数组

创建数组:

Array := [Item1, Item2, ..., ItemN]
Array := Array(Item1, Item2, ..., ItemN)

获取项:

Value := Array[Index]

对项进行赋值:

Array[Index] := Value

附加项:

Array.Insert(Value)

在指定的索引插入一项或多项:

Array.Insert(Index, Value, Value2, ...)

移除项:

RemovedValue := Array.Remove(Index)

如果数组不是空的, 那么 MinIndexMaxIndex 分别返回数组中当前使用的最小和最大的索引. 由于最小的索引几乎总是 1, 所以 MaxIndex 经常返回项目数. 对数组内容进行依次循环可以通过索引或 For 循环实现. 例如:

array := ["one", "two", "three"]

; 从 1 到项目数进行重复:
Loop % array.MaxIndex()
    MsgBox % array[A_Index]

; 枚举数组内容:
For index, value in array
    MsgBox % "Item " index " is '" value "'"

关联数组

关联数组是包含唯一键集合和值集合的对象, 其中每个键和一个值关联. 键可以为字符串, 整数或对象, 而值可以为任何类型. 关联数组可以用如下方法创建:

Array := {KeyA: ValueA, KeyB: ValueB, ..., KeyZ: ValueZ}
Array := Object("KeyA", ValueA, "KeyB", ValueB, ..., "KeyZ", ValueZ)

使用 {key:value} 表示法时, 对于仅由单词字符组成的键, 其两边的引号标记是可选的. 可以使用任何表达式作为键, 但使用变量作为键时, 它必须包围在小括号中. 例如, {(KeyVar): Value}{GetKey(): Value} 都是合法的.

获取项:

Value := Array[Key]

对项进行赋值:

Array[Key] := Value

移除项:

RemovedValue := Array.Remove(Key)

枚举项:

array := {ten: 10, twenty: 20, thirty: 30}
For key, value in array
    MsgBox %key% = %value%

关联数组可以是稀疏分布的 - 即 {1:"a",1000:"b"} 仅包含两个键值对, 而不是 1000.

到现在, 您也许已经注意到关联数组使用与简单数组非常相似的语法. 事实上, 在 v1.x 中它们是相同的东西. 然而, 把 [] 视为简单线性数组有助于保持其作用清晰, 并且改善您脚本与 AutoHotkey 未来版本的兼容性, 未来版本中可能改变实现方式.

对象

获取属性:

Value := Object.Property

设置属性:

Object.Property := Value

调用方法:

ReturnValue := Object.Method(Parameters)

使用可推算的方法名调用方法:

ReturnValue := Object[MethodName](Parameters)

COM 对象和用户定义对象的一些属性可以接受参数:

Value := Object.Property[Parameters]
Object.Property[Parameters] := Value

相关: 对象, 文件对象, Func 对象, COM 对象

已知限制:

释放对象

脚本不会显式的释放对象。当到对象的最后一个引用被释放时,会自动释放这个对象。当某个保存引用的变量被赋为其他值时,会自动释放它原来保存的引用。例如:

obj := {}  ; 创建对象。
obj := ""  ; 释放最后一个引用,因此释放对象。

同样地,当另一个对象的某个字段被赋为其他值或从对象中移除时,保存在这个字段中的引用会被释放。这同样适用于数组,因为它是真正的对象。

arr := [{}]  ; 创建包含对象的数组。
arr[1] := {}  ; 再创建一个对象,隐式释放第一个对象。
arr.Remove(1)  ; 移除并释放第二个对象。

由于在释放一个对象时,到这个对象的所有引用都必须被释放,所以包含循环引用的对象无法被自动释放。例如,如果 x.child 引用 yy.parent 引用了 x,则清除 xy 是不够的,因为父对象仍然包含到这个子对象的引用,反之亦然。要避免此问题,请首先移除循环引用。

x := {}, y := {}             ; 创建两个对象。
x.child := y, y.parent := x  ; 创建循环引用。

y.parent := ""               ; 在释放对象前必须移除循环引用。
x := "", y := ""             ; 如果没有上一行,则此行无法释放对象。

想了解更多高级用法和细节,请参阅引用计数

备注

语法

对于所有类型的对象都支持数组语法 (方括号) 和对象语法 (句点).

同时, 对象引用自身也可以用在表达式中:

如果在不期望对象的地方使用对象, 那么它被视为空字符串. 例如, MsgBox %object% 显示空的 MsgBox 且 object + 1 产生空字符串. 由于这种特性可能会变化, 所以不要依赖它.

当方法调用紧接着赋值运算符, 那么它等同于用参数来设置属性. 例如, 下面的方式是等同的:

obj.item(x) := y
obj.item[x] := y

还支持复合赋值例如 x.y += 1--arr[1].

对于哪种值可以在通过 [], {}new 运算符创建的对象中作为键使用的一些限制:

扩展用法

函数引用 [v1.1.00+]

如果变量 func 包含一个函数名, 此函数可以通过两种方式进行调用: %func%()func.(). 然而,由于前者每次都需要解析函数名,所以多次调用时效率低下。为了改善性能, 脚本可以获取到函数的引用并保存以供后面使用:

Func := Func("MyFunc")

通过引用调用函数时, 必须使用下面的语法:

RetVal := Func.(Params)

有关函数引用的附加属性的详细信息,请参阅 Func 对象

已知限制:

数组嵌套

在 AutoHotkey 中可以通过透明地把数组存储到其他数组中来支持“多维”数组。例如, 表格可以表示为行数组, 这里每个行自身是一个列数组. 此时, xy 列的内容可以用以下两种方法的其中一个进行设置:

table[x][y] := content  ; A
table[x, y] := content  ; B

如果 table[x] 不存在, AB 在两个方面有区别:

类似 table[a, b, c, d] := value 这样的多维赋值按以下方式处理:

这种行为仅适用于由脚本创建的对象, 而不适合特殊的对象类型例如 COM 对象或 COM 数组.

函数数组

函数数组是包含函数名或引用的简单数组. 例如:

array := [Func("FirstFunc"), Func("SecondFunc")]

; 调用每个函数, 传递 "foo" 参数:
Loop 2
    array[A_Index].("foo")

; 调用每个函数, 隐式地把数组自己作为参数传递:
Loop 2
    array[A_Index]()

FirstFunc(param) {
    MsgBox % A_ThisFunc ": " (IsObject(param) ? "object" : param)
}
SecondFunc(param) {
    MsgBox % A_ThisFunc ": " (IsObject(param) ? "object" : param)
}

为了向后兼容,如果array[A_Index]含有函数名而非函数引用时,在第二种形式中array将不会作为参数被传递。但是, 如果 array[A_Index] 继承array.base[A_Index], 那么 array 将作为参数被传递.

自定义对象

由脚本创建的对象可以不包含预定义结构。相应的每个对象可以从其对象中继承属性和方法(在其他地方被称为“原型”或“类”)。还可以随时添加或移除对象中的属性和方法,这些改变会影响它的所有派生对象。更多复杂或专用方案,可通过定义元函数来覆盖它所派生对象的标准行为。

对象只是普通对象,通常有两种创建方法:

class baseObject {
    static foo := "bar"
}
; 或
baseObject := {foo: "bar"}

要继承其他对象来创建新对象,脚本可以赋值为 base 属性或使用 new 关键字

obj1 := Object(), obj1.base := baseObject
obj2 := {base: baseObject}
obj3 := new baseObject
MsgBox % obj1.foo " " obj2.foo " " obj3.foo

可随时重新赋值对象的 base,这样能有效覆盖该对象继承的所有属性和方法。

原型

原型或 base 对象和其他任何对象一样创建和操作. 例如, 带有单属性和单方法的普通对象可以这样创建:

; 创建对象.
thing := {}
; 存储值.
thing.foo := "bar"
; 通过存储函数引用创建方法.
thing.test := Func("thing_test")
; 调用方法.
thing.test()

thing_test(this) {
   MsgBox % this.foo
}

调用 thing.test() 时, thing 会自动被插入到参数列表的开始处. 然而, 为了能够向后兼容, 通过名称 (而不是通过引用) 把函数直接保存到对象中 (而不是继承自基对象) 时这种情况不会发生. 按照约定, 通过结合对象 "类型" 和方法名来命名函数.

如果另一个对象继承自某个对象,那么这个对象被称为原型

other := {}
other.base := thing
other.test()

此时, otherthing 继承了 footest. 这种继承是动态的, 所以如果 thing.foo 被改变了, 这改变也会由 other.foo 表现出来. 如果脚本赋值给 other.foo, 值存储到 other 中并且之后对 thing.foo 任何改变都不会影响 other.foo. 调用 other.test() 时, 它 这里的 参数包含到 other 而不是 thing 的引用.

[v1.1.00+]

为了简便,可以使用“class”关键字创建基对象。基类定义看起来类似这样:

class ClassName extends BaseClassName
{
    InstanceVar := Expression
    static ClassVar := Expression

    class NestedClass
    {
        ...
    }

    Method()
    {
        ...
    }
}

在加载脚本时, 这里会创建对象并将其存储到全局 (而在 v1.1.05+, 超级全局) 变量 ClassName. 在 v1.1.05 之前, 要在函数中引用这个类, 如果函数不处于 假设全局模式, 那么需要进行声明, 例如 global ClassName. 如果存在 extends BaseClassName,那么 BaseClassName 必须为另一个类的完整名称(从 v1.1.11 开始,对于早期的版本它们所定义的无关紧要)。每个类的完整名称存储在 object.__Class.

类定义可以包含变量声明, 方法定义和内嵌的类定义.

    InstanceVar := Expression

实例变量 声明 [v1.1.01+] 在每次使用 new 关键字创建类的新实例时都进行求值. 在调用 __New() 方法之前对所有这样的声明进行求值,包括在基类中定义的那些。表达式 可以通过 this 访问其他实例变量和方法, 但其他所有的变量引用都假定为全局的. 要访问实例变量, 总是要指定目标对象; 例如, this.InstanceVar.

    static ClassVar := Expression

类变量 声明 [v1.1.00.01+] 仅在 自动执行段 之前根据它们在脚本中出现的顺序被求值一次. 每个声明保存值到类对象中. 如同类对象包含的任何值或方法, 这个值可以被派生对象继承. 表达式 中的任何变量引用都假定为全局的. 要访问类变量, 总是要指定类或或派生对象; 例如, ClassName.ClassVar.

    class NestedClass
    {
        ...
    }

嵌套类 定义允许类对象存储到另一个类对象中而不作为单独的全局变量. 在上面的例子中, class NestedClass 创建了一个对象并把它保存到 ClassName.NestedClass. 因此, NestedClass 可以被派生自 ClassName 的任何类或对象继承.

    Method()
    {
        ...
    }

方法 定义看起来和函数定义相同. 每个方法都有一个名称为 this 的隐藏参数, 它实际上包含了指向继承自此类的对象的引用. 不过, 它也可以包含指向此类自身或派生类的引用, 取决于如何调用这个方法. 方法被 通过引用 存储到类对象中.

除了隐藏参数 this, 方法定义中还可以使用伪关键字 base 以调用包含了此方法定义的类的基类. 例如, 在上面的方法中 base.Method() 相当于 BaseClassName.Method.(this). 注意它与this.base.base.Method()在其他两方面有区别:

base 仅在后面跟着点 . 或方括号 [] 时才有特殊含义, 所以像 obj := base, obj.Method() 这样的代码将不起作用. 通过把base赋为非空值可以禁用它的特殊行为,但是不建议这样做。因为变量 base 必须为空, 所以如果脚本中不含有 #NoEnv 指令那么性能可能会降低.

创建和销毁

每当使用 new 关键字 [需要 v1.1.00+] 创建派生对象时, 那么调用由其基对象定义的 __New 方法. 此方法可以接受参数, 初始化对象并通过返回值覆盖 new 运算符的结果. 销毁对象时, 则调用 __Delete. 例如:

m1 := new GMem(0, 20)
m2 := {base: GMem}.__New(0, 30)

class GMem
{
    __New(aFlags, aSize)
    {
        this.ptr := DllCall("GlobalAlloc", "uint", aFlags, "ptr", aSize, "ptr")
        if !this.ptr
            return ""
        MsgBox % "New GMem of " aSize " bytes at address " this.ptr "."
        return this  ; 使用“new”运算符时可以省略此行。
    }

    __Delete()
    {
        MsgBox % "Delete GMem at address " this.ptr "."
        DllCall("GlobalFree", "ptr", this.ptr)
    }
}

元函数

方法语法:
class ClassName {
    __Get([Key, Key2, ...])
    __Set([Key, Key2, ...], Value)
    __Call(Name [, Params...])
}

函数语法:
MyGet(this [, Key, Key2, ...])
MySet(this [, Key, Key2, ...], Value)
MyCall(this, Name [, Params...])

ClassName := { __Get: Func("MyGet"), __Set: Func("MySet"), __Call: Func("MyCall") }

元函数定义了向目标对象中请求不存在的键时的行为。例如, 如果 obj.key 尚未赋值, 那么它会调用 __Get 元函数. 同样地, obj.key := value 调用 __Setobj.key() 调用 __Call. 这些元函数(或方法)需要在 obj.baseobj.base.base 或类似的基中定义。

当脚本获取、设置或调用目标对象中的键不存在时,按如下方式调用基对象:

如果元函数把匹配的键保存在对象中但未 return,则行为类似于该键原本就存在于对象中。使用 __Set 的示例,请参阅子类化数组的数组

如果操作仍为得到处理,则检查是否有内置方法或属性:

如果操作仍未得到处理,

已知限制:

动态属性

__Get__Set 可以用来实现在某些情况下可以计算或限制的值的属性. 例如, 可以用来实现含 R, G, B 和 RGB 属性的 "Color" 对象, 这里只有 RGB 值是实际存储的:

red  := new Color(0xff0000), red.R -= 5
cyan := new Color(0x00ffff)

MsgBox % "red: " red.R "," red.G "," red.B " = " red.RGB
MsgBox % "cyan: " cyan.R "," cyan.G "," cyan.B " = " cyan.RGB

class Color
{
    __New(aRGB)
    {
        this.RGB := aRGB
    }

    __Get(aName)
    {
        if (aName = "R")
            return (this.RGB >> 16) & 255
        if (aName = "G")
            return (this.RGB >> 8) & 255
        if (aName = "B")
            return this.RGB & 255
    }

    __Set(aName, aValue)
    {
        if aName in R,G,B
        {
            aValue &= 255

            if      (aName = "R")
                this.RGB := (aValue << 16) | (this.RGB & ~0xff0000)
            else if (aName = "G")
                this.RGB := (aValue << 8)  | (this.RGB & ~0x00ff00)
            else  ; (aName = "B")
                this.RGB :=  aValue        | (this.RGB & ~0x0000ff)

            ; 必须使用 'Return' 表示不应该创建一个新键值对.
            ; 这里也定义了在 'x := clr[name] := val' 中将要存储到 'x' 中的内容:
            return aValue
        }
    }
}

对象函数化

生成一个调用(例如obj.func(param))时,obj.func可以包含函数名或对象。如果obj.func包含对象,则可以用obj代替方法名调用此对象,像在(obj.func)[obj]()那样。在大多数情况下obj.func[obj]不存在,作为替代而调用obj.func的__Call 元函数。这可以用来以一种抽象的方式改变函数调用的行为, 如以下示例所示:

; 为函数数组创建原型.
FuncArrayType := {__Call: "FuncType_Call"}
; 创建函数数组.
funcArray := {1: "One", 2: "Two", base: FuncArrayType}
; 把此数组作为方法创建对象.
obj := {func: funcArray}
; 调用方法.
obj.func("foo", "bar")

FuncType_Call(func, obj, params*)
{
    ; 调用函数列表.
    Loop % ObjMaxIndex(func)
        func[A_Index](params*)
}

One(param1, param2) {
    ListVars
    Pause
}
Two(param1, param2) {
    ListVars
    Pause
}

把这种技术和类定义组合在一起为定义类似于前面部分的动态属性提供了便利的方法:

blue := new Color(0x0000ff)
MsgBox % blue.R "," blue.G "," blue.B

class Properties
{
    __Call(aTarget, aName, aParams*)
    {
        ; 如果 this 属性对象包含此半属性的定义, 那么调用它.
        ; 小心别使用 this.HasKey(aName), 因为这样会递归进 __Call.
        if IsObject(aTarget) && ObjHasKey(this, aName)
            return this[aName].(aTarget, aParams*)
    }
}

class Color
{
    __New(aRGB)
    {
        this.RGB := aRGB
    }

    class __Get extends Properties
    {
        R() {
            return (this.RGB >> 16) & 255
        }
        G() {
            return (this.RGB >> 8) & 255
        }
        B() {
            return this.RGB & 255
        }
    }

    ;...
}

子类化数组嵌套

多参数赋值 例如 table[x, y] := content 会隐式地创建一个新对象, 这个新对象一般不含基, 因此没有自定义方法或特殊行为. __Set 可以用来初始化这样的对象, 如下所示.

x := {base: {addr: Func("x_Addr"), __Set: Func("x_Setter")}}

; 赋值, 隐式调用 x_Setter 来创建子对象.
x[1,2,3] := "..."

; 获取值并调用示例方法.
MsgBox % x[1,2,3] "`n" x.addr() "`n" x[1].addr() "`n" x[1,2].addr()

x_Setter(x, p1, p2, p3) {
    x[p1] := new x.base
}

x_Addr(x) {
    return &x
}

由于 x_Setter 含有四个必需参数, 所以只有在有两个或更多键参数时才会调用它. 当上面的赋值出现时, 会发生下面的情况:

默认基对象

当非对象值用于对象语法时, 则调用 默认基对象. 这可以用于调试或为字符串, 数字和/或变量定义全局的类对象行为. 默认基可以使用带任何非对象值的 .base 进行访问; 例如, "".base. 尽管默认基无法像 "".base := Object() 这样进行 set, 不过它可以有自己的基如同在 "".base.base := Object() 中那样.

自动初始化变量

当使用空变量作为 set 运算的目标时, 它直接被传递给 __Set 元函数, 这样它就有机会插入新对象到变量中. 为简洁起见, 此示例不支持多个参数; 如果需要, 可以使用 可变参数函数 实现.

"".base.__Set := Func("Default_Set_AutomaticVarInit")

empty_var.foo := "bar"
MsgBox % empty_var.foo

Default_Set_AutomaticVarInit(ByRef var, key, value)
{
    if (var = "")
        var := Object(key, value)
}

伪属性

对象 "语法糖" 可以适用于字符串和数字.

"".base.__Get := Func("Default_Get_PseudoProperty")
"".base.is    := Func("Default_is")

MsgBox % A_AhkPath.length " == " StrLen(A_AhkPath)
MsgBox % A_AhkPath.length.is("integer")

Default_Get_PseudoProperty(nonobj, key)
{
    if (key = "length")
        return StrLen(nonobj)
}

Default_is(nonobj, type)
{
    if nonobj is %type%
        return true
    return false
}

注意也可以使用内置函数, 不过这时不能省略大括号:

"".base.length := Func("StrLen")
MsgBox % A_AhkPath.length() " == " StrLen(A_AhkPath)

调试

如果不希望把一个值视为对象, 每当调用非对象值可以显示警告:

"".base.__Get := "".base.__Set := "".base.__Call := Func("Default__Warn")

empty_var.foo := "bar"
x := (1 + 1).is("integer")

Default__Warn(nonobj, p1="", p2="", p3="", p4="")
{
    ListLines
    MsgBox A non-object value was improperly invoked.`n`nSpecifically: %nonobj%
}

实现

引用计数

当脚本不再引用对象时, AutoHotkey 使用基本引用计数结构来自动释放对象使用的资源. 脚本作者不应该明确调用此结构, 除非在直接处理指向对象的未托管指针时. 想了解更多细节,请参阅 ObjAddRef

; 增加对象的引用数以 "使其保持活跃":
ObjAddRef(address)
...
; 减少对象的引用数, 这样可以释放它:
ObjRelease(address)

然而, 最初通过 Object(obj) 获取地址时不需要使用 ObjAddRef.

通常每个新的对象地址副本都应被视为对象引用, 除非脚本会负责在适当时调用 ObjAddRef 和/或 ObjRelease. 例如, 每当通过类似 x := address 这样的方式复制地址时, 应调用 ObjAddRef 来增加引用数. 同样地, 每当含对象地址特殊副本的脚本结束时, 它应该调用 ObjRelease. 这样确保了当指向对象的最后一个引用失去时对象被释放 - 而不是在这之前.

要在指向对象的最后一个引用被释放时运行代码, 实现 __Delete 元函数.

已知限制:

尽管当程序退出时操作系统会回收对象所使用的内存, 但只有在指向对象的所有引用都释放时才会调用 __Delete. 这样释放了不会由操作系统自动回收的其他资源, 例如临时文件, 所以这很重要.

指向对象的指针

在一些罕见的情况中, 可能需要通过 DllCall 传递对象到外部代码或把它存储到二进制数据结构以供以后取回. 通过 x := &obj 可以获取对象的地址; 然而, 如果变量 obj 被清除, 此对象可能被过早释放. 为确保不发生这样的情况, 按上面演示那样使用 ObjAddRef 或按下面演示那样使用 Object():

address := Object(object)

此外, 还可以使用此函数把地址转换回引用:

object := Object(address)

在这两种情况中, 对象的 引用数 都会自动增加, 这样对象不会被过早释放.

注意此函数同样适用于不是由 Object() 创建的对象, 例如 COM 对象包装器文件对象.