2.12数据结构-对象(类)

作者:河许人 2024年4月13日,第11次更新,字数30264个,案例代码126段

在AutoHotkey中,对象(也称为类)是一种重要的编程概念,特别是自从AutoHotkey v2.0版本开始。大幅优化了对象编程,使其更现代化、更具灵活性。对象编程在AutoHotkey中变得至关重要,无论是对于新手还是有经验的开发者。

一、什么是对象?

在前文中,我们已经了解了变量和不同类型的数组,如简单(线性)数组、关联数组、多维数组等。这些都是常见的数据容器,用来存储和组织数据。对象不再是简单数据容器了,而是是一种数据结构,它可以包含数据和与这些数据相关的操作。对象将数据和操作封装在一起,使得代码更加模块化、可维护和可重用。其实在ahk中对象的概念非常宽泛,变量也是对象的一种形式,数组、还有我们常规认识的{}的这种形式都是对象.

以简单的比喻来说,对象就像是一个容器,可以存放各种类型的数据和方法。例如,我们可以创建一个名为”person”的对象,它包含了姓名、年龄、性别等属性,并且提供了一些方法如获取姓名、修改年龄等。

对象通常由一组属性和方法组成:

  • 属性:对象的属性是描述对象特征的数据。
  • 方法:对象的方法就是函数。
;这些是我们前面课程中了解到的数据容器
var := "社区" ;字符串型
var := 123 ;整型
var := 123.123 ;f浮点型
MyArray := ["张三", "李四", "王五"] ;简单(线性)数组
MyMap := map("一班","张三", "二班","李四", "三班","王五") ;关联联数组
; 定义一个名为Person的对象
Person := {} 
Person.Name := "" ; 设置姓名属性
Person.Age := 0 ; 设置年龄属性
Person.SetName := SetName ; 设置姓名的方法
Person.GetName := GetName ; 获取姓名的方法
Person.SetAge := SetAge ; 设置年龄的方法
Person.GetAge := GetAge ; 获取年龄的方法
; 设置姓名的方法
SetName(this,name) {
    this.Name := name
}
; 获取姓名的方法
GetName(this) {
    return this.Name
}
; 设置年龄的方法
SetAge(this,age) {
    this.Age := age
}

; 获取年龄的方法
GetAge(this) {
    return this.Age
}

; 示例用法
Person.SetName("河许人")
Person.SetAge(18)

MsgBox  "姓名: " . Person.GetName() . "`n年龄: " . Person.GetAge()

二、为什么用对象?

对象是编程中属性(数据)和方法(函数)的统一整合,是我们学习过的核心概念的结合体。数据和函数是编程的两个关键概念,而对象则将它们有机地结合在一起。在对象中,我们可以将数据存储在属性中,而方法则允许我们对这些数据进行操作和处理。

在获取或设置值时,对象的属性工作方式类似于键。然而,与简单的键相比,属性的值可以包含更复杂的数据结构或者可以动态计算生成的值,这使得属性更加灵活和功能强大。在 AHK v1 中,键和属性的语法是等价的,它们指向相同的内容,无论是键(或值属性)还是动态属性,都可以通过相同的语法来访问和操作。

对象是“复用”核心思想的重要延伸,是面向对象编程的关键发展。在编程中,复用是一种重要的原则,它允许我们有效地利用已有的代码和功能,减少重复劳动并提高代码的可维护性和可扩展性。在 AutoHotkey 中,最初的复用形式是简单的“标签”,但由于无法传递参数,对不同的数据进行处理时显得不够灵活。随后引入了“函数”,它在标签的基础上实现了参数传递和返回值,使得代码更加模块化和可复用。而对象则进一步将数据域和方法封装在一起,提供了更高级别的抽象和灵活性,使得代码的复用性达到了新的高度。

关于对象的应用是在 AutoHotkey 版本11.20.00之后开始完善的,并持续进行着改进和扩展。通过引入对象的概念,AutoHotkey 在面向对象编程方面逐渐变得更加完善和成熟。通过对象的递进关系,AutoHotkey 向着更加“面向对象”的方向发展,不断改进和完善,相信在编写相对大型的项目时,AutoHotkey 将会更加得心应手,提供更高效、灵活和可维护的编程体验。

综上所述,对象不仅是数据和方法的集成体,更是编程中复用核心思想的重要体现。学习和应用对象,有助于提高代码的可重用性、可维护性和可扩展性,是编写高质量软件的关键一步。

三、对象详解

AutoHotkey对象, 所有的内容, 都可以归结为对 (键) 与 (值) 的操作

(一)对象分类

对象分为内置对象和自定义对象。

2.12数据结构-对象(类)

(二)创建与释放

1.创建对象

对象:={}
对象:=object()
对象:=object.call()

2.释放对象

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

对象:= ""  ; 释放最后一个引用, 因此释放对象.

类似地, 当属性或数组元素被赋为其他值或从对象中删除时, 存储在属性或数组元素中的引用将被释放.

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

(三)对象运算符

1. 成员访问 (Member Access):

  • 用法: Alpha.Beta
  • 解释: 访问对象 Alpha 的属性 Beta

示例:

; 创建对象
obj := {Name: "John", Age: 30}
;这等同于:
obj := Object()
obj.Name := "John"
obj.Age := 30

; 访问对象的属性
MsgBox  obj.Name  ; 显示 "John"

2. 方法调用 (Method Call):

  • 用法: Alpha.Beta()
  • 解释: 调用对象 Alpha 的名为 Beta 的方法。

示例:

; 定义对象及方法
obj := {加法:AddNumbers}

AddNumbers(this,x, y) 
{
    return x + y
}

; 调用对象的方法
result := obj.加法(5, 3)  ; 返回 8
MsgBox result

3. 带参数的成员访问 (Member Access with Parameters):

  • 用法: Alpha.Beta[Param]
  • 解释: 使用参数 Param 访问对象 Alpha 的属性 Beta

示例:

; 创建对象及带参数的方法
obj := {水果:["苹果", "香蕉", "橘子"]}


; 使用参数访问对象的属性
果品1 := obj.水果[1]  ; 返回 "苹果"
MsgBox 果品1

4. 运行时确定属性或方法的名称 (Runtime Property or Method Name):

  • 用法: Alpha.%vBeta%, Alpha.%vBeta%[Param], Alpha.%vBeta%()
  • 解释: 在运行时根据变量 vBeta 的值访问对象 Alpha 的属性或调用方法。

示例:

; 创建对象及动态属性
propName := "Name"
obj := {Name: "Alice", Age: 25}

; 运行时确定属性名访问对象
value := obj.%propName%  ; 返回 "Alice"
MsgBox value

5. 默认属性访问 (Default Property Access):

  • 用法: Alpha[Index]
  • 解释: 使用 Index 作为参数访问对象 Alpha 的默认属性。

示例:

; 创建数组
fruits := ["Apple", "Banana", "Orange"]

; 使用索引访问数组元素
fruit := fruits[2]  ; 返回 "Banana"
MsgBox fruit

6. 创建数组 (Array Creation):

  • 用法: [A, B, C]
  • 解释: 创建一个包含变量 ABC 的数组。

示例:

; 创建包含变量的数组
a := 10
b := 20
c := 30
myArray := [a, b, c]  ; 数组包含值 10, 20, 30

MsgBox myArray[1] ;返回10

7. 创建对象 (Object Creation):

  • 用法: {Prop1: Value1, Prop2: Value2}
  • 解释: 创建一个包含属性 Prop1Prop2 的对象。

示例:

; 创建对象
班级学号:="一班005"
姓名 := "John"
年龄 := 35

person := {%班级学号%: 姓名, 年龄: 年龄}  ; 创建对象
MsgBox "姓名: " . person.一班005 . "`n年龄: " . person.年龄

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

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

8. 可变函数调用 (Variable Function Call):

  • 用法: MyFunc(Params*)
  • 解释: 使用可变参数调用函数 MyFuncParams 必须是可枚举对象。

示例:

; 定义函数 MyFunc
MyFunc(params*) 
{
    total := 0
    for each, param in params
        total += param
    return total
}

; 调用带可变参数的函数
result := MyFunc(1, 2, 3)  ; 返回 6
MsgBox result

通过这些示例,你可以更好地理解在 AutoHotkey 中如何使用对象的运算符进行成员访问、方法调用、动态属性访问等操作。对象的灵活性和功能性可以帮助你编写更高效、模块化的代码,并充分利用面向对象编程的优势。

(四)扩展用法

1.自定义对象

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

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

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

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

(1)专用

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

; 创建对象.
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

(2)委托

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

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

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 属性, 包含一个对象, 该类的所有实例都基于该对象, 而类本身成为任何直接子类的基对象.

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

2.创建基对象

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

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

(五)注意事项

对象 是 AutoHotkey 的复合或抽象数据类型. 对象可以由任意数量的 属性(可以检索或设置) 和 方法(可以调用) 组成. 每个属性或方法的名称和效果取决于特定的对象或对象类型.

  • 对象是不能被包含的; 它们只是引用. 例如, alpha := [] 创建一个新数组并在 alpha 中存储一个引用. bravo := alpha 复制引用(不是对象) 到 bravo, 因此两者都引用同一个对象. 当一个数组或变量被认为包含一个对象时, 它实际上包含的是对该对象的引用.
  • 只有在引用同一个对象时, 两个对象引用才被视为相等的.
  • 当结果需要布尔值时, 对象总是被视为 true , 例如在 if obj!obj 或 obj ? x : y.
  • 每个对象都有一个唯一的地址(内存中的位置), ObjPtr 函数可以检索该地址, 但通常不直接使用. 此地址唯一标识对象, 但仅在释放对象之前.
  • 在某些情况下, 当一个对象被使用在不期望对象的任何上下文中, 它可能会被当作一个空字符串. 例如, MsgBox(myObject) 显示一个空的消息框. 在其他情况下, 可能会抛出 TypeError(这应该成为未来的标准).

注意: 所有从对象派生的对象都具有额外的共享行为, 属性和方法.

对象的一些使用方法包括:

  • 包含项目或 元素 的集合. 例如, 数组包含一系列项, 而 Map 则将键与值相关联. 对象允许将一组值视为一个值, 赋值给单个变量, 传递给函数或从函数返回, 等等.
  • 表示真实或概念性的东西. 例如: 如屏幕上某个位置的 X 坐标和 Y 坐标; 通讯录中的联系人, 包括姓名, 电话号码, 电子邮件地址等. 通过将对象与其他对象组合, 对象可用于构建更复杂的信息集.
  • 封装一个或一组服务, 允许脚本的其他部分将重点放在任务上, 而不是该任务是如何执行的. 例如, File 对象提供了文件读取或写入数据的方法. 如果将信息写入文件的脚本函数接受 File 对象作为参数, 它不需要知道文件是如何打开的. 可以重复利用相同的函数将信息写入其他目标, 如 TCP/IP 套接字或 WebSocket(通过用户定义的对象).
  • 以上的组合. 例如, Gui 表示 GUI 窗口; 它向脚本提供了创建和显示图形用户界面的方法; 它包含一组控件, 并通过 Title 和 FocusedCtrl 等属性提供有关窗口的信息.

适当的使用对象(特别是类) 能产生 模块化 和 可重复使用 的代码. 模块化代码通常更容易测试, 理解和维护. 例如, 人们可以改进或修改一段代码, 而不需要知道其他段的细节, 也不必对这些段做相应的修改. 可重复使用的代码节省了时间, 避免了一遍又一遍地为相同或相似的任务编写和测试代码.

(六)杂项

1.原始值

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

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

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

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

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

2.添加属性和方法

通过修改该类型的原型对象, 可以为该类型的所有值添加属性和方法. 但是, 由于原始值不是对象并且不能具有自己的属性或方法, 因此原始原型对象不会从 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 和属性设置器, 但它们没有用, 因为应将原始值视为不可变的.

3.引用计数

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

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

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

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

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

已知限制:

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

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

4.对象的指针

在一些罕见的情况中, 可能需要通过 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)

四、类

学习到这部分,已经是入门阶段最难的部分了,其实不是知识难,而是可查资料太少了,案例很多,但没人给你解释都是啥意思。

(一)类是什么?

在 AutoHotkey 中,类就是使用 class 定义的一种对象。有些特殊的方法和属性,也有继承、嵌套等应用。

类有两种:

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

(二)基本概念

1.术语:对象/实例/类。

  • 简单来说:
    • 类是一个蓝图对象。
    • 实例是基于类蓝图创建的对象。
    • AutoHotkey 有一个基本对象类,以及其他内置类(在下面列出,例如 RegExMatch,SafeArray)。

2.术语:实例和静态变量、方法(极端重要,必须理解)

(1)实例变量

实例变量是属于类的每个实例(对象)独立拥有的数据。每个对象都有自己的实例变量副本。在类的定义中,可以使用类似赋值的语法来声明和初始化实例变量,无需使用 this. 前缀:

实例变量名 := 表达式

每当使用 ClassName() 创建一个类的新实例时,系统会自动初始化这些实例变量。这些初始化操作会在所有基类声明被求值之后、但在调用 __New 方法之前执行。系统会自动创建一个名为 __Init 的方法来实现这一过程。__Init 方法会被调用,并将实例变量的声明插入其中,确保在对象初始化时设置这些变量。

在实例变量的声明中,可以通过 this 关键字访问其他实例变量和方法。全局变量可以被读取,但不能被赋值。在表达式中进行额外的赋值操作(或使用引用操作符)通常会在 __Init 方法中创建一个局部变量。例如,x := y := 1 会设置 this.x 和一个局部变量 y(初始化器完成后会释放局部变量)。

要访问实例变量,必须使用 this 关键字指定目标对象,例如 this.InstanceVar

从外部访问实例变量时,首先要实例化 ClassName(),然后可以修改实例的变量值。这样的修改操作不会影响类本身的定义。

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

(2)静态/类变量

静态或类变量属于整个类,而不是类的每个实例。它们的值可以被所有对象和子类继承。声明方式与实例变量类似,但要加上 static 关键字:

static 类变量名 := 表达式

这些变量只在初始化类时计算一次,会自动通过一个名为 __Init 的静态方法进行处理。

在声明静态变量时,可以像声明实例变量那样使用其他变量和方法,但不能使用 this 关键字。

如果想在其他地方给类变量赋值,必须明确指定类名,例如 ClassName.ClassVar := Value。子类可以继承父类的类变量,如果子类没有定义同名变量,它会继承父类的值。但是,如果在子类中对这个变量进行赋值,它会在子类中创建一个新的变量。

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

在外部访问时,直接使用类名和类变量的形式,例如 ClassName.ClassVar。需要注意的是,这些变量在实例化时不会被继承。

3.术语:对象,键/方法。

  • 对象将数据/变量(键)和函数(方法)的概念结合起来。
; 键:
value := MyObj["MyKey"] ; 获取键的值
MyObj["MyKey"] := "NEW VALUE" ; 设置键的值

; 方法:
output := MyObj.MyMethod(Arg1, Arg2, Arg3) ; 调用方法

4.术语:属性。

  • 属性可以像键一样具有值,可以进行获取/设置。在 AHK v2 中称为“值属性”。
  • 属性可以像方法一样,其值由 getter/setter 函数生成。在 AHK v2 中称为“动态属性”。
  • 在使用时,这两种类型的属性看起来是一样的。(虽然在定义时看起来不同。)
; 获取属性的值:
value := MyObj.MyProperty
MyObj.MyProperty := "NEW VALUE" ; 设置属性的值
  • 在 AHK v1 中,键和属性共享相同的命名空间。obj["key"]obj.property 语法通常是可互换的。
  • 在 AHK v2 中,键和属性是不同的。obj["key"]obj.property 语法是分开的。

5.术语:自定义类。

  • 用户可以创建自定义类,即自己的类蓝图。
  • 如果创建一个名为“MyClass”的自定义类,将创建一个名为“MyClass”的变量。可以通过 ListVars 或“View, Variables and their contents”(在 AHK 的“主窗口”上)查看它。
  • (例如,如果通过 obj := {} 创建基本对象实例,并定义一个名为“MyClass”的类,那么 IsObject(obj)IsObject(MyClass) 都将报告为 true。)
  • 自定义类确定实例对象的键/属性/方法。还可以阻止添加新的键/属性/方法。

6.6 个方法:__Init/__New/__Get/__Set/__Call/__Delete

  • 创建自定义类时,以下 6 个方法对于自定义非常重要。
    • __Init(脚本不应使用):处理创建对象实例时的情况。
    • __New:处理创建对象实例时的情况。
    • __Get/__Set:处理获取/设置不存在的键/属性。
    • __Call:处理调用存在/不存在的方法。
    • __Delete:处理删除对象实例时的情况。

7.术语:基础对象。

  • 对象的基础对象通常包含决定该对象如何工作的键/属性/方法。
  • 在 AHK v1 中,基本对象没有基础对象。
  • 在 AHK v1 中,使用“class”关键字创建的类对象没有基础对象。(除非手动添加。)
  • 通过“new”关键字创建的实例对象是类的实例,实例对象的基础对象是类对象。
  • 如果尝试访问对象的键/属性/方法,但找不到该项,则会检查对象的基础对象是否存在该项。
  • 如果存在,则继续检查基础对象的基础对象,以此类推,直到达到没有基础对象的基础对象。

8.术语:枚举器。

2 个方法:_NewEnum/Next

  • for 循环 调用对象的 _NewEnum 方法。
  • _NewEnum 方法返回一个称为枚举器对象的对象。
  • 枚举器对象将具有一个 Next 方法,该方法将返回键/值对,直到没有键/值对为止。
  • 枚举器对象可能返回不基于对象内容的键/值对,而是计算得到的键/值对(例如平方数)。它可以无限期返回键/值对。
  • 通常,Next 方法会 ByRef 输出 2 个值(键名和值),但它也可以输出其他数量的值。

9.术语:超类/子类,嵌套类

  • 超类/子类:超类具有一些特征,子类继承这些特征并具有自己的特征。
  • 对于基于子类的对象,如果超类和子类共享一个特征,则子类优先。
  • 嵌套类:在另一个类内部定义的类。

(三)基础入门

1.创建类

创建类很简单,根据上面的分类,具有原型对象的需要使用extends关键字,没有原型对象的直接使用class关键字就可以的。

;创建有原型的类
class 有原型的类 extends 父类
{
	MyMethod()
	{
	}
}
;创建没有原型类
class 没有原型的类
{
	MyMethod()
	{
	}
}

2.超类/子类

(1)什么是继承?

多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可。

多个类可以称为子类,单独这个类称为父类超类或者基类

子类可以直接访问父类中的非私有的属性和行为。

通过 extends 关键字让类与类之间产生继承关系。

  • 当创建超类的实例时,不影响子类。
  • 当创建子类的实例时,如果子类中不存在某个键/属性/方法,将会检查超类。
  • 例如,在子类的实例中,如果超类和子类中都有同名的方法,则子类中的方法优先。
;AutoHotkey v2.0 父类和子类的关系
class MySuperClass
{
	Method1()
	{
		return A_ThisFunc
	}
	Method2()
	{
		return A_ThisFunc
	}
}

class MySubClass extends MySuperClass
{
	Method2()
	{
		return A_ThisFunc
	}
	Method3()
	{
		return A_ThisFunc
	}
}

oArray1 := MySuperClass()
oArray2 := MySubClass()
vOutput := oArray1.__Class 
. "`r`n" oArray1.base.__Class 
. "`r`n" oArray1.base.base.__Class 
. "`r`n" oArray1.Method1() 
. "`r`n" oArray1.Method2() 
. "`r`n" "==============="
. "`r`n" oArray2.__Class 
. "`r`n" oArray2.base.__Class 
. "`r`n" oArray2.base.base.__Class 
. "`r`n" oArray2.Method1() 
. "`r`n" oArray2.Method2() 
. "`r`n" oArray2.Method3() 
A_Clipboard := vOutput
MsgBox vOutput

MsgBox oArray2.Method2() 
MsgBox oArray2.base.base.Method2() 

;==============================

;output:
;MySuperClass
;MySuperClass
;Object
;MySuperClass.Prototype.Method1
;MySuperClass.Prototype.Method2
;===============
;MySubClass
;MySubClass
;MySuperClass
;MySuperClass.Prototype.Method1
;MySubClass.Prototype.Method2
;MySubClass.Prototype.Method3

3.嵌套类
嵌套类是指在一个类的内部定义另一个类的情况。在面向对象编程中,一个类可以包含其他类的定义,这些被包含的类称为嵌套类或内部类。

class ClassName extends BaseClassName
{
    class NestedClass
    {
        ...
    }
}

嵌套类定义允许类对象与外部类的静态或类变量关联,而不是与独立的全局变量关联。举例来说,在上面的代码中,class NestedClass 创建了一个类对象,并将其存储在 ClassName.NestedClass 中。其他类可以继承这个 NestedClass,或者使用自己的嵌套类来替代它。在这种情况下,可以通过 WhichClass.NestedClass() 来实例化适当的类。

然而,嵌套一个类并不会导致它与外部类有任何特殊的关系。嵌套类不会自动实例化,也不会与外部类的实例产生连接,除非在脚本中显式地建立连接。

另外,由于 Object 方法的工作方式,调用 WhichClass.NestedClass() 会隐式地将 WhichClass 作为第一个参数传递给 NestedClass。这实际上相当于调用 WhichClass.NestedClass.Call(WhichClass)。除非重写了 static Call() 方法,否则这个参数会自动传递给 __New 方法。

  • 嵌套类可以从包含它的类中访问,但不会列在其内容中。
AutoHotkey v1.1 嵌套
class MyOuterClass
{
	class MyInnerClass
	{
	}
}
obj1 := new MyOuterClass
obj2 := new MyOuterClass.MyInnerClass
;obj3 := new MyInnerClass ;causes error in AHK v2

;MyInnerClass is not listed in obj1's contents
vOutput := ""
for vKey, vValue in obj1
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput ;(blank)

vOutput := ""
for vKey, vValue in obj2
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput ;(blank)

;however, obj1.MyInnerClass is listed as an object
MsgBox, % IsObject(obj1.MyInnerClass)
class MyOuterClass
{
	class MyInnerClass
	{
	}
}
obj1 := MyOuterClass()
obj2 := (MyOuterClass.MyInnerClass)()

4.实例化对象

实例化是面向对象编程中的一个重要概念,指的是根据类的定义创建类的实例(也称为对象)。在程序执行过程中,类是一个抽象的模板或蓝图,而实例则是根据这个模板创建出来的具体对象,具有独特的属性和行为。

下面详细解读实例化的过程和相关概念:

1. 类和对象的关系

  • 类(Class):类是一种数据结构,用于描述具有相同属性(数据成员)和方法(函数成员)的对象的集合。它定义了对象的属性和行为。
  • 对象(Object):对象是类的实例化结果,是内存中具体存在的实体,具有类定义的属性和行为。每个对象都是类的一个实例。

2. 实例化过程

实例化是通过类来创建对象的过程。当程序执行创建对象的代码时,会按照以下步骤进行实例化:

  • 选择合适的类:首先根据程序的需要,选择要实例化的类。类定义了对象的属性和方法,确定了对象的行为。
  • 分配内存空间:在内存中为对象分配足够的空间,用于存储对象的数据成员和方法。
  • 初始化对象:调用类的构造函数(如果有的话)来初始化对象的状态。构造函数是一种特殊的方法,在对象创建时自动调用,用于初始化对象的属性。
  • 返回对象的引用:实例化完成后,返回指向对象的引用或指针。通过这个引用,程序可以访问和操作对象的属性和方法。

3. 实例化的特点和作用

  • 封装性:实例化是面向对象编程中实现封装的重要手段。通过类和对象的机制,可以将数据和操作封装在一起,提高代码的安全性和可维护性。
  • 继承性:实例化过程中,如果类之间存在继承关系,子类会继承父类的属性和方法,从而实现代码的复用和扩展。
  • 多态性:实例化时可以根据具体的对象类型调用相应的方法,实现多态的特性,提高代码的灵活性和扩展性。

在AutoHotkey v1.1中,通过new关键字进行实例化,在AutoHotkey v2.0版本,删除了new关键字,直接使用类名()进行实例化。

;AutoHotkey v1.1
;创建类
class MyClass
{
	MyMethod()
	{
	}
}
;实例化
obj2 := new MyClass
;AutoHotkey v2.0
;创建类
class MyClass
{
	MyMethod()
	{
	}
}
;实例化
obj2 := MyClass()

5.对象的基

(1)基本特征

  • 当子类扩展超类时,超类是子类的基。
  • 例如,这里 C.baseBC.base.baseA
;autohotkey v2.0
obj := C()
MsgBox obj.__Class ;C ;(这等同于 obj.base.__Class)
MsgBox obj.base.__Class ;C
MsgBox obj.base.base.__Class ;B
MsgBox obj.base.base.base.__Class ;A

MsgBox A.base.__Class ;Class
MsgBox B.base.__Class ;Class
MsgBox C.base.__Class ;Class

class A
{
}
class B extends A
{
}
class C extends B
{
}
;autohotkey v1.1
obj := new C
MsgBox, % obj.__Class ; C ;(这等同于 obj.base.__Class)
MsgBox, % obj.base.__Class ; C
MsgBox, % obj.base.base.__Class ; B
MsgBox, % obj.base.base.base.__Class ; A

MsgBox, % A.base.__Class ; 空白
MsgBox, % B.base.__Class ; A
MsgBox, % C.base.__Class ; B
class A
{
}
class B extends A
{
}
class C extends B
{
}

(2)修改基

;AutoHotkey v1.1 修改基对象
class MyClass
{
}

vOutput := ""

obj := new MyClass
vOutput .= obj.HasKey("base") " " obj.__Class " " obj.base.__Class "`r`n"

obj := new MyClass
obj.base := {__Class:"MyClassNew"}
vOutput .= obj.HasKey("base") " " obj.__Class " " obj.base.__Class "`r`n"

obj := new MyClass
ObjSetBase(obj, {__Class:"MyClassNew"})
vOutput .= obj.HasKey("base") " " obj.__Class " " obj.base.__Class "`r`n"

obj := new MyClass
ObjRawSet(obj, "base", {__Class:"MyClassNew"})
vOutput .= obj.HasKey("base") " " obj.__Class " " obj.base.__Class "`r`n"

Clipboard := vOutput
MsgBox, % vOutput

;结果:
;0 MyClass MyClass ;原始
;0 MyClassNew MyClassNew ;应用了 obj.base
;0 MyClassNew MyClassNew ;应用了 ObjSetBase
;1 MyClass MyClassNew ;应用了 ObjRawSet
;AutoHotkey v2.0 修改基对象
class MyClass
{
    static foo:="h"
}

vOutput := ""

obj := MyClass()
vOutput .= obj.HasOwnProp("base") " " obj.__Class " " obj.base.__Class "`r`n"

obj := MyClass()
obj.base := {__Class:"MyClassNew"}
vOutput .= obj.HasOwnProp("base") " " obj.__Class " " obj.base.__Class "`r`n"

obj := MyClass()
ObjSetBase(obj, {__Class:"MyClassNew"})
vOutput .= obj.HasOwnProp("base") " " obj.__Class " " obj.base.__Class "`r`n"

obj := MyClass()
obj:=obj.DefineProp('base',{value:{__class:"MyClassNew"}})
vOutput .= obj.HasOwnProp("base") " " obj.__Class " " obj.base.__Class "`r`n"

a_Clipboard := vOutput
MsgBox vOutput

;results:
;0 MyClass MyClass ;original
;0 MyClassNew MyClassNew ;obj.base applied
;0 MyClassNew MyClassNew ;ObjSetBase applied
;1 MyClass MyClassNew ;DefineProp applied

这些示例展示了以下内容:

  • 如果创建了一个名为 ‘base’ 的键(/值属性),这将覆盖 ‘base’ 属性。
  • 因此,’obj.base.__Class’ 获取来自名为 ‘base’ 的属性的内容。
  • 但是,当 ‘obj.__Class’ 无法找到名为 ‘__Class’ 的属性时,它会检查基础对象,而不是检查名为 ‘base’ 的键(/值属性)。因此,在这方面,创建 ‘base’ 键并没有覆盖 ‘base’ 属性。

一些定义/修改基础对象的示例,以改变获取/设置值的方式:

;修改一个 AHK 数组:

;编辑基础对象:
oArray.base := []
oArray.base.__Class := "hello"
vOutput := ""
for vKey, vValue in oArray.base
    vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput

;==============================

;添加一个 __Get 属性,从而更改默认的返回值:
oArray := []
MsgBox, % oArray[1] ;(空白)
oArray.base := {"__Get":"MyFuncReturnHello"}
MsgBox, % oArray[1] ;hello

MyFuncReturnHello()
{
    return "hello"
}

;==============================

;临时添加一个 __Set 方法,从而阻止创建新键:
oArray := []
oArray[1] := "a"
oArray.base := {"__Set":"MyFuncPreventObjSet"}
oArray[2] := "b"

oArray.base.Delete("__Set") ;恢复设置键值的能力

;oArray.Delete("base") ;无法执行,因为 'base' 不是键(/值属性)

;注意:这些尝试修复(恢复设置)没有起作用,
;因为我们尝试设置一个属性 'base' 的值,
;但 MyFuncPreventObjSet 正在阻止这个操作
;oArray.base := {"__Set":""}
;oArray.base := {}
;oArray.base := ""

oArray[3] := "c"
MsgBox, % oArray[1] "_" oArray[2] "_" oArray[3]

MyFuncPreventObjSet()
{
    MsgBox, % A_ThisFunc
    return
}

;==============================

6.AutoHotkey 的基本类对象

AutoHotkey 将 3 个概念合并为一个内置的基本对象类型:

  • 线性数组(例如,obj[1] := “value”)(又称列表/向量)
  • 关联数组(例如,obj[“key”] := “value”)(又称字典/映射)
  • 默认类(自定义/内置,包括键/值属性/动态属性/方法,可以添加/修改/删除,用于创建自定义类)

例如,创建 AHK 基本对象的一些方式。 注意:在AutoHotkey v1.1版本中,AHK 基本对象没有基对象,而在v2.0版本是有基对象的。下面两段代码进行了验证:

;AutoHotkey v 1.1 中基本对象的继承关系验证
;in each case obj is a basic object, and identical:
obj := ["a", "b", "c"]
obj := Array("a", "b", "c")
obj := {1:"a", 2:"b", 3:"c"}
obj := Object(1,"a", 2,"b", 3,"c")
obj := StrSplit("a,b,c", ",")

;这里,MyClass 也是一个基本对象:
class MyClass
{
	MyMethod()
	{
	}
}

MsgBox, % IsObject(ObjGetBase(obj)) ;0
MsgBox, % IsObject(ObjGetBase(MyClass)) ;0

;note: an instance of MyClass is *not* a basic object:
obj2 := new MyClass
MsgBox, % IsObject(ObjGetBase(obj2)) ;1
;AutoHotkey v2.0中继承关系验证
obj := ["a", "b", "c"]
obj := Array("a", "b", "c")
obj := {1:"a", 2:"b", 3:"c"}
obj := Object()
Obj.1:="a"
obj := StrSplit("a,b,c", ",")


class MyClass
{
	MyMethod()
	{
	}
}

MsgBox IsObject(ObjGetBase(obj)) ;1
MsgBox IsObject(ObjGetBase(MyClass)) ;1

obj2 := MyClass()
MsgBox IsObject(ObjGetBase(obj2)) ;1

注意:在 AutoHotkey 中,线性数组是具有 0 或多个整数键的数组,键从 1 开始。

当创建自定义类时,使用 AHK 基本对象作为模板。

内置的键/属性/方法可以被重写/移除,也可以添加额外的键/属性/方法。

默认情况下,对于可以添加到自定义类的键没有限制(它的行为类似于关联数组)。这可以通过指定自定义的 __Set 方法(稍后会讨论)来改变。

(1)内置方法和属性

下面是AutoHotkey v1.1基本对象的内置方法和属性列表,在上面对象部分已经对v 2.0的方法和属性进行了讲解,这里不再进行赘述。

基本对象:仅适用于整数键的方法(7):

  • InsertAt / RemoveAt:在任意位置添加/移除键(并移动后续键)
  • Push / Pop:在数组末尾添加/移除键
  • MinIndex / MaxIndex / Length:获取最小/最大键索引以及键的数量

基本对象:适用于所有键的方法(8):

  • Delete:删除键
  • Count:获取键的数量
  • SetCapacity / GetCapacity:设置/获取对象的键计数容量或值的字符串大小容量
  • GetAddress:获取值的字符串缓冲区地址(如果值是字符串)
  • _NewEnum:返回一个用于遍历对象键/值的枚举器对象
  • HasKey:检查对象是否包含指定键
  • Clone:创建对象的浅复制

基本对象:属性:

  • base:对象实例所基于的对象(如果在实例中未找到键/属性/方法,则检查基对象)

额外的方法:

  • __Init:处理对象实例创建时的初始化(不应由脚本使用)
  • __New:处理对象实例创建时的初始化
  • __Get / __Set:处理获取/设置不存在的键/属性
  • __Call:处理调用不存在的方法
  • __Delete:处理对象实例的删除

额外的方法(进一步):

  • Next:用于遍历对象键的方法
  • ToString:返回一个字符串(注意:AHK v2 中的 String 函数会检查对象是否有 ToString 方法)

额外的属性:

  • __Class:对象实例的类名

一些额外的注意事项

  • 键名可以是整数类型(正数/0/负数)或字符串类型。
  • (键名也可以是对象,例如 o := {[]:1}。)
  • 键名是唯一的,不能有两个相同名称的键。
  • 当使用超出某一大小的整数作为键名时,这些键名会以字符串形式存储(而不是整数)。
  • 在 AHK v1 中,键名是不区分大小写的。区分大小写的替代方法是使用 Scripting.Dictionary 对象或具有 __Call 方法的自定义类,该方法可以根据传递给它的键名的大小写执行操作。
  • 值可以是字符串/整数/浮点数/对象引用/函数引用,以及可能的其他实体。(AutoHotkey 基本对象非常灵活,对值的类型没有限制。)
  • 当对对象进行 for 循环时,整数键名(及其值)按数字顺序返回,然后按字母顺序返回字符串键名(及其值)。 (因此,要将整数视为字符串,可以使用前缀字符(例如 ‘z’)用于整数和字符串。)
  • 注意:创建与方法/属性同名的键可能会阻止对该方法/属性的访问。例如,如果创建名为 ‘HasKey’ 的键,这将干扰 HasKey 方法的使用。

AUTOHOTKEY 对象函数

  • 以下是与对象相关的所有内置 AutoHotkey 函数的列表供参考。
  • (此列表旨在完整,但可能会有其他/未来的函数符合条件。)

具有方法等效的函数

  • 注意:AutoHotkey 有内置方法,但这些方法可以被覆盖。
  • 例如,可以通过定义自定义的 HasKey 方法来覆盖 HasKey 方法。
  • 例如,如果存在称为 HasKey 的键(/值属性),则 HasKey 方法会失败。
  • 许多(但不是所有)ObjXXX 函数的目的是绕过任何已被覆盖的方法行为,而是使用默认行为。

AHK v1.1 对象方法

基本对象:仅适用于整数键的方法(7):

  • ObjInsertAt / ObjRemoveAt
  • ObjPush / ObjPop
  • ObjMinIndex / ObjMaxIndex / ObjLength

基本对象:适用于所有键的方法(8):

  • ObjDelete
  • ObjCount
  • ObjSetCapacity / ObjGetCapacity
  • ObjGetAddress
  • ObjNewEnum [注意:没有下划线,与 ‘_NewEnum’ 不同]
  • ObjHasKey
  • ObjClone

AHK v2.0 对象方法

  • 静态方法:
    • Call: 创建一个新对象.
  • 方法:
    • Clone: 返回对象的一个浅拷贝.
    • DefineProp: 定义一个新的自有属性.
    • DeleteProp: 删除对象拥有的属性.
    • GetOwnPropDesc: 返回给定自有属性的描述符, 兼容于 DefineProp.
    • HasOwnProp: 如果对象拥有该名称的属性, 则返回 1(true).
    • OwnProps: 枚举一个对象自有的属性.
  • 属性:
    • Base: 检索或设置一个对象的基对象.
  • 函数:
    • ObjSetBase: 设置对象的基对象.
    • ObjGetCapacity, ObjSetCapacity: 检索或设置对象所包含属性的容量.
    • ObjOwnPropCount: 检索对象包含的自有属性的数量.
    • ObjHasOwnProp, ObjOwnProps: 等同于相应的预定义方法, 但不能被覆盖.

其他函数

  • Array / Object:创建一个数组/对象,分别相当于 obj := []obj := {}(注意:在 AHK v1 中,这两者都创建基本对象)(注意:AHK v2 使用 []/{}/Map() 分别创建数组/对象/映射)
  • ComObjActive / ComObjArray / ComObjConnect / ComObjCreate / ComObject / ComObjError / ComObjFlags / ComObjGet / ComObjQuery / ComObjType / ComObjValue:COM 对象不在本章节中讨论
  • IsFunc:检查是否存在具有特定名称的函数(AHK v1 仅限:可以检查对象是否为函数引用,AHK v2 中可以使用hasmethod代替)
  • IsObject:检查变量是否为对象
  • ObjAddRef / ObjRelease:修改对象的引用计数(使用 ObjAddRef,然后 ObjRelease,可以检索计数,而不更改它)
  • ObjBindMethod:为对象方法创建一个 BoundFunc 对象(即存储一个方法,以及可选的一些初始参数,作为对象变量)(即创建一个行为类似函数的变量)
  • ObjGetBase / ObjSetBase:获取/设置对象的基对象
  • ObjRawGet / ObjRawSet:获取/设置键(/值属性)的值,绕过任何自定义类行为
  • String:基于变量返回一个字符串,将检查对象是否有 ‘ToString’ 方法
  • StrSplit:基于分隔符拆分字符串创建一个数组
  • Type:获取变量的类型(AutoHotkey v2.0)

8.自定义类示例:动物园类

这里是一个示例自定义类,包含多个键、方法和属性,以便您看看它的样子。

定义类之后,我们使用 for 循环列出了 4 个对象的内容(2 对象对):

  1. 实例对象(从类蓝图创建的单个对象),及其基础对象。
  2. 类对象(类蓝图存储为类对象),及其基础对象。(在 AHK v1 中,类对象没有基础对象,除非手动添加。)

一般来说,在 AHK v1 中,自定义类的实例具有基础对象。基础对象存储影响对象工作方式的内容。

但是 AHK 基础对象和类对象可以手动设置基础对象。

(注意:尝试探查 AHK 基础对象的基础对象,以列出任何键/属性/方法,不会显示任何内容。因为它没有基础对象。)

;AutoHotkey v1.1 
class MyZooClass
{
	MyValueProperty := "MyValuePropertyValue"
	static MyStaticProperty := "MyStaticPropertyValue"

	MyMethod()
	{
		return "MyMethodValue"
	}

	MyDynamicProperty
	{
		get
		{
			return "MyDynamicPropertyValue"
		}
		set
		{
		}
	}

	__New()
	{
	}
	__Get()
	{
	}
	__Set()
	{
	}
	__Call()
	{
	}
	__Delete()
	{
	}
}

obj := new MyZooClass

vOutput := ""

vOutput .= "实例对象:`r`n"
for vKey, vValue in obj
	vOutput .= vKey " " vValue "`r`n"
vOutput .= "`r`n"

vOutput .= "实例对象的基:`r`n"
for vKey, vValue in obj.base
	vOutput .= vKey " " vValue "`r`n"
vOutput .= "`r`n"

vOutput .= "class 对象:`r`n"
for vKey, vValue in MyZooClass
	vOutput .= vKey " " vValue "`r`n"
vOutput .= "`r`n"

vOutput .= "class 对象的基:`r`n"
for vKey, vValue in MyZooClass.base
	vOutput .= vKey " " vValue "`r`n"
vOutput .= "`r`n"

Clipboard := vOutput
MsgBox, % vOutput

/*;instance object:
;MyValueProperty MyValuePropertyValue

;instance object base:
;__Call
;__Class MyZooClass [note: created automatically, we didn't define this explicitly]
;__Delete
;__Get
;__Init [note: created automatically, we didn't define this explicitly]
;__New
;__Set
;MyDynamicProperty
;MyMethod
;MyStaticProperty MyStaticPropertyValue

;[identical to the contents of the instance object base]
;class object:
;__Call
;__Class MyZooClass [note: created automatically, we didn't define this explicitly]
;__Delete
;__Get
;__Init [note: created automatically, we didn't define this explicitly]
;__New
;__Set
;MyDynamicProperty
;MyMethod
;MyStaticProperty MyStaticPropertyValue

;class object base:
;(空)
*/

MsgBox, % IsObject(obj) " " IsObject(obj.base) ;1 1
MsgBox, % IsObject(MyZooClass) " " IsObject(MyZooClass.base) ;1 0
;AutoHotkey v2.0
class MyZooClass
{
	;MyValueProperty := "MyValuePropertyValue"
	static MyStaticProperty := "MyStaticPropertyValue"

	MyMethod()
	{
		return "MyMethodValue"
	}

	shortProperty
	{
		get => "MyDynamicPropertyValue"
		set => ""
	}

	__New()
	{
	}
	__Get()
	{
	}
	__Set()
	{
	}
	__Call()
	{
	}
	__Delete()
	{
	}
}

obj := MyZooClass()

vOutput := ""

MsgBox(obj.shortProperty) ;String
MsgBox(Type(obj.MyMethod)) ;Func
;AutoHotkey 父类子类的关系
class MySuperClass
{
	Method1()
	{
		return A_ThisFunc
	}
	Method2()
	{
		return A_ThisFunc
	}
}

class MySubClass extends MySuperClass
{
	Method2()
	{
		return A_ThisFunc
	}
	Method3()
	{
		return A_ThisFunc
	}
}

oArray1 := new MySuperClass
oArray2 := new MySubClass
vOutput := oArray1.__Class ;MySuperClass
. "`r`n" oArray1.base.__Class ;MySuperClass
. "`r`n" oArray1.base.base.__Class ;(blank)
. "`r`n" oArray1.Method1() ;MySuperClass.Method1
. "`r`n" oArray1.Method2() ;MySuperClass.Method2
. "`r`n" oArray1.Method3() ;(blank)
. "`r`n" "==============="
. "`r`n" oArray2.__Class ;MySubClass
. "`r`n" oArray2.base.__Class ;MySubClass
. "`r`n" oArray2.base.base.__Class ;MySuperClass
. "`r`n" oArray2.Method1() ;MySuperClass.Method1
. "`r`n" oArray2.Method2() ;MySubClass.Method2
. "`r`n" oArray2.Method3() ;MySubClass.Method3
Clipboard := vOutput
MsgBox, % vOutput

;oArray2 can access both versions of 'Method2'
MsgBox, % oArray2.Method2() ;MySubClass.Method2
MsgBox, % oArray2.base.base.Method2() ;MySuperClass.Method2

;==============================

;output:
;MySuperClass
;MySuperClass
;
;MySuperClass.Method1
;MySuperClass.Method2
;
;===============
;MySubClass
;MySubClass
;MySuperClass [base of base is superclass]
;MySuperClass.Method1
;MySubClass.Method2 [subclass Method2 takes precedence over superclass Method2]
;MySubClass.Method3
obj := new C
MsgBox, % obj.__Class ;C ;(this points to obj.base.__Class)
MsgBox, % obj.base.__Class ;C
MsgBox, % obj.base.base.__Class ;B
MsgBox, % obj.base.base.base.__Class ;A

MsgBox, % A.base.__Class ;(blank)
MsgBox, % B.base.__Class ;A
MsgBox, % C.base.__Class ;B

class A
{
}
class B extends A
{
}
class C extends B
{
}

从检查对象内容中我们可以学到:

  • 当基于类对象创建实例对象时,实例对象的基础对象是类对象的克隆。
  • 创建一个 __Class 属性,对应于类定义开头指定的类名称。

关于 __Init

  • 尽管我们在类对象中定义了 MyValueProperty,但我们在类对象的内容中没有看到它列出。(MyValueProperty 是通过 __Init 方法创建的。)
  • 列出了 __Init 方法,即使我们没有指定一个。__Init 方法创建 MyValueProperty。如果从类定义中移除 MyValueProperty := "MyValuePropertyValue" 这行,则在内容列表中不会出现 __Init 方法。
  • 因此,__Init 方法是由 AHK 自动为我们创建的,它用于创建对象实例的键(/属性值)。

在 AHK v1 中,基础对象和自定义类对象没有基础对象。(虽然它们可以随后手动设置基础对象。)

自定义类的实例对象具有类对象作为其基础对象。

编辑基础对象时要小心,它会影响共享该基础对象的现有和未来实例对象。

其他说明: 类定义中 ‘base’ 关键字的可用性。

obj := new MyClass

obj.prop := 1
MyClass.base := {prop:2}
MyClass.prop := 3
obj.MyMethod()

obj.base.prop := 4
obj.MyMethod()

class MyClass
{
    MyMethod()
    {
        MsgBox, % this.prop ; 1
        MsgBox, % base.prop ; 2
        MsgBox, % MyClass.base.prop ; 2
        MsgBox, % this.base.prop ; 3 然后 4
        MsgBox, % MyClass.prop ; 3 然后 4
    }
}

注意: 我还没有找到关于 AHK 基础对象是否具有基础对象的明确答案。

例如,这表明 AHK 基础对象没有基础对象:

; AHK v1
obj := []
MsgBox, % IsObject(obj.base) ; 0;报告 AHK 基础对象没有基础对象

但可以说在概念上,AHK 基础对象确实具有默认基础对象。例如,如果一个类的实例没有自己的 MyMethod 方法,它会检查基础对象中是否有 MyMethod。类似地,如果 AHK 基础对象没有自己的自定义 HasKey 方法,它会回退到默认的 HasKey 方法。

注意: AHK 文档讨论了非对象的“默认基础对象”。例如,当执行 "mystring".method()myvar.method() 时,如果 var 不包含对象,则定义了会发生什么。

这一章暗示了我们将在接下来的章节中讨论的几乎所有内容。

还将提及的其他 3 个内容是:超类/子类(父类/子类,扩展类)、嵌套类(类内类)和枚举器对象。

超类/子类和嵌套类将在接下来的两章中介绍。

之后,我们将更详细地研究自定义类的特定方面。

最后,我们将考虑枚举器对象。这是因为与枚举器相关的问题与通常适用于类的问题有些分离。

__CLASS 属性和基础对象:追踪基础

  • 如果我们定义一个空的类,它将产生一个与 AHK 基本对象相同的对象,唯一的区别是类名。
  • 从类对象创建的任何实例对象都会在其基础对象中具有一个名为 ‘__Class’ 的属性。实例对象的基础对象就是类对象本身。’__Class’ 的值是类的名称。
  • 在下面的示例中,我们将演示直接修改 ‘base’ 属性的值,并通过 ‘obj.base := ‘ 和 ObjSetBase 分配一个基础对象。
  • 我们还将通过 for 循环演示列出对象的键/值对。
;AutoHotkey v1.1  如果我们定义一个空类,它将产生一个对象与 AHK 基对象相同
class MyEmptyClass
{
}

MsgBox, % MyEmptyClass.__Class ;MyEmptyClass

obj1 := {} ;basic AHK object
obj2 := new MyEmptyClass
obj3 := new MyEmptyClass() ;equivalent to the line above

MsgBox, % obj1.__Class ;(blank)
MsgBox, % obj2.__Class ;MyEmptyClass
MsgBox, % obj3.__Class ;MyEmptyClass

MsgBox, % obj1.base.__Class ;(blank)
MsgBox, % obj2.base.__Class ;MyEmptyClass
MsgBox, % obj3.base.__Class ;MyEmptyClass

;obj1 does not contain a __Class property:
vOutput := ""
for vKey, vValue in obj1
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput ;(blank)

;obj1's base does not contain a __Class property:
vOutput := ""
for vKey, vValue in obj1.base
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput ;(blank)

;obj2 does not contain a __Class property:
vOutput := ""
for vKey, vValue in obj2
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput ;(blank)

;obj2's base *does* contain a __Class property:
vOutput := ""
for vKey, vValue in obj2.base
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput ;__Class MyEmptyClass

MsgBox, % IsObject(obj1.base) ;0 ;reports that the AHK basic object has no base object
MsgBox, % IsObject(obj2.base) ;1

;change the objects' class names:
obj1.base.__Class := "NewClassName1" ;doesn't work
obj2.base.__Class := "NewClassName2"
MsgBox, % obj1.__Class ;(blank)
MsgBox, % obj2.__Class ;NewClassName2

obj1.base := {__Class:"NewClassName3"}
obj2.base := {__Class:"NewClassName4"}
MsgBox, % obj1.__Class ;NewClassName3
MsgBox, % obj2.__Class ;NewClassName4

ObjSetBase(obj1, {__Class:"NewClassName5"})
ObjSetBase(obj2, {__Class:"NewClassName6"})
MsgBox, % obj1.__Class ;NewClassName5
MsgBox, % obj2.__Class ;NewClassName6
;AutoHotkey v2.0 空类
class MyEmptyClass
{
}

MsgBox MyEmptyClass.__Class ;Class

obj1 := {} ;object
obj2 :=MyEmptyClass()

MsgBox obj1.__Class ;object
MsgBox obj2.__Class ;MyEmptyClass


MsgBox obj1.base.__Class ;object
MsgBox obj2.base.__Class ;MyEmptyClass


MsgBox IsObject(obj1.base) ;1 
MsgBox IsObject(obj2.base) ;1

;change the objects' class names:
obj1.base.__Class := "NewClassName1" 
obj2.base.__Class := "NewClassName2"
MsgBox obj1.__Class ;NewClassName1
MsgBox obj2.__Class ;NewClassName2

obj1.base := {__Class:"NewClassName3"}
obj2.base := {__Class:"NewClassName4"}
MsgBox obj1.__Class ;NewClassName3
MsgBox obj2.__Class ;NewClassName4

ObjSetBase(obj1, {__Class:"NewClassName5"})
ObjSetBase(obj2, {__Class:"NewClassName6"})
MsgBox obj1.__Class ;NewClassName5
MsgBox obj2.__Class ;NewClassName6

从上面的测试中,我们看到 obj2.base 有一个名为 ‘__Class’ 的属性,但 obj2 没有这样的属性。 obj2.base.__Class 返回了预期的值。 obj2 没有名为 ‘__Class’ 的属性,因此它检查它的基础对象,所以 obj2.__Class 返回 obj2.base.__Class 的值。 一般来说,对于对象,如果 obj.prop 不存在,会检查 obj.base.propobj.base.base.prop 等(你可以将其称为“追踪基础”)。 如果找到了这样的属性,则返回属性的值。如果没有找到属性,则返回空字符串。

  • 注意:当定义一个类时,会创建一个与类名相同的类对象(变量),其中包含该对象。可以通过 ListVars 或 ‘View, Variables and their contents’ 查看。
  • 因此,实例对象和类对象都是存储在变量中的对象。
;这个类定义会创建一个名为 'MyClass' 的变量
class MyClass
{
}

;创建一个实例对象
obj := new MyClass

;引用一个实例对象
MsgBox, % IsObject(obj) ;1

;引用一个类对象
MsgBox, % IsObject(MyClass) ;1

;获取类名
MsgBox, % obj.__Class ;MyClass
MsgBox, % MyClass.__Class ;MyClass
MsgBox, % obj.base.__Class ;MyClass
MsgBox, % MyClass.base.__Class ;(空白)
  • 注意:我们在“动物园类和基础对象”章节中学到,类对象没有基础对象,并且是 AHK 基本对象。
  • 一些对比 AHK 基本对象、自定义类对象和自定义类对象实例的测试: (注意:在 AHK v1 中,HasKey 用于检查键(/值属性)的存在。)
;AutoHotkey v1.1 比较 AHK 基本对象、自定义类
class MyClass
{
}

;a custom class object:
MsgBox, % MyClass.HasKey("base") ;0
MsgBox, % IsObject(MyClass.base) ;0
MsgBox, % MyClass.base.__Class ;(blank)

;an instance of a custom class object:
obj := new MyClass
MsgBox, % obj.HasKey("base") ;0
MsgBox, % IsObject(obj.base) ;1
MsgBox, % obj.base.__Class ;MyClass

;for comparison, an AHK basic object:
obj := []
MsgBox, % obj.HasKey("base") ;0
MsgBox, % IsObject(obj.base) ;0
MsgBox, % obj.base.__Class ;(blank)
;AutoHotkey v2.0 比较 AHK 基本对象、自定义类
class MyClass
{
}

;a custom class object:
MsgBox IsObject(MyClass.base) ;1
MsgBox MyClass.base.__Class ;class

;an instance of a custom class object:
obj := MyClass()
MsgBox IsObject(obj.base) ;1
MsgBox obj.base.__Class ;MyClass

;for comparison, an AHK basic object:
myarray := []
MsgBox IsObject(myarray.base) ;1
MsgBox myarray.base.__Class ;array
  • 注意:为什么尝试的修复方法没有起作用尚未完全解释清楚。

(四)键/属性/方法

1.类:值属性

以下是一些关于值属性和类的经典操作,将在下面进行介绍。

注意:在 AHK v1 中,键和值属性是相同的东西。

2.预先填充对象的值属性

在类体内定义值属性的示例,使用/不使用 ‘static’。

当使用 ‘static’ 时,属性将放置在基础对象中(类对象中)。

当省略 ‘static’ 时,属性的局部副本将放置在实例对象中。

;AutoHotkey v1.1 获取、删除属性值
class MyKeyClass
{
    static prop1 := "value1" ; 仅有一个副本,在类对象中
    prop2 := "value2" ; 每个实例对象中有一个本地副本
}

obj := new MyKeyClass
MsgBox, % obj.prop1 ; value1;从基础对象获取值
MsgBox, % obj.prop2 ; value2
MsgBox, % obj.base.prop1 ; value1
MsgBox, % obj.base.prop2 ; (空白)
obj.Delete("prop1") ; 无效,'prop1' 存在于基础对象中,而不是对象本身
obj.Delete("prop2") ; 删除 prop2
MsgBox, % obj.prop1 ; value1
MsgBox, % obj.prop2 ; (空白)

obj.base.Delete("prop1") ; 删除 prop1
vOutput := ""
for vKey, vValue in obj.base
    vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput

; 这显示 MyKeyClass.prop1 也已被删除:
vOutput := ""
for vKey, vValue in MyKeyClass
    vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput
;AutoHotkey v2.0 属性值获取与删除
class MyKeyClass
{
	prop2 := "value2" ;one local copy for all instance objects
}

obj := MyKeyClass()
MsgBox obj.prop2 ;value2
obj.Deleteprop("prop2") ;prop2 删除
MsgBox obj.prop2 ;报错

尝试删除 ‘prop1’,只检查对象本身。

尝试检索 ‘prop1’ 的内容,首先检查对象本身,发现没有这样的属性,然后检查基础对象。

记录每次为特定类创建对象

这是一个保持实例计数并为每个对象分配问题编号的示例,它使用后面讨论的 __New 方法。

;AutoHotkey v1.1 _new
class MyIncrementClass
{
	static issued := 0
	__New()
	{
		MyIncrementClass.issued++
		this.num := MyIncrementClass.issued
	}
}

;create new instances each with an issue number
obj1 := new MyIncrementClass
obj2 := new MyIncrementClass
obj3 := new MyIncrementClass
MsgBox, % obj1.num ;1
MsgBox, % obj2.num ;2
MsgBox, % obj3.num ;3

;get total number of classes issued
MsgBox, % MyIncrementClass.issued ;3
;AutoHotkey v2.0 _new
class MyIncrementClass
{
	static issued := 0
	__New()
	{
		MyIncrementClass.issued++
		this.num := MyIncrementClass.issued
	}
}

;create new instances each with an issue number
obj1 := MyIncrementClass()
obj2 := MyIncrementClass()
obj3 := MyIncrementClass()
MsgBox obj1.num ;1
MsgBox obj2.num ;2
MsgBox obj3.num ;3

;get total number of classes issued
MsgBox MyIncrementClass.issued ;3

具有属性的对象(更改现有和未来实例的属性值)

在此示例中,更改所有实例的属性内容。

;AutoHotkey v1.1 父类属性变,实例的属性都变,实例属性变,父类不变
class MyPropClass
{
	static prop := "MyValue"
}
obj1 := new MyPropClass
obj2 := new MyPropClass
MsgBox, % obj1.prop ;MyValue
MsgBox, % obj2.prop ;MyValue

MyPropClass.prop := "MyValueNEW"
MsgBox, % obj1.prop ;MyValueNEW
MsgBox, % obj2.prop ;MyValueNEW

MsgBox

;note: if we assign to obj1.prop this will create obj1.prop (and will not override MyPropClass.prop aka obj.base.prop)
obj1.prop := "hello"
MsgBox, % obj1.prop ;hello
MsgBox, % obj1.base.prop ;MyValueNEW
MsgBox, % obj2.prop ;MyValueNEW ;reads obj2.base.prop
MsgBox, % MyPropClass.prop ;"MyValueNEW"

我们可以使用动态属性引用全局变量。更改全局变量的值将影响未来和现有实例。

class MyClass {
    myInstanceVar := 0  ; 实例变量

    ; 构造函数
    __new() {
        ; 自增实例变量
        this.myInstanceVar++
    }

    ; 获取实例变量值的方法
    GetInstanceVar() {
        return this.myInstanceVar
    }

    ; 设置实例变量值的方法
    SetInstanceVar(newValue) {
        this.myInstanceVar := newValue
    }
}

; 创建 MyClass 的两个实例
obj1 := MyClass()
obj2 := MyClass()

; 显示实例变量的初始值
MsgBox("obj1 的实例变量值: " . obj1.GetInstanceVar()) ; 输出: 1
MsgBox("obj2 的实例变量值: " . obj2.GetInstanceVar()) ; 输出: 1

; 修改实例变量的值
obj1.SetInstanceVar(10)

; 显示更新后的实例变量值
MsgBox("obj1 的更新后实例变量值: " . obj1.GetInstanceVar()) ; 输出: 10
MsgBox("obj2 的实例变量值仍然为: " . obj2.GetInstanceVar()) ; 输出: 1
class MyClass {
    static myStaticVar := 0

    ; 构造函数
    static __new() {
        ; 增加静态变量的值
        this.myStaticVar++
    }

    ; 获取静态变量的方法
    static GetStaticVar() {
        return this.myStaticVar
    }

    ; 设置静态变量的方法
    static SetStaticVar(newValue) {
        this.myStaticVar := newValue
    }
}

; 创建 MyClass 的实例
obj1 := MyClass()
obj2 := MyClass()

; 显示初始静态变量的值
MsgBox("obj1 的静态变量值: " . MyClass.GetStaticVar()) ; 输出: 2
MsgBox("obj2 的静态变量值: " . MyClass.GetStaticVar()) ; 输出: 2

; 设置新的静态变量值
MyClass.SetStaticVar(10)

; 显示更新后的静态变量值
MsgBox("obj1 的更新后静态变量值: " . MyClass.GetStaticVar()) ; 输出: 10
MsgBox("obj2 的更新后静态变量值: " . MyClass.GetStaticVar()) ; 输出: 10
;AutoHotkey v1.1 可以引入全局变量
global vGblMyValue := "MyValue"
class MyKeyClass
{
	MyProperty
	{
		get
		{
			global vGblMyValue
			return vGblMyValue
		}
		set
		{
		}
	}
}

obj1 := new MyKeyClass
MsgBox, % obj1.MyProperty ;MyValue

vGblMyValue := "MyValueNEW"
MsgBox, % obj1.MyProperty ;MyValueNEW

obj2 := new MyKeyClass
MsgBox, % obj2.MyProperty ;MyValueNEW
;AutoHotkey v2.0
global vGblMyValue := "MyValue"
class MyKeyClass
{
	MyProperty
	{
		get
		{
			global vGblMyValue
			return vGblMyValue
		}
		set
		{
		}
	}
}

obj1 := MyKeyClass()
MsgBox obj1.MyProperty ;MyValue

vGblMyValue := "MyValueNEW"
MsgBox obj1.MyProperty ;MyValueNEW

obj2 := MyKeyClass()
MsgBox obj2.MyProperty ;MyValueNEW

其他操作

保持类实例列表

在这个示例中:

  • MyKeepTrackClass 类有一个静态属性 instances,它是一个数组,用于存储所有创建的 MyKeepTrackClass 实例的引用。
  • __New() 构造函数中,每次创建一个新实例时,会将该实例的引用 &this 添加到 instances 数组中,并且设置实例的属性 prop 初始值为 "MyValue"
  • UpdateAllProps(vValue) 方法用于更新所有实例的 prop 属性为指定的值 vValue。首先,它会更新当前实例的 prop 属性为 vValue。然后,它会遍历 instances 数组中的所有实例引用,将每个实例的 prop 属性都设置为 vValue

通过调用 MyKeepTrackClass.UpdateAllProps("MyValueNEW") 方法后,所有实例的 prop 属性都被更新为 "MyValueNEW"。这个更新是通过静态属性 instances 中存储的实例引用来实现的。

;AutoHotkey v1.1
class MyKeepTrackClass
{
    static instances := []  ; 静态属性,用于存储所有实例的引用

    __New()
    {
        this.prop := "MyValue"  ; 每个实例的属性 prop 初始值为 "MyValue"
        this.instances.Push(&this)  ; 将当前实例的引用添加到静态数组 instances 中
    }

    UpdateAllProps(vValue)
    {
        this.prop := vValue  ; 更新当前实例的属性 prop 为指定的值 vValue

        ; 遍历静态数组 instances 中的所有实例引用
        for _, vAddr in this.instances
        {
            obj := Object(vAddr)  ; 将引用转换为对象
            if IsObject(obj)
                obj.prop := vValue  ; 更新数组中每个实例的属性 prop 为指定的值 vValue
        }
    }
}

; 创建两个 MyKeepTrackClass 的实例
obj1 := new MyKeepTrackClass
obj2 := new MyKeepTrackClass

MsgBox, % obj1.prop ; 输出:MyValue
MsgBox, % obj2.prop ; 输出:MyValue

; 调用类的静态方法 UpdateAllProps,更新所有实例的 prop 属性为 "MyValueNEW"
MyKeepTrackClass.UpdateAllProps("MyValueNEW")
; 也可以使用实例的方法调用:obj1.UpdateAllProps("MyValueNEW")

MsgBox, % obj1.prop ; 输出:MyValueNEW
MsgBox, % obj2.prop ; 输出:MyValueNEW

当从一个类派生多个实例时,修改一个实例的基类会影响所有实例。

以下是一种方法,可以保留一个实例的特性,但允许对其基类进行任何编辑,而不影响其他实例。

class MyPropClass
{
	static prop := "MyValue"
}

obj1 := new MyPropClass
obj2 := new MyPropClass

MsgBox, % obj1.prop ;MyValue
MsgBox, % obj2.prop ;MyValue

MyPropClass.prop := "MyValueNEW"

MsgBox, % obj1.prop ;MyValueNEW
MsgBox, % obj2.prop ;MyValueNEW

base2 := ObjGetBase(obj2)
ObjSetBase(obj2, ObjClone(base2))

MyPropClass.prop := "MyValueNEWER"

MsgBox, % obj1.prop ;MyValueNEWER"
MsgBox, % obj2.prop ;MyValueNEW" ;此时值不受影响

当我们使用 MyPropClass.prop := "MyValueNEW" 时,会修改所有实例的 prop 值,因为这是一个静态属性。为了使 obj2prop 值保持不受影响,我们首先使用 ObjGetBase(obj2) 获取 obj2 的基类,然后使用 ObjClone(base2) 创建一个基类的克隆,并将其设置为 obj2 的新基类,这样就使得 obj2prop 值不再受到 MyPropClass.prop 的改变影响。

预先为对象填充键(更改未来实例但不影响现有实例)

要预先为对象填充键,并确保类的未来实例反映更新的键值,而不影响现有实例,您可以在AutoHotkey中使用静态属性和实例属性的组合。目标是使每个实例在实例化时捕获类的键值快照。

以下是实现此目的的方法:

class MyPropClass
{
    static prop := "MyValue"  ; 定义静态属性 'prop' 并初始化为 "MyValue"

    __New()
    {
        this.prop := MyPropClass.prop  ; 将当前类的 'prop' 值分配给每个实例的 'prop' 属性
    }
}

obj1 := new MyPropClass  ; 创建第一个实例

MyPropClass.prop := "MyValueNEW"  ; 更改类的 'prop' 值以影响未来实例
obj2 := new MyPropClass  ; 创建第二个实例

MyPropClass.prop := "MyValueNEWER"  ; 再次更改类的 'prop' 值以影响未来实例
obj3 := new MyPropClass  ; 创建第三个实例

; 显示每个实例的属性值
MsgBox, % obj1.prop  ; 显示: MyValue
MsgBox, % obj2.prop  ; 显示: MyValueNEW
MsgBox, % obj3.prop  ; 显示: MyValueNEWER

在上面的示例中:

  • MyPropClass 是一个类,具有静态属性 prop,最初设置为 "MyValue"
  • __New() 方法是AutoHotkey类中的特殊方法,在创建类的新实例时自动调用。
  • __New() 方法中,this.prop := MyPropClass.propMyPropClass 的静态属性 prop 的当前值分配给每个实例的 prop 属性。这样可以在实例创建时捕获静态属性的值快照。

当您将 MyPropClass.prop 更改为 "MyValueNEW",然后再更改为 "MyValueNEWER" 时,这些更改仅影响在每次更改后创建的未来实例,而不影响现有实例的捕获初始值。

类内部的 ‘static’ 使用与其他地方的区别

  • 在类定义内部: 当 static 在类体内部使用时(class MyPropClass { ... }),它定义了一个在类的所有实例之间共享的键值属性(在本例中为 prop)。
  • 访问静态值: 可以使用 myclass.prop(针对类本身)或 myinstance.base.prop(针对类的实例)来访问类内部定义的静态值。
  • 更改静态值: 可以在以后的某个时间修改静态值(例如 MyPropClass.prop := "MyValueNEW")以影响未来创建的实例。
  • 与其他位置的静态值的对比: 这与在函数内部或动态属性的 getter/setter 中使用静态值的方式不同,在这些情况下,无法直接访问静态值。

在AutoHotkey中,方法、属性 getter(property.get)和属性 setter(property.set)都是函数,可以使用 A_ThisFunc 来访问它们的函数名。这种区别有助于理解静态值在类定义内部与语言的其他上下文中操作方式的不同。

类:属性

这里有一个简单的类,其中包含一个动态属性,其工作方式几乎与键/值属性相同。 有一个区别是,会创建一个存储属性值的键/值属性。

class MyPropertyClass
{
	MyProperty
	{
		get
		{
			return this._MyProperty
		}
		set
		{
			this._MyProperty := value
			return value
		}
	}
}
  • 动态属性的功能类似于键,但在幕后的工作方式类似于方法。
  • 它们允许具有动态生成值的属性,并允许只读属性。
  • 一个只读属性应该定义当尝试设置其值时会发生什么,否则可能会创建一个同名的键/值属性,并且该键会优先于属性。将返回键的值,而不是动态属性的值。
  • 注意:在编程中,对于仅用于内部使用的键/属性/方法,通常以下划线 ‘_’ 开头命名是很常见的。
  • 注意:当在类中定义动态属性时,AutoHotkey会将它们定义为函数,函数名的形式为 ‘class.property.get’ 和 ‘class.property.set’(可以使用 A_ThisFunc 查看)。
class MyPropertyClass
{
	; MyProperty 类似于普通键
	MyProperty
	; MyProperty[] ; 上面一行的等价写法(方括号是可选的)
	{
		get
		{
			; MsgBox, % A_ThisFunc ; MyPropertyClass.MyProperty.get
			; 使用特殊变量 'this',表示对象实例:
			return this._MyProperty
		}
		set
		{
			; MsgBox, % A_ThisFunc ; MyPropertyClass.MyProperty.set
			; 使用特殊变量 'value',例如对于 'obj.MyProperty := "hello"',value 等于 'hello'
			this._MyProperty := value
		}
	}

	MyReadOnlyProperty
	{
		get
		{
			return "ReadOnlyValue"
		}
		set ; 这是必需的,以使其成为只读属性,否则可以创建一个名为 'MyReadOnlyProperty' 的键
		{
		}
	}

	; 写入属性将非常不寻常
	MyWriteOnlyProperty
	{
		get
		{
		}
		set
		{
			_MyWriteOnlyProperty := value
		}
	}

	Now
	{
		get
		{
			return A_Now
		}
		set
		{
		}
	}
}

obj := new MyPropertyClass

; MyProperty 外观上与键类似:
obj.MyProperty := "MyPropertyValue"
MsgBox, % obj.MyProperty ; MyPropertyValue
obj["MyKey"] := "MyKeyValue"
MsgBox, % obj["MyKey"] ; MyKeyValue

; 但是,MyProperty 也使用一个名为 '_MyProperty' 的键/值属性来存储其值:
MsgBox, % obj._MyProperty ; MyPropertyValue

; 此赋值不起作用,因为属性是只读的:
obj.MyReadOnlyProperty := "new value"
MsgBox, % obj.MyReadOnlyProperty ; ReadOnlyValue

; 获取当前日期和时间:
MsgBox, % obj.Now
Sleep, 1000
MsgBox, % obj.Now

; obj 包含各种键/值属性:
vOutput := ""
for vKey, vValue in obj
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput
; 输出:
; _MyProperty MyPropertyValue
; MyKey MyKeyValue

; obj 的基类包含各种属性:
vOutput := ""
for vKey, vValue in ObjGetBase(obj)
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput
; 输出:
; __Class MyPropertyClass
; MyProperty
; MyReadOnlyProperty
; MyWriteOnlyProperty
; Now

获取属性信息的示例代码:

; AHK v2
; 使用 AHK v2 来使用 Type 函数
class MyPropertyClass
{
	MyProperty
	{
		get
		{
			; MsgBox, % A_ThisFunc
			return "MyPropertyValue"
		}
		set
		{
			; MsgBox, % A_ThisFunc
		}
	}
}

; 将属性从类复制到数组:

; 一些测试:
MsgBox(IsFunc("MyPropertyClass.MyProperty")) ; 0
MsgBox(IsObject(MyPropertyClass.MyProperty)) ; 0
MsgBox(Type(MyPropertyClass.MyProperty)) ; String ; 注意:返回值是一个字符串
oProp := ObjRawGet(MyPropertyClass, "MyProperty")

MsgBox(IsObject(oProp)) ; 1
MsgBox(Type(oProp)) ; Property

MsgBox(IsFunc("MyPropertyClass.MyProperty.get")) ; 2
; MsgBox(IsObject(MyPropertyClass.MyProperty.get)) ; 错误:无法调用对象。
; MsgBox(Type(MyPropertyClass.MyProperty.get)) ; Property

; 将属性分配给对象:
obj := {}
MsgBox(obj.MyProperty) ; (空);如预期
oProp := ObjRawGet(MyPropertyClass, "MyProperty")
ObjRawSet(obj, "MyProperty", oProp)
MsgBox(obj.MyProperty) ; MyPropertyValue

; 将属性分配给对象:
obj := {}
MsgBox(obj.MyProperty) ; (空);如预期
for vKey, vValue in MyPropertyClass
{
	if (vKey = "MyProperty")
		ObjRawSet(obj, "MyProperty", vValue)
}
MsgBox(obj.MyProperty) ; MyPropertyValue

以上是关于使用类属性的信息和示例代码。通过动态属性,您可以模拟类似于键/值属性的行为,并了解如何在类定义中定义和使用属性。

类:方法

这里是一个带有一些方法的简单类。

  • 方法本质上是存储在类中的函数。
  • 注意:在 AutoHotkey 中,方法是以 ‘class.method’ 形式命名的函数。
  • 注意:IsObjectType 函数演示了方法是类型为 ‘Func’ 的对象。
class MyMethodClass
{
	MyMethod()
	{
		;MsgBox, % A_ThisFunc ;MyMethodClass.MyMethod
		return "MyMethod"
	}
	Add(num1, num2)
	{
		return num1 + num2
	}
	Concatenate(text1, text2)
	{
		return text1 text2
	}
}

obj := new MyMethodClass
MsgBox, % IsFunc("MyMethodClass.MyMethod") ;2 (一个带有 1 个参数的函数)
MsgBox, % IsObject(obj.MyMethod) ;1

MsgBox, % obj.MyMethod() ;MyMethod
MsgBox, % obj.Add(1, 2) ;3
MsgBox, % obj.Concatenate("abc", "def") ;abcdef

; 仅适用于 AHK v1:使用动态方法名称调用方法(计算方法名称)
var := "MyMethod"
MsgBox, % obj[var]() ;MyMethod
MsgBox, % obj["Add"](1, 2) ;3
MsgBox, % obj["Concatenate"]("abc", "def") ;abcdef
  • 函数 vs 方法
    • 在函数中,您显式指定对象(作为第一个参数)。
    • 在方法中,您不需要显式指定对象,它是一个隐式的第一个参数,可以通过 this 访问。

以下是使用 this 的示例:

; 显式参数(obj):
MyFunc(obj)
{
	MsgBox, % obj.Length()
	MsgBox, % &obj
}

class MyThisClass
{
	; 隐式的 'this' 参数:
	MyMethod()
	{
		MsgBox, % this.Length()
		MsgBox, % &this
	}
}

oArray := new MyThisClass
oArray.Push("a", "b", "c")
MsgBox, % oArray.Length() ;3
MsgBox, % &oArray ;(地址)
oArray.MyMethod() ;将给出相同的值:3 和(地址)

oArray := ["a", "b", "c"]
oArray.base := {MyMethod: Func("MyFunc")}
MsgBox, % oArray.Length() ;3
MsgBox, % &oArray ;(地址)
MyFunc(oArray) ;将给出相同的值:3 和(地址)

以下是将函数作为方法添加到对象的示例:

class MyMethodClass
{
	MyMethod()
	{
		MsgBox, % this.MyProp
	}
	MyMethod2(var1, var2, var3)
	{
		MsgBox, % this.MyProp " " var1 " " var2 " " var3
	}
}

MyFunc(obj)
{
	MsgBox, % obj.MyProp
}

MyFunc2(obj, var1, var2, var3)
{
	MsgBox, % obj.MyProp " " var1 " " var2 " " var3
}

; 使用类:
myobj := {}
myobj.MyProp := "value"

; 函数期望 1 个参数,但我们传递了 0 个参数,
; 对象作为隐式的第一个参数传递
myobj.MyMethod := Func("MyFunc")
myobj.MyMethod() ;value
myobj.MyMethod.Call(myobj) ;value;等同于上面一行(但显式传递了对象)

; 函数期望 4 个参数,但我们传递了 3 个参数,
; 对象作为隐式的第一个参数传递
myobj.MyMethod2 := Func("MyFunc2")
myobj.MyMethod2(1, 2, 3) ;value 1 2 3
myobj.MyMethod2.Call(myobj, 1, 2, 3) ;value 1 2 3;等同于上面一行(但显式传递了对象)

以下是动态方法调用的更多示例:

oArray := ["a", "b", "c"]
vMethod := "Length"
MsgBox, % oArray.Length() ;3
MsgBox, % oArray[vMethod]() ;3
MsgBox, % ObjBindMethod(oArray, vMethod).Call() ;3
oFunc := ObjBindMethod(oArray, vMethod)
MsgBox, % oFunc.Call() ;3
MsgBox, % %oFunc%() ;3

以上是关于方法的信息和示例代码。方法允许您在类中定义和使用函数,并了解如何使用隐式的 this 参数访问对象属性和方法。

类:键/属性/方法与优先级

  • 在类定义中,不能同时存在同名的类键(/值属性)、实例键(/值属性)、动态属性或方法。如果取消注释任何 2 个(或更多),所有这 4 个项都会导致“重复声明”错误。
; 所有这 4 个项如果取消注释,会导致“重复声明”错误
class MySameNameClass
{
	; 类键(/值属性):
	static item := 1

	; 实例键(/值属性):
	item := 2

	; 动态属性:
	/*
	item
	{
		get
		{
			return 3
		}
		set
		{
		}
	}
	*/

	; 方法:
	/*
	item()
	{
		return 4
	}
	*/
}
  • 在类中不能同时存在同名的方法和动态属性。会出现“重复声明”错误。
  • 如果键(/值属性)和方法共享相同的名称,则键(/值属性)优先级更高。
  • 如果键(/值属性)和动态属性共享相同的名称,则键(/值属性)优先级更高。

以下示例演示了一些同名问题:

class MySameNameClass
{
	MyKeyOrProperty
	{
		get
		{
			return "property"
		}
		set
		{
		}
	}
	MyKeyOrMethod()
	{
		return "method"
	}
}

obj := new MySameNameClass

; 当不存在与属性/方法同名的键时,测试发生的情况:
MsgBox, % "测试 0:默认行为"
MsgBox, % obj.MyKeyOrProperty ;property
MsgBox, % obj.MyKeyOrProperty() ;property
MsgBox, % obj.MyKeyOrMethod ;(空)
MsgBox, % obj.MyKeyOrMethod() ;method

; 创建同名的键后,测试存在同名的键时的情况:
ObjRawSet(obj, "MyKeyOrProperty", "key")
ObjRawSet(obj, "MyKeyOrMethod", "key")
MsgBox, % "测试 1A"
MsgBox, % obj.MyKeyOrProperty ;key
MsgBox, % obj.MyKeyOrProperty() ;(空)
MsgBox, % obj.MyKeyOrMethod ;key
MsgBox, % obj.MyKeyOrMethod() ;(空)

; 删除键后,恢复原始行为:
obj.Delete("MyKeyOrProperty")
obj.Delete("MyKeyOrMethod")
MsgBox, % "测试 1B"
MsgBox, % obj.MyKeyOrProperty ;property
MsgBox, % obj.MyKeyOrProperty() ;property
MsgBox, % obj.MyKeyOrMethod ;(空)
MsgBox, % obj.MyKeyOrMethod() ;method

; 创建名为 'base' 的键(/值属性)不会阻止方法/属性:
ObjRawSet(obj, "base", "key")
MsgBox, % "测试 2A"
MsgBox, % obj.MyKeyOrProperty ;property
MsgBox, % obj.MyKeyOrProperty() ;property
MsgBox, % obj.MyKeyOrMethod ;(空)
MsgBox, % obj.MyKeyOrMethod() ;method

; 删除键后,恢复原始行为:
obj.Delete("base")
MsgBox, % "测试 2B"
MsgBox, % obj.MyKeyOrProperty ;property
MsgBox, % obj.MyKeyOrProperty() ;property
MsgBox, % obj.MyKeyOrMethod ;(空)
MsgBox, % obj.MyKeyOrMethod() ;method

; 创建名为 'base' 的键(/值属性)也会阻止方法/属性:
obj.base := "key"
MsgBox, % "测试 3A"
MsgBox, % obj.MyKeyOrProperty ;(空)
MsgBox, % obj.MyKeyOrProperty() ;(空)
MsgBox, % obj.MyKeyOrMethod ;(空)
MsgBox, % obj.MyKeyOrMethod() ;(空)

; 尝试删除键(/值属性),但无法恢复原始行为:
; 这会删除名为 'base' 的键,但不会删除属性 'base'
obj.Delete("base")
MsgBox, % "测试 3B"
MsgBox, % obj.MyKeyOrProperty ;(空)
MsgBox, % obj.MyKeyOrProperty() ;(空)
MsgBox, % obj.MyKeyOrMethod ;(空)
MsgBox, % obj.MyKeyOrMethod() ;(空)

; 将 base 设置为类对象,恢复原始行为:
obj.base := MySameNameClass
MsgBox, % "测试 3C"
MsgBox, % obj.MyKeyOrProperty ;property
MsgBox, % obj.MyKeyOrProperty() ;property
MsgBox, % obj.MyKeyOrMethod ;(空)
MsgBox, % obj.MyKeyOrMethod() ;method
  • 若要创建覆盖方法的键,请直接分配一个键,例如 obj["key"] := value,或使用 ObjRawSet
  • 若要创建覆盖动态属性的键,分配键(/值属性)是行不通的,例如 obj["key"] := value,这只会为现有属性分配一个值,因此请使用 ObjRawSet
class MyClass
{
	MyProperty
	{
		get
		{
			return "property"
		}
		set
		{
		}
	}
	MyMethod()
	{
		return "method"
	}
}

obj := new MyClass

;==============================

MsgBox, % obj.MyProperty ;property

; 添加键(/值属性)将失败,
; 因为 MyProperty 是动态属性,
; 这会尝试更改 MyProperty 的值(但 MyProperty 是只读的):
obj["MyProperty"] := "new value"
MsgBox, % obj.MyProperty ;property

; 但是,ObjRawSet 可以成功添加键,键的值将被检索,而不是动态属性的值:
ObjRawSet(obj, "MyProperty", "new value")
MsgBox, % obj.MyProperty ;new value

; 删除键后,方法恢复正常工作:
obj.Delete("MyProperty")
MsgBox, % obj.MyProperty ;property

;==============================

MsgBox, % obj.MyMethod() ;method

; 添加键后,方法调用失败:
obj["MyMethod"] := "new value"
MsgBox, % obj.MyMethod() ;(空)

; 删除键后,方法恢复正常工作:
obj.Delete("MyMethod")
MsgBox, % obj.MyMethod() ;method

; 或者,添加键后,方法调用失败:
ObjRawSet(obj, "MyMethod", "new value")
MsgBox, % obj.MyMethod() ;(空)

; 再次删除键后,方法恢复正常工作:
obj.Delete("MyMethod")
MsgBox, % obj.MyMethod() ;method

示例:替换方法和键

这里有一个示例,演示如何使用同名的键阻止方法工作,但如果添加的键是一个像方法一样运行的 func 对象,则即使添加了键,调用方法也会像以前一样正常工作。

obj := ["a", "b", "c"]
MsgBox, % obj.Length() ;3
MsgBox, % obj.Length ;(空)

; 如果创建名为 'Length' 的键(/值属性),方法将失败:
obj.Length := "value"
MsgBox, % obj.Length() ;(空)
MsgBox, % obj.Length ;value

; 如果删除名为 'Length' 的键(/值属性),方法将再次正常工作:
obj.Delete("Length")
MsgBox, % obj.Length() ;3
MsgBox, % obj.Length ;(空)

; 再次创建名为 'Length' 的键(/值属性),方法将再次失败:
obj.Length := "value"
MsgBox, % obj.Length() ;(空)
MsgBox, % obj.Length ;value

; 如果创建基于 ObjLength 的 `func` 对象,并将其分配给名为 'Length' 的键(/值属性),
; 这是另一种方法可以使方法再次正常工作:
obj.Length := Func("ObjLength")
MsgBox, % obj.Length() ;3
MsgBox, % obj.Length ;(空)

以上是关于键、属性、方法和优先级的详细信息和示例代码。

(五)元函数(Meta-Functions)

1.元函数和其作用

在面向对象编程中,元函数是指在请求对象中未找到键(属性或方法)时会触发的特殊方法。以下是常见的元函数及其作用:

  • __Get(Name, Params)
    • 用于处理对未定义属性的获取操作。
    • 可以自定义返回值或抛出 PropertyError
  • __Set(Name, Params, Value)
    • 用于处理对未定义属性的赋值操作。
    • 可以创建新属性并设置其值。
  • __Call(Name, Params)
    • 处理对未定义方法的调用操作。
    • 可以自定义响应或抛出 MethodError

2.元函数的调用方式

  • 当访问对象的不存在的属性时,会调用 __Get
  • 当对对象的不存在方法进行调用时,会调用 __Call
  • 当给对象的不存在属性赋值时,会调用 __Set

3.动态属性和元函数的应用

元函数与动态属性概念密切相关,通过元函数可以实现在运行时动态定义和处理属性。与静态属性定义不同,动态属性在每次访问时可以计算出一个值,而不需要事先定义每个属性。

例如,可以利用元函数实现代理对象,通过网络或其他通道发送属性请求,并在收到响应后返回相应的值。这种情况下,每个属性的具体操作相同,只是属性名称不同,因此元函数能够以属性名称作为参数处理这些动态属性请求。

4.元函数列表

以下是常见的元函数及其作用:

  • __Init(不应使用):处理对象实例创建时的行为。
  • __New:处理对象实例创建时的行为。
  • __Get / __Set:处理获取/设置不存在的键/属性时的行为。
  • __Call:处理调用已存在/不存在的方法时的行为。
  • __Delete:处理对象实例删除时的行为。

这些元函数是面向对象编程中重要的组成部分,通过自定义这些方法,可以更好地控制类的行为,实现更灵活和动态的对象操作。

下面是一个示例脚本,其中的消息框报告每次调用方法时的情况:

class MyNotifyClass
{
	__Init() ;新对象创建
	{
		MsgBox, % A_ThisFunc
	}
	__New() ;新对象创建
	{
		MsgBox, % A_ThisFunc
	}
	__Get() ;尝试检索不存在的键的值
	{
		MsgBox, % A_ThisFunc
	}
	__Set() ;尝试设置不存在的键的值
	{
		MsgBox, % A_ThisFunc
	}
	__Call() ;尝试使用方法
	{
		MsgBox, % A_ThisFunc
	}
	__Delete() ;对象被删除
	{
		MsgBox, % A_ThisFunc
	}
	MyMethod()
	{
	}
}

obj := new MyNotifyClass ;__Init 和 __New

var := obj["key"] ;__Get(如果键不存在)
obj["key"] := "value" ;__Set(如果键不存在)
var := obj["key"] ;注意:键已存在,因此不会调用 __Get
obj["key"] := "value" ;注意:键已存在,因此不会调用 __Get

obj.NotAMethod() ;__Call;调用不存在的方法,触发 __Call
obj.MyMethod() ;__Call;调用存在的方法,触发 __Call

obj := "" ;__Delete

注意:像obj := ""这样的语句有时会删除对象,有时不会。 如果在obj := ""之前,obj指向的对象引用计数为1,则删除对象并调用 __Delete 方法。 如果在obj := ""之前,obj指向的对象引用计数大于1,则将对象的引用计数减少1。

此示例演示了定义类的两种方式:函数语法(老派)和方法语法(新派):

MyFunc(obj, params*)
{
	MsgBox, % A_ThisFunc
}

class MyClass
{
	;'obj'参数不需要,如果需要,使用'this',它是一个隐式参数
	__Delete(params*)
	{
		MsgBox, % A_ThisFunc
	}
}

fn := Func("MyFunc")
obj1 := new MyClass
obj2 := {}, obj2.base := {__Delete: fn}
obj1 := ""
obj2 := ""

任何正常的函数,如果用作方法,应该有一个必需的参数,其他参数应该都是可选的。否则,调用可能会失败(在 AHK v1 中会静默失败)。

下面是一个示例,演示用作 __Delete 方法的任何自定义函数都应该有一个必需的参数和其他参数都是可选的:

MyFunc1(obj, params*)
{
	MsgBox, % A_ThisFunc
}

MyFunc2(obj, param1, params*)
{
	MsgBox, % A_ThisFunc
}

fn1 := Func("MyFunc1")
fn2 := Func("MyFunc2")
obj1 := {}, obj1.base := {__Delete: fn1}
obj2 := {}, obj2.base := {__Delete: fn2}

; 在 AHK v1 中,我们不会看到 MyFunc2 的消息框
; 在 AHK v2 中,会显示错误
obj1 := ""
obj2 := "" ; 错误:缺少必需参数。(AHK v2 错误)

以上是关于元函数的一些细微差别。在接下来的章节中,将讨论每个元函数的 return 的可能影响。


元函数+: __NEW / __DELETE

  • __New 方法允许在对象创建时执行操作。
  • __Delete 方法允许在对象被删除时执行操作。
global g_CountObjCurrent := 0
global g_CountObjEver := 0
class MyCountClass
{
	__New()
	{
		g_CountObjCurrent++
		g_CountObjEver++
	}
	__Delete()
	{
		g_CountObjCurrent--
	}
}

MsgBox, % g_CountObjCurrent " " g_CountObjEver ;0 0
obj1 := new MyCountClass
MsgBox, % g_CountObjCurrent " " g_CountObjEver ;1 1
obj2 := new MyCountClass
MsgBox, % g_CountObjCurrent " " g_CountObjEver ;2 2
obj3 := new MyCountClass
MsgBox, % g_CountObjCurrent " " g_CountObjEver ;3 3
obj1 := ""
MsgBox, % g_CountObjCurrent " " g_CountObjEver ;2 3
obj2 := ""
MsgBox, % g_CountObjCurrent " " g_CountObjEver ;1 3
obj3 := ""
MsgBox, % g_CountObjCurrent " " g_CountObjEver ;0 3

__Delete 方法可以作为一种技巧,允许您在对象被删除时执行特定操作。

class MyTempSetSCSClass
{
	__New(vMode)
	{
		this.orig := A_StringCaseSense
		StringCaseSense, % vMode
	}
	__Delete()
	{
		StringCaseSense, % this.orig
	}
}

; 定义了三个函数,用于移除大写字母A:

; 这个函数将 StringCaseSense 设置为 On,但不恢复到之前的状态:
Func1(vText)
{
	StringCaseSense, On
	return StrReplace(vText, "A")
}

; 这个函数将 StringCaseSense 设置为 On,并且在完成后将其恢复到之前的状态:
Func2(vText)
{
	vSCS := A_StringCaseSense
	StringCaseSense, On
	vRet := StrReplace(vText, "A")
	StringCaseSense, % vSCS
	return vRet
}

; 类似于 Func2,此函数将 StringCaseSense 设置为 On,并在完成后将其恢复到之前的状态:
;(注意:当返回完成时,obj 被删除,并且状态被重置)
Func3(vText)
{
	obj := new MyTempSetSCSClass("On")
	return StrReplace(vText, "A")
}

vOutput := ""

StringCaseSense, Off
vOutput .= Func1("Aa") " " A_StringCaseSense "`r`n"

StringCaseSense, Off
vOutput .= Func2("Aa") " " A_StringCaseSense "`r`n"

StringCaseSense, Off
vOutput .= Func3("Aa") " " A_StringCaseSense "`r`n"

; 我们可以看到每个函数都移除了大写字母A
; 并且 Func1 未能重置 A_StringCaseSense
MsgBox, % vOutput
  • 注意:如果在 __Delete 方法中使用 return,对象仍然会被删除。
  • 注意:如果在 __New 方法中使用 return,那么将覆盖对象的创建方式。甚至可以返回一个字符串而不是对象。
class MyNewReturnClass1
{
	__New()
	{
		return ["a", "b", "c"]
	}
}
class MyNewReturnClass2
{
	__New()
	{
		return "hello"
	}
}
class MyNewReturnClass3
{
	__New()
	{
		return this
	}
}
class MyNewReturnClass4 ;与 MyNewReturnClass3 等效(除了类名)
{
	__New()
	{
	}
}

obj1 := new MyNewReturnClass1
obj2 := new MyNewReturnClass2
obj3 := new MyNewReturnClass3
obj4 := new MyNewReturnClass4
MsgBox, % obj1.Length() ;3
MsgBox, % obj2 ;hello
MsgBox, % obj3.__Class
MsgBox, % obj4.__Class

这是两个功能等效的类,它们在对象创建时创建属性。 一个在类的主体中指定了赋值,另一个在 __New 方法内部指定了赋值。

class MyClass1
{
	prop := "value"
}

class MyClass2
{
	__New()
	{
		this.prop := "value"
	}
}

obj1 := new MyClass1
obj2 := new MyClass2
MsgBox, % obj1.prop
MsgBox, % obj2.prop

这是一个创建对象的示例,演示了如何指定参数,并演示了如何使用动态名称创建类。

class MyClass1
{
	__New()
	{
		MsgBox, % A_ThisFunc
	}
}

class MyClass2
{
	__New(vText1, vText2)
	{
		MsgBox, % A_ThisFunc "`r`n" vText1 " " vText2
	}
}

obj := new MyClass1
obj := new MyClass2("a", "b")

class1 := "MyClass1"
class2 := "MyClass2"
obj := new %class1%
obj := new %class2%("a", "b")

元函数+: __INIT

  • __Init 是一个未记录的方法,在脚本启动时调用。
  • 下面是一些关于 __Init 的实验,展示了它的一些特性。
  • __Init 向实例对象添加键(/值属性):
; 这会导致 'Duplicate declaration' 错误:
class MyInitClass
{
	var := 1

	__Init()
	{
	}
}

__Init 不会向类对象添加键(/值属性):

; 这不会导致 'Duplicate declaration' 错误:
class MyInitClass
{
	static var := 1

	__Init()
	{
	}
}

__Init__New 之前被调用:

; `__Init` 在 `__New` 之前被调用:
class MyInitAndNewClass
{
	__Init()
	{
		MsgBox, % A_ThisFunc
	}
	__New()
	{
		MsgBox, % A_ThisFunc
	}
}
obj := new MyInitAndNewClass

在类主体中定义变量会导致存在 __Init 方法:

; 在类主体中定义变量
class MyPropNoClass
{
}
class MyPropYesClass
{
	prop := 1
}
class MyPropStaticClass
{
	static prop := 1
}

obj1 := new MyPropNoClass
obj2 := new MyPropYesClass
obj3 := new MyPropStaticClass

; 测试 MyPropNoClass:
vOutput := ""
for vKey, vValue in ObjGetBase(obj1)
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput

; 测试 MyPropYesClass:
; MyPropYesClass 实例具有一个 __Init 类
vOutput := ""
for vKey, vValue in ObjGetBase(obj2)
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput

; 测试 MyPropStaticClass:
; MyPropStaticClass 实例没有 __Init 类
vOutput := ""
for vKey, vValue in ObjGetBase(obj3)
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput

关于 __Class 属性的测试:

; 注意:
; class MyClass
; {
; }
; 实际上等同于:
; class MyClass
; {
; 	static __Class := "MyClass"
; }

class MyAssignClassClass
{
	; static __Class := "hello" ; 错误: 重复声明
	; __Class := "hello" ; 错误: 重复声明
}

下面的示例展示了 __Init 的作用。

; 使用 Call 时,必须显式指定对象,在其他上下文中对象会被隐式传递。
class MyClass
{
	prop := "value"
}

MyClass.__Init.Call(obj:={})
MsgBox, % obj.prop

__Init 中使用 ‘return’ 看起来没有效果。

class MyClass
{
	__Init()
	{
		this.prop := "value"
		return
	}
}

obj := new MyClass
MsgBox, % obj.prop

MyClass.__Init.Call(obj:={})
MsgBox, % obj.prop


元函数: __CALL

  • __Call 是一个方法,当脚本尝试执行一个不存在的(或存在的)方法时调用。
class MyCallClass
{
	__Call(vMethod)
	{
		MsgBox, % "尝试调用存在/不存在的方法:`r`n" vMethod
	}
	MyMethod()
	{
	}
}

obj := new MyCallClass
obj.MyMethod()
obj.NotAMethod()

使用 Call 的一个用途是通过将多个方法合并成一个方法,而不是有多个单独的方法。

class MyCaseClass1
{
	Upper(vText)
	{
		return Format("{:U}", vText)
	}
	Title(vText)
	{
		return Format("{:T}", vText)
	}
	Lower(vText)
	{
		return Format("{:L}", vText)
	}
}

class MyCaseClass2
{
	__Call(vMethod, vText)
	{
		if vMethod in % "Upper,Title,Lower"
			return Format("{:" SubStr(vMethod, 1, 1) "}", vText)
		else
			return
	}
}

obj1 := new MyCaseClass1
obj2 := new MyCaseClass2
MsgBox, % obj1.Upper("hello")
MsgBox, % obj2.Upper("hello")

使用 Call 的另一个用途是拥有区分大小写的方法名称。在AutoHotkey中,通常方法名是不区分大小写的。

class MyCallClass
{
	__Call(vMethod)
	{
		if (vMethod == "HELLO")
			return "HELLO WORLD"
		else if (vMethod == "Hello")
			return "Hello World"
		else if (vMethod == "hello")
			return "hello world"
	}
}
obj := new MyCallClass
MsgBox, % obj.HELLO() ;HELLO WORLD
MsgBox, % obj.Hello() ; Hello World
MsgBox, % obj.hello() ;hello world

;if a key is created with the same name as one of the anticipated values in the __Call method, this prevents the __Call method from being called:
ObjRawSet(obj, "hello", "MyValue")
MsgBox, % obj.HELLO() ;(blank)
MsgBox, % obj.Hello() ;(blank)
MsgBox, % obj.hello() ;(blank)
  • 这个示例演示了每次调用类似方法的东西时(即方法名和括号,以及可选的参数),都会调用 __Call 方法。
  • 即使方法(或属性)存在,也会调用 __Call 方法。
  • 如果存在与方法同名的键(/值属性),则不会调用 __Call 方法。
  • 使用 __Call 方法的一个用途是监视每次调用方法或属性。
class MyCallClass
{
	MyValueProperty := "MyValuePropertyValue"

	__Call(vMethod)
	{
		MsgBox, % A_ThisFunc "`r`n" "调用的方法:`r`n" vMethod
		;return "x"
	}

	MyMethod()
	{
		return "MyMethodValue"
	}

	MyDynamicProperty
	{
		get
		{
			return "MyDynamicPropertyValue"
		}
		set
		{
		}
	}
}

obj := new MyCallClass

MsgBox, % obj.MyMethod() ;MyMethodValue ;__Call 被调用
MsgBox, % obj.MyDynamicProperty() ;MyDynamicPropertyValue ;__Call 被调用
MsgBox, % obj.MyValueProperty() ;(blank) ;因为 MyValueProperty 是一个键(/值属性)
MsgBox, % obj.MyNonExistentEntity() ;(blank) ;__Call 被调用

;注意:因为这些调用缺少括号,__Call 不会被调用:
MsgBox, % obj.MyMethod ;(blank) ;因为省略了括号
MsgBox, % obj.MyDynamicProperty ;MyDynamicPropertyValue
MsgBox, % obj.MyValueProperty ;MyValuePropertyValue
MsgBox, % obj.MyNonExistentEntity ;(blank)
  • 如果在 __Call 方法中使用了 ‘return’,那么任何将要调用的方法/属性都不会被调用。
  • 此外,’return’ 指定的值将被返回。

元函数: __GET

  • __Get 是一个方法,当脚本尝试检索不存在的键的值时调用。
  • 这里我们将返回不存在键的默认值从空字符串改为数字0。
  • __Get 方法中使用 ‘return’ 的效果是指定一个默认值,用于当未找到键时。
  • 默认返回值为0可用于创建统计信息(频率计数)。
class MyGetClass
{
	__Get(vKey)
	{
		return 0
	}
}

obj1 := {}
obj2 := new MyGetClass
MsgBox, % obj1["NonExistentKey"] ;(blank)
MsgBox, % obj2["NonExistentKey"] ;0

obj1["key"]++
obj2["key"]++
MsgBox, % obj1["key"] ;(blank)
MsgBox, % obj2["key"] ;1

;otherwise to increment a key:
obj3 := {}
obj3["key"] := obj3.HasKey("key") ? obj3["key"]+1 : 1
MsgBox, % obj3["key"] ;1

在这里,我们使用 Format 函数将数组的默认值更改为0。

obj1 := {}
obj2 := {base:{__Get:Func("Format").Bind("{}", 0)}}
MsgBox, % obj1.key ;(blank)
MsgBox, % obj2.key ;0
MsgBox, % obj2.key+3 ;3

这个示例演示了 __Get 方法仅在不存在的键上调用:

class MyGetClass
{
	__Get(vKey)
	{
		MsgBox, % A_ThisFunc
	}
}

obj := new MyGetClass
MsgBox, % obj["key"] ;(blank)
obj["key"] := "value"
MsgBox, % obj["key"] ;value


元函数: __SET

  • __Set 是一个方法,当脚本尝试设置一个不存在的键的值时调用。
  • 在这个类中,我们只允许将正整数分配为键。
class MySetClass
{
	__Set(vKey, vValue)
	{
		if RegExMatch(vValue, "^\d+$")
			ObjRawSet(this, vKey, vValue)
		else
			MsgBox, % "错误: 无效值:`r`n" vValue
		return ;不包含这一行的话,类会将 this[vKey] := vValue 进行分配
	}
}

obj := new MySetClass
obj["key1"] := 123
obj["key2"] := "abc"
MsgBox, % obj["key1"] ;123
MsgBox, % obj["key2"] ;(空白)

注意:如果在类内部尝试像这样分配一个键:

this[vKey] := vValue

而不是这样:

ObjRawSet(this, vKey, vValue)

将会导致 __Set 方法陷入无限循环。

  • ObjRawSet 可以绕过 __Set 方法。

__GET/__SET: 监视每次获取/设置键的操作

  • 这里是实现的方法,可以在键已存在时调用 __Get / __Set 方法。
  • 以下是两种能够监视每次获取/设置操作的方法。
  • 在这个示例中,我们使用了一个全局对象。
  • 所有的获取/设置操作都是对外部对象进行的,由于实例对象中不创建键,因此 __Get / __Set 元函数会不断被调用。
global oData := {}
class MyClass
{
	__New()
	{
		global oData
		oData[&this] := {}
	}
	__Get(vKey)
	{
		global oData
		MsgBox, % A_ThisFunc "`r`n" vKey "=" oData[&this, vKey]
		return oData[&this, vKey]
	}
	__Set(vKey, vValue)
	{
		global oData
		oData[&this, vKey] := vValue
		MsgBox, % A_ThisFunc "`r`n" vKey "=" vValue
		return ;不包含这一行,键会自动分配到实例对象
	}
	__Delete()
	{
		global oData
		oData.Delete(&this)
	}
}

obj := new MyClass

;每次调用 __Set 方法:
Loop 3
	obj["key"] := "value" A_Index

;每次调用 __Get 方法:
Loop 3
	MsgBox, % obj["key"]

;检查当前存在的实例对象数量:
obj2 := new MyClass
obj3 := new MyClass

MsgBox, % oData.Count() ;3
obj := ""
MsgBox, % oData.Count() ;2
obj2 := ""
obj3 := ""
MsgBox, % oData.Count() ;0
  • 在这个示例中,我们使用一个类对象来为所有实例对象存储数据。
  • 这可能是一个更好的方法,因为通常在编程中,使用全局变量是不可取的。
class MyClass
{
	static Data := {}
	__New()
	{
		ObjRawSet(MyClass.Data, &this, {})
	}
	__Get(vKey)
	{
		MsgBox, % A_ThisFunc "`r`n" vKey "=" MyClass.Data[&this, vKey]
		return MyClass.Data[&this, vKey] ;有效
	}
	__Set(vKey, vValue)
	{
		MyClass.Data[&this, vKey] := vValue
		MsgBox, % A_ThisFunc "`r`n" vKey "=" vValue
		return ;不包含这一行,键会自动分配到实例对象
	}
	__Delete()
	{
		MyClass.Data.Delete(&this)
	}
}

obj := new MyClass

;每次调用 __Set 方法:
Loop 3
	obj["key"] := "value" A_Index

;每次调用 __Get 方法:
Loop 3
	MsgBox, % obj["key"]

;检查当前存在的实例对象数量:
obj2 := new MyClass
obj3 := new MyClass

MsgBox, % MyClass.Data.Count() ;3
obj := ""
MsgBox, % MyClass.Data.Count() ;2
obj2 := ""
obj3 := ""
MsgBox, % MyClass.Data.Count() ;0

__GET/__SET: 同时获取和设置

  • ‘当获取变为设置时,那么它就不再是获取了。’
  • 我们考虑当执行 var := obj["key"] := value 时返回的内容。
  • 我们进行了设置:obj["key"] := value
  • 和获取:var := obj["key"]
class MyGetSetClass
{
	__Get(k)
	{
		return "未找到键"
	}
	__Set(k, v)
	{
		ObjRawSet(this, k, v)
		return "键给定初始值"
	}
}

;__Get 输出 vs. __Set 输出
obj := new MyGetSetClass
var1 := obj["key"]
var2 := obj["key"] := "MyValue" ;看起来同时进行了获取和设置
var3 := obj["key"]
var4 := obj["key"] := "MyValue" ;看起来同时进行了获取和设置
var5 := obj["key"]
MsgBox, % var1 ;未找到键
MsgBox, % var2 ;键给定初始值
MsgBox, % var3 ;MyValue
MsgBox, % var4 ;MyValue;不是 '键给定初始值',因为 __Set 仅对不存在的键调用
MsgBox, % var5 ;MyValue

通用类示例: 日期和路径拆分

class MyDateClass
{
	__New(vDate:="")
	{
		if (vDate = "")
		{
			ObjRawSet(this, "date", A_Now)
		}
		else
		{
			;将截断的日期(例如 'yyyyMMdd')扩展为 14 个字符:
			FormatTime, vDate, % vDate, yyyyMMddHHmmss
			if vDate is not date
				return ;返回空字符串,不返回对象
			ObjRawSet(this, "date", vDate)
		}
	}
	Format(vFormat)
	{
		FormatTime, vDate, % this.date, yyyyMMddHHmmss
		return FormatTime(vFormat, vDate)
	}
	Add(vNum, vUnit, vFormat="yyyyMMddHHmmss")
	{
		vDate1 := this.date
		EnvAdd, vDate1, % vNum, % vUnit
		if !(vFormat == "yyyyMMddHHmmss")
			FormatTime, vDate1, % vDate1, % vFormat
		return vDate1
	}
	Diff(vDate2, vUnit)
	{
		vDate1 := this.date
		EnvSub, vDate1, % vDate2, % vUnit
		return vDate1
	}
	__Get(vKey)
	{
		FormatTime, vDate, % this.date, % vKey
		return vDate
	}
	__Set()
	{
		return
	}
}

; 创建当前日期和时间的日期对象:
oDate := new MyDateClass
MsgBox, % oDate.date
MsgBox, % oDate.Format("yyyy")
MsgBox, % oDate.Format("ddd") " " oDate.Format("HH:mm:ss dd/MM/yyyy")
MsgBox, % oDate.Add(365, "d", "ddd") " " oDate.Add(365, "d", "HH:mm:ss dd/MM/yyyy")

; 创建特定日期的日期对象:
oDate.date := 20030303030303
MsgBox, % oDate.Format("ddd") " " oDate.Format("HH:mm:ss dd/MM/yyyy")
oDate := ""

; 创建特定日期的日期对象:
oDate := new MyDateClass(20060504030201)
MsgBox, % oDate.date
MsgBox, % oDate.Format("ddd") " " oDate.Format("HH:mm:ss dd/MM/yyyy")

; 注意:如果删除了 __Set 中的 'return' 行,
; 我们可以写入任意键:
; oDate["abc"] := "hello"
; MsgBox, % oDate["abc"]

(六)枚举器和杂项

枚举器: _NewEnum 和 Next

  • 与遍历对象中的每个项目相关的两个重要方法是 _NewEnum 和 Next。
  • 当对对象应用 for 循环时,会调用对象的 _NewEnum 方法。该方法创建一个枚举器对象,然后将其传递给 for 循环。
  • 枚举器对象只需要一个方法,即 Next 方法。
  • Next 方法通过指定两个 ByRef 变量(key 和 value)确定在 for 循环期间输出哪些键/值对以及以什么顺序输出。
  • 它可以输出除 2 个外的其他 ByRef 变量。
  • 此外,Next 方法确定 for 循环何时结束。它通过将该方法的返回值指定为 0(结束循环)或非零整数(继续循环)来实现此目的。
  • 注意:通常,枚举器对象类将具有两个方法:__New 方法用于准备信息,Next 方法用于输出信息。

示例: 重建 AHK 基本对象的功能

class MyClass
{
	_NewEnum()
	{
		return new MyEnumClass(this)
	}
}

class MyEnumClass
{
	__New(obj)
	{
		this.data := obj
		this.temp := []

		oEnum := ObjNewEnum(obj)
		while oEnum.Next(k)
			this.temp.Push(k)

		this.count := this.temp.Length()
		this.index := 1
	}
	Next(ByRef k:="", ByRef v:="")
	{
		if (this.index > this.count)
			return 0
		k := this.temp[this.index]
		v := this.data[k]
		this.index++
		return 1
	}
}

oArray := new MyClass
oArray.q := "Q", oArray.w := "W", oArray.e := "E"
for vKey, vValue in oArray
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput
  • 使用 Enum 方法直接可以实现同时执行 2 个 ‘for 循环’。
  • 可以同时遍历 2 个对象。
oArray1 := {a:1, b:2, c:3}
oArray2 := {d:4, e:5, f:6, g:7, h:8}
oEnum1 := ObjNewEnum(oArray1)
oEnum2 := ObjNewEnum(oArray2)

vOutput := ""
Loop
{
	if vRet1 := oEnum1.Next(vKey, vValue)
		vOutput .= vKey " " vValue "`r`n"
	if vRet2 := oEnum2.Next(vKey, vValue)
		vOutput .= vKey " " vValue "`r`n"
	if !vRet1 && !vRet2
		break
}
MsgBox, % vOutput

自定义枚举器可用于创建无限 for 循环。

class MyClass
{
	_NewEnum()
	{
		return new MyEnumClass
	}
}

class MyEnumClass
{
	__New()
	{
		this.index := 1
	}
	Next(ByRef k:="", ByRef v:="")
	{
		k := this.index
		v := this.index ** 2
		this.index++
		return 1
	}
}

oArray := new MyClass
vOutput := ""
for vKey, vValue in oArray
{
	vOutput .= vKey " " vValue "`r`n"
	if (vKey = 20)
		break
}
MsgBox, % vOutput

使用 Next 函数允许循环生成超过 2 个输出值。

class MyEnumClass
{
	__New()
	{
		this.index := 1
	}
	Next(ByRef n1:="", ByRef n2:="", ByRef n3:="", ByRef n4:="")
	{
		n1 := this.index
		n2 := this.index ** 2
		n3 := this.index ** 3
		n4 := this.index ** 4
		this.index++
		return 1
	}
}

oEnum := new MyEnumClass
vOutput := ""
while oEnum.Next(vNum1, vNum2, vNum3, vNum4)
{
	vOutput .= Format("{}`t{}`t{}`t{}", vNum1, vNum2, vNum3, vNum4) "`r`n"
	if (A_Index = 20)
		break
}
MsgBox, % vOutput
  • 以下是更多枚举器示例的链接。
  • 注意:链接中所需的类/方法用于枚举器对象可以出现在不同位置:例如作为单独的类、嵌套类或与其他方法并列在类中。

每当使用 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 最好是完全自包含的, 而不依赖于任何全局变量或静态变量.

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

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 关键字构造的类对象.

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

3.嵌套类

4.方法

方法定义看起来和函数定义相同. 每个方法定义都会创建一个 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().

6.属性

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

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

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

参数可以通过将它们括在属性名称右侧的方括号中来定义, 并以相同的方式传递(但在无参数时应省略). 除了使用方括号这点不同, 属性参数的定义方法与方法参数相同 – 支持可选参数, 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, 然后将结果作为方法调用.

7.胖箭头属性

胖箭头语法可以用来定义 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 的调试器能够通过调用枚举器列出它们之后可以获取或设置特定项.

#requires AutoHotkey 2.0+

; 定义一个名为 CE 的类
class CE {
	__init(){
		this.data := [ 111, 222, 333, 444 ] ; 初始化数据列表
	}
	
	__enum( * )
    {
		iKey := 0
		enum_get( &key,&value, * )
        { ; &iKey, &value
			iKey++
			if iKey > this.data.length
				return false  ; 如果超出数据长度则停止迭代
			else 
            {
				key := iKey  ; 设置迭代变量 key
				value := this.data[iKey]  ; 设置迭代变量 value
				return true  ; 继续迭代
			}
		}
		return enum_get
	}
}

; 创建 CE 类的实例 E
E := CE()

; 使用 for 循环迭代 E 实例
for key, value in E { ; for key, value in E {
	msgBox key ":" value  ; 显示 key 和 value
}
#requires AutoHotkey 2.0+
obj := list_of_stuff()

; 使用单个变量/参数迭代
For value in obj {
    msgbox value
}

; 使用两个变量/参数迭代
For i, value in obj {
    msgbox i ' = ' value
}

class list_of_stuff {
    list := ['a', 'b', 'c'] ; 可根据需要存储数据

    __Enum(n) {
        i := 1                      ; 计数器
        total := this.list.Length   ; 迭代限制 - 这些变量对以下封闭环境是静态的

        return (n = 1) ? enum1 : enum2 ; 必须返回一个返回 TRUE 或 FALSE 的函数

        ; 封闭函数 #1 - 仅有一个 FOR 循环变量
        enum1(ByRef a) {
            If (i <= total) {       ; 检查限制
                a := this.list[i]   ; 设置 FOR 循环变量
                i++                 ; 增加计数器
                return true         ; 返回 TRUE 继续迭代
            } Else
                return false        ; 返回 FALSE 停止 FOR 循环
        }

        ; 封闭函数 #2 - 有两个 FOR 循环变量
        enum2(ByRef a, ByRef b) {
            If (i <= total) {       ; 检查限制
                a := i              ; 设置 FOR 循环变量
                b := this.list[i]
                i++                 ; 增加计数器
                return true         ; 返回 TRUE 继续迭代
            } Else
                return false        ; 返回 FALSE 停止 FOR 循环
        }
    }
}

使用三元运算符进行压缩:

#requires AutoHotkey 2.0+
obj := list_of_stuff()

; 使用单个变量/参数迭代
For value in obj {
    msgbox value
}

; 使用两个变量/参数迭代
For i, value in obj {
    msgbox i ' = ' value
}
class list_of_stuff {
    list := ['a','b','c'] 
    
    __Enum(n) {
        i := 1, total := this.list.Length
        
        return (n=1) ? enum1 : enum2 
        
        enum1(&a) => (i <= total) ? (a := this.list[i], i++, true) : false
        
        enum2(&a, &b) => (i <= total) ? (a := i, b := this.list[i], i++) : false
    }
}

只关注类和__enum,可以更浓缩

class list_of_stuff {
    list := ['a','b','c']  ; 数据列表
    
    __Enum(n) { ; FOR 循环中的参数数量
        L := this.list, i := 1, T := L.Length ; 紧缩这些变量名,下面会变得混乱
        
        return enum
        
        enum(&a, &b?) => (i <= T) ? ((n=1)?a:=L[i]:(a:=i,b:=L[i]), i++) : false ; 第二个参数是可选的,使用 '?' 表示未设置
    } ;                         ...  (n=1) ? a:=L[i] : (a:=i,b:=L[i]), i++
} ;                                 1 个变量 -> a = value,2 个变量 -> a = index,b = value,然后递增 i++

; 更小的版本...

class list_of_stuff {
    list := ['a','b','c']  ; 数据列表
    
    __Enum(n) {
        L := this.list, i := 1, T := L.Length
        
        return ((&a, &b?) => (i <= T) ? ((n=1)?a:=L[i]:(a:=i,b:=L[i]), i++) : false)
    } ; 我们实际上不需要函数名,将其包装在括号中
}

; ... 更小...

class list_of_stuff {
    list := ['a','b','c']  ; 数据列表
    
    __Enum(n) { ; 将 L、i 和 T 压缩到 return 行的开头 (T = total)
        return (L := this.list, i := 1, T := L.Length, (&a, &b?) => (i <= T) ? ((n=1)?a:=L[i]:(a:=i,b:=L[i]), i++) : false)
    }
}

; 最终,这是最紧凑的版本...

class list_of_stuff {
    list := ['a','b','c']  ; 数据列表
    
    __Enum(n) => ; 粗箭头语法,与使用 return 相同
        (L := this.list, i := 1, T := L.Length, (&a, &b?) => (i <= T) ? ((n=1)?a:=L[i]:(a:=i,b:=L[i]), i++) : false)
}

(三)__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, 然后调用结果的默认属性. 这是语言语法的一部分, 所以不依赖于对象.

(四)初始化

当对类的引用第一次计算时, 每个类都会被自动初始化. 例如, 如果 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
}

给TA捐赠
共{{data.count}}人
人已捐赠
其他教程

【学习】仿手动获取知乎文章内容

2022-2-5 21:07:45

其他教程

AHK调用opencv(一)打开图片

2022-2-11 16:17:30

3 条回复 A文章作者 M管理员
  1. ahkjoo

    你好,
    我想调用微信的截图DLL,使用CMD可以完成,vbs也可以。但是在ahsk里面调用不成功。
    DllCall(“prcscrn.dllprcscrn”)不知道什么原因,请教一下

  2. ahkjoo

    你好,
    我想调用微信的截图DLL,使用CMD可以完成,vbs也可以。但是在ahk里面调用不成功。
    DllCall(“prcscrn.dll prcscrn”)不知道什么原因,请教一下

    • AHK中文社区

      DllCall(“D:Program FilesWeChatPrScrn.dllPrScrn”)

个人中心
购物车
优惠劵
有新私信 私信列表
搜索