对象

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

相关话题:

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

Result := IsObject(expression)

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

目录

基本用法

数组

创建数组:

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 := MyObject.Property
MyObject.Property := Value

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

Value := MyObject.%Expression%
MyObject.%Expression% := Value

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

ReturnValue := MyObject.Method(Parameters)

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

ReturnValue := MyObject.%Expression%(Parameters)

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

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

对象可能支持索引: MyArray[Index] 实际上调用 MyArray__Item 属性, 并将 Index 作为参数传递.

对象字面量(文本对象)

可以在表达式中使用对象字面量来创建即时生成的对象. 对象字面量由一对大括号({}) 包含一组以逗号分隔的名称-值对组成. 每一对都由一个字面的(未加引号) 属性名和一个值(子表达式) 组成, 它们之间用冒号(:) 分隔. 例如:

Coord := {X: 13, Y: 240}

这等同于:

Coord := Object()
Coord.X := 13
Coord.Y := 240

每个名称-值对都会导致一个值属性被定义, 但可以设置 Base(具有与普通赋值相同的限制).

名称替换允许通过计算表达式变量来确定属性名称. 例如:

parts := StrSplit("key = value", "=", " ")
pair := {%parts[1]%: parts[2]}
MsgBox pair.key

释放对象

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

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

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

扩展用法

数组嵌套

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

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.Has(this.i[x, y]) ? super[this.i[x, y]] : false
        set => super[this.i[x, y]] := value
    }
    i[x, y] => this.Width * (y-1) + x
}

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 := {foo: "bar"} 创建上面的对象. 当使用 {property:value} 表示法时, 属性不能使用引号.

调用 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.

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

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

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

创建基对象

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

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

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

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

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

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

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

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() 将无法工作, 因为 boxBox 都解析为同一事物. 试图以这种方式重新分配一个顶层(非嵌套) 类会导致加载时错误.

在本文档中, 单词 "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
{
    ...
}

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

每个嵌套类定义都使用 getcall 访问函数生成动态属性, 而不是简单的值属性. 这是为了支持以下行为(其中类 X 包含嵌套类 Y):

方法

方法定义看起来和函数定义相同. 每个方法定义都会创建一个 Func, 带有名为 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 后面必须有下列符号之一: .[(.

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

属性

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

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

Property 是用户定义的名称, 用于标识属性. 如, obj.Property 将调用 get, 而 obj.Property := value 将调用 set. 在 getset 内, 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 访问其基类中定义的属性. 如果没有定义 GetSet, 则可以从基对象继承它. 如果没有定义 Get, 则属性可以返回从基继承的值. 如果在该类和所有基对象中没有定义 Set(或被继承的值属性所掩盖), 尝试设置该属性会导致抛出异常.

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

属性定义以与 DefineProp 相同的方式定义属性的 getset 访问函数, 而方法定义则定义 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 实际上是一个默认属性名(如果已经定义了这样一个属性):

例如:

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).

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

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

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

动态属性

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

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

原始值

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

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

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

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

添加属性和方法

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

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

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

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

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

实现

引用计数

当脚本不再引用对象时, AutoHotkey 使用基本的引用计数机制来自动释放对象所使用的资源. 理解这种机制对于正确管理对象的生命周期至关重要, 从而允许在不再需要时删除对象, 而不是在此之前删除对象.

每当存储引用时, 对象的引用计数就会增加. 当一个引用被释放时, 计数用于确定该引用是否是最后一个引用. 如果是, 则删除该对象; 否则, 计数递减. 下面的例子展示了在一些简单的情况下如何计算引用:

a := {Name: "Bob"}  ; Bob 的引用计数为 1
b := [a]            ; Bob 的引用计数增加到 2
a := ""             ; Bob 的引用计数减为 1
c := b.Pop()        ; Bob 被转换, 引用计数仍为 1
c := ""             ; Bob 被删除...

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

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

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

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

引用计数的问题

仅仅依赖引用计数有时会造成 catch-22 的情况: 对象被设计为在删除时释放其资源, 但只有在其资源首次被释放时才会被删除. 具体来说, 当这些资源是其他对象或函数时, 通常会间接地保留对对象的引用.

循环引用引用循环 是指对象直接或间接地引用自身. 如果作为循环一部分的每个引用都包含在计数中, 那么除非手动中断循环, 否则无法删除该对象. 例如, 下面的代码创建了一个引用循环:

parent := {}  ; parent: 1(引用计数)
child := {parent: parent}  ; parent: 2, child: 1
parent.child := child  ; parent: 2, child: 2

如果变量 parentchild 被重新赋值, 每个对象的引用计数将递减为 1. 脚本将无法访问这两个对象, 但不会删除它们, 因为最后的引用没有被释放.

循环通常不那么明显, 并且可能涉及多个对象. 例如, ShowRefCycleGui 演示了一个包含 Gui, MenuBar, Menu 和 closures 的循环. 如果处理程序对象有对图形用户界面的引用, 使用单独的对象处理图形用户界面事件也容易造成循环.

对对象的非循环引用也可能造成问题. 例如, 依赖于 SetTimer 或 OnMessage 等内置函数的对象通常会导致程序持有对该对象的间接引用. 这将防止对象被删除,这意味着它不能使用 __New 和 __Delete 来管理计时器或消息监视器.

下面是解决上述问题的几个策略.

避免循环: 如果引用循环有问题, 请避免创建它们. 例如, 不设置 parent.childchild.parent. 这往往不切实际, 因为相关对象可能需要一种相互引用的方法.

当为 OnEvent(Gui) 定义事件处理程序时, 避免在闭包或绑定函数中捕获源 Gui, 而是利用 Gui 或 Gui.Control 参数. 对于 Add(Menu) 和回调的 Menu 参数也是如此, 当然, 需要引用 Gui 的菜单项不能使用这种方法.

在某些情况下, 可以通过不依赖于计数引用的间接方法检索另一个对象. 例如, 保留一个 HWND 并使用 GuiFromHwnd(hwnd) 来检索一个 Gui 对象. 在窗口可见时, 保留引用对于防止删除是不必要的, 因为 Gui 本身可以处理这个问题.

中断循环: 如果脚本可以避免依赖引用计数, 而是直接管理对象的生命周期, 那么它只需要在对象被删除时打破这个循环:

child.parent := unset  ; parent: 1, child: 2
child := unset  ; parent: 1, child: 1
parent := unset  ; both deleted

Dispose: __Delete 在最后一个引用被释放时被调用, 因此人们可能会认为像 myGui := "" 这样的简单赋值是触发对象删除的清理步骤. 有时, 当不再需要对象时, 我们会显式地执行此操作, 但它既不可靠, 也不能真正显示代码的意图. 另一种模式是定义一个释放对象资源的 Dispose 或 Destroy 方法, 并将其设计为在第二次调用时不做任何事情. 然后也可以从 __Delete 调用它, 作为保护措施.

遵循此模式的对象在被 disposed 时仍然需要打破任何引用循环, 否则一些内存将不会被回收, 而且该对象引用的其他对象也不会调用 __Delete 方法.

在调用 Destroy 时, 由 Gui 对象的事件处理程序, 菜单栏或事件接收对象引起的循环会自动 "中断", 因为它释放了这些对象. (在 ShowRefCycleGui 示例中演示了这一点.) 然而, 这不会破坏脚本添加的新属性引起的循环, 因为 Destroy 并不会删除它们.

与 Dispose 模式类似, InputHook 有一个必须显式调用的 Stop 方法, 因此它不依赖于 __Delete 来表示其操作何时应该结束. 在操作时, 程序实际上保留了对对象的引用, 以防止对象被删除, 但这成为一种优势而不是缺陷: 事件回调仍然可以被调用, 并将接收 InputHook 作为参数. 当操作结束时, 如果脚本没有对 InputHook 的引用,则释放内部引用, 并删除 InputHook.

指针: 存储任意数量的指针值都不会影响对象的引用计数, 因为指针只是一个整数. 使用 ObjPtr 获取的指针可以通过将其传递给 ObjFromPtrAddRef 来生成引用. 必须使用该函数的 AddRef 版本, 因为当临时引用自动释放时, 引用计数将递减.

例如, 假设一个对象需要每秒更新一些属性. 计时器保存对回调函数的引用, 该回调函数将对象绑定为参数. 通常, 这将防止在删除计时器之前删除对象. 存储指针而不是引用允许对象在不考虑计时器的情况下被删除, 因此它可以通过 __New 和 __Delete 自动管理.

a := SomeClass()
Sleep 5500  ; 让计时器运行 5 次.
a := ""
Sleep 3500  ; 防止暂时退出以显示计时器已停止.

class SomeClass {
  __New() {
        ; 必须存储闭包, 以便以后可以删除计时器.
        ; 每次需要调用方法时, 合成一个计数引用.
        this.Timer := (p => ObjFromPtrAddRef(p).Update()).Bind(ObjPtr(this))
        SetTimer this.Timer, 1000
    }
  __Delete() {
        SetTimer this.Timer, 0
        ; 如果该对象被真正删除, 则所有属性将被删除
        ; 并调用以下 __Delete 方法.
        ; 这只是为了确认, 通常不会使用.
        this.Test := {__Delete: test => ToolTip("object deleted")}
    }
    ; 这只是为了演示计时器正在运行.
    ; 假设这个类还有其他用途.
    count := 0
  Update() => ToolTip(++this.count)
}

这种方法的缺点是指针不能直接作为对象使用, 并且不能被 Type调试器识别为对象. 脚本必须绝对确定在对象删除后不使用指针, 因为这样做是无效的, 并且结果将是不确定的.

如果在多个地方都需要指针引用, 那么封装它可能是有意义的. 例如, b := ObjFromPtrAddRef.Bind(ObjPtr(this)) 将产生一个 BoundFunc, 可以调用(b()) 来检索引用, 而 ((this, p) => ObjFromPtrAddRef(p)).Bind(ObjPtr(this)) 可以用作属性 getter(属性将返回一个引用).

未计数引用: 如果对象的引用计数包含引用, 则称其为 计数引用, 否则称其为 未计数引用. 后者的思想是允许脚本存储一个引用, 但不妨碍对象被删除.

注意: 这是关于对象的引用计数如何根据脚本的逻辑与给定引用相关, 并且不影响引用本身的性质. 程序仍然会尝试在正常情况下自动释放引用, 因此 弱引用强引用 这两个术语并不合适.

计数引用可以通过简单地递减对象的引用计数变成未计数引用. 这 必须 在释放引用之前反转, 而释放引用 必须 在对象被删除之前发生. 由于未计数引用的目的是允许在不首先手动取消引用的情况下删除对象, 因此通常必须在该对象自己的 __Delete 方法中纠正计数.

例如, 上一个示例中的 __New 和 __Delete 可以用下面的方法代替.

  __New() {
        ; 必须存储 BoundFunc, 以便以后删除计时器.
        SetTimer this.Timer := this.Update.Bind(this), 1000
        ; 减少 ref 计数, 以补偿 Bind 执行的 AddRef.
        ObjRelease(ObjPtr(this))
    }
  __Delete() {
        ; 增加 ref 计数, 以便可以安全地释放 BoundFunc
        ; 中的 ref.
        ObjPtrAddRef(this)
        ; 删除计时器, 释放对 BoundFunc 的引用.
        SetTimer this.Timer, 0
        ; 释放 BoundFunc.
        ; 由于 BoundFunc 中的循环引用已被重新计算
        ; 这可能不会自动发生.
        this.Timer := unset
        ; 如果该对象真的被删除, 所有属性都将被删除
        ; 并将调用下面的 __Delete 方法.
        ;  这只是为了确认, 通常不会使用.
        this.Test := {__Delete: test => ToolTip("object deleted")}
    }

无论未计数的引用存储在何处以及它的用途如何, 通常都可以应用此方法. 关键是:

引用计数的递增和递减次数必须与未计数的引用次数相同. 如果脚本不能准确地预测某个函数将存储多少引用, 那么这样做可能是不切实际的.

对象的指针

作为创建对象的一部分, 分配一些内存来保存对象的基本结构. 这个结构体本质上就是对象本身, 所以我们称它的地址为 指向对象的指针. 地址是一个整数值, 它对应于当前进程的虚拟内存中的一个位置, 并且只在对象被删除之前有效.

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

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

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

ObjRelease(address)

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

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

myObject := ObjFromPtr(address)

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

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