函数类似于子程序(Gosub), 不过它还可以从调用者那里接受参数(输入). 而且, 函数还可以返回值给调用者. 参考下面的例子, Add 函数接受两个参数并返回它们之和:
Add(x, y) { return x + y ; "Return" 可直接返回表达式的结果. }
上面就是一个函数 定义, 因为它创建了一个名称为 "Add"(不区分大小写) 的函数, 并且确立了调用它时必须准确的提供两个参数(x 和 y). 要调用此函数, 请把它的结果通过 := 运算符赋值给变量. 例如:
Var := Add(2, 3) ; 数字 5 将被保存到 Var.
当然, 调用函数时也可以不保存其返回值:
Add(2, 3)
不过这种情况下, 函数的任何返回值都会被丢弃; 所以除非函数还有除了返回值之外的功能(译者注: 比如函数内修改了全局变量; 通过 ByRef 修改了变量值...等等), 否则这次调用毫无意义.
由于函数调用属于表达式语法, 所以在其参数列表中的任何变量名称都不应该用百分号包围. 同时, 表示原义字符串的变量则应该用双引号包围. 例如:
if InStr(MyVar, "fox") MsgBox The variable MyVar contains the word fox.
最后, 通常在命令的参数中也可以调用函数(除了像 StringLen 中的 OutputVar 和 InputVar 这种参数之外). 另外, 在不支持表达式的参数中调用函数时必须加上 "% " 前缀(百分号加一个空格), 例如:
MsgBox % "The answer is: " . Add(3, 2)
在原生支持表达式的参数中也允许加上 "% " 前缀, 但它只是会被忽略.
定义函数时, 其参数都在其名称后面的括号中列出(函数名和左括号之间不能有空格). 如果函数不接受任何参数, 请把括号留空, 例如: GetCurrentTimestamp().
GetCurrentTimestamp()
ByRef 参数: 从函数的角度看, 参数本质上是局部变量, 除非它们被定义为 ByRef, 例如:
Swap(ByRef Left, ByRef Right) { temp := Left Left := Right Right := temp }
在上述例子中, ByRef 的使用让相应的参数变成从调用者传递进来的变量的一个别名. 换句话说, 参数和调用者的变量都引用内存中相同的内容. 这样使得 Swap 函数可以通过移动 Left 的内容到 Right 中来修改调用者的变量, 反之亦然.
与之相比, 在上述例子中如果没有使用 ByRef, Left 和 Right 将是调用者变量的副本, 因此 Swap 函数不会对外部产生影响.
由于 return 只能返回一个值给函数的调用者, 所以可以使用 ByRef 返回更多的结果. 这是通过函数将调用者传递进来的变量(通常为空值的变量) 赋值实现的.
传递大字符串给函数时, 使用 ByRef 提高了性能, 并且通过避免生成字符串的副本节约了内存. 同样地, 使用 ByRef 送回长字符串给调用者通常比类似 Return HugeString 的方式执行的更好.
Return HugeString
[AHK_L 60+]: 如果传递给 ByRef 参数的变量是不可修改的, 那么函数会表现的就像没有 "ByRef" 关键字一样. 例如, Swap(A_Index, i) 保存 A_Index 的值到 i 中, 但是当 Swap 函数返回时赋给 Left 的值会被丢弃.(译者注: 因为 A_Index 是只读的内置变量.)
Swap(A_Index, i)
[v1.1.01+]: IsByRef() 函数可以用来判断调用者是否为指定的 ByRef 参数提供了变量.
已知限制:
foo.bar
Var
++Var
Var*=2
MyFunc(Var, Var++)
定义函数时, 可以把它的一个或多个参数标记为可选的. 这可以通过在参数后添加一个 :=(在 [v1.1.09] 及以后的版本) 或 =, 后面跟着参数的默认值来表示. 参数的默认值必须是下列形式中的一种: true, false, 原义的整数, 原义的浮点数或引号包围的/原义的字符串例如 "fox" 或 ""(但在 [1.0.46.13] 之前的版本中字符串只支持 "").
:=
=
true
false
允许使用 =(没有冒号) 是为了向后兼容, 但是已经不推荐使用, 而且 AutoHotkey v2 将不再允许这样做. 不管使用哪种操作符, 字符串格式的默认值总是被双引号包围的.
下面这个函数中的 Z 参数就是一个可选参数:
Add(X, Y, Z:=0) { return X + Y + Z }
当调用者传递 三个 参数给上面的函数时, Z 的默认值被忽略. 但当调用者仅传递 两个 参数时, Z 自动接受默认值 0.
可选参数不能孤立地放在参数列表的中间. 换句话说, 在首个可选参数右边的所有参数都必须定义为可选的. [AHK_L 31+]: 调用函数时可以省略参数列表中间的可选参数, 如下所示: 从 [v1.1.12] 开始, 动态调用函数或方法时也支持这种特性.
MyFunc(1,, 3) MyFunc(X, Y:=2, Z:=0) { ; 注意: 这里的 Z 必须是可选参数. MsgBox %X%, %Y%, %Z% }
从 [v1.0.46.13] 开始, ByRef 参数也支持默认值; 例如: MyFunc(ByRef p1 = ""). 每当调用者省略这样的参数时, 函数会创建一个包含默认值的局部变量; 不过, 这种情况下 "ByRef" 关键字没有任何意义.
MyFunc(ByRef p1 = "")
如介绍中所说, 函数可以返回一个值给调用者.
Test := returnTest() MsgBox % Test returnTest() { return 123 }
如果要从函数中返回额外的结果, 可以使用 ByRef:
returnByRef(A,B,C) MsgBox % A "," B "," C returnByRef(ByRef val1, ByRef val2, ByRef val3) { val1 := "A" val2 := 100 val3 := 1.1 return }
[v1.0.97+]: 可以使用对象和数组返回多值甚至是命名值:
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.MaxIndex() 可以确定最大的索引值(这里为参数的数目). 但是如果没有参数, MaxIndex 会返回空字符串.
params[1]
params[2]
params.MaxIndex()
注意:
虽然可变参数函数可以 接受 可变数目的参数, 不过在函数调用中使用相同的语法可以把数组作为参数传递给 任何 函数:
substrings := ["one", "two", "three"] MsgBox % Join("`n", substrings*)
Object.Property[Params*]
MyFunc(x, y*)
MyFunc(x*, y)
*
局部变量是特定于单个函数的, 只在该函数内可见. 因此, 局部变量可能具有与全局变量相同的名称, 并且两个变量都有单独的内容. 不同的函数也可以安全地使用相同的变量名.
当函数返回值时, 所有非静态的局部变量都自动释放(变为空).
像 Clipboard, ErrorLevel 和 A_TimeIdle 这样的内置变量不会是局部的(它们可以从任何地方访问), 并且不能被重新声明.
默认情况下, 函数假定是局部的. 在函数内访问或创建的变量默认为局部的, 但以下情况除外:
如下所示, 也可以重写默认值(通过声明变量或改变函数的模式).
强制局部模式 [v1.1.27+]: 如果函数的第一行是单词 "local", 则假定所有变量引用(甚至是动态的) 都是局部的, 除非它们在函数 内 声明为全局的. 与默认模式不同, 强制局部模式具有以下行为:
要在函数中引用现有的全局变量(或创建新的), 需要在使用前声明此变量为全局的. 例如:
LogToFile(TextToLog) { global LogFileName ; 此全局变量之前已经在函数外的某个地方赋值了. FileAppend, %TextToLog%`n, %LogFileName% }
假设全局模式: 如果函数需要访问或创建大量的全局变量, 通过在函数的首行使用单词 "global" 或声明局部变量可以把函数定义为假设其所有的变量都是全局的(除了它的参数). 例如:
SetDefaults() { global ; 如果此函数的首行是类似于 "local MyVar" 这样的, 那么这个单词可以省略. MyGlobal := 33 ; 把 33 赋值给全局变量, 必要时首先创建这个变量. local x, y:=0, z ; 在这种模式中局部变量必须进行声明, 否则会假设它们为全局的. }
函数还可以使用这种假设全局模式来创建全局数组, 例如赋值给 Array%A_Index% 的循环.
Array%A_Index%
超级全局变量 [v1.1.05+]: 如果全局声明出现在任何函数的外面, 默认情况下它可以对所有函数有效 (强制局部模式的函数除外). 这样可以避免在每个函数中重复声明变量的需要. 不过, 如果声明了含有相同名称的函数参数或局部变量, 那么它会优先于全局变量. 由 class 关键字创建的变量也是超级全局的.
静态变量总是隐式的局部变量, 但和局部变量的区别是它们的值在多次调用期间是记住的. 例如:
LogToFile(TextToLog) { static LoggedLines := 0 LoggedLines += 1 ; 保持局部的计数(它的值在多次调用期间是记住的). global LogFileName FileAppend, %LoggedLines%: %TextToLog%`n, %LogFileName% }
静态初始化: 在 [1.0.46] 以前的版本中, 所有的静态变量都是以空值开始; 所以要检查静态变量首次被使用的唯一方法是检查它是否为空值. [v1.0.46+]: 静态变量可以初始化为 "" 外的其他值, 通过在后面跟着 := 或 = 及后面这些形式的其中一种: true, false, 原义的整数, 原义的浮点数或引号包围的/原义的字符串, 如 "fox". 例如: static X:=0, Y:="fox". 每个静态变量只初始化一次(在脚本开始执行前).
""
"fox"
static X:=0, Y:="fox"
[AHK_L 58+]: 支持 Static var := expression. 根据这些表达式在脚本中出现的顺序对它们进行计算, 紧接着才进入脚本的自动执行段.
Static var := expression
假设静态模式 [v1.0.48+]: 通过在函数的首行使用单词 "static" 可以把函数定义为假设其所有的变量都是静态的(除了它的参数). 例如:
GetFromStaticArray(WhichItemNumber) { static static FirstCallToUs := true ; 静态声明初始化仍然只运行一次(在脚本执行前). if FirstCallToUs ; 在首次调用而不在后续的调用时创建静态数组. { FirstCallToUs := false Loop 10 StaticArray%A_Index% := "Value #" . A_Index } return StaticArray%WhichItemNumber% }
在假设静态模式中, 任何非静态变量都必须声明为局部变量或全局变量(例外情况与假设局部模式相同, 除非强制局部模式也有效).
[v1.1.27+]: 可以通过指定 local 然后 static 的方式将强制局部模式和假设静态模式组合在一起使用, 如下面演示的那样. 允许函数使用强制局部模式的规则, 但默认情况下创建静态变量.
local
static
global MyVar := "This is global" DemonstrateForceStatic() DemonstrateForceStatic() { local static MyVar := "This is static" ListVars MsgBox }
通过逗号分隔多个变量这样可以在同一行声明它们, 例如:
global LogFileName, MaxRetries := 5 static TotalAttempts := 0, PrevResult
[v1.0.46+]: 通过后面跟着 := 或 = 及任意表达式, 局部或全局变量可以在声明的同一行初始化(在声明时 = 运算符和 := 作用相同). 与静态初始化不同, 在每次调用函数时都会对局部变量和全局变量进行初始化, 但只在控制流实际达到它们所在的语句时. 换句话说, 像 local x := 0 这样的一行和写成单独的两行有同样的效果: local x 后面跟着 x := 0.
local x := 0
local x
x := 0
因为单词 local, global 和 static 都是在脚本运行时立即处理的, 所以不能使用 IF 语句有条件的声明变量. 换句话说, IF 或 ELSE 的区块内的声明无条件对声明和函数的闭括号之间的所有行生效. 同时还需注意当前还不支持声明动态变量, 例如 global Array%i%.
global Array%i%
对于创建伪数组的命令(例如 StringSplit), 如果假设全局模式没有生效或数组的首个元素已经声明为局部变量时创建的数组是局部的(如果函数的某个参数被传递时也是如此, 即使此参数为 ByRef 类型, 因为参数类似于局部变量). 相反地, 如果首个元素已经声明为全局的, 那么创建全局的数组. 不过, 后面 混乱的常见根源 也适用于这些情况. StringSplit 创建的数组首个元素为 ArrayName0. 对于其他创建数组的命令, 例如 WinGet List, 首个元素为 ArrayName(即没有数字). [v1.1.27+]: 当强制局部模式生效时, 这些命令遵循与普通变量引用一致的规则; 也就是说, 任何未声明为全局的伪数组元素都将是局部的, 即使其他元素声明为全局的.
在函数(强制局部模式生效时除外) 中, 任何动态变量引用, 例如 Array%i%, 总是解析为局部变量, 仅当这样名称的局部变量不存在而全局变量存在时才解析为全局变量. 如果两者都不存在并且需要创建此变量时, 当假设全局模式没有生效时它被创建为局部变量. 因此, 仅当函数被定义为假设全局函数时, 函数中才可以手动创建全局数组(通过使用类似 Array%i% := A_Index 的方法).
Array%i%
Array%i% := A_Index
混乱的常见根源: 任何 非 动态引用都会在脚本运行时创建那个变量. 例如, 在函数外使用时, MsgBox %Array1% 会在脚本运行的时候创建 Array1 为全局变量. 相反地, 在函数中使用 MsgBox %Array1% 会在脚本运行的时候创建 Array1 为函数的一个局部变量(除非假设全局模式生效), 即使 Array 和 Array0 都声明为全局的.
MsgBox %Array1%
[v1.0.47.06+]: 通过百分号可以动态调用函数(包括内置函数). 例如,%Var%(x, "fox") 将调用名称保存在 Var 中的函数. 同样地, Func%A_Index%() 将调用 Func1() 或 Func2() 等, 这取决于 A_Index 的当前值.
%Var%(x, "fox")
Func%A_Index%()
[v1.1.07.00+]: 在 %Var%() 中的 Var 可包含函数名或函数对象. 如果此函数不存在, 则调用默认基对象的 __Call 元函数.
%Var%()
如果由于下面的某个原因无法调用函数, 计算包含调用的表达式时可能会过早静默停止, 这样可能会产生问题.
If IsFunc(VarContainingFuncName)
最后, 对函数的动态调用比正常调用稍慢, 因为正常的调用在脚本开始运行前解析(查询).
当在表达式中使用 AND, OR 和三元运算符时, 会对它们进行优化来提高性能(不管当前是否存在函数调用). 通过不计算表达式中那些不影响最终结果的部分来进行优化运算. 为了说明这个概念, 请看这个例子:
if (ColorName != "" AND not FindColor(ColorName)) MsgBox %ColorName% could not be found.
在上面的例子中, 如果 ColorName 变量为空则永远不会调用 FindColor() 函数. 这是由于 AND 的左侧结果将为 false, 因此其右边不可能让最终的结果为 true.
由于此特性, 所以需要注意到, 如果在 AND 或 OR 的右侧调用函数, 那么函数可能永远不会产生副作用(例如改变全局变量的内容).
还需要注意在嵌套的 AND 和 OR 串联表达式的求值优化. 例如, 在后面的表达式中每当 ColorName 为空时只会进行最左边的比较. 这是因为此时最左边的比较已经足以确定最终的结果:
if (ColorName = "" OR FindColor(ColorName, Region1) OR FindColor(ColorName, Region2)) break ; 搜索内容为空或找到了匹配.
从上面的例子可以看出, 任何耗时的函数一般应该在 AND 或 OR 的右侧调用从而提高性能. 这种方法还能避免调用接受了一个不合适的值例如空字符串的函数.
[v1.0.46+]: 三元条件运算符(?:) 也通过不计算丢弃的分支进行求值优化.
尽管在一个函数中不能定义其他函数, 但它可以含有子程序. 与其他子程序一样使用 Gosub 运行它们且使用 Return 返回(此时 Return 属于 Gosub 而不是函数).
已知限制: 当前每个子程序(标签) 的名称在整个脚本中必须是唯一的. 如果存在重复的标签, 在运行前程序会通知您.
如果函数使用 Gosub 跳转到公共子程序(在函数外部的子程序), 那么所有外面的变量都是全局的而且在子程序返回前无法引用函数自身的局部变量. 不过, A_ThisFunc 仍将包含正在执行的函数名称.
尽管不能使用 Goto 从函数中跳转到外面, 但可以在函数中使用 Gosub 到外部/公共的子程序, 然后在那里使用 Goto.
尽管一般不鼓励使用 Goto, 但它能用来在同个函数中的一个位置跳转到其他位置. 这样有助于简化包含许多个返回点而所有这些点在返回前需要进行一些清理的复杂函数.
函数可以包含能被外部调用的子程序, 例如计时器, GUI g-标签和菜单项. 通常把它们封装到一个单独的文件中供 #Include 使用, 这样避免了它们和脚本的自动执行段的冲突. 不过, 还存在下面这些限制:
如果函数内的执行流在遇到 Return 前到达了函数的闭括号, 那么函数结束并返回空值(空字符串) 给其调用者. 当函数明确省略 Return 的参数时也返回空值.
当函数使用 Exit 命令终止当前线程时, 其调用者不会接收到返回值. 例如, 这个语句 Var := Add(2, 3) 中, 如果 Add() 退出了那么 Var 会保持不变. 如果函数出现了运行时错误, 例如运行了一个不存在的文件(当 UseErrorLevel 选项不存在时), 也会出现同样的情况.
Var := Add(2, 3)
Add()
如果要返回一个额外的容易记住的值, 那么函数可以修改 ErrorLevel 的值来实现.
要使用一个或多个空值(空字符串) 调用函数, 可以使用空的引号对, 例如: FindColor(ColorName, "").
FindColor(ColorName, "")
因为调用函数不会开启新线程, 所以函数对设置例如 SendMode 和 SetTitleMatchMode 做出的任何改变对其调用者同样有效.
函数的调用者可以传递不存在的变量或数组元素给它, 当函数期望和 ByRef 一致的参数时这很有用. 例如, 调用 GetNextLine(BlankArray%i%) 会自动地创建局部或全局的变量 BlankArray%i%(是局部还是全局取决于调用者是否在函数内并且它的假设全局模式是否有效).
GetNextLine(BlankArray%i%)
BlankArray%i%
在函数中使用 ListVars 时, 它会显示函数的局部变量及其内容. 这样可以帮助调试脚本.
您也许会发现, 如果给复杂函数中的特定变量加上独特的前缀会让函数更容易阅读和维护. 例如, 使用 "p" 或 "p_" 开头来命名函数中的每个参数可以让它们的性质一目了然, 尤其是当函数中有许多局部变量吸引您的注意力的时候. 同样地, 前缀 "r" 或 "r_" 可以用于 ByRef 参数, 而 "s" 或 "s_" 可以用于静态变量.
在定义函数时可以使用 One True Brace(OTB) 风格. 例如:
Add(x, y) { return x + y }
可以使用 #Include 指令(即使在脚本的顶部) 从外部文件中加载函数.
说明: 当脚本的执行流遇到函数定义时, 它会跳过函数(使用一种瞬时的方法) 并在函数闭括号后恢复执行. 因此, 开始执行时执行流不会陷入函数中, 也不会因为在脚本的最顶部存在一个或多个函数而影响到自动执行段.
脚本中可以不需要通过 #Include 而调用外部文件中的函数. 要达到此目的, 必须在下面某个库文件夹中存在与函数同名的文件:
%A_ScriptDir%\Lib\ ; 本地库, 脚本所在目录中的 Lib 文件夹 - 需要 [AHK_L 42+]. %A_MyDocuments%\AutoHotkey\Lib\ ; 用户库. directory-to-the-currently-running-AutoHotkey.exe\Lib\ ; 标准库, 当前运行的 AutoHotkey.exe 所在目录中的 Lib 文件夹.
例如, 当脚本调用不存在的函数 MyFunc() 时, 程序会在用户库中搜索名为 "MyFunc.ahk" 的文件. 如果没有找到, 会继续在标准库中搜索此文件. 如果仍然没有找到匹配的文件且函数的名称含有下划线(例如 MyPrefix_MyFunc), 那么程序会在两个库中搜索名为 MyPrefix.ahk 的文件, 如果找到则加载它. 这样使得 MyPrefix.ahk 可以同时包含 MyPrefix_MyFunc 函数和其他名称以 MyPrefix_ 开始的相关函数.
MyFunc()
MyPrefix_MyFunc
MyPrefix.ahk
MyPrefix_
[AHK_L 42+]: 支持本地库且本地库优先于用户库和标准库.
只有如 MyFunc() 这样直接的函数调用才能自动导入库. 如果仅动态或间接调用该函数, 就像计时器或 GUI 事件, 那么库必须在脚本中明确的(显式) 导入, 例如: #Include <MyFunc>
#Include <MyFunc>
虽然库文件通常只包含和它的文件名称相同的单个函数, 但它还可以包含仅被此函数调用的私有函数和子程序. 然而, 这样的函数应该使用相当明确清晰的名称, 因为它们仍属于全局命名空间; 也就是说, 可以在脚本的任意位置调用它们.
如果库文件使用 #Include, 那么 #Include 的工作目录为库文件自身所在的目录. 利用这个特性可以重定向到较大的库文件, 其中包含此函数及相关内容.
脚本编译器(ahk2exe) 同样支持库函数. 不过, 它要求在编译器目录的上一级目录中存在 AutoHotkey.exe 的副本(通常是这样). 如果不存在 AutoHotkey.exe, 编译器仍可以运行但无法自动包含库函数.
从库中导入的函数和其他函数执行的一样好, 因为在脚本开始执行前已经预加载了它们.
在内置函数参数列表末尾的任何可选参数可以完全省略. 例如, WinExist("Untitled - Notepad") 是有效的, 因为它的其他三个参数被认为是空的.
WinExist("Untitled - Notepad")
如果脚本定义了与内置函数同名的函数, 那么内置函数会被覆盖. 例如, 脚本会调用它自己定义的 WinExist() 函数来代替标准的那个. 然而, 这样脚本将无法调用原来的函数.
在 DLL 文件中的外部函数可以使用 DllCall() 调用.
下列内置函数都可以通过点击它们的名称查看详细信息.
Polyethene 的命令函数: 为每个有 OutputVar 的 AutoHotkey 命令提供了可调用的函数. 可以通过 #Include 把这个库包含在任何脚本中.