对象

AutoHotKey

对象

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

  • 取值.
  • 设置值.
  • 调用方法 (即可以对目标对象进行某些操作的函数).

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

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

Result := IsObject(expression)

对象类型包括:

目录

基本用法

简单数组

创建数组:

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

获取项:

Value := Array[Index]

对项进行赋值:

Array[Index] := Value

插入一个或多个东东到给定序号位置:

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

追加一个或多个东东:

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

移除项:

RemovedValue := Array.RemoveAt(Index)

移除最后一项:

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 "'"

关联数组

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

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.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 未来版本的兼容性, 未来版本中可能改变实现方式.

对象

对于所有类型的对象, 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 对象

已知限制:

  • 当前 x.y[z]() 会被视为 x["y", z](), 这是不受支持的. 作为一种变通方法, (x.y)[z]() 首先计算 x.y, 然后把结果作为方法调用的目标. 请注意 x.y[z].Call() 没有这个限制, 因为对它求值的方式和 (x.y[z]).Call() 一样.

释放对象

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

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 := ""             ; 如果没有上一行, 则此行无法释放对象.

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

备注

语法

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

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

  • 当对象引用使用 = == != <> 中的一种比较运算符和其他值比较时, 仅在两个值都为指向相同对象的引用时它们才被视为相等的.
  • 进行逻辑运算时对象总是被视为 true, 例如在 if obj, !objobj ? x : y 中.
  • 使用 & 取址运算符可以获取对象的地址. 它从此对象创建时到此对象的最后一个引用被 释放 期间唯一标识此对象.

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

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

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

通用支持 x.y += 1--arr[1] 这样的复合赋值.

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

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

  • 整数键使用本机有符号整数类型存储. AutoHotkey 32 位支持使用从 -2147483648 到 2147483647 范围内的整数作为键. AutoHotkey 支持 64 位整数, 但仅 AutoHotkey 64 位版本才支持完整范围的整数在对象中作为键使用.
  • 由上面这点可知, 整数值的字符串格式不会被保留. 例如, x[0x10], x[16]x[00016] 都是等同的. 这点同样适用于不含小数点的数值型字符串.
  • 用引号括起来的原义字符串在 v1.x 中被视为纯非数值型, 所以 x[1]x["1"] 等同的. 同时, 如果原义字符串和另一个值串联在一起 (如同在 "0x" x), 结果被视为纯非数值型. 不过, 这不适用于变量, 所以 x[1]x[y:="1"] 是等同的. 此问题将在 v2 中解决, 所以脚本应避免使用用引号括起来的原义字符串作为键.
  • 不支持使用浮点数作为键 - 而是把它们转换成字符串. 在 v1.x 中, 浮点数文字保留它们原始的格式, 而纯浮点数 (例如 0+1.0Sqrt(y) 的结果) 被强制转换成当前的 浮点格式. 考虑到一致和清晰, 脚本中应避免使用浮点文字作为键.
  • 默认情况下, 关键字 "base" 用于获取或设置对象的 基对象, 所以不要在普通业务逻辑中用它保存普通的值. 然而, 如果用来保存其他意义的值的时候它就像普通字符串一样, 不要紧张. (例如 ObjRawSet(Object, "base", "")Object.SetCapacity("base", 0)), 这里的 "base" 只是普通的字符串而已.
  • 尽管 内置方法 的名称可以作为键名来使用, 这样将阻止同名对应的方法被调用, 比如一个键的名称是 "Length" , 那么你将无法调用 obj.Length() 方法. (除非正好键的值是对应方法的别名, 比如 ObjLength).

扩展用法

函数引用 [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)      ; 不推荐

关于函数对象引用的属性详细描述, 见 函数对象.

数组嵌套

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

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

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

  • A 失败而 B 会自动创建一个对象并把它存储到 table[x] 中.
  • 如果 tablebase 定义了 元函数, 可以用如下方式调用它们:
    table.base.__Get(table, x)[y] := content   ; A
    table.base.__Set(table, x, y, content)     ; B
    因此, B 可以让对象为全面赋值定义定制的行为.

类似 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 将作为参数被传递.

自定义对象

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

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

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()				; 方法, 类定义中的函数称作方法
    {
        ...
    }

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

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

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

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

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

一个 instance variable 实例变量 是类的每个实例都拥有独立的副本(实例是从类派生的每个对象). 他们如同普通赋值一样被声明, 但 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.Method() 总是调用基类的当前方法的定义, 即使 this 完全是从那个类派生的 sub-class 或其他类派生而来的.
  • base.Method() 隐式传递 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 (类对象) 默认包含这个键.

元函数 (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 或类似的基中定义.

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

  • 如果此基对象定义了相应的元函数, 那么调用它. 如果元函数明确 return , 则把返回值作为运算的结果 (不受调用元函数方式的影响) 并把控制权归还脚本. 其他情况继续按以下方法进行.

    Set: 如果元函数处理赋值, 则它应返回所赋的值. 这样允许赋值链, 如 a.x := b.y := z . 返回值可能与 z 的原始值不同 (即如果对所赋的值施加限制的话).

  • 在基类自身的区域中搜索键.
  • [v1.1.16+]: 如果找到指向属性的键, 且已实现 getset (依据当前需求), 引用属性并返回. 如果是方法调用, 则触发 get .
  • 如果未找到键, 则递归调用该基对象自己的基对象 (从继承列表顶部开始重复应用每个步骤). 如果仍未结束, 则再次搜索该基对象以寻找匹配键, 以防止元函数触发添加.

    为了顾及向下兼容性, 即便找到了此键仍然会执行 set 操作 (除非它的定义中实现了 set 的属性).

  • 如果在 getset 指定了多个参数且找到了键, 则检查其值. 如果那个值为对象, 则调用它处理剩余参数, 不做进一步处理.
  • 如果找到了键,
    Get : 返回值.
    Call : 尝试调用一个值, 传递目标对象为第一个参数 (this). 值应该是一个函数名或是一个 函数对象.

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

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

  • Get : 如果该键是"base", 则返回对象的基.
  • Set : 如果该键是"base", 则设置对象的基(如果值不是对象则移除).
  • Call : 适用时调用 内置方法.

如果操作仍未得到处理,

  • GetCall 返回空字符串.
  • Set : 如果只给出了一个键参数, 则保存键和值到目标对象中并返回所赋的值. 如果给出了多个参数, 则创建新对象并把首个参数作为键保存, 然后调用新对象处理剩余参数. (请参阅 数组的数组.)

已知限制:

  • 由于 return 等同于 return "", 所以可以使用 return 从元函数"退出"而不覆盖默认行为. (这种逻辑可能在未来的版本中改变)
  • 请参阅 Exit 限制.

动态属性

属性语法 可用于定义属性, 对每次读写值进行设置, 但前提是必须知道每个属性才能定义单独的脚本. 而 __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
        }
        ; NOTE: 这里用 'return' 终止 this._RGB 和 this.RGB 的逻辑.
    }
    
    ; 元函数可以混合多个属性:
    RGB {
        get {
            ; 返回它的十六进制格式:
            return format("0x{:06x}", this._RGB)
}
        set {
            return this._RGB := value
        }
}
}

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

对象作为函数

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

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

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 含有四个必需参数, 所以只有在有两个或更多键参数时才会调用它. 当上面的赋值出现时, 会发生下面的情况:

  • x[1] 不存在, 所以调用 x_Setter(x,1,2,3) (由于参数过少所以 "..." 不会被传递).
    • x[1] 被赋值为与 x 含有相同基的新对象.
    • 不返回任何值 – 赋值继续.
  • x[1][2] 不存在, 所以调用 x_Setter(x[1],2,3,"...").
    • x[1][2] 被赋值为与 x[1] 含有相同基的新对象.
    • 不返回任何值 – 赋值继续.
  • x[1][2][3] 不存在, 但由于 x_Setter 需要四个参数而这里只有三个 (x[1][2], 3, "..."), 所以不会调用它且赋值正常完成.

默认基对象

当非对象值用于对象语法时, 则调用 默认基对象. 这可以用于调试或为字符串, 数字和/或变量定义全局的类对象行为. 默认基可以使用带任何非对象值的 .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 对象.