関数

目次

導入と簡単な例

関数は、呼び出すことで実行可能な再利用可能なコードブロックです。関数は、オプションでパラメータ(入力)を受け取り、値(出力)を返すことができます。2つの数値を受け取り、その和を返す次のような単純な関数を考えてみましょう:

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

上記は、"Add"という名前の関数(大文字小文字を区別しない)を作成し、それを呼び出す人は正確に2つのパラメータ(xとy)を提供しなければならないと定めていることから、関数定義として知られています。関数を呼び出すには、その結果を:=演算子で変数に代入します。事例:

Var := Add(2, 3)  ; Varに数字5が格納されます。

また、関数は戻り値を保存せずに呼び出すことができます:

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関数は、の内容をに移動したり、逆に右の内容を右に移動したりして、呼び出し側の変数を変更することができます。

一方、上記の例でByRefを使用しなかった場合、LeftRightは呼び出し元の変数のコピーとなるため、Swap関数は外部に影響を与えることはありません。しかし、その代わりに、各 VarRef を明示的に非参照にするように関数を変更することができます。事例:

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

returnは関数の呼び出し側に1つの値しか返せないので、VarRefは余分な結果を返すために使用することができます。これは、呼び出し元が、関数が値を格納する変数(通常は空)への参照を渡すことで実現されます。

大きな文字列を関数に渡す場合、ByRefを使用すると、文字列のコピーを作成する必要がないため、パフォーマンスが向上し、メモリを節約することができます。同様に、ByRefを使用して長い文字列を呼び出し元に送り返すと、通常、Return HugeStringのようなものよりも性能が良くなります。しかし、関数が受け取るのは、文字列への参照ではなく、変数への参照です。将来の改良により、これらの目的でのByRefの使用に取って代わる可能性があります。

既知の制限事項:

オプションのパラメータ

関数を定義する際、1つまたは複数のパラメータをオプションとしてマークすることができます。

:=の後に、リテラル数値、"fox"や""などの引用/リテラル文字列、またはパラメータをデフォルト値で初期化する必要がある場合に毎回評価されるべき式を追加します。例えば、X:=[]とすると、毎回新しいArrayが作成されます。

デフォルトで設定されていないパラメータを定義する場合は、?または:= unsetを付加します。

次の関数は、そのZパラメータがオプションと表示されています:

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

呼び出し元が上記の関数に3つのパラメータを渡すと、Zのデフォルト値は無視されます。しかし、呼び出し側が2つのパラメータだけを渡すと、Zは自動的に値0を受け取ります。

オプションのパラメータをパラメータリストの途中で孤立させることはできません。つまり、最初のオプショナルパラメータの右側にあるすべてのパラメータもオプショナルと表示されなければならない。ただし、オプションのパラメータは、以下のように、関数を呼び出す際に、パラメータリストの途中から省略することができます:

MyFunc(1,, 3)
MyFunc(X, Y:=2, Z:=0) {  ; この場合でも、Zはオプションでなければならないことに注意してください。
    MsgBox X ", " Y ", " Z
}

ByRefパラメータは、デフォルト値にも対応しています(例:MyFunc(&p1 := ""))。呼び出し側がこのようなパラメータを省略した場合、関数はローカル変数を作成し、デフォルト値を格納します。

アンセットパラメータ

デフォルト値を与えずにパラメータをオプションとしてマークするには、キーワード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]に、2番目のパラメータはparams[2]に、といった具合に。配列なので、params.Lengthでパラメータの数を決定することができます。

非可変型関数を、その関数が受け入れる以上のパラメータで呼び出そうとすると、エラーとみなされます。余ったパラメータを格納する配列を作成することなく、関数が任意の数のパラメータを受け取ることを許可するには、最後のパラメータとして*を記述します(パラメータ名を指定しない)。

注:"可変型"パラメータは、フォーマルパラメータリストの最後にのみ出現することができます。

可変型関数の呼び出し

可変型関数は可変数のパラメータを受け取ることができますが、関数呼び出しに同じ構文を適用することで、パラメータの配列を任意の関数に渡すことができます:

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

注:

既知の制限事項:

ローカル変数とグローバル変数

ローカル変数

ローカル変数は、1つの関数に固有のもので、その関数の内部でのみ見ることができます。その結果、ローカル変数はグローバル変数と同じ名前を持ちながら、別々の内容を持つことがあります。また、別々の関数が同じ変数名を安全に使用することができます。

All local variables which are not static are automatically freed (made empty) when the function returns, with the exception of variables which are bound to a closure or VarRef (such variables are freed at the same time as the closure or VarRef).

A_ClipboardA_TimeIdleなどの組み込み変数は、決してローカルではなく(どこからでもアクセス可能)、再宣言することはできません。(Objectなどの組み込みクラスはグローバル変数としてあらかじめ定義されているので、この限りではありません。)

ファンクションはデフォルトでassume-localです。assume-local関数の内部でアクセスまたは作成された変数は、以下の例外を除き、デフォルトでローカルとなります:

また、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"。Static declarations are evaluated the same as local declarations, except that after a static initializer (or group of combined initializers) is successfully evaluated, it is effectively removed from the flow of control and will not execute a second time.

ネストした関数は、外側の関数の非静的なローカル変数を取り込まないように、静的に宣言することができます。

アシュームスタティックモード:関数の最初の行を "static"とすることで、宣言されていないローカル変数はすべて(パラメータを除いて)静的であると仮定するように定義することができる。事例:

GetFromStaticArray(WhichItemNumber)
{
    static
    static FirstCallToUs := true  ; 静的宣言のイニシャライザは一度だけ実行されます。
    if FirstCallToUs  ; 最初の呼び出し時には静的配列を作成し、それ以降の呼び出し時には作成しない。
    {
        FirstCallToUs := false
        StaticArray := []
        Loop 10
            StaticArray.Push("Value #" . A_Index)
    }
    return StaticArray[WhichItemNumber]
}

assume-staticモードでは、staticであってはならない変数は、ローカルまたはグローバルとして宣言しなければならない(assume-localモードと同じ例外あり)。

ローカルとグローバルの詳細

以下の例のように、カンマで区切ることで複数の変数を同じ行で宣言することができます:

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

変数は、宣言と同じ行で、代入を続けて初期化することができる。静的イニシャライザとは異なり、ローカルとグローバルのイニシャライザは、関数が呼ばれるたびに実行されます。つまり、local x := 0のような行は、2つの行を別々に書いたのと同じ効果がある:local xに続いてx := 0。どのような代入演算子でも使用できますが、global HitCount += 1のような複合代入では、変数にあらかじめ値が代入されていることが必要です。

スクリプトの起動時にlocalglobalstaticという言葉が即座に処理されるため、IF文によって変数を条件付きで宣言することができない。つまり、IFのブロックやELSEのブロック内の宣言は、関数全体に対して無条件に効力を発揮します(ただし、宣言に含まれる初期化子は条件付きで残っています)。Array1Array99などの変数への非動的な参照は、すでにアドレスに解決されているため、global Array%i% のような動的な宣言は不可能です。

関数を動的に呼び出す

関数呼び出し式は通常、リテラル関数名で始まりますが、呼び出しの対象は、関数オブジェクトを生成する任意の式にすることができます。GetKeyState("Shift")という式では、GetKeyStateは実際には変数参照ですが、通常は組み込み関数を含む読み取り専用変数を指します。

関数呼び出しは、スクリプトの開始前ではなく、スクリプトの実行中に呼び出しのターゲットが決定される場合、動的であると言われます。通常の関数呼び出しと同じ構文が使用されます。唯一の明らかな違いは、非動的呼び出しではロード時に特定のエラーチェックが実行されますが、動的呼び出しでは実行時にのみ実行されることです。

例えば、MyFunc()は、MyFuncに含まれる関数オブジェクトを呼び出しますが、これは実際の関数名であったり、関数が割り当てられた変数であったりすることがあります。

関数呼び出しのターゲットとして、ダブルデフを含む他の式を使用することができます。例えば、MyArray[1]()はMyArrayの最初の要素に含まれる関数を呼び出し、%MyVar%()はMyVarに含まれる名前変数に含まれる関数を呼び出します。つまり、パラメータリストの前にある式をまず評価して関数オブジェクトを取得し、そのオブジェクトを呼び出すのです。

以下の理由により、目標値を呼び出すことができない場合は、Errorが発生します:

関数の呼び出し側は、一般に、関数を呼び出す前に各パラメータの意味と個数を知っておく必要があります。しかし、動的呼び出しの場合、関数呼び出しに合わせて関数を書くことが多く、その場合、パラメータ値の誤りではなく、関数定義の誤りが原因で失敗することがあります。

簡略化されたブーリアン評価

AND、OR三項演算子中で使用すると、(関数呼び出しの有無にかかわらず)性能を向上させるために短絡させることができます。ショートサーキットは、式の最終結果に影響を与える可能性のない部分の評価を拒否することで動作します。この概念を説明するために、この例を考えてみましょう:

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   ; 検索するものがない、または一致するものが見つかった。

上記の例で示したように、一般的に高価な(時間のかかる)関数は、パフォーマンスを高めるためにANDまたはORの右辺で呼び出す必要があります。また、関数のパラメータに空文字列などの不適切な値が渡された場合に、その関数が呼び出されないようにすることも可能です。

三項条件演算子(?:)も、ロス分岐を評価しないことで短絡させる。

ネストされた関数

ネストされた関数は、他の関数の中で定義されたものです。事例:

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

ネストされた関数は、それを直ちに囲む関数の外では名前によってアクセスできませんが、その関数の内部では、他のネストされた関数の内部も含めて、どこでもアクセスできます(例外あり)。

デフォルトでは、ネストされた関数は、それを囲む関数の静的変数に、動的にもアクセスすることができます。しかし、入れ子になった関数内の非動的代入は、外側の関数にその変数に対する宣言も非動的代入もない場合、通常ローカル変数に解決されます。

デフォルトでは、以下の条件を満たす場合、入れ子関数は外側の関数の非静的ローカル変数を自動的に「キャプチャ」します:

  1. 外側の関数は、次のうち少なくとも1つの方法で変数を参照する必要があります:
    1. localで宣言したり、パラメータやネストされた関数として宣言することで、その効果を発揮します。
    2. 代入や参照演算子(&)の非動的な対象として。
  2. 内部関数(またはその内部にネストされた関数)は、変数を非動的に参照する必要があります。

変数を捕捉した入れ子の関数はクロージャと呼ばれます。

外側の関数の非静的なローカル変数は、キャプチャしない限り動的にアクセスすることはできません。

明示的な宣言は、それを包含する関数のローカル変数よりも常に優先されます。例えば、local xは、外側の関数のxとは無関係に、現在の関数にローカルな変数を宣言します。外側関数のグローバル宣言は、明示的な宣言で上書きされる場合を除き、ネストした関数にも影響します。

関数がassume-globalと宣言された場合、その関数ので作られたローカル変数や静的変数は、関数自身やそのネストした関数から直接アクセスできません。一方、静的と仮定された入れ子の関数は、その関数自体が静的と宣言されていない限り、外側の関数の変数を参照することができます。

関数はデフォルトでassume-localであり、これはネストされた関数、assume-static関数の内部であっても同様です。ただし、外側の関数がassume-globalの場合、入れ子になった関数は、外側の関数のローカル変数やスタティック変数を参照できる以外は、デフォルトでassume-globalと同じように動作します。

各関数定義は、関数そのものを含む読み取り専用の変数、つまりFuncまたはClosureオブジェクトを作成します。使用例については、以下をご覧ください。

スタティック関数

つまり、外側の関数を呼び出すたびに、同じFuncを参照することになります。キーワードstaticは、入れ子になった関数を明示的にstaticと宣言するために使用します。この場合、外側の関数のstaticでないローカル変数は無視されます。事例:

outer() {
    x := "outer value"
    static inner() {
        x := "inner value"  ; innerのローカルに変数を作成します。
        MsgBox type(inner)  ; "Func"と表示されます。
    }
    inner()
    MsgBox x  ; "outer value"を表示する
}
outer()

静的関数は、明示的に静的と宣言されていない限り、自身のボディの外にある他のネストした関数を参照することはできません。なお、関数がassume-staticであっても、関数パラメータを参照すれば、non-staticの入れ子関数はクロージャになる可能性があります。

クロージャ

クロージャとは、自由変数の集合に束縛されたネストされた関数のことです。自由変数は、外側の関数のローカル変数で、ネストした関数でも使用されます。クロージャは、1つまたは複数のネストした関数が、外側の関数が戻った後でも、外側の関数と変数を共有することを可能にします。

クロージャを作るには、外側の関数の変数を参照する入れ子関数を定義するだけです。事例:

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でメモ帳を起動、または起動します。
app_hotkey "#n", "ahk_class Notepad", "notepad.exe"
; Win+WでWordPadを起動、または起動します。
app_hotkey "#w", "ahk_class WordPadClass", "wordpad.exe"

入れ子になった関数は、外側の関数の非静的なローカル変数をキャプチャすれば、自動的にクロージャとなります。クロージャそのものに対応する変数(activateなど)も非静的なローカル変数なので、クロージャを参照するネストした関数は自動的にクロージャになります。

外側の関数を呼び出すたびに、以前の呼び出しとは異なる新しいクロージャが作成されます。

It is best not to store a reference to a closure in any of the outer function's free variables, since that creates a reference cycle which must be broken (such as by clearing the variable) before the closure can be freed. However, a closure may safely refer to itself and other closures by their original variables without creating a problematic reference cycle. 事例:

timertest() {
    x := "tock!"
    tick() {
        MsgBox x           ; x はクロージャになる。
        SetTimer tick, 0   ; クロージャーのオリジナル変数を使うのが無難です。
        ; SetTimer t, 0    ; Capturing t would create a reference cycle.
    }
    t := tick              ; これは、tが上で捕捉されていないので、大丈夫です。
    SetTimer t, 1000
}
timertest()

Each time the outer function is called, all of its free variables are allocated as a set. This one set of free variables is linked to all of the function's closures. If the closure's original variable (tick in the example above) is captured by another closure within the same function, its lifetime is bound to the set. The set is deleted only when no references exist to any of its closures, except those in the original variables. This allows closures to refer to each other without causing a problematic reference cycle.

Closures which are not captured by other closures can be deleted before the set. All of the free variables within the set, including captured closures, cannot be deleted while such a closure exists.

復帰・退出・総評

関数内の実行の流れがReturnに出会う前に関数の閉じブレースに到達した場合、関数は終了し、呼び出し元に空白の値(空文字列)を返します。また、関数が明示的にReturnのパラメータを省略した場合は、空白の値が返されます。

関数がExitを使用して現在のスレッドを終了させる場合、その呼び出し元は戻り値をまったく受け取りません。例えば、Var := Add(2, 3)という文は、Add()が終了してもVarは変更されないままです。Throwやランタイムエラー(存在しないファイルを実行したなど)により、関数が終了した場合も同様です。

1つ以上の空白の値(空文字列)を持つ関数を呼び出すには、この例のように空の引用符の組を使用します:FindColor(ColorName, "")

関数を呼び出しても新しいスレッドは始まらないので、関数でSendModeSetTitleMatchModeなどの設定を変更すると、その呼び出し元にも反映されます。

関数内で使用すると、ListVarsは関数のローカル変数をその内容とともに表示します。This can help debug a script.

スタイルと命名規則

複雑な関数は、特殊な変数に明確な接頭辞をつけると読みやすく、保守しやすくなります。例えば、関数のパラメータリストの各パラメータに "p"や "p_"という先頭の名前を付けると、特に数十個のローカル変数が競合している関数では、その特殊性が一目瞭然になる。同様に、ByRefパラメータには接頭辞 "r"または "r_"を、静的変数には"s"または "s_"を使用することができる。

機能定義にOne True Brace(OTB)スタイルをオプションで使用することができる。事例:

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

複数のスクリプト間で関数を共有するための#Includeの使い方

#Include指令は、外部ファイルから関数をロードするために使用することができます。

組み込み関数でも

スクリプトが同名の関数を定義している場合、組み込み関数はオーバーライドされます。例えば、スクリプトに独自のカスタムWinExist関数を持たせて、標準の関数の代わりに呼び出すことができます。しかし、その場合、スクリプトは元の関数を呼び出す方法がなくなります。

DLLファイルに存在する外部関数は、DllCallで呼び出すことができる。

すべての内蔵機能の一覧を得るには、「アルファベット順の機能インデックス」を参照してください。