注意: 该页面正在开发进程中.
对象 组合了多个 属性 和 方法:
相关话题:
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 := 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.
对象或派生类的任何实例都可以是基对象, 但只能将对象赋值为具有相同原生类型的对象的基. 这是为了确保内置方法始终能够识别对象的原生类型, 并且仅对具有正确二进制结构的对象进行操作.
基对象可以用两种不同的方式定义:
基对象可以赋值到其他对象的基属性, 但通常在创建对象时隐式地设置该对象的基.
任何对象都可以用作具有相同原生类型的任何其他对象的基. 下面的例子基于在之前的专用的例子(运行前将两者结合):
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 关键字:
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
在方法或属性的 getter/setter 中, 关键字 super
可以代替 this
来访问在派生类中被重写的方法或属性的超类版本. 例如, 上面定义的类中的 super.Method()
通常会调用 BaseClassName 中定义的 Method 的版本. 注意:
super.Method()
总是调用与当前方法的原始定义相关联的类或原型对象的基, 即使 this
是从该类的 subclass 或其他一些完全的类派生出来的.super.Method()
隐式地将 this
作为第一个(隐藏的) 参数.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(NumberOfVars)
当对象被传递给 for-loop 时, 将调用 __Enum 方法. 此方法应返回一个枚举器, 该枚举器将返回对象包含的项, 如数组元素. 如果未定义, 则不能将对象直接传递给 for-loop, 除非它具有枚举器-兼容的 Call(调用) 方法.
NumberOfVars 包含传递给 for-loop 的变量数量. 如果 NumberOfVars 为 2, 则期望枚举器将项的键或索引分配给第一个参数, 将值分配给第二个参数. 每个键或索引都应该作为 __Item 属性的参数而被接受. 这使基于 DBGp 的调试器能够通过调用枚举器列出它们之后可以获取或设置特定项.
当索引操作符(数组语法) 与对象一起使用时, 将调用 _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) }
属性或方法的名称.
参数数组. 这只包括 ()
或 []
之间的参数, 所以可能是空的. 元函数被期望处理诸如 x.y[z]
这样的情况, 其中 x.y
是未定义的.
被赋值的值.
元函数定义了调用未定义的属性或方法时会发生什么. 例如, 如果 obj.unk
没有被赋值, 那么它会调用 __Get 元函数. 同样地, obj.unk := value
调用 __Set, 而 obj.unk()
调用 __Call.
属性和方法可以在对象本身或其任何基对象中定义. 通常, 要为每个属性调用一个元函数, 必须避免定义任何属性. 可以使用属性定义或 DefineProp 来覆盖内置属性(如 Base).
如果定义了一个元函数, 它必须执行任何所需的默认操作. 例如, 可能会出现以下情况:
任意可调用对象可用作元函数, 通过将其赋值给相关属性.
在以下情况下, 不调用元函数:
x[y]
: 使用不带属性名称的方括号仅会调用 __Item 属性.x()
: 调用对象本身仅会调用 Call
方法. 这包括内置函数(如 SetTimer 和 Hotkey) 进行的内部调用.__Call
.属性语法和 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)
一般来说, 对象地址的每个新副本都应该被视为对象的另一个引用, 所以脚本必须在获得副本之后立即调用 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)unixetc