AHK v1 写到一定程度,很多人都会遇到对象和类。刚开始会觉得它们很“高级”,像是写大型软件才需要的东西。其实我更愿意把它们看成整理脚本的工具:数据多了,用对象装起来;动作和数据经常一起出现,就考虑写成类。

这篇不准备把对象系统全部讲完。真要完整讲,会牵扯元函数、基对象、枚举器、引用计数,容易越写越远。这里先讲最小够用的心法,能让你看懂大部分 v1 类库,也能开始写自己的小封装。

我建议的学习顺序

如果你刚开始学对象和类,可以按这个顺序来:

  1. 先用 []{} 做装数据的容器。
  2. 学会 for key, value in obj 遍历解析出来。
  3. 学会对象嵌套,比如数组里放对象。
  4. 再写一个只有 __New 和两三个方法的小类。
  5. 最后再碰继承、base、元函数这些内容。

对象先当容器用

最开始不要急着想“面向对象”。对象最朴素的用途,就是把一组相关数据放在一起。

#Requires AutoHotkey v1.1

user := {}
user.name := "张三"
user.city := "杭州"
user.score := 95

MsgBox, % user.name " / " user.city " / " user.score

这比写一堆散变量更清楚。尤其是循环处理多个人、多个窗口、多个任务时,对象能让代码少很多临时变量。

tasks := []
tasks.Push({name: "截图", status: "完成"})
tasks.Push({name: "识别", status: "等待"})

for index, task in tasks
    MsgBox, % index ". " task.name " - " task.status

在 v1 里,数组和关联数组本质上都属于基础对象。[] 更适合按顺序存一组东西,{} 更适合按名字存一组东西。

点号和中括号大多是一回事

v1 里很多时候 obj.nameobj["name"] 指向的是同一个值。

obj := {}
obj.name := "AHK66"

MsgBox, % obj["name"]

点号适合字段名固定、写起来像属性的场景;中括号适合字段名来自变量的场景。

; 需结合上段内容
key := "name"
MsgBox, % obj[key]

这一点很实用。比如你从 Excel 表头、JSON 字段、配置项里拿到一个名字,就可以动态访问对象。

什么时候需要类

如果对象只是临时装数据,普通对象就够了。只有当你发现“同一类数据总要配同一批函数”时,类才开始变得有意义。

比如一个计数器,它有自己的数值,也有增加、减少、清零这些动作。如果每次都把对象和函数分开放,代码会越来越散。写成类以后,数据和动作就放到一起了。

counter := new Counter(10)
counter.Add(5)
counter.Sub(3)
MsgBox, % counter.value

class Counter
{
    __New(start := 0)
    {
        this.value := start
    }

    Add(n := 1)
    {
        this.value += n
    }

    Sub(n := 1)
    {
        this.value -= n
    }
}

这里的 Counter 是类,counter 是实例。类像图纸,实例才是真正拿来用的对象。

this 就是当前对象

类的方法里最重要的词是 this。它代表当前这个实例。

; 需结合上段内容
Add(n := 1)
{
    this.value += n
}

这句的意思是:把当前计数器自己的 value 增加 n。如果你创建两个计数器,它们各自有各自的 value

; 需结合上段内容
a := new Counter(1)
b := new Counter(100)

a.Add(9)
b.Sub(20)

MsgBox, % "a=" a.value "`nb=" b.value

理解 this 以后,看类库会轻松很多。很多类库里不是写全局变量,而是把状态存在 this.xxx 里。

__New 负责初始化

__New 会在 new ClassName() 时自动运行。它最常见的用途是接收参数、保存初始状态、准备资源。

class FileJob
{
    __New(path)
    {
        this.path := path
        SplitPath, path, fileName
        this.fileName := fileName
    }

    Show()
    {
        MsgBox, % this.fileName "`n" this.path
    }
}

我建议新手先把 __New 当成“构造这个对象时要做的准备工作”。不要一开始就在里面塞太复杂的逻辑,先让它把对象需要的基础数据放好。

方法不是越多越好

初学类的时候,容易把所有函数都塞进类里。这样反而会变乱。我的判断标准很简单:这个函数是否高度依赖这个对象自己的数据?如果是,就适合做成方法;如果不是,保留普通函数也很好。

比如 counter.Add() 依赖 counter.value,适合做方法。一个通用的 TrimText(text) 不依赖某个对象,就没必要硬塞进类。

base 可以先知道,不必急着深挖

v1 的类背后有一个重要概念叫 base。简单说,当对象自己没有某个方法时,AHK 会去它的基对象里找。用 class 创建实例时,实例的 base 通常就是类对象。

所以你能这样调用:

; 需结合上段内容
counter := new Counter()
counter.Add()

counter 自己不一定真的保存了 Add 这个函数,但它的基对象里有,AHK 会顺着关系找到它。这个机制也是继承、方法查找、元函数的基础。

不过新手不需要一上来就手动改 base。先会定义类、创建实例、用 this 保存状态,已经能写出很多干净的封装。

继承只在有明显共性时用

v1 支持 extends,但我不建议为了显得高级而写继承。继承适合“确实有一个基础版本,另一个版本只是扩展它”的情况。

class Logger
{
    Write(text)
    {
        FileAppend, %text%`n, %A_ScriptDir%\log.txt, UTF-8
    }
}

class TimeLogger extends Logger
{
    Write(text)
    {
        FormatTime, now,, yyyy-MM-dd HH:mm:ss
        base.Write("[" now "] " text)
    }
}

base.Write() 表示调用父类里的同名方法。这个例子里,子类只负责加时间,真正写文件仍然交给父类。

最常见的几个坑

  • 把类名变量覆盖掉。比如类叫 Box,不要写 Box := new Box
  • 忘记用 this.,结果把实例变量写成了局部变量。
  • 在对象里创建了和内置方法同名的键,比如 HasKey,导致方法访问变怪。
  • 把类写得太大,一个类什么都管,最后比散函数还难维护。
  • 循环引用没有断开,复杂对象释放时可能不如预期。

对大多数日常脚本来说,前两个坑最常见。只要记住“实例状态放 this.xxx,类名不要拿来当实例变量名”,就能少很多奇怪问题。

 

对象和类不是必须一口吃完。AHK v1 很多优秀库其实也只是把几个状态和几个动作包在一起,并没有写得很玄。先把小对象、小类写顺,后面看别人的库就会自然很多。

相关教程

声明:站内资源为整理优化好的代码上传分享与学习研究,如果是开源代码基本都会标明出处,方便大家扩展学习路径。请不要恶意搬运,破坏站长辛苦整理维护的劳动成果。本站为爱好者分享站点,所有内容不作为商业行为。如若本站上传内容侵犯了原著者的合法权益,请联系我们进行删除下架。