# 对象

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

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

相关话题:

- 对象: 对象的一般解释.
- 对象协议: 关于脚本如何与对象交互的细节.

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

    Result := IsObject(expression)

对象类型包括:

- 对象 - 脚本化关联数组.
- 文件 - 为文件输入/输出提供接口.
- 函数对象 - Func, 绑定函数或用户定义的.
- ComObject - 包装 IDispatch 接口(COM 或 "自动化" 对象).

对对象的支持需要 [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()

如果数组不是空的, 那么 MinIndex 和 MaxIndex/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 对象

已知限制:

- 当前 `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` 引用 `y` 且 `y.parent` 引用了 `x`, 则清除 `x` 和 `y` 是不够的, 因为父对象仍然包含到这个子对象的引用, 反之亦然. 要避免此问题, 请首先移除循环引用.

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

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

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

### 备注

#### 语法

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

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

- 当对象引用使用 `=`, `==`, `!=` 或 `<>` 中的一种比较运算符和其他值比较时, 仅在两个值都为指向相同对象的引用时它们才被视为相等的.
- 进行逻辑运算时对象总是被视为 true, 例如在 `if obj`, `!obj` 或 `obj ? 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` 运算符创建的对象中可以将值作为键使用的一些限制:

- 整数键使用本机有符号整数类型存储. 32 位 AutoHotkey 支持使用从 -2147483648 到 2147483647 范围内的整数作为键, 并截断该范围以外的值. AutoHotkey 支持 64 位整数, 但仅 AutoHotkey 64 位版本才支持完整范围的整数在对象中作为键使用.
- 整数值的字符串格式不会被保留. 例如, 在 AutoHotkey v1 中, `0x10` 和 `16` 是两个不同的字符串(这与 AutoHotkey v2 不同), 但 `x[0x10]`, `x[16]` 和 `x[00016]` 是等同的, 因为只使用数字值.
- 不含小数点的数值型字符串被转换为纯整数, 所以受到上述的限制. 64 位范围之外的数字字符串可能产生不一致的结果.
- 用引号括起来的原义字符串在 v1.x 中被视为纯非数值型, 所以 `x[1]` 和 `x["1"]` 是 不 等同的. 同时, 如果原义字符串和另一个值串联在一起(如同在 `"0x" x`), 结果被视为纯非数值型. 不过, 这不适用于变量, 所以 `x[1]` 和 `x[y:="1"]` 是等同的. 此问题将在 AutoHotkey v2 中解决, 所以脚本应避免使用用引号括起来的原义数字作为键.
- 不支持使用浮点数作为键 - 而是把它们转换成字符串. 在 v1.x 中, 浮点数文字保留它们原始的格式, 而纯浮点数(例如 `0+1.0` 或 `Sqrt(y)` 的结果) 被强制转换成当前的浮点格式. 考虑到一致和清晰, 脚本中应避免使用浮点文字作为键.
- 默认情况下, 字符串键 "base" 对应对象的 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)      ; 不推荐

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

### 数组嵌套

AutoHotkey 通过显式地把数组存储到其他数组中来支持 "多维" 数组. 例如, 表格可以表示为行数组, 其中每一行本身是一个列数组. 在这种情况下, `x` 行 `y` 列的内容可以用以下两种方法的其中一个进行设置:

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

如果 `table[x]` 不存在, A 和 B 在两个方面有区别:

- A 失败而 B 会自动创建一个对象并把它存储到 `table[x]` 中.
- 如果 `table` 的 base 定义了元函数, 可以用如下方式调用它们:

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

## 自定义对象

由脚本创建的对象可以不包含预定义结构. 相反, 每个对象可以从其 `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()

此时, other 从 thing 继承了 foo 和 test. 这种继承是动态的, 所以如果 `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`. 如果对象 x 是 ClassName 派生的实例对象, 且 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. 在 get 或 set 内, `this` 指向被引用的对象. set, `value` 中包含正要赋予的值.

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

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

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

内部 get 与 set 是独立的两方法, 故不可共享变量(除非存储于 `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 Object(类对象) 默认包含这个键.

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

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

如果脚本被任何方式直接终止, 包括托盘菜单, ExitApp, 或 Exit(当脚本不是持续运行时), 任何尚未返回的函数都没有机会返回. 因此, 这些函数的局部变量所引用的任何对象都不会被释放, 所以 __Delete 也不会被调用.

当脚本退出时, 全局变量和静态变量所包含的对象会按照任意的, 实现定义的顺序自动释放. 当 __Delete 在这个过程中被调用时, 一些全局变量或静态变量可能已经被释放, 但对象本身包含的任何引用仍然有效. 因此 __Delete 最好是完全自包含的, 而不依赖于任何全局变量或静态变量.

### 元函数(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` 调用 __Set 而 `obj.key()` 调用 __Call. 这些元函数(或方法) 需要在 `obj.base`, `obj.base.base` 或类似的基中定义.

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

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

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

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

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

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

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

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

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

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

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

如果操作仍未得到处理,

- Get 和 Call 返回空字符串.
- 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
            }
            ; 注意: 这里用 '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` 含有四个必需参数, 所以只有在有两个或更多键参数时才会调用它. 当上面的赋值出现时, 会发生下面的情况:

- `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 对象.

dftg