函数| AutoHotkey
文章推薦指數: 80 %
然而, 当函数传递给它自己全局变量, 静态变量或ByRef 参数时不会产生这样的问题. 如果一个参数在函数调用中被解析为一个变量(例如 Var 或 ++Var 或 Var*=2 ) ...
函数
目录
介绍和简单示例
参数
可选参数
返回值给调用者
可变参数函数
局部和全局变量
动态调用函数
优化布尔求值
在函数中使用子程序
Return,Exit及一般说明
使用#Include在多个脚本间共享函数
函数库:标准库和用户库
内置函数
介绍和简单示例
函数类似于子程序(Gosub),不过它还可以从调用者那里接受参数(输入).而且,函数还可以返回值给调用者.参考下面的例子,Add函数接受两个参数并返回它们之和:
Add(x,y)
{
returnx+y;"Return"可直接返回表达式的结果.
}
上面就是一个函数定义,因为它创建了一个名称为"Add"(不区分大小写)的函数,并且确立了调用它时必须准确的提供两个参数(x和y).要调用此函数,请把它的结果通过:=运算符赋值给变量.例如:
Var:=Add(2,3);数字5将被保存到Var.
当然,调用函数时也可以不保存其返回值:
Add(2,3)
不过这种情况下,函数的任何返回值都会被丢弃;所以除非函数还有除了返回值之外的功能(译者注:比如函数内修改了全局变量;通过ByRef修改了变量值...等等),否则这次调用毫无意义.
由于函数调用属于表达式语法,所以在其参数列表中的任何变量名称都不应该用百分号包围.同时,表示原义字符串的变量则应该用双引号包围.例如:
ifInStr(MyVar,"fox")
MsgBoxThevariableMyVarcontainsthewordfox.
最后,通常在命令的参数中也可以调用函数(除了像StringLen中的OutputVar和InputVar这种参数之外).另外,在不支持表达式的参数中调用函数时必须加上"%"前缀(百分号加一个空格),例如:
MsgBox%"Theansweris:".Add(3,2)
在原生支持表达式的参数中也允许加上"%"前缀,但它只是会被忽略.
参数
定义函数时,其参数都在其名称后面的括号中列出(函数名和左括号之间不能有空格).如果函数不接受任何参数,请把括号留空,例如:GetCurrentTimestamp().
ByRef参数:从函数的角度看,参数本质上是局部变量,除非它们被定义为ByRef,例如:
Swap(ByRefLeft,ByRefRight)
{
temp:=Left
Left:=Right
Right:=temp
}
在上述例子中,ByRef的使用让相应的参数变成从调用者传递进来的变量的一个别名.换句话说,参数和调用者的变量都引用内存中相同的内容.这样使得Swap函数可以通过移动Left的内容到Right中来修改调用者的变量,反之亦然.
与之相比,在上述例子中如果没有使用ByRef,Left和Right将是调用者变量的副本,因此Swap函数不会对外部产生影响.
由于return只能返回一个值给函数的调用者,所以可以使用ByRef返回更多的结果.这是通过函数将调用者传递进来的变量(通常为空值的变量)赋值实现的.
传递大字符串给函数时,使用ByRef提高了性能,并且通过避免生成字符串的副本节约了内存.同样地,使用ByRef送回长字符串给调用者通常比类似ReturnHugeString的方式执行的更好.
[AHK_L60+]:如果传递给ByRef参数的变量是不可修改的,那么函数会表现的就像没有"ByRef"关键字一样.例如,Swap(A_Index,i)保存A_Index的值到i中,但是当Swap函数返回时赋给Left的值会被丢弃.(译者注:因为A_Index是只读的内置变量.)
[v1.1.01+]:IsByRef()函数可以用来判断调用者是否为指定的ByRef参数提供了变量.
已知限制:
对象的属性在ByRef的参数中不会被视为变量.例如,如果foo.bar传递给ByRef参数,那么它将表现的就像没有ByRef关键字一样.
不能传递Clipboard,内置变量或环境变量给函数的ByRef参数,无论脚本中有没有#NoEnv语句.
尽管函数可以递归调用它自己,但是如果它传递它自己的一个局部变量或非ByRef参数给自己的ByRef,那么新一层的ByRef参数将引用它自己那个名称的局部变量而不是之前层的.然而,当函数传递给它自己全局变量,静态变量或ByRef参数时不会产生这样的问题.
如果一个参数在函数调用中被解析为一个变量(例如Var或++Var或Var*=2),它左边或右边的其他参数可能在它被传递给函数前修改这个变量.例如,当Var初始为0时MyFunc(Var,Var++)会意外地传递1和0,即使函数的首个参数不是ByRef类型时.因为这种行为是违反常规的,所以可能在将来的版本中改变.
ByRef不直接支持函数调用COM客户端,或调用COM方法.作为替代,脚本必须接受或传递一个wrapperobject(包装对象)包含特殊的VarType(变量类型)和变量内存地址.
可选参数
定义函数时,可以把它的一个或多个参数标记为可选的.这可以通过在参数后添加一个:=(在[v1.1.09]及以后的版本)或=,后面跟着参数的默认值来表示.参数的默认值必须是下列形式中的一种:true,false,原义的整数,原义的浮点数或引号包围的/原义的字符串例如"fox"或""(但在[1.0.46.13]之前的版本中字符串只支持"").
允许使用=(没有冒号)是为了向后兼容,但是已经不推荐使用,而且AutoHotkeyv2将不再允许这样做.不管使用哪种操作符,字符串格式的默认值总是被双引号包围的.
下面这个函数中的Z参数就是一个可选参数:
Add(X,Y,Z:=0){
returnX+Y+Z
}
当调用者传递三个参数给上面的函数时,Z的默认值被忽略.但当调用者仅传递两个参数时,Z自动接受默认值0.
可选参数不能孤立地放在参数列表的中间.换句话说,在首个可选参数右边的所有参数都必须定义为可选的.[AHK_L31+]:调用函数时可以省略参数列表中间的可选参数,如下所示:从[v1.1.12]开始,动态调用函数或方法时也支持这种特性.
MyFunc(1,,3)
MyFunc(X,Y:=2,Z:=0){;注意:这里的Z必须是可选参数.
MsgBox%X%,%Y%,%Z%
}
从[v1.0.46.13]开始,ByRef参数也支持默认值;例如:MyFunc(ByRefp1="").每当调用者省略这样的参数时,函数会创建一个包含默认值的局部变量;不过,这种情况下"ByRef"关键字没有任何意义.
返回值给调用者
如介绍中所说,函数可以返回一个值给调用者.
Test:=returnTest()
MsgBox%Test
returnTest(){
return123
}
如果要从函数中返回额外的结果,可以使用ByRef:
returnByRef(A,B,C)
MsgBox%A","B","C
returnByRef(ByRefval1,ByRefval2,ByRefval3)
{
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"]
returnTest
}
returnArray2(){
x:=456
y:="EFG"
return[x,y]
}
returnObject(){
Test:={id:789,val:"HIJ"}
returnTest
}
可变参数函数[AHK_L60+]
定义函数时,在最后一个参数后面写一个星号来标记此函数为可变参数的,这样让它可以接收可变数目的参数:
Join(sep,params*){
forindex,paraminparams
str.=param.sep
returnSubStr(str,1,-StrLen(sep))
}
MsgBox%Join("`n","one","two","three")
调用可变参数函数时,通过保存在函数的最后参数中的对象可以访问剩余的参数.函数的首个超出参数是params[1],第二个是params[2]以此类推.和所有的标准对象一样,使用params.MaxIndex()可以确定最大的索引值(这里为参数的数目).但是如果没有参数,MaxIndex会返回空字符串.
注意:
"可变"参数只可以出现在显式参数(形参)列表的末尾.
RegEx调出函数不是可变参数的,虽然允许使用"可变的"参数但是那只是留空而已.
回调函数通过地址而不是数组传递剩余参数.
可变参数函数调用
虽然可变参数函数可以接受可变数目的参数,不过在函数调用中使用相同的语法可以把数组作为参数传递给任何函数:
substrings:=["one","two","three"]
MsgBox%Join("`n",substrings*)
注意:
在源数组中参数的编号从1开始.
数组中的可选参数可以完全省略.
调用自定义函数时数组参数可以包含命名项,在其他情况下则不支持命名项.
目标函数也可以是可变的,此时命名项被复制,即使它们没有一致的形式参数.
这样的语法还可以用于调用对象的方法或获取对象的属性;例如,Object.Property[Params*].从[v1.1.12]开始,它还可以用于设置属性.
已知限制:
只有最右边的那个参数才可以这样展开.例如,支持MyFunc(x,y*)但不支持MyFunc(x*,y).
在星号(*)和参数列表中最后的形式参数间不能存在任何的非空白字符.
局部变量和全局变量
局部变量
局部变量是特定于单个函数的,只在该函数内可见.因此,局部变量可能具有与全局变量相同的名称,并且两个变量都有单独的内容.不同的函数也可以安全地使用相同的变量名.
当函数返回值时,所有非静态的局部变量都自动释放(变为空).
像Clipboard,ErrorLevel和A_TimeIdle这样的内置变量不会是局部的(它们可以从任何地方访问),并且不能被重新声明.
默认情况下,函数假定是局部的.在函数内访问或创建的变量默认为局部的,但以下情况除外:
超级全局变量,包括classes(类).
动态变量引用,如果不存在同名的局部变量,则可以解析为现有的全局变量.
创建伪数组的命令,即使只声明第一个元素,也可以将所有创建的元素作为全局变量.
如下所示,也可以重写默认值(通过声明变量或改变函数的模式).
强制局部模式[v1.1.27+]:如果函数的第一行是单词"local",则假定所有变量引用(甚至是动态的)都是局部的,除非它们在函数内声明为全局的.与默认模式不同,强制局部模式具有以下行为:
函数内部没有声明时,超级全局变量(包括类)不能被访问.
动态变量引用遵循与非动态变量引用相同的规则.只有在函数中声明的全局变量才能被访问.
创建伪数组的StringSplit和其他命令遵循与非动态变量引用相同的规则(避免共同的混淆源).
LocalSameAsGlobal警告永远不会被强制局部函数中的变量引发.
全局变量
要在函数中引用现有的全局变量(或创建新的),需要在使用前声明此变量为全局的.例如:
LogToFile(TextToLog)
{
globalLogFileName;此全局变量之前已经在函数外的某个地方赋值了.
FileAppend,%TextToLog%`n,%LogFileName%
}
假设全局模式:如果函数需要访问或创建大量的全局变量,通过在函数的首行使用单词"global"或声明局部变量可以把函数定义为假设其所有的变量都是全局的(除了它的参数).例如:
SetDefaults()
{
global;如果此函数的首行是类似于"localMyVar"这样的,那么这个单词可以省略.
MyGlobal:=33;把33赋值给全局变量,必要时首先创建这个变量.
localx,y:=0,z;在这种模式中局部变量必须进行声明,否则会假设它们为全局的.
}
函数还可以使用这种假设全局模式来创建全局数组,例如赋值给Array%A_Index%的循环.
超级全局变量[v1.1.05+]:如果全局声明出现在任何函数的外面,默认情况下它可以对所有函数有效(强制局部模式的函数除外).这样可以避免在每个函数中重复声明变量的需要.不过,如果声明了含有相同名称的函数参数或局部变量,那么它会优先于全局变量.由class关键字创建的变量也是超级全局的.
静态变量
静态变量总是隐式的局部变量,但和局部变量的区别是它们的值在多次调用期间是记住的.例如:
LogToFile(TextToLog)
{
staticLoggedLines:=0
LoggedLines+=1;保持局部的计数(它的值在多次调用期间是记住的).
globalLogFileName
FileAppend,%LoggedLines%:%TextToLog%`n,%LogFileName%
}
静态初始化:在[1.0.46]以前的版本中,所有的静态变量都是以空值开始;所以要检查静态变量首次被使用的唯一方法是检查它是否为空值.[v1.0.46+]:静态变量可以初始化为""外的其他值,通过在后面跟着:=或=及后面这些形式的其中一种:true,false,原义的整数,原义的浮点数或引号包围的/原义的字符串,如"fox".例如:staticX:=0,Y:="fox".每个静态变量只初始化一次(在脚本开始执行前).
[AHK_L58+]:支持Staticvar:=expression.根据这些表达式在脚本中出现的顺序对它们进行计算,紧接着才进入脚本的自动执行段.
假设静态模式[v1.0.48+]:通过在函数的首行使用单词"static"可以把函数定义为假设其所有的变量都是静态的(除了它的参数).例如:
GetFromStaticArray(WhichItemNumber)
{
static
staticFirstCallToUs:=true;静态声明初始化仍然只运行一次(在脚本执行前).
ifFirstCallToUs;在首次调用而不在后续的调用时创建静态数组.
{
FirstCallToUs:=false
Loop10
StaticArray%A_Index%:="Value#".A_Index
}
returnStaticArray%WhichItemNumber%
}
在假设静态模式中,任何非静态变量都必须声明为局部变量或全局变量(例外情况与假设局部模式相同,除非强制局部模式也有效).
[v1.1.27+]:可以通过指定local然后static的方式将强制局部模式和假设静态模式组合在一起使用,如下面演示的那样.允许函数使用强制局部模式的规则,但默认情况下创建静态变量.
globalMyVar:="Thisisglobal"
DemonstrateForceStatic()
DemonstrateForceStatic()
{
local
static
MyVar:="Thisisstatic"
ListVars
MsgBox
}
关于局部和全局变量的更多信息
通过逗号分隔多个变量这样可以在同一行声明它们,例如:
globalLogFileName,MaxRetries:=5
staticTotalAttempts:=0,PrevResult
[v1.0.46+]:通过后面跟着:=或=及任意表达式,局部或全局变量可以在声明的同一行初始化(在声明时=运算符和:=作用相同).与静态初始化不同,在每次调用函数时都会对局部变量和全局变量进行初始化,但只在控制流实际达到它们所在的语句时.换句话说,像localx:=0这样的一行和写成单独的两行有同样的效果:localx后面跟着x:=0.
因为单词local,global和static都是在脚本运行时立即处理的,所以不能使用IF语句有条件的声明变量.换句话说,IF或ELSE的区块内的声明无条件对声明和函数的闭括号之间的所有行生效.同时还需注意当前还不支持声明动态变量,例如globalArray%i%.
对于创建伪数组的命令(例如StringSplit),如果假设全局模式没有生效或数组的首个元素已经声明为局部变量时创建的数组是局部的(如果函数的某个参数被传递时也是如此,即使此参数为ByRef类型,因为参数类似于局部变量).相反地,如果首个元素已经声明为全局的,那么创建全局的数组.不过,后面混乱的常见根源也适用于这些情况.StringSplit创建的数组首个元素为ArrayName0.对于其他创建数组的命令,例如WinGetList,首个元素为ArrayName(即没有数字).[v1.1.27+]:当强制局部模式生效时,这些命令遵循与普通变量引用一致的规则;也就是说,任何未声明为全局的伪数组元素都将是局部的,即使其他元素声明为全局的.
在函数(强制局部模式生效时除外)中,任何动态变量引用,例如Array%i%,总是解析为局部变量,仅当这样名称的局部变量不存在而全局变量存在时才解析为全局变量.如果两者都不存在并且需要创建此变量时,当假设全局模式没有生效时它被创建为局部变量.因此,仅当函数被定义为假设全局函数时,函数中才可以手动创建全局数组(通过使用类似Array%i%:=A_Index的方法).
混乱的常见根源:任何非动态引用都会在脚本运行时创建那个变量.例如,在函数外使用时,MsgBox%Array1%会在脚本运行的时候创建Array1为全局变量.相反地,在函数中使用MsgBox%Array1%会在脚本运行的时候创建Array1为函数的一个局部变量(除非假设全局模式生效),即使Array和Array0都声明为全局的.
动态调用函数
[v1.0.47.06+]:通过百分号可以动态调用函数(包括内置函数).例如,%Var%(x,"fox")将调用名称保存在Var中的函数.同样地,Func%A_Index%()将调用Func1()或Func2()等,这取决于A_Index的当前值.
[v1.1.07.00+]:在%Var%()中的Var可包含函数名或函数对象.如果此函数不存在,则调用默认基对象的__Call元函数.
如果由于下面的某个原因无法调用函数,计算包含调用的表达式时可能会过早静默停止,这样可能会产生问题.
调用不存在的函数,这可以通过使用IfIsFunc(VarContainingFuncName)来避免.除了内置函数,被调用函数的定义必须真实存在于脚本中,通过类似#Include或对库函数的非动态调用的方法.
传递过少的参数,这可以通过检查IsFunc()的返回值来避免(这是强制参数的数目加上一的数字).[v1.0.48+]:注意允许传递过多的参数;每个额外的参数在被完全计算(包括任何函数调用)后丢弃.
最后,对函数的动态调用比正常调用稍慢,因为正常的调用在脚本开始运行前解析(查询).
优化布尔求值
当在表达式中使用AND,OR和三元运算符时,会对它们进行优化来提高性能(不管当前是否存在函数调用).通过不计算表达式中那些不影响最终结果的部分来进行优化运算.为了说明这个概念,请看这个例子:
if(ColorName!=""ANDnotFindColor(ColorName))
MsgBox%ColorName%couldnotbefound.
在上面的例子中,如果ColorName变量为空则永远不会调用FindColor()函数.这是由于AND的左侧结果将为false,因此其右边不可能让最终的结果为true.
由于此特性,所以需要注意到,如果在AND或OR的右侧调用函数,那么函数可能永远不会产生副作用(例如改变全局变量的内容).
还需要注意在嵌套的AND和OR串联表达式的求值优化.例如,在后面的表达式中每当ColorName为空时只会进行最左边的比较.这是因为此时最左边的比较已经足以确定最终的结果:
if(ColorName=""ORFindColor(ColorName,Region1)ORFindColor(ColorName,Region2))
break;搜索内容为空或找到了匹配.
从上面的例子可以看出,任何耗时的函数一般应该在AND或OR的右侧调用从而提高性能.这种方法还能避免调用接受了一个不合适的值例如空字符串的函数.
[v1.0.46+]:三元条件运算符(?:)也通过不计算丢弃的分支进行求值优化.
在函数中使用子程序
尽管在一个函数中不能定义其他函数,但它可以含有子程序.与其他子程序一样使用Gosub运行它们且使用Return返回(此时Return属于Gosub而不是函数).
已知限制:当前每个子程序(标签)的名称在整个脚本中必须是唯一的.如果存在重复的标签,在运行前程序会通知您.
如果函数使用Gosub跳转到公共子程序(在函数外部的子程序),那么所有外面的变量都是全局的而且在子程序返回前无法引用函数自身的局部变量.不过,A_ThisFunc仍将包含正在执行的函数名称.
尽管不能使用Goto从函数中跳转到外面,但可以在函数中使用Gosub到外部/公共的子程序,然后在那里使用Goto.
尽管一般不鼓励使用Goto,但它能用来在同个函数中的一个位置跳转到其他位置.这样有助于简化包含许多个返回点而所有这些点在返回前需要进行一些清理的复杂函数.
函数可以包含能被外部调用的子程序,例如计时器,GUIg-标签和菜单项.通常把它们封装到一个单独的文件中供#Include使用,这样避免了它们和脚本的自动执行段的冲突.不过,还存在下面这些限制:
如果它们所在的函数曾经被正常调用,那么这样的子程序应该只使用静态和全局变量(不使用局部变量).这是由于中断函数调用线程的子程序线程(反之亦然)可以改变被中断线程看到的局部变量的值.此外,在任何时候函数返回到其调用者时,它包含的所有局部变量都被置空来释放占用的内存.
这样的子程序应该只使用全局变量(不使用静态变量)作为GUI控件的变量.
当一个子程序线程进入函数后,在此线程中任何对动态变量的引用都被视为是全局的(包括创建数组的那些命令).
Return,Exit及一般说明
如果函数内的执行流在遇到Return前到达了函数的闭括号,那么函数结束并返回空值(空字符串)给其调用者.当函数明确省略Return的参数时也返回空值.
当函数使用Exit命令终止当前线程时,其调用者不会接收到返回值.例如,这个语句Var:=Add(2,3)中,如果Add()退出了那么Var会保持不变.如果函数出现了运行时错误,例如运行了一个不存在的文件(当UseErrorLevel选项不存在时),也会出现同样的情况.
如果要返回一个额外的容易记住的值,那么函数可以修改ErrorLevel的值来实现.
要使用一个或多个空值(空字符串)调用函数,可以使用空的引号对,例如:FindColor(ColorName,"").
因为调用函数不会开启新线程,所以函数对设置例如SendMode和SetTitleMatchMode做出的任何改变对其调用者同样有效.
函数的调用者可以传递不存在的变量或数组元素给它,当函数期望和ByRef一致的参数时这很有用.例如,调用GetNextLine(BlankArray%i%)会自动地创建局部或全局的变量BlankArray%i%(是局部还是全局取决于调用者是否在函数内并且它的假设全局模式是否有效).
在函数中使用ListVars时,它会显示函数的局部变量及其内容.这样可以帮助调试脚本.
风格和命名约定
您也许会发现,如果给复杂函数中的特定变量加上独特的前缀会让函数更容易阅读和维护.例如,使用"p"或"p_"开头来命名函数中的每个参数可以让它们的性质一目了然,尤其是当函数中有许多局部变量吸引您的注意力的时候.同样地,前缀"r"或"r_"可以用于ByRef参数,而"s"或"s_"可以用于静态变量.
在定义函数时可以使用OneTrueBrace(OTB)风格.例如:
Add(x,y){
returnx+y
}
使用#Include在多个脚本间共享函数
可以使用#Include指令(即使在脚本的顶部)从外部文件中加载函数.
说明:当脚本的执行流遇到函数定义时,它会跳过函数(使用一种瞬时的方法)并在函数闭括号后恢复执行.因此,开始执行时执行流不会陷入函数中,也不会因为在脚本的最顶部存在一个或多个函数而影响到自动执行段.
函数库:标准库和用户库[v1.0.47+]
脚本中可以不需要通过#Include而调用外部文件中的函数.要达到此目的,必须在下面某个库文件夹中存在与函数同名的文件:
%A_ScriptDir%\Lib\;本地库,脚本所在目录中的Lib文件夹-需要[AHK_L42+].
%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_开始的相关函数.
[AHK_L42+]:支持本地库且本地库优先于用户库和标准库.
只有如MyFunc()这样直接的函数调用才能自动导入库.如果仅动态或间接调用该函数,就像计时器或GUI事件,那么库必须在脚本中明确的(显式)导入,例如:#Include
延伸文章資訊
- 1函數- AutoHotKey.tw - Google Sites
同樣地, 使用ByRef 送回長字串給呼叫者通常比類似 Return HugeString 的方式執行的更好. [AHK_L 60+]: 如果傳遞給ByRef 參數的不是可修改的變數, 那麼函數...
- 2returning values from an AutoHotkey function with byref
- 3Return multiple values from AutoHotkey functions with byRef
In my initial video I talked through how & why to use AutoHotkey functions as well as how to retu...
- 4Function design: byref or global? - Ask for Help - AutoHotkey
Function design: byref or global? - posted in Ask for Help: I am designing a function which chang...
- 5Functions - Definition & Usage - AutoHotkey
[AHK_L 60+]: If something other than a modifiable variable is passed to a ByRef parameter, the fu...