函数

目录

介绍和简单示例

函数是可以通过 调用 来执行的可重用代码块. 函数可以选择接受参数(输入) 并返回值(输出). 参考下面的简单函数, 其接受两个数字并返回它们的和:

Add(x, y)
{
    return x + y
}

上面就是一个函数的 定义, 因为它创建了一个名称为 "Add"(不区分大小写) 的函数, 并且确立了调用它时必须准确的提供两个参数(x 和 y). 要调用此函数, 把它的结果通过 := 运算符赋值给变量. 例如:

Var := Add(2, 3)  ; 数字 5 将被保存到 Var.

当然, 函数调用时也可以不保存其返回值:

Add(2, 3)
Add 2, 3  ; 如果在行的开头使用, 括号可以省略.

不过这种情况下, 函数的任何返回值都会被丢弃; 所以除非函数还有除了返回值之外的功能, 否则这次调用毫无意义.

在表达式中, 函数调用 "计算出" 函数的返回值. 可以将返回值赋给如上所示的变量, 也可以像下面一样地直接使用:

if InStr(MyVar, "fox")
    MsgBox "The variable MyVar contains the word fox."

参数

定义函数时, 函数的参数都在其名称后的括号中列出(函数名和左括号之间不能有空格). 如果函数不接受任何参数, 请把括号内留空; 例如: GetCurrentTimestamp().

ByRef 参数: 从函数的角度看, 参数本质上是局部变量, 除非它们被定义为 ByRef 如下例所示:

a := 1, b := 2
Swap(&a, &b)
MsgBox a ',' b

Swap(&Left, &Right)
{
    temp := Left
    Left := Right
    Right := temp
}

在上面的例子中, 使用 & 需要调用者传递一个 VarRef, 它通常对应于调用者的一个变量. 每个参数都成为 VarRef 所代表的变量的别名. 换句话说, 参数和调用者的变量都引用内存中相同的内容. 这样使得 Swap 函数可以通过移动 Left 的内容到 Right 中来改变调用者的变量, 反之亦然.

与之相比, 如果在上面的例子中没有使用 ByRef, 那么 LeftRight 将是调用者变量的副本, 因此 Swap 函数不会对外部产生任何影响. 然而, 函数可以改为显式地解引用每个 VarRef. 例如:

Swap(Left, Right)
{
    temp := %Left%
    %Left% := %Right%
    %Right% := temp
}

由于 return 只能返回一个值给函数的调用者, 所以可以使用 VarRef 返回更多的结果. 这是通过函数将调用者传递进来的变量的引用(通常为空值的) 赋值实现的.

传递大字符串给函数时, 使用 ByRef 提高了性能, 并且通过避免生成字符串的副本节约了内存. 同样地, 使用 ByRef 送回长字符串给调用者通常比类似 Return HugeString 的方式执行的更好. 然而, 函数接收到的不是对字符串的引用, 而是对 变量 的引用. 未来的改进可能会取代 ByRef 在这些方面的使用.

已知限制:

可选参数

在定义函数时, 可以把它的一个或多个参数标记为可选的.

在参数后追加 := 后跟文本的数字, 加引号的/原义的字符串, 如 "fox" 或 "", 或一个表达式, 该表达式应该在每次需要初始化参数时使用其默认值. 例如, X:=[] 每次都会创建一个新的 Array(数组).

附加 ?:= unset 来定义一个默认未设置的参数.

下面这个函数中的 Z 参数就是一个可选参数:

Add(X, Y, Z := 0) {
    return X + Y + Z
}

当调用者传递 三个 参数给上面的函数时, Z 的默认值被忽略. 但当调用者仅传递 两个 参数时, Z 自动接受默认值 0.

可选参数不能孤立地放在参数列表的中间. 换句话说, 在首个可选参数右边的所有参数都必须定义为可选的. 然而, 调用函数时可以省略参数列表中间的可选参数, 如下所示:

MyFunc(1,, 3)
MyFunc(X, Y:=2, Z:=0) {  ; 注意: 这里的 Z 必须是可选参数.
    MsgBox X ", " Y ", " Z
}

ByRef 参数也支持默认值; 例如: MyFunc(&p1 := ""). 每当调用者省略这样的参数时, 函数会创建一个包含默认值的局部变量; 换句话说, 此时函数的行为就与 "&" 符号不存在时一样.

Unset 参数

要将参数标记为可选而不提供默认值, 请使用关键字 unset? 后缀. 在这种情况下, 只要省略了参数, 相应的变量就没有值. 请使用 IsSet 来确定参数是否被赋值, 如下所示:

MyFunc(p?) { ; 等同于 MyFunc(p := unset)
    if IsSet(p)
        MsgBox "Caller passed " p
    else
        MsgBox "Caller did not pass anything"
}

MyFunc(42)
MyFunc

当参数值为空时, 试图读取参数值将被视为错误, 这与任何未初始化变量一样. 即使参数没有值, 也要将可选参数传递给另一个函数, 可以使用 maybe 操作符(var?). 例如:

Greet(title?) {
    MsgBox("Hello!", title?)
}

Greet "Greeting"  ; 标题是 "Greeting"
Greet             ; 标题是 A_ScriptName

返回值给调用者

介绍中所说, 函数可以返回一个值给调用者.

MsgBox returnTest()

returnTest() {
    return 123
}

如果要从函数中返回额外的结果, 可以使用 ByRef (&):

returnByRef(&A,&B,&C)
MsgBox A "," B "," C

returnByRef(&val1, &val2, val3)
{
    val1 := "A"
    val2 := 100
    %val3% := 1.1  ; 使用 %, 因为参数中省略了 &.
    return
}

可以使用对象数组返回多个值, 甚至是已命名的值:

Test1 := returnArray1()
MsgBox Test1[1] "," Test1[2]

Test2 := returnArray2()
MsgBox Test2[1] "," Test2[2]

Test3 := returnObject()
MsgBox Test3.id "," Test3.val

returnArray1() {
    Test := [123,"ABC"]
    return Test
}

returnArray2() {
    x := 456
    y := "EFG"
    return [x, y]
}

returnObject() {
    Test := {id: 789, val: "HIJ"}
    return Test
}

可变参数函数

定义函数时, 在最后一个参数后面写一个星号(*) 来标记此函数为可变参数的, 这样让它可以接收可变数目的参数:

Join(sep, params*) {
    for index,param in params
        str .= param . sep
    return SubStr(str, 1, -StrLen(sep))
}
MsgBox Join("`n", "one", "two", "three")

调用可变参数函数时, 通过保存在函数的最后参数中的对象可以访问剩余的参数. 函数的首个超出参数是 params[1], 第二个是 params[2], 以此类推. 因其是一个数组, params.Length 能被用于确定参数的数目.

尝试使用多于其接受的参数的方式来调用非可变参数函数被认为是错误的. 要允许函数接受任意数量的参数, 而 创建数组来存储多余的参数, 请将 * 写入最后一个参数(没有参数名).

注意: "可变" 参数只可以出现在显式参数(形参) 列表的末尾.

可变参数函数的调用

虽然可变参数函数可以 接受 可变数目的参数, 不过在函数调用中使用相同的语法可以把数组作为参数传递给 任何 函数:

substrings := ["one", "two", "three"]
MsgBox Join("`n", substrings*)

注意:

已知限制:

局部和全局变量

局部变量

局部变量是特定于单个函数的, 只在该函数内可见. 因此, 局部变量可能与全局变量具有相同的名称, 但有不同的内容. 不同的函数也可以安全地使用相同的变量名.

当函数返回值时, 所有非静态的局部变量都自动释放(变为空), 除了绑定到闭包VarRef(这样的变量在闭包或 VarRef 释放时被释放) 的变量.

A_ClipboardA_TimeIdle 这样的内置变量永远不会是局部的(它们可以从任何地方访问), 并且不能被重新声明. (这不适用于内置类, 如 Object; 它们被预定义为全局变量.)

默认情况下, 函数是假定-局部的. 在假定-局部函数内访问或创建的变量默认为局部的, 但以下情况除外:

默认值也可以被覆盖, 通过使用 local 关键字来声明变量, 或者通过改变函数的模式来覆盖(如下所示).

全局变量

假定-局部函数中的任何变量引用, 如果只是读取的话, 可以解析为全局变量. 然而, 如果一个变量在赋值中使用或使用引用操作符(&), 它默认是自动局部的. 这就允许函数读取全局变量或调用全局或内置函数, 而无需在函数内部声明, 同时当被赋值的局部变量的名称与全局变量的名称重合时, 可以保护脚本免受意外的副作用. 例如:

LogToFile(TextToLog)
{
    ; LogFileName 是之前在这个函数之外的某个地方被赋予的值.
    ; FileAppend 是一个包含内置函数的预定义全局变量.
    FileAppend TextToLog "`n", LogFileName
}

否则, 如果要在函数中引用一个现有的全局变量(或者创建一个新的变量), 在使用它之前先声明这个变量为全局变量. 例如:

SetDataDir(Dir)
{
    global LogFileName
    LogFileName := Dir . "\My.log"
    global DataDir := Dir  ; 声明与赋值相结合, 如所述.
}

假定-全局模式: 如果函数需要访问或创建大量的全局变量, 通过在函数的首行使用单词 "global", 从而假定其所有变量都是全局的(参数除外). 例如:

SetDefaults()
{
    global
    MyGlobal := 33  ; 把 33 赋值给全局变量, 必要时首先创建这个变量.
    local x, y:=0, z  ; 在这种模式中局部变量必须进行声明, 否则会假设它们为全局的.
}

静态变量

静态变量总是隐式的局部变量, 但和局部变量的区别是它们的值在多次调用期间是记住的. 例如:

LogToFile(TextToLog)
{
    static LoggedLines := 0
    LoggedLines += 1  ; 保持局部的计数(它的值在多次调用期间是记住的).
    global LogFileName
    FileAppend LoggedLines ": " TextToLog "`n", LogFileName
}

静态变量可以在声明的同一行初始化, 方法是在它后面跟 := 和任意表达式. 例如: static X:=0, Y:="fox". 静态声明的计算与 local 声明相同, 只是在静态初始化器(或组合初始化器组) 被成功计算后, 它将有效地从控制流中移除, 并且不会再执行第二次.

可以将套嵌函数声明为静态, 以阻止他们捕获外部函数的非静态局部变量.

假定-静态模式: 函数的第一行是单词 "static", 将函数定义为假定-静态模式, 假定其所有未声明变量都是静态的(参数除外). 例如:

GetFromStaticArray(WhichItemNumber)
{
    static
    static FirstCallToUs := true  ; 每个静态声明初始化仍然只运行一次.
    if FirstCallToUs  ; 在首次调用时创建静态数组, 后续的调用时不再创建.
    {
        FirstCallToUs := false
        StaticArray := []
        Loop 10
            StaticArray.Push("Value #" . A_Index)
    }
    return StaticArray[WhichItemNumber]
}

在假定-静态模式中, 任何非静态变量都必须声明为局部或全局变量(与假定-局部模式的例外情况相同.)

关于局部和全局变量的更多信息

如下面的例子所示, 通过逗号分隔, 可以在同一行声明多个变量:

global LogFileName, MaxRetries := 5
static TotalAttempts := 0, PrevResult

通过在变量后面赋值, 变量可以在声明的同一行进行初始化. 与静态变量初始化不同, 局部和全局变量的初始化在每次调用函数时都执行. 换句话说, 像 local x := 0 和写成单独的两行的效果是一样的: local x 后面跟着 x := 0. 局部和全局初始化允许使用任何赋值运算符, 但是像 global HitCount += 1 这样的复合赋值需要变量之前已经被赋值.

因为单词 local, globalstatic 都是在脚本运行时立即处理的, 所以不能使用 If 语句有条件地声明变量. 换句话说, If 或 Else 的区块内的声明无条件对声明和函数的闭括号之间的所有行生效(但声明中包含的任何初始化仍然是有条件的). 像 global Array%i% 这样的动态声明是不可能的, 因为所有对 Array1Array99 这样的变量的非动态引用都已经被解析为地址了.

动态调用函数

虽然函数调用表达式通常以原义的函数名称开头, 但调用的目标可以是任何产生函数对象的表达式. 在表达式 GetKeyState("Shift") 中, GetKeyState 实际上是一个变量引用, 尽管它通常指的是一个包含内置函数的只读变量.

如果一个函数调用的目标是在脚本运行时确定的, 而不是在脚本启动前确定的, 那么这个函数调用就被称为是 动态的. 与普通函数调用的语法相同; 唯一明显的区别是, 对于非动态调用, 某些错误检查在加载时进行, 而对于动态调用, 只有在运行时才进行.

例如, MyFunc() 将调用 MyFunc 包含的函数对象 , 它可以是一个函数的实际名称, 也可以只是一个被赋值到函数的变量.

其他表达式也可以作为函数调用的目标, 包括双重解引. 例如, MyArray[1]() 将调用 MyArray 的第一个元素所包含的函数, 而 %MyVar%() 将调用变量, 其 名称 包含在 MyVar 中. 换句话说, 首先对参数列表前面的表达式进行计算, 得到函数对象, 然后调用该对象.

如果目标值不是可以被调用的类型, 则抛出 Error:

在调用函数之前, 函数的调用者一般应该知道每个参数的含义以及有多少个参数. 但是, 对于动态调用来说, 函数通常是为了适应函数调用而编写的, 在这种情况下, 失败的原因可能是函数定义的错误而不是参数值的错误.

短路型布尔值的计算

当在表达式中使用 AND, OR三元运算符时, 它们会短路以提高性能(无论当前是否存在函数调用). 短路操作是通过不计算表达式中那些不影响最终结果的部分来进行优化运算. (译者注: 只要碰到了 true 或者等价于 true 的就短路, 只要短路了就不会继续往后执行了, 碰到 false 就短路的情况也是同样.) 为了说明这个概念, 请看这个例子:

if (ColorName != "" AND not FindColor(ColorName))
    MsgBox ColorName " could not be found."

在上面的例子中, 如果 ColorName 变量为空, 则永远不会调用 FindColor() 函数. 这是由于 AND 的左侧结果为 false, 因此其右边不可能让最终的结果为 true.

由于此特性, 所以需要注意到, 如果在 ANDOR 的右侧调用函数, 那么该函数产生的任何副作用(例如改变全局变量的内容) 可能永远不会发生.

还需要注意在嵌套的 ANDOR 串联表达式的求值短路. 例如, 在下面的表达式中每当 ColorName 为空时, 只会进行最左边的比较. 这是因为此时最左边的比较已经足以确定最终的结果:

if (ColorName = "" OR FindColor(ColorName, Region1) OR FindColor(ColorName, Region2))
    break   ; 搜索内容为空或找到了匹配.

从上面的例子可以看出, 任何耗时的函数一般应该在 ANDOR 的右侧调用从而提高性能. 这种技术也可以用来防止一个函数在它的一个参数会被传递一个它认为不合适的值(如空字符串) 时被调用.

三元条件运算符(?:) 也通过不计算丢弃的分支来实现短路.

嵌套函数

嵌套 函数是在另一个函数中定义的函数. 例如:

outer(x) {
    inner(y) {
        MsgBox(y, x)
    }
    inner("one")
    inner("two")
}
outer("title")

嵌套函数不能在直接包含它的函数外部通过名称访问, 但是可以在该函数内部的任何地方访问, 包括在其他嵌套函数内部(有例外).

默认情况下, 嵌套函数可以访问包围它的函数中的任何静态变量, 甚至是动态变量. 但是, 如果外部函数既没有声明也没有非动态赋值, 那么嵌套函数内部的非动态赋值通常会解析为局部变量.

默认情况下, 当满足以下要求时, 嵌套函数会自动 "捕获" 外部函数的非静态局部变量:

  1. 外部(outer) 函数必须以下列至少一种方式引用这个变量:
    1. 通过用 local 声明它, 作为一个参数, 或套嵌函数.
    2. 作为赋值或引用操作符(&) 的非动态目标.
  2. 内部(inner) 函数(或嵌套在其内部的函数) 必须非动态地引用变量.

捕获了变量的嵌套函数称为 closure(闭包).

外部函数的非静态局部变量不能被动态访问, 除非它们已经被捕获.

显式声明总是优先于包围它们的函数中的局部变量. 例如, local x 声明了一个当前函数的局部变量, 与外部函数中的任何 x 无关. 外部函数中的全局声明也会影响嵌套函数, 除非被显式声明覆盖.

如果函数声明为假定-全局, 那么在该函数 之外 创建的任何局部或静态变量都不能被该函数本身或其任何嵌套函数直接访问. 相比之下, 假定静态的嵌套函数仍然可以引用外部函数中的变量, 除非函数被声明为静态.

默认情况下, 函数是假定-局部的, 其嵌套函数也是如此, 即使是假定-静态函数中的函数也是如此. 但是, 如果外部函数是假定-全局, 那么嵌套函数默认情况下表现为假定-全局, 除非它们可以引用外部函数的局部和静态变量.

每个函数定义都创建一个包含函数本身的只读变量; 也就是说, 创建一个 Func闭包对象. 请参阅下面的例子来了解如何使用这个方法.

静态函数

任何嵌套函数不捕获变量的都是自动静态的; 也就是说, 对外部函数的每次调用都引用同一个 Func. 关键字 static 可用于显式声明嵌套函数为静态, 在这种情况下, 外部函数的任何非静态局部变量都会被忽略. 例如:

outer() {
    x := "outer value"
    static inner() {
        x := "inner value"  ; 创建一个从局部到内部的变量
        MsgBox type(inner)  ; 显示 "Func"
    }
    inner()
    MsgBox x  ; 显示 "outer value"
}
outer()

静态函数不能引用自身函数体之外的其他嵌套函数, 除非显式声明为静态. 注意, 即使函数是假定静态的, 非静态嵌套函数在引用函数参数时也可能成为闭包.

闭包

闭包 是与 自由变量 集绑定的嵌套函数. 自由变量是 outer 函数的局部变量, 嵌套函数也使用这些局部变量. 闭包允许一个或多个嵌套函数与 outer 函数共享变量, 即使 outer 函数返回后也是如此.

要创建一个闭包, 只需定义一个引用外部函数变量的嵌套函数. 例如:

make_greeter(f)
{
    greet(subject)  ; 这是 f 的闭包.
    {
        MsgBox Format(f, subject)
    }
    return greet  ; 返回闭包.
}

g := make_greeter("Hello, {}!")
g(A_UserName)
g("World")

闭包也可以与内置函数一起使用, 如 SetTimerHotkey. 例如:

app_hotkey(keyname, app_title, app_path)
{
    activate(keyname)  ; 这是 app_title 和 app_path 的闭包.
    {
        if WinExist(app_title)
            WinActivate
        else
            Run app_path
    }
    Hotkey keyname, activate
}
; Win+N 激活或启动 Notepad.
app_hotkey "#n", "ahk_class Notepad", "notepad.exe"
; Win+W 激活或启动 WordPad.
app_hotkey "#w", "ahk_class WordPadClass", "wordpad.exe"

如果一个嵌套函数捕获了外部函数的任何非静态局部变量, 那么它就自动成为一个闭包. 对应于闭包本身的变量(如 activate) 也是一个非静态局部变量, 所以任何引用闭包的嵌套函数都是自动闭包.

对外部函数的每次调用都会创建新的闭包, 与之前的任何调用不同.

最好不要将对闭包的引用存储在闭包自己的任何一个自由变量中, 因为这将创建一个循环引用, 而这个循环引用必须在闭包被释放之前被打破(例如通过清除变量). 然而, 一个闭包可以安全地用它们的原始变量来引用自己和其他闭包, 而不会产生循环引用. 例如:

timertest() {
    x := "tock!"
    tick() {
        MsgBox x           ; x 使其成为闭包.
        SetTimer tick, 0   ; 使用闭包的原始变量是安全的.
        ; SetTimer t, 0    ; 捕获 t 将创建循环引用.
    }
    t := tick              ; 这是可行的, 因为上面的代码没有捕获 t.
    SetTimer t, 1000
}
timertest()

Return, Exit 及一般说明

如果函数内的执行流在遇到 Return 前到达了函数的闭括号, 那么函数结束并返回空值(空字符串) 给其调用者. 当函数显式省略 Return 的参数时, 也返回空值.

当函数使用 Exit 终止当前线程时, 其调用者不会接收到返回值. 例如, 这个语句 Var := Add(2, 3) 中, 如果 Add() 退出了那么 Var 会保持不变. 如果因为 Throw 或运行时错误(如 运行一个不存在的文件), 也会发生同样的事情.

要使用一个或多个空值(空字符串) 调用函数, 可以使用空的引号对, 例如: FindColor(ColorName, "").

因为调用函数不会开启新线程, 所以函数对设置(如 SendModeSetTitleMatchMode) 做出的任何改变对其调用者同样有效.

在函数中使用 ListVars 时, 它会显示函数的局部变量及其内容. 这样可以帮助调试脚本.

样式和命名约定

如果给复杂函数中的特定变量加上独特的前缀, 您可能会发现它们更易于阅读和维护. 例如, 在函数的参数列表中以 "p" 或 "p_" 开头命名每个参数, 可以让它们的性质一目了然, 尤其是当函数中有大量的局部变量吸引您的注意力的时候. 类似地, 前缀 "r" 或 "r_" 可用于 ByRef 参数, 而 "s" 或 "s_" 可用于静态变量.

在定义函数时可以选择使用 One True Brace(OTB) 样式. 例如:

Add(x, y) {
    return x + y
}

使用 #Include 在多个脚本间共享函数

可以使用 #Include 指令从外部文件中加载函数.

内置函数

如果脚本定义了与内置函数同名的函数, 那么内置函数会被覆盖. 例如, 脚本会调用它自己定义的 WinExist() 函数来代替标准的那个. 然而, 这样脚本将无法调用原来的函数.

在 DLL 文件中的外部函数可以使用 DllCall() 调用.

获取所有内置函数的列表, 请参阅函数列表.

unixetc