对象

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

相关话题:

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 引用 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 := {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()

此时, 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() 创建.

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

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

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 的隐藏的第一个参数, 同时还定义了一个属性, 用于调用该方法或检索其函数对象.

有两种类型的方法:

下面的方法定义创建了一个与 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. 在 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 实际上是一个默认属性名(如果已经定义了这样一个属性):

例如:

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

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)

一般来说, 对象地址的每个新副本都应该被视为对象的另一个引用, 所以脚本必须在获得副本之后立即调用 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)
0x8