# 对象

注意: 该页面正在开发进程中.

对象 组合了多个 属性 和 方法:

相关话题:

- 对象: 对象的一般解释.
- 对象协议: 关于脚本如何与对象交互的细节.
- 函数对象: 可以被 调用 的对象.

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

    Result := IsObject(expression)

有关标准对象类型的列表, 请参阅内置类. 这包括两种基本类型:

- AutoHotkey 对象是对象类的实例. 它们支持专用的属性, 并具有发现哪些属性存在的方法. 从对象中派生了数组(线性数组), 映射(关联数组) 和所有用户自定义的和内置的类.
- COM 对象 如由 ComObject 创建的那些对象. 这些是由外部库实现的, 因此通常与 AutoHotkey 对象的行为不同. ComObject 通常表示实现 IDispatch 接口的 COM 或 "Automation" 对象, 但也用于包装特定类型的值以传递给 COM 对象和函数.

## 目录

- 基本用法 - 数组, 映射(关联数组), 对象, 释放对象
- 扩展用法 - 数组嵌套
- 自定义对象 - 创建基对象, 类, 创建和销毁, 元函数
- 原始值
- 实现 - 引用计数, 对象的指针

## 基本用法

### 数组

创建数组:

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

检索项目(或 数组元素):

    Value := MyArray[Index]

更改项目的值(`Index`(索引) 必须介于 1 和 Length(长度) 之间, 或等效的反向索引):

    MyArray[Index] := Value

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

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

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

    MyArray.Push(Value, Value2, ...)

使用 RemoveAt 方法移除项目:

    RemovedValue := MyArray.RemoveAt(Index)

使用 Pop 移除最后一项:

    RemovedValue := MyArray.Pop()

Length 返回数组中的项目数. 通过索引或 For-循环可以遍历数组的内容. 例如:

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

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

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

    ; 同样的事情再来一次:
    For value in MyArray
        MsgBox "Item " A_Index " is '" value "'"

### 映射(关联数组)

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

    MyMap := Map("KeyA", ValueA, "KeyB", ValueB, ..., "KeyZ", ValueZ)

检索一个项目, 其中 `键` 可以是变量或表达式:

    Value := MyMap[Key]

赋值项目:

    MyMap[Key] := Value

使用 Delete 方法移除项目:

    RemovedValue := MyMap.Delete(Key)

枚举项目:

    MyMap := Map("ten", 10, "twenty", 20, "thirty", 30)
    For key, value in MyMap
        MsgBox key ' = ' value

### 对象

对象可以有 属性 和 项目(如, 数组元素). 项目可以使用 `[]` 来访问, 如前面几节所示. 属性通常通过在一个点后面加上一个标识符(只是一个名称) 来访问. 方法 是可以被调用的属性.

示例:

检索或设置一个原义名称为 Property 的属性:

    Value := Object.Property

    Object.Property := Value

检索或设置通过计算表达式或变量来确定名称的属性:

    Value := Object.%Expression%

    Object.%Expression% := Value

调用一个原义名称为 Method 的属性/方法:

    ReturnValue := Object.Method(Parameters)

调用通过计算表达式或变量来确定名称的属性/方法:

    ReturnValue := Object.%Expression%(Parameters)

在检索或赋值属性时, 有些属性可以接受参数:

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

事实上, 数组索引语法 `MyArray[Index]` 实际上调用 `MyArray` 的 __Item 属性, 并将 `Index` 作为参数传递.

### 释放对象

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

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

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

## 扩展用法

### 数组嵌套

尽管不支持 "多维" 数组, 但是脚本可以组合多个数组或映射. 例如:

    grid := [[1,2,3],
             [4,5,6],
             [7,8,9]]
    MsgBox grid[1][3] ; 3
    MsgBox grid[3][2] ; 8

自定义对象可以通过定义一个 __Item 属性来实现多维支持. 例如:

    class Array2D extends Array {
        __new(x, y) {
            this.Length := x * y
            this.Width := x
            this.Height := y
        }
        __Item[x, y] {
            get => super[this.Width * (y-1) + x]
            set => super[this.Width * (y-1) + x] := value
        }
    }

    grid := Array2D(4, 3)
    grid[4, 1] := "#"
    grid[3, 2] := "#"
    grid[2, 2] := "#"
    grid[1, 3] := "#"
    gridtext := ""
    Loop grid.Height {
        y := A_Index
        Loop grid.Width {
            x := A_Index
            gridtext .= grid[x, y] || "-"
        }
        gridtext .= "`n"
    }
    MsgBox gridtext

真实的脚本应执行错误检查并覆盖其他方法, 如 __Enum 以支持枚举.

## 自定义对象

创建自定义对象通常有两种方法:

- 专用: 创建对象并添加属性.
- 委托: 在共享 基对象 或类中定义属性.

元函数可用于进一步控制对象的行为方式.

注意: 在本节中, 对象 是 Object 类的任何实例. 本节不适用于 COM 对象.

### 专用

属性和方法(可调用属性) 通常可以随时添加到新对象中. 例如, 一个具有一个属性和一个方法的对象可以这样构造:

    ; 创建对象.
    thing := {}
    ; 存储值.
    thing.foo := "bar"
    ; 定义一个方法.
    thing.test := thing_test
    ; 调用方法.
    thing.test()

    thing_test(this) {
       MsgBox this.foo
    }

调用 `thing.test()` 时, thing 会自动被插入到参数列表的开始处. 按照约定, 函数是通过结合对象的 "类型" 和方法名来命名的, 但这不是必要条件.

在上面的示例中, test 在被定义后可能会被赋值到其他函数或值, 在这种情况下, 原始函数将丢失, 并且无法通过此属性进行调用. 另一种方法是定义一个只读方法, 如下所示:

    thing.DefineProp 'test', {call: thing_test}

另请参阅: DefineProp

### 委托

对象是 基于原型的. 也就是说, 没有在对象本身中定义的任何属性都可以在对象的基中定义. 这被称为 委托继承 或 差异继承, 因为对象可以只实现使其不同的部分, 而将其余部分委托给它的基.

虽然基对象通常也称为原型, 但我们使用 "类的原型" 来表示该类的每个实例所基于的对象, 而使用 "基" 来表示一个实例所基于的对象.

AutoHotkey 的对象设计主要受 JavaScript 和 Lua, 略带 C# 的影响. 我们使用 `obj.base` 代替 JavaScript 的 `obj.__proto__` 和 `cls.Prototype` 代替 JavaScript 的 `func.prototype`. (使用类对象代替构造函数.)

对象的基也用于标识其类型或类. 例如, `x := []` 创建 基于 `Array.Prototype` 的对象, 这意味着表达式 `x is Array` 并且 `x.HasBase(Array.Prototype)` 为 true, 而 `type(x)` 返回 "Array". 每个类的 Prototype 都是基于其基类的 Prototype, 所以 `x.HasBase(Object.Prototype)` 也为 true.

对象或派生类的任何实例都可以是基对象, 但只能将对象赋值为具有相同原生类型的对象的基. 这是为了确保内置方法始终能够识别对象的原生类型, 并且仅对具有正确二进制结构的对象进行操作.

基对象可以用两种不同的方式定义:

- 通过创建一个普通对象.
- 通过定义一个类. 每个类都有一个 Prototype 属性, 包含一个对象, 该类的所有实例都基于该对象, 而类本身成为任何直接子类的基对象.

基对象可以赋值到其他对象的基属性, 但通常在创建对象时隐式地设置该对象的基.

### 创建基对象

任何对象都可以用作具有相同原生类型的任何其他对象的基. 下面的例子基于在之前的专用的例子(运行前将两者结合):

    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 的引用.

### 类

> 在面向对象编程中, 类是一个可扩展的程序代码模板, 为状态(成员变量) 和行为实现(成员函数或方法) 提供初始值. Wikipedia

从根本上讲, 类 是具有某些共同属性或属性的一组或一类事物. 在 AutoHotkey 中, `class` 定义了该类实例共享的属性(以及方法, 这些方法是可调用的属性). 一个 实例 只是一个继承了类的属性的对象, 通常也可以被识别为属于该类(如表达式 `instance is ClassName`). 实例通常通过调用 ClassName() 创建.

由于对象是动态的且基于原型, 因此每个类都包含两部分:

- 类具有原型对象, 该类的所有实例都基于该对象. 原型对象包含所有适用于该类实例的方法和动态属性. 这包括所有没有 `static` 关键字的属性和方法.
- 类本身是一个对象, 只包含静态方法和属性. 这包括所有带有 `static` 关键字的的所有属性和方法, 以及所有嵌套的类. 这些不适用于特定实例, 可以通过按名称引用类本身来使用.

下面展示了类定义的大部分元素:

    class ClassName extends BaseClassName
    {
        InstanceVar := 表达式

        static ClassVar := 表达式

        class NestedClass
        {
            ...
        }

        Method()
        {
            ...
        }

        static Method()
        {
            ...
        }

        Property[Parameters] ; 仅在有参数时使用方括号.
        {
            get {
                return 属性的值
            }
            set {
                存储或以其他方式处理 值
            }
        }

         ShortProperty
        {
            get => 计算属性值的表达式
            set => 存储或以其他方式处理 值 的表达式
        }

         ShorterProperty => 计算属性值的表达式
    }

加载脚本时, 它构造一个类对象并将其存储在全局常量(只读变量) ClassName 中. 如果存在 `extends BaseClassName`, 那么 BaseClassName 必须为另一个类的全名. 每个类的全名存储在 `ClassName.Prototype.__Class`.

因为类本身是通过一个变量来访问的, 类名不能在同一个上下文中同时用于引用类和创建一个单独的变量(比如保存类的一个实例). 例如, `box := Box()` 将无法工作, 因为 `box` 和 `Box` 都解析为同一事物. 试图以这种方式重新分配一个顶层(非嵌套) 类会导致加载时错误.

在本文档中, 单词 "class" 本身通常表示用 `class` 关键字构造的类对象.

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

#### 实例变量

实例变量 是类的每个实例都拥有自己的副本. 它们被声明并且表现得像普通的赋值, 但 `this.` 前缀被忽略(仅限于类主体内时):

    InstanceVar := Expression

每次使用 ClassName() 创建类的新实例时, 都会计算这些声明, 在所有基类声明被求值之后, 但在调用 __New 之前. 这是通过自动创建一个名为 __Init 的方法来实现的, 该方法包含对 `super.__Init()` 的调用, 并将每个声明插入其中. 因此, 单个类定义不能同时包含 __Init 方法和实例变量声明.

Expression 可以通过 `this` 访问其他实例变量和方法. 全局变量可以被读取, 但不能被赋值. 在表达式中的额外赋值(或使用引用操作符) 通常会在 __Init 方法中创建一个局部变量. 例如, `x := y := 1` 将会设置 `this.x` 和一个局部变量 `y`(一旦所有初始化器被计算, 这个变量就会被释放).

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

支持形如 `x.y := z` 的声明语法, 但前提是 `x` 已在类中定义. 例如, `x := {}, x.y := 42` 声明了 `x` 并初始化了 `this.x.y`.

#### Static/Class 变量

静态/类变量属于类, 但是它们的值可以被子类继承. 和实例变量一样声明, 但使用 static 关键字:

    static ClassVar := Expression

这些声明只在初始化类时被计算一次. 为此, 会自动定义一个名为 __Init 的静态方法.

每个声明都像普通的属性赋值一样, 以类对象为目标. Expression 与实例变量的解释相同, 除了 `this` 引用类本身.

如果要在其他任何地方给类变量赋值, 请始终指定类对象; 例如, `ClassName.ClassVar := Value`. 如果一个子类不拥有该名称的属性, `Subclass.ClassVar` 也可以用来检索值; 因此, 如果该值是对象的引用, 则默认情况下子类将共享该对象. 然而, `Subclass.ClassVar := y` 将值存储在 Subclass, 而不是 ClassName 中.

支持形如 `x.y := z` 的声明, 但前提是 `x` 已在类中定义. 如: `static x:={},x.y:=42` 声明了 `x` 并初始化了`ClassName.x.y`. 因为 Prototype 是在每个类中隐式定义的, `static Prototype.sharedValue := 1` 可以用来设置由类的所有实例动态继承的值(直到被实例本身的一个属性所覆盖)

#### 嵌套类

嵌套类定义允许类对象与外部类的静态/类变量关联, 而不是与单独的全局变量关联. 在上面的例子中, `class NestedClass` 构造了一个类对象并将其存储在 `ClassName.NestedClass`. 子类可以继承 NestedClass 也可以用自己的嵌套类覆盖它(在这种情况下, 可以使用 `(WhichClass.NestedClass)()` 实例化任何合适的类).

    class NestedClass
    {
        ...
    }

嵌套一个类并不意味着与外部类有任何特殊的关系. 嵌套类不会自动实例化, 嵌套类的实例也不会与外部类的实例有任何连接, 除非脚本显式地建立连接.

然而, 由于 Object 方法的工作方式, `WhichClass.NestedClass()` 隐式传递 WhichClass 为第一个参数, 等同于 `WhichClass.NestedClass.Call(WhichClass)`. 除非 `static Call()` 被重写, 否则此参数会自动传递给 __New.

#### 方法

方法定义看起来和函数定义相同. 每个方法定义都会创建一个 Func, 带有名为 `this` 的隐藏的第一个参数, 同时还定义了一个属性, 用于调用该方法或检索其函数对象.

有两种类型的方法:

- 实例方法定义如下, 并附加到类的原型, 这使得它们可以通过类的任何实例进行访问. 当方法被调用时, `this` 引用类的一个实例.
- 静态方法是在方法名之前加上独立的关键字 `static` 来定义的. 它们被附加到类对象本身, 但也被子类继承, 所以 `this` 要么引用类本身, 要么引用子类.

下面的方法定义创建了一个与 `target.DefineProp('Method', {call: funcObj})` 相同类型的属性. 默认情况下, `target.Method` 返回 funcObj, 而试图赋值到 `target.Method` 会抛出错误. 这些默认值可以通过定义属性或调用 DefineProp 来覆盖.

    Method()
    {
        ...
    }

胖箭头语法可以用来定义一个单行方法, 返回一个表达式:

    Method() => Expression

#### Super

在方法或属性的 getter/setter 中, 关键字 `super` 可以代替 `this` 来访问在派生类中被重写的方法或属性的超类版本. 例如, 上面定义的类中的 `super.Method()` 通常会调用 BaseClassName 中定义的 Method 的版本. 注意:

- `super.Method()` 总是调用与当前方法的原始定义相关联的类或原型对象的基, 即使 `this` 是从该类的 subclass 或其他一些完全的类派生出来的.
- `super.Method()` 隐式地将 `this` 作为第一个(隐藏的) 参数.
- 由于不知道 ClassName 在基对象链中的位置(或是否存), ClassName 本身被用作起点. 因此, `super.Method()` 主要等同于 `(ClassName.Prototype.base.Method)(this)`(但当 Method 是静态时, 没有 Prototype). 然而, `ClassName.Prototype` 在加载时被解析.
- 如果该属性没有在超类中定义, 或者不能被调用, 则抛出错误.

关键字 `super` 后面必须有下列符号之一: `.[(`.

`super()` 等同于 `super.call()`.

#### 属性

属性定义创建一个动态属性, 它会调用一个方法, 而不是简单地存储或返回一个值.

    Property[Parameters]
    {
        get {
            return 属性值
        }
        set {
            存储或以其他方式处理 值
        }
    }

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

参数可以通过将它们括在属性名称右侧的方括号中来定义, 并以相同的方式传递(但在无参数时应省略). 除了使用方括号这点不同, 属性参数的定义方法与方法参数相同 - 支持可选参数, ByRef 和可变参数.

如果调用了一个带参数的属性, 但没有定义任何参数, 参数将自动转发给 get 返回的对象的 __Item 属性. 例如, `this.Property[x]` 与 `(this.Property)[x]` 或 `y := this.Property, y[x]` 具有相同的效果. 空方括号(`this.Property[]`) 总是会导致调用 __Item 属性的 属性值, 但是像 `this.Property[args*]` 这样的可变数量调用只有在参数数为非零的情况下才会有这种效果.

静态属性可以在属性名之前加上独立的关键字 `static` 来定义. 在这种情况下, `this` 指的是类本身或子类.

set 的返回值会被忽略. 例如, `val := obj.Property := 42` 总是赋值 `val := 42` 不管该属性做什么, 除非它抛出异常或退出线程.

每个类可定义部分或完整的属性. 如果一个类覆盖了属性, 可用 `super.Property` 访问其基类中定义的属性. 如果没有定义 Get 或 Set, 则可以从基对象继承它. 如果没有定义 Get, 则属性可以返回从基继承的值. 如果在该类和所有基对象中没有定义 Set(或被继承的值属性所掩盖), 尝试设置该属性会导致抛出异常.

同时具有 get 和 set 的属性定义实际上创建了两个独立的函数, 它们不共享局部或静态变量或嵌套函数. 与方法一样, 每个函数都有一个名为 `this` 的隐藏参数, 而 set 有名为 `value` 的第二个隐藏参数. 任何显式定义的参数都在这些参数之后.

属性定义以与 DefineProp 相同的方式定义属性的 get 和 set 访问函数, 而方法定义则定义 call 访问函数. 任何类都可以包含同名的属性定义和方法定义. 如果调用一个没有 call 访问函数的属性(方法), 则以没有参数的方式调用 get, 然后将结果作为方法调用.

#### 胖箭头属性

胖箭头语法可以用来定义 getter 或 setter 属性, 它返回一个表达式:

    ShortProperty[Parameters]
    {
        get => 计算属性值的表达式
        set => 存储或以其他方式处理 值 的表达式
    }

当只定义 getter 时, 大括号和 `get` 可以省略:

    ShorterProperty[Parameters] => 计算属性值的表达式

在这两种情况下, 除非定义了参数, 否则必须省略方括号.

### __Enum 方法

    __Enum(NumberOfVars)

当对象被传递给 for-loop 时, 将调用 __Enum 方法. 此方法应返回一个枚举器, 该枚举器将返回对象包含的项, 如数组元素. 如果未定义, 则不能将对象直接传递给 for-loop, 除非它具有枚举器-兼容的 Call(调用) 方法.

NumberOfVars 包含传递给 for-loop 的变量数量. 如果 NumberOfVars 为 2, 则期望枚举器将项的键或索引分配给第一个参数, 将值分配给第二个参数. 每个键或索引都应该作为 __Item 属性的参数而被接受. 这使基于 DBGp 的调试器能够通过调用枚举器列出它们之后可以获取或设置特定项.

### __Item 属性

当索引操作符(数组语法) 与对象一起使用时, 将调用 _item 属性. 在下面的示例中, 属性被声明为静态的, 以便可以在 Env 类本身上使用索引运算符. 有关另一个例子, 请参阅 Array2D.

    class Env {
        static __Item[name] {
            get => EnvGet(name)
            set => EnvSet(name, value)
        }
    }

     Env["PATH"] .= ";" A_ScriptDir  ; 只影响此脚本和子进程.
    MsgBox Env["PATH"]

`__Item` 实际上是一个默认属性名(如果已经定义了这样一个属性):

- 当有参数时 `object[params]` 等同于 `object.__Item[params]`.
- `object[]` 等同于 `object.__Item`.

例如:

    obj := {}
    obj[] := Map()     ; 等同于 obj.__Item := Map()
    obj["base"] := 10
    MsgBox obj.base = Object.prototype  ; True
    MsgBox obj["base"]                  ; 10

注意: 当显式属性名与空括号组合时, 如 `obj.prop[]`, 它是作为两个独立的操作来处理的: 首先检索 `obj.prop`, 然后调用结果的默认属性. 这是语言语法的一部分, 所以不依赖于对象.

### 创建和销毁

每当使用 ClassName() 的默认实现创建对象时, 都会调用新对象的 `__New` 方法, 以便允许自定义初始化. 传递给 `ClassName()` 的任何参数都会被转发到 `__New`, 因此可能会影响对象的初始内容或如何构造它. 销毁对象时, 则调用 `__Delete`. 例如:

    m1 := GMem(0, 10)
    m2 := {base: GMem.Prototype}, m2.__New(0, 30)

    ; 注意: 对于一般的内存分配, 请使用 Buffer().
    class GMem
    {
        __New(aFlags, aSize)
        {
            this.ptr := DllCall("GlobalAlloc", "UInt", aFlags, "Ptr", aSize, "Ptr")
            if !this.ptr
                throw MemoryError()
            MsgBox "New GMem of " aSize " bytes at address " this.ptr "."
        }

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

__Delete 不可被任何具有属性名 "__Class" 的对象所调用. 原型对象默认包含该属性.

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

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

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

#### 类初始化

当对类的引用第一次计算时, 每个类都会被自动初始化. 例如, 如果 MyClass 还没有初始化, `MyClass.MyProp` 会导致类在属性被检索之前被初始化. 初始化包括调用两个静态方法: __Init 和 __New.

`static __Init` 是为每个类自动定义的, 并且如果指定了基类, 则始终以对基类的引用开始, 以确保它被初始化. 静态/类变量和嵌套类按照它们被定义的顺序进行初始化, 除非在前一个变量或类初始化期间引用了嵌套类.

如果类定义或继承了一个 `static __New` 方法, 则在 __Init 之后立即被调用. 需要注意的是, __New 可以为定义它的类调用一次, 而 为每个没有定义它的子类调用一次(或调用 `super.__New()`). 这可以用来为每个子类执行共同的初始化任务, 或者在使用子类之前以某种方式修改它们.

如果 `static __New` 不打算作用于派生类, 这可以通过检查 `this` 的值来避免. 在某些情况下, 使用方法删除本身就足够了, 比如用 `this.DeleteProp('__New')`; 然而, 如果一个子类嵌套在基类中, 或者在静态/类变量的初始化过程中被引用, 那么 __New 的第一次执行可能是针对一个子类.

一个类的定义也有引用类的效果. 换句话说, 当脚本启动期间执行达到类定义时, 除非脚本已经引用了该类, 否则会自动调用 __Init 和 __New. 但是, 如果执行被阻止到达类的定义, 例如通过 `return` 或无限循环, 那么只有当类被引用时才会被初始化.

一旦自动初始化开始, 它就不会再发生在同一个类上. 这通常不是一个问题, 除非多个类相互引用. 例如, 考虑下面的两个类. 当 `A` 先被初始化时, 计算 `B.SharedArray` (A1) 会导致 `B` 在检索和返回值之前被初始化, 但是 `A.SharedValue` (A3) 是未定义的并且不会导致 `A` 的初始化, 因为它已经在进行了. 换句话说, 如果 `A` 先被访问或初始化, 顺序是 A1 到 A3; 否则是 B1 到 B4:

    MsgBox A.SharedArray.Length
    MsgBox B.SharedValue

    class A {
        static SharedArray := B.SharedArray   ; A1          ; B3
        static SharedValue := 42                            ; B4
    }

    class B {
        static SharedArray := StrSplit("XYZ") ; A2          ; B1
        static SharedValue := A.SharedValue   ; A3 (Error)  ; B2
    }

### 元函数

    class ClassName {
        __Get(Name, Params)
        __Set(Name, Params, Value)
        __Call(Name, Params)
    }

Name

  属性或方法的名称.

Params

  参数数组. 这只包括 `()` 或 `[]` 之间的参数, 所以可能是空的. 元函数被期望处理诸如 `x.y[z]` 这样的情况, 其中 `x.y` 是未定义的.

Value

  被赋值的值.

元函数定义了调用未定义的属性或方法时会发生什么. 例如, 如果 `obj.unk` 没有被赋值, 那么它会调用 __Get 元函数. 同样地, `obj.unk := value` 调用 __Set, 而 `obj.unk()` 调用 __Call.

属性和方法可以在对象本身或其任何基对象中定义. 通常, 要为每个属性调用一个元函数, 必须避免定义任何属性. 可以使用属性定义或 DefineProp 来覆盖内置属性(如 Base).

如果定义了一个元函数, 它必须执行任何所需的默认操作. 例如, 可能会出现以下情况:

- Call: 抛出 MethodError.
- 如果给出了参数, 则抛出异常(没有对象可将参数转发到).
- Get: 抛出 PropertyError.
- Set: 使用给定值定义一个新属性, 例如通过调用 DefineProp.

任意可调用对象可用作元函数, 通过将其赋值给相关属性.

在以下情况下, 不调用元函数:

- `x[y]`: 使用不带属性名称的方括号仅会调用 __Item 属性.
- `x()`: 调用对象本身仅会调用 `Call` 方法. 这包括内置函数(如 SetTimer 和 Hotkey) 进行的内部调用.
- 对其他元函数或双下划线方法的内部调用不会触发 `__Call`.

#### 动态属性

属性语法和 DefineProp 可用于定义属性, 这些属性在每次求值时计算出一个值, 但是必须预先定义每个属性. 相比之下, __Get 和 __Set 可用于实现只有在调用时才知道的属性.

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

## 原始值

原始值, 如字符串和数字, 不能有自己的属性和方法. 然而, 原始值支持与对象相同类型的委托. 也就是说, 对原始值的任何属性或方法调用都被委托给预定义的原型对象, 也可以通过相应类的 Prototype 属性访问. 以下类与原始值相关:

- 原始值 (继承 Any)
  - 数字
    - 浮点数
    - 整数
  - 字符串

虽然检查字符串的类型通常更快, 但是可以通过检查值是否具有给定的基来测试值的类型. 例如, 如果 n 是一个纯整数或浮点数, 则 `n.HasBase(Number.Prototype)` 或 `n is Number` 为真, 但如果 n 是一个数字字符串, 则不为真, 因为字符串不是从数字派生而来的. 相比之下, 如果 n 是数字或数字字符串, `IsNumber(n)` 为真.

ObjGetBase 和 Base 属性在适当的时候返回预定义的原型对象之一.

注意, 对于 AutoHotkey 的类型层次结构中的任何值, `x is Any` 通常为真, 而对于 COM 对象则为假.

### 添加属性和方法

通过修改该类型的原型对象, 可以为该类型的所有值添加属性和方法. 但是, 由于原始值不是对象并且不能具有自己的属性或方法, 因此原始原型对象不会从 `Object.Prototype` 派生. 换句话说, 默认情况下无法访问诸如 DefineProp 和 HasOwnProp 之类的方法. 可以间接调用它们. 例如:

    DefProp := {}.DefineProp
    DefProp( "".base, "Length", { get: StrLen } )
    MsgBox A_AhkPath.length " == " StrLen(A_AhkPath)

尽管原始值可以从其原型继承值属性, 但是如果脚本尝试在原始值上设置值属性, 则会引发异常. 例如:

    "".base.test := 1  ; 不要轻易尝试.
    MsgBox "".test  ; 1
    "".test := 2  ; 错误: 属性是只读的.

尽管可以使用 __Set 和属性设置器, 但它们没有用, 因为应将原始值视为不可变的.

## 实现

### 引用计数

当脚本不再引用对象时, AutoHotkey 使用基本引用计数机制来自动释放对象使用的资源. 脚本作者不应该显式地调用这种机制, 除非打算直接处理未托管的对象的指针.

表达式中的函数, 方法或运算符返回的临时引用在表达式的计算完成或中止后释放. 在下面的例子中, 新的 GMem 对象只有在 MsgBox 返回后才被释放.

    MsgBox DllCall("GlobalSize", "ptr", GMem(0, 20).ptr, "ptr")  ; 20

注意: 在本例中, `.ptr` 可以省略, 因为 Ptr 参数类型允许对象具有 `Ptr` 属性. 但是, 上面显示的模式甚至可以用于其他属性名.

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

已知限制:

- 循环引用必须被打破, 对象才能被释放. 有关详情和例子, 请参阅释放对象.
- 虽然程序退出时, 静态和全局变量中的引用会自动释放, 但非静态局部变量或表达式求值堆栈中的引用不会. 只有在函数或表达式能够正常完成时, 才会释放这些引用.

虽然当程序退出时, 操作系统会回收对象占用的内存, 但是除非释放了对对象的所有引用, 否则不会调用 __Delete. 如果它释放了操作系统不能自动回收的其他资源, 比如临时文件, 那么这一点很重要.

### 对象的指针

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

    ObjAddRef(address := ObjPtr(myObject))
    address := ObjPtrAddRef(myObject)

脚本还必须在对象使用该引用完成时通知该对象:

    ObjRelease(address)

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

要将地址转换为一个合适的引用, 请使用 ObjFromPtr 函数:

    myObject := ObjFromPtr(address)

ObjFromPtr 假定 address 是一个引用计数, 并声称对它的所有权. 换句话说, `myObject := ""` 会导致原本由 address 代表的引用被释放. 之后, address 必须被认为是无效的. 如果要改用一个新的引用, 可以使用下面的一种方法:

    ObjAddRef(address), myObject := ObjFromPtr(address)
    myObject := ObjFromPtrAddRef(address)

dftg