对象

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

相关话题:

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

Result := IsObject(expression)

对象类型包括:

对对象的支持需要 [AHK_L 31+], 但是一些特性可能需要更后面的版本.

目录

基本用法

简单数组 [v1.1.21+]

创建数组:

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

获取项:

Value := Array[Index]

对项进行赋值:

Array[Index] := Value

使用 InsertAt 方法插入一个或多个项到指定索引:

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

使用 Push 方法追加一个或多个项:

Array.Push(Value, Value2, ...)

使用 RemoveAt 方法移除项:

RemovedValue := Array.RemoveAt(Index)

使用 Pop 方法移除最后一项:

RemovedValue := Array.Pop()

如果数组不是空的, 那么 MinIndexMaxIndex/Length 分别返回数组中当前使用的最小和最大的索引. 因为最小的索引几乎总是 1, 所以 MaxIndex 通常返回项目的数量. 如果没有整数键, MaxIndex 返回空, 而 Length 返回 0. 循环遍历数组的内容可以通过索引或 For 循环实现. 例如:

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

; 从 1 依次递加到数组的项目数:
Loop % array.Length()
    MsgBox % array[A_Index]

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

关联数组 [v1.1.21+]

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

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

使用 Delete 方法移除项:

RemovedValue := Array.Delete(Key)

枚举项:

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

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

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

对象 [AHK_L 31+]

对于所有类型的对象, 符号 Object.LiteralKey 能用于访问属性, 数组元素或方法, 其中 LiteralKey 是标识符或整数, Object 是任意表达式. 标识符是不带引号的字符串, 可以包含字母数字字符, 下划线和 [v1.1.09+] 非 ASCII 字符. 例如, match.Pos 等同于 match["Pos"]arr.1 等同于 arr[1]. 圆点后不能有空格.

示例:

获取属性:

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.RemoveAt(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] 这样的复合赋值.

[v1.1.20+]: 当获取或设置属性时, 参数可以省略. 例如, x[,2]. 脚本可以利用这一点来定义属性元函数的参数的默认值. 方法名称也可以完全省略, 如 x[](a) 所示. 脚本可以利用这一点来定义 __Call 元函数的首个参数的默认值, 因为它并不提供其他的返回值. 注意, 这并不同于 x.(a), 而相当于 x[""](a). 如果在调用 COM 对象时省略了属性或方法的名称, 它的 "默认成员" 将被调用.

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

扩展用法

函数引用 [v1.1.00+]

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

Func := Func("MyFunc")

可使用如下语法通过引用来调用函数:

RetVal := %Func%(Params)     ; 需要 [v1.1.07+]
RetVal := Func.Call(Params)  ; 需要 [v1.1.19+]
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].Call("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 将作为参数被传递.

自定义对象

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

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

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() 时, 它的 this 参数包含 other 而不是 thing 的引用.

[v1.1.00+]

从根本上讲, "类"是具有相同属性和行为的一类事物. 由基类原型定义了一系列属性和行为的对象, 就可以被称为 对象. 为了方便, 用 "Class" 关键字定义基对象可以像下面这样:

class ClassName extends BaseClassName
{
    InstanceVar := Expression		; 实例变量(实例属性)
    static ClassVar := Expression	; 静态变量(类属性)

    class NestedClass			; 嵌套类
    {
        ...
    }

    Method()				; 方法, 类定义中的函数称作方法
    {
        ...
    }

    Property[]  			; 属性定义, 方括号是可选的
    {
        get {
            return ...
        }
        set {
            return ... := value		; value 在此处为关键字, 不可用其他名称
        }
    }
}

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

因为类是通过变量引用的, 类名不能在同一个上下文中同时用于引用类和创建一个单独的变量(比如保存类的一个实例). 例如, box := new Box 将会用其本身的实例替换 Box 中的类对象. [v1.1.27+]: 每当尝试覆盖一个类时, #Warn ClassOverwrite 允许在加载时显示一个警告.

在本文档中, "class" 这个单词表示一个类对象是由 class 构造的.

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

Instance Variables(实例变量)[v1.1.01+]

一个 实例变量 是类的每个实例都拥有独立的副本(实例是从类派生的每个对象). 他们如同普通赋值一样被声明, 但 this. 前缀被忽略(仅限于类体内部时):

InstanceVar := Expression

这些声明在使用 new 关键字建立类的新实例时被计算. 处于此原因方法名 __Init 被保留, 脚本中不可使用. __New() 方法在所有此类声明计算完毕后被调用, 包括在基类中定义的那些. 表达式 可以通过 this 访问其他实例变量和方法, 但其他所有的变量引用都假定为全局的.

要访问实例变量, 总是要指定目标对象; 例如, this.InstanceVar.

[v1.1.08+]: 支持形如 x.y := z 的声明语法, 假设 x 已在类中声明. 例如, x := {}, x.y := 42 声明了 x 并初始化了 this.x.y.

Static/Class Variables(静态/类变量)[v1.1.00.01+]

静态/类变量属于类, 且可被派生对象继承(包括子类). 和实例变量一样声明, 但使用 static 关键字:

static ClassVar := Expression

静态声明仅在自动执行段按他们在脚本中出现的顺序计算一次. 每个声明保存值到类对象中. 表达式 中的任何变量引用都假定为全局的.

要对类变量赋值, 必须总是指定类的完整名称; 例如, ClassName.ClassVar := Value. 如果对象 xClassName 派生的实例对象, 且 x 本身没有 "ClassVar" 键, 那么 x.ClassVar 可以用来动态获取 ClassName.ClassVar 的值. 不过, x.ClassVar := y 的值保存在 x, 而不是 ClassName 中.

[v1.1.08+]: 支持形如 x.y := z 的声明语法, 假设 x 已在类中声明. 如: static x:={},x.y:=42 声明了 x 并初始化了ClassName.x.y.

Nested Classes(嵌套类)

嵌套类定义允许类对象存储到另一个类对象中而不作为单独的全局变量. 在上面的例子中, class NestedClass 创建了一个对象并把它保存到 ClassName.NestedClass.
子类可继承 嵌套类 或使用自己的嵌套类覆盖它(所以 new this.NestedClass 可用于实例化任何可用的嵌套类).

class NestedClass
{
    ...
}

方法

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

Method()
{
    ...
}

在方法的内部, 伪关键字 base 可用于在派生类中访问父类中的同名方法或属性. 例如, base.Method() 在类定义中将会调用 BaseClassName 中定义的 Method 版本. 元函数不会被调用; 其他情况下, base.Method() 的表现类似 BaseClassName.Method.Call(this). 就是说,

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

Properties(属性)[v1.1.16+]

属性定义允许当脚本获取或设置一个指定键时调用一个方法.

Property[]
{
    get {
        return ...
    }
    set {
        return ... := value
    }
}

Property 是用户定义的名称, 用于标识属性. 如, obj.Property 将调用 get, 而 obj.Property := value 将调用 set. 在 getset 内, this 指向被引用的对象. set, value 中包含正要赋予的值.

可在属性名右后使用方括号包裹以传递参数, 可用于定义及调用中. 除了使用方括号这点不同, 属性的参数和方法的参数定义方法完全一样 - 可选参数, ByRef 和可变参数也都支持.

getset 的返回值, 成为引用属性的子表达式的结果. 如, val := obj.Property := 42 存储 set 的返回值至 val.

每个类可定义部分或完整的属性. 如果一个类覆盖了属性, 可用 base.Property 访问定义于其基类中的属性. 如果 getset 未定, 会交由基类处理. 如果 set 未定义, 且未被元表或基类处理, 赋予的值被存储于对象中, 相当于禁用了属性.

内部 getset 是独立的两方法, 故不可共享变量(除非存储于 this中).

Meta-functions(元方法) 提供了广泛的控制属性访问, 方法调用的机制, 但更复杂及易错.

创建和销毁

每当使用 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)
    }
}

__Delete 不可被任何含有 "__Class" 键的对象调用. Class objects(类对象) 默认包含这个键.

如果类有定义这些方法的父类, 则通常应该调用 base.__New()(带有适当的参数) 和 base.__Delete(). 否则, 只调用方法的最派生的定义, 不包括目标对象本身中的任何定义.

[v1.1.28+]: 如果在 __Delete 执行时抛出了异常或运行时错误, 并且未在 __Delete 中处理, 则它就像从一个新线程调用 __Delete. 也就是说, 显示一个错误对话框并 __Delete 返回, 但是线程不会退出(除非它已经退出). 在 v1.1.28 之前, 未处理的异常会导致不一致的行为.

元函数(Meta-Functions)

方法语法:
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.base, obj.base.base 或类似的基中定义.

元函数的定义一般和方法一样, 但不遵循相同的规则(除了被脚本显式调用的时候). 它们必须在基对象中定义; 目标对象本身的任何定义都会被忽略. 适用于目标对象的 __Get, __Set 和 __Call 的每个定义都会根据下面的规则自动调用, 而且不应该调用 base.__Get(key) 或类似的方法. __New__Delete 必须在基对象中定义, 但其他方面的行为就像方法一样.

注意: AutoHotkey v2 用更传统的方法替换了元函数.

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

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

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

如果操作仍未得到处理,

已知限制:

动态属性

属性语法可用于定义属性, 对每次读写值进行设置, 但前提是必须知道每个属性才能定义单独的脚本. 而 __Get__Set 可用于管理动态属性.

例如, 一个"代理"对象可通过发出网络请求来创建(或者是其他通道). 远程服务器必须回应一个属性的值, 然后代理就会返回这个值给调用者. 虽然每个属性名称都是提前知道的, 但也不必为每个属性都定义一个逻辑, 因为每个属性所做的事都一样(发送一个网络请求). 元函数接受属性名称作为参数, 所以是这种情况的最佳解决方案.

元函数 __Get__Set 的另一种用途是用相同的逻辑处理一组相关的属性. 下面的例子实现了一个 "Color" 对象, 它拥有 R, G, B 和 RGB 属性, 但只有 RGB 属性的值是实际保存的:

red  := new Color(0xff0000), red.R -= 5
cyan := new Color(0), cyan.G := 255, cyan.B := 255

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
    }
    
    static Shift := {R:16, G:8, B:0}

    __Get(aName)
    {
        ; 注意: 如果这里用 this.Shift 将导致死循环! 因为 this.Shift 将递归调用 __Get 元方法.
        shift := Color.Shift[aName]  ; 将位元数赋值给 shift.
        if (shift != "")  ; 检查是否为已知属性.
            return (this.RGB >> shift) & 0xff
        ; 注意: 这里用 'return' 可终止 this.RGB 属性调用逻辑.
    }

    __Set(aName, aValue)
    {
        if ((shift := Color.Shift[aName]) != "")
        {
            aValue &= 255  ; 截取为适合的范围.

            ; 计算并保存新的 RGB 值.
            this.RGB := (aValue << shift) | (this.RGB & ~(0xff << shift))

            ; 'Return' 表示一个新的 键值对 被创建.
            ; 同时还定义了 'x := clr[name] := val' 中的 'x' 所保存的值是什么:
            return aValue
        }
        ; 注意: 这里用 'return' 终止 this.stored_RGB 和 this.RGB 的逻辑.
    }

    ; 元函数可以混合多个属性:
    RGB {
        get {
            ; 返回它的十六进制格式:
            return format("0x{:06x}", this.stored_RGB)
}
        set {
            return this.stored_RGB := value
        }
}
}

然而, 还可以用属性语法实现一个中央方法, 来替代本例中这种调用相同代码逻辑的一组属性. 由于使用元函数出错的风险较大, 应该尽量避免使用(见上面代码中的注意).

对象作为函数

对于利用对象实现函数的基本思路, 请参考函数对象章节.

函数对象还可以实现元函数的功能, 效果和前面几节中定义动态属性一样. 尽管推荐用属性语法替代, 下面的例子演示了如何利用元函数的潜能实现新的概念或结构, 甚至改变脚本代码的结构.

; 此示例需要 FunctionObject class 才能工作.
blue := new Color(0x0000ff)
MsgBox % blue.R "," blue.G "," blue.B

class Properties extends FunctionObject
{
    Call(aTarget, aName, aParams*)
    {
        ; 如果该属性保存了一个半属性的定义则调用它.
        if ObjHasKey(this, aName)
            return this[aName].Call(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 使用基本引用计数机制来自动释放对象使用的资源. 脚本作者不应该显式地调用这种机制, 除非打算直接处理未托管的对象的指针.

目前在 AutoHotkey v1.1 中, 表达式创建的临时引用(并不会在任何地方保存) 在使用后立即释放. 比如, Fn(&{}) 传递了一个无效的地址给函数, 由于 {} 返回临时引用, 导致取址运算之后被立即释放.

如果希望在对象的最后一个引用被释放后运行一段代码, 可通过 __Delete 元函数实现.

已知限制:

操作系统会在程序退出时自动回收对象占用的内存, 如果对象的所有引用已经被释放, 那么 __Delete 不会被调用. 所以当引用对象被提前释放而不是通过操作系统自动回收时, 可能造成比较严重的后果, 比如产生临时文件. (译者注: 也就是程序如果因为类似死循环这种情况所造成的意外崩溃, 可能导致某些资源的内存无法被操作系统自动回收. 所以老大在这里强调的意思是让咱们小心使用元函数.)

对象的指针

在一些罕见的情况中, 可能需要通过 DllCall() 传递对象到外部代码或把它存储到二进制数据结构以供以后检索. 可以通过 address := &object 来检索对象的地址; 不过, 这样实际上创建了一个对象的两个引用, 但程序只知道对象中的一个. 如果对象的最后一个 已知 引用被释放, 该对象将被删除. 因此, 脚本必须设法通知对象它的引用增加了. 有两种方法实现:

; 方法 #1: 显式地增加引用计数.
address := &object
ObjAddRef(address)

; 方法 #2: 使用 Object(), 增加一个引用并返回地址.
address := Object(object)

这个函数也可以把地址转换回引用:

object := Object(address)

无论用的上述哪种方法, 脚本都需要在完成对象的引用之后通知对象:

; 减少对象的引用计数, 以允许它被释放:
ObjRelease(address)

一般来说, 对象地址的每个新副本都应该被视为对象的另一个引用, 所以脚本必须在获得副本之后立即调用 ObjAddRef(), 并在丢弃副本之前立即调用 ObjRelease(). 例如, 每当通过类似 x := address 这样复制地址时, 就应该调用一次 ObjAddRef. 同样的, 当脚本使用 x 完时(或者用其他值覆盖 x), 就应该调用一次 ObjRelease.

注意, Object() 函数甚至可以在对象创建之前就可以使用, 比如 COM 对象File 对象.