オブジェクト

提供: AutoHotkey Wiki
移動: 案内検索

概要[編集]

文字列型と数値型に加えて、AutoHotkey_Lでは オブジェクト型 をサポートするようにした。
他の型の値と同様に、オブジェクトは変数に格納したり、関数との値の授受を行ったり、他のオブジェクトの中に格納したりすることできる。

オブジェクトは「参照型」であることを銘記しておくこと。 food := pizza のように代入した場合、food は元の pizza オブジェクトを参照する。 同様に、 Eat(pizza) というように関数の引数に渡した場合でも、元の pizza オブジェクトが Eat() 関数に渡される。 したがって、関数内では元のオブジェクトを変更することが出来る。もし関数の引数が ByRef を用いている場合は、 関数は pizza の参照先のポインタを変更して他のオブジェクトに付け替えることになる。

IsObject() を利用することで、変数内容がオブジェクトであるかを調べることが出来る:

Result := IsObject(expression)

現時点で、オブジェクトには以下の型がある。

  • Object - スクリプトによって拡張可能な連想配列
  • File - ファイル入出力のインターフェイスとなる
  • Func - 関数
  • Enumerator - 列挙型。オブジェクト内のフィールドを参照するときに利用する
  • ComObject - IDispatch インターフェイスをラッピングしたもの(COMまたは*オートメーション*オブジェクト)

文法[編集]

利用可能な演算子[編集]

オブジェクトに対して利用可能な演算子は以下の通り。

  • 直接比較(=, ==, !=, <>) はオブジェクトの参照で比較を行う。オブジェクトと非オブジェクトは等価になることはない。
  • アドレス参照(&Object)は、ネイティブインターフェイスにおけるオブジェクトのポインタを返す。これは各オブジェクトをユニークに識別することが出来る。参照回数計測の項も参照のこと。
  • 三項演算子(a ? b : c)はどのオペランドでもオブジェクトが利用可能。
  • 式の中で真偽値を必要としている場合、オブジェクト単体は真となる。
  • ドット(.)と各括弧([~])は次節以降で説明する。
  • フィールドへの代入演算子は、通常代入(:=)の他に復号代入(+= .= 等)も利用できる。
  • 可変長引数 Func(Params*) では Params はオブジェクトとなる。

その他の演算子ではオブジェクトは空文字列として扱われる。

オブジェクトを呼び出す[編集]

オブジェクトの機能を呼び出すには、文法的に二つの形式が利用できる。Object[params] 形式と Object.param 形式。
どちらの場合でも、オブジェクトは、単なる変数であったり、より複雑なオブジェクトの評価を行う式であったりする。
操作の結果としての値は、オブジェクト毎に定義されたものとなりる。 例えば、SET操作は、それが正常終了した場合は、格納された値を返す。

Object[Params][編集]

角括弧はオブジェクト操作であることを意味し、パラメタリストを区切るのに用る。

Value  := Object[ Params ]             ; GET
Result := Object[ Params ] := Value    ; SET
Result := Object[ Param  ]( Params )   ; CALL

CALLでは角括弧内のParam は一つで無ければならない。
呼び出したいメソッド名や、対象オブジェクト内にある何らかの関数に関連付ける"キー"を指定する。

Object.Param[編集]

最初のパラメタが英数字及びアンダースコアで構成されているリテラル値であるときに、こちらの代替構文も利用可能:

Value  := Object.Param
Result := Object.Param := Value
Result := Object.Param( Params )

以下のやりかたはパラメタ1の直書きを必要するが、いろいろな追加パラメタを指定できる:

Value  := Object.Param1[ Param2, Param3, ... ]
Result := Object.Param1[ Param2, Param3, ... ] := Value
Result := Object.Param1( Param2, Param3, ... )

通常は丸括弧(())は角括弧([])とは異なるが、代入演算子(:=)が直後に続く場合は同等と見なされる。

Result := Object.Param1( Param2, Param3, ... ) := Value

ドット(.)が式の先頭、もしくは+*の様な演算子に続いてある場合、浮動小数点数の一部と見なす。
ドットの次にスペース続いた場合、文字列結合演算子として扱う。 このためスペースを置かない場合はエラーとなる。
その他の引用符外にあるドットは(.=を除いて)オブジェクトアクセスの演算子として扱う。

参照回数計測[編集]

基本的な参照回数計測の機構を用いて、オブジェクトはスクリプトのどこからも参照されなくなった時点でメモリから解放される。
通常、スクリプトからはこの機構を明示的に呼び出す必要はなく、スクリプトがオブジェクトを扱うことによって自動的に呼び出される。

しかしながら、adress := &Objectのように、オブジェクトのアドレスを得た場合は参照回数は増加しない。
オブジェクトに対する参照が全て無くなったときに、アドレスが示すオブジェクトが有効であるようにするには以下のようにする:

; 「生存中」であることを示すために、オブジェクトの参照回数を増やす
ObjAddRef(address)
...
; オブジェクトの参照回数を減少させて、解放対象にする。
ObjRelease(address)

オブジェクトの最後の参照が解放されるときに、何らかのコードを実行したい場合は __Delete() メタ関数を実装すること。

既知の制限事項
  • 循環参照に対しては例外も考慮もしない。 スクリプトからのオブジェクトにアクセスする方法が全くなくなっていても、何らかの参照が存在する限りオブジェクトは解放されない。
  • プログラム終了時にはグローバル変数や(関数内の)スタティック変数は自動的に解放されるが、 スタティックでないローカル変数や、式の中に出現するような一時記憶領域にある参照は、自動的には解放されない。

プロセスの終了時にオブジェクトが占拠していたメモリはOSが回収する。 しかし、オブジェクトに何らかの自動クリーンアップルーチンを仕込んでおいたとしても、オブジェクトが解放される時点ではそれは実行されない。 オブジェクトがOSの回収対象とならない何らかのリソースを解放する事をしようとしている場合には注意が必要となる。

Object()[編集]

拡張可能な汎用オブジェクトは以下のようにして作成する。

Object := Object()
Object := Object(Key1, Value1 [, Key2, Value2 ... ])

パラメタは無しまたは、キーと値のペアで指定する。従ってパラメタは偶数個または0個でなければならない。 ただし単一パラメタモードの場合はこの限りではない。

; 明示的な初期化:
obj := Object()
obj[a] := b
obj[c] := d

; インライン初期化:
obj := Object(a, b, c, d)

連想配列[編集]

どのオブジェクトもキーと値の組をその内部に格納できる。
キーと値の双方に数値・文字列・オブジェクトが利用できる。
値はオブジェクトにキーに紐付けて格納し、キーを指定することで取り出すことができる。

array := Object()
array["a"] := "alpha"
array["b"] := "bravo"
MsgBox % array["a"] . " " . array["b"]

当然のことながらドット文法も利用可能で、上記のサンプルに追加すると以下のようになる:

array.a := "apple"
MsgBox % array.a . " " . array.b

【訳注】 キーには日本語含めあらゆる文字が利用可能(実際はオブジェクトを利用することも可能)。
ただし、ドット文法で参照する場合には利用できないので、

obj["挨拶"] := "こんにちは!"
MsgBox, % obj["挨拶"]

のように常に角括弧文法でアクセスする必要がある。

キーと値の組を削除するには _Remove() を利用する。

整数のキーはネイティブな32ビット符号付き整数として扱い、その範囲は-2147483648~2147483647である。
この範囲外の整数値は丸められる(key << 32 >> 32 と同様な処理)。表現形式は問われないので、[0x10]と[16]と[00016]は等価となる。
キーは連続している必要はないので、[1]と[1000]のみが値を持っている場合、オブジェクトも実際に2つのキーと値の組のみしか持っていないことになる。

数値としての浮動小数点数はキーとして有効ではない。 キーに浮動小数点数を指定した場合は文字列に変換される。
リテラルで指定した小数はそのままの文字列がキーとなるが、式の結果としての純粋な浮動小数点値(0+0.1 や Sqrt(y) など)は小数フォーマットに準じて文字列に変換されたものがキーとなる。

注意: キー名の"base"は、_Insert()で用いられる場合を除き、特殊な意味を持つ。

組み込みメソッド[編集]

オブジェクトのメソッドを参照のこと。

配列の配列[編集]

オブジェクトは他のオブジェクトを格納することが出来るので、配列の配列もまた実現可能。
例を挙げると、 table が行を格納した配列オブジェクトでさらに各行は列を格納した配列オブジェクトとした場合の、行xyの内容は以下の二つの方法でセットすることができる:

table[x][y] := content  ; A
table[x, y] := content  ; B

入れ子オブジェクトの table[x] が存在しない場合には、以下に示すようにAとBの挙動は異なるので注意が必要となる:

  • Aは失敗するが、Bでは自動的にオブジェクトを作成してtable[x]に格納する。
  • table の base が メタ関数 定義している場合は以下の通り呼び出される:
table.base.__Get(table, x)[y] := content   ; A
table.base.__Set(table, x, y, content)     ; B

結果としてBは総合的な定義としての独自挙動を定義することを可能とする。

関数の配列[編集]

きちんとした関数参照はまだサポートされていないが、関数の配列はオブジェクトに関数名を格納することで擬似的に実現できる。
array[index]」が関数の名称を格納している場合、以下の二例は等価となる。

array[index](param)
n := array[index]
%n%(param)

※ 変数index組み込みメソッド名や、 ベースオブジェクトで定義しているメソッド名、 または関数を表すオブジェクト、 を格納している場合は二例のうち上のものしか動作しない。

既知の制限事項:
  • 実行中の関数やサブルーチンでオブジェクト呼び出しがあるとき、Exitコマンドを実行すると即座に呼び出し元に復帰するため、オブジェクト呼び出しが新規スレッドを作るような振る舞いになる。 とは言っても、Exitコマンドは適切な時期をみてスクリプト実行を停止させてる。
  • 現時点では、「x.y[z]()」は「x["y",z]()」と同じ扱いとなり、サポートされない記述法となっている。。回避策として、「`(x.y)[z]()」とすることで、x.y を強制的に最初に評価するようしてから、対象となるオブジェクトがコールされるようにするようにすればよい。括弧のエスケープ(`()は、行頭で利用されるときは式が最初に評価されるようになり、それ以外の括弧はヒアドキュメントの開始とみなされる。

Object(address | object)[編集]

オブジェクト参照からインターフェイスのポインタを取得したり、その逆を行う時に利用する。これは高度な機能なので慎重に利用しすること(詳細は参照回数計測の項を参考)。 この機能は RegisterCallback() を利用してコールバック関数を作成し A_EventInfo からオブジェクトを取り出すなどの場合に有用となりうる。

address := Object(object)
object := Object(address)

どちらの場合でも、オブジェクトの参照回数は自動的に加算され自動的に解放されることがなくなる。
一般に、ObjAddRefやObjReleaseを適切に適宜呼び出すような場合を除き、アドレスのコピーはどちらもオブジェクト参照として扱わる。
例えば、「x := address」となるスクリプトで、address が通常の生存期間で解放されるようならば、x の生存期間が終了する時点で「ObjAddRef(x := address)」および「ObjRelease(x)」を利用するのを考慮すべきである。
これにより、最後の参照が失われたときにきちんとオブジェクトが解放され、参照がある間は解放されないようにすることができる。

この機能は Object() で作成された一般的なオブジェクトだけでなく、COMオブジェクトやファイルオブジェクトにも適応可能である。

拡張性[編集]

オブジェクトの振る舞いは、特殊なbaseメカニズムを介して変更したり拡張したりすることが出来る。オブジェクトがキー(最初のもしくは単一のパラメタのこと)を指定して呼び出されたときで、そのキーに何も結びつく内容を持たない場合に、baseオブジェクトが呼び出される。標準のbaseオブジェクトを呼だされるとき、以下のようなことが行われる:

  • オブジェクト自身が __Get, __Set, __Call メタ関数を持っている時は、それを呼び出す。Return コマンドを使った場合、その後の処理は一切行われない。どの操作でも Return の戻り値が式の値として用いられる。
Set操作: 操作の成功時に、__Set は該当フィールドに割り当てる新規の値(恐らく元々割り当てしようとしていた値とは異なる)を戻さなければならない。 こうすることで「a.x := b.y := z」のような連続した代入を許可することになる。 割り当て操作を失敗させたい場合は空文字列を返す必要がある。
  • GetまたはCall操作の時、自分自身のフィールドで該当するものを探す。
Get操作: 見つかった場合、該当フィールドの値を返す
Call操作: 対象オブジェクトを第1引数にして該当フィールド(に格納された文字列の示す関数)を呼び出す
  • 再帰的にbaseオブジェクトを呼び出していく。これによりオブジェクトはbaseオブジェクトの、さらにbaseのbaseのオブジェクト…(以下略)、から属性を「継承」することになる。

どのbaseも操作をしない場合に限り、処理は通常通りに行われる:

  • Get操作: キーが "base" の場合、ベースオブジェクト自身が取得される。
  • Set操作: キーが "base" の場合、ベースオブジェクトがセットされる、キー"base"に格納されていた既存の非オブジェクト値は削除される。
それ以外の場合は新規にキーと値の組が作成されオブジェクト内に格納される。
  • Call操作: 組み込みメソッドが使われる

例: 値のみを継承

Color := Object("R", 0, "G", 0, "B", 0)
blue := Object("B", 255, "base", Color)
cyan := Object("G", 255, "base", blue)
MsgBox % "blue: " blue.R "," blue.G "," blue.B
MsgBox % "cyan: " cyan.R "," cyan.G "," cyan.B

例: 前の例に加え、RGB値を取得するメソッドを定義。

Color.GetRGB := "Color_GetRGB"
MsgBox % cyan.GetRGB()  ; 65535と表示.
Color_GetRGB(clr) {
    Return clr.R << 16 | clr.G << 8 | clr.B
}

メタ関数[編集]

メタ関数を使うとオブジェクトがどう振る舞うべきかをきちんと定義することができる。呼び出しの時点で、Get,Set,Callの各操作の内容に従い、パラメタリストは元のオブジェクト自身を含んだものが渡される。もし何らかの戻り値を返した場合、該当の操作の戻り値とみなして操作は終了し、後続の処理は一切行われない。Returnコマンドに遭遇せずに関数が終了した場合は、前項で述べたような仕組みで処理は続行する。

例: 以下は、上の例を改変して、色オブジェクトにRGB値単体を格納できるようにしたもの

Color := Object("RGB", 0
                , "__Set", "Color_Set"
                , "__Get", "Color_Get")
blue := Object("base", Color, "B", 255)
cyan := Object("base", Color, "RGB", 0x00ffff)
MsgBox % "blue: " blue.R "," blue.G "," blue.B " = " blue.RGB
MsgBox % "cyan: " cyan.R "," cyan.G "," cyan.B " = " cyan.RGB
Color_Get(clr, name)
{
    If name = R
        Return (clr.RGB >> 16) & 255
    If name = G
        Return (clr.RGB >> 8) & 255
    If name = B
        Return clr.RGB & 255
}
Color_Set(clr, name, val)
{
    If name in R,G,B
    {
        val &= 255
        If      name = R
            clr.rgb := (val << 16) | (clr.rgb & ~0xff0000)
        Else If name = G
            clr.rgb := (val << 8)  | (clr.rgb & ~0x00ff00)
        Else  ; name = B
            clr.rgb :=  val        | (clr.rgb & ~0x0000ff)
        ; 'Return' は新たなキーと値のペアを格納したくないときに使う
        ; また、x := clr[name] := val の中で 'x' にどんな値が格納されたかを示す。
        Return val
    }
}

重要: Object()関数に複数パラメタを与えてオブジェクトを生成するとき、各パラメタは左から順に評価されてセットされていく。 このため、以下の例ようにキー"B" が キー"base" よりも先行するときには、キー"B"を処理する段階では単にキーと値が格納されるだけでRGB値は更新されない。

c := Object("B", 255, "base", Color)

この振る舞いを利用して、メタ関数のバイパスを行ったり、パフォーマンスを上げるために特定のキーにbaseを適応させないといったことができる。

既知の制限事項:

  • どのメタ関数もパラメタとして受けとることができるのは定義されたものだけで、余分なパラメタは捨てられる。 セット操作におけるR値は最終パラメタとして扱われるので、多くのパラメタが渡さるような場合は最初に破棄するようにすべきである。
関数の定義としていくらか多めパラメタを定義しておけば、パラメタの数が異なっていても対応することが可能。 実際に引数として渡されたかとうかを判断するには、予め引数のデフォルト値から変更されたかどうかで判断する。 その場合は何らかの値をこの目的のために予約しておかなければならない。
  • Return がパラメタなしで利用されたときは Return "" と同等に扱われるので、 メタ関数の適応を「避ける」目的で使うことはできない。以下に例を挙げる:
; 正しくない方法:
x_Set(x, name, value)
{
    If name != foo
        Return  ; これによりキーと値の組は生成されない(キー"foo"である場合を含む)
    ;...
    Return x.real_foo := modified_value
}
; 正しい方法:
x_Set(x, name, value)
{
    If name = foo
    {
        ;...
        Return x.real_foo := modified_value
    }
}
  • See also Exit limitation.

クリーンアップ[編集]

ファイルやネットワークのハンドル、DllCallで確保したメモリのポインタ等のオブジェクトに関連付けられているリソースは、 __Delete() メタ関数を実装することによって自動的に管理することが出来るようになる。 以下に例を挙げる:

; __Deleteメタ関数を付与してオブジェクトを生成しする:
obj := Object("base", Object("__Delete", "MemoryObject_Delete"))
; リソースを確保してオブジェクトに関連付ける:
obj.ptr := DllCall("GlobalAlloc", "uint", 0, "uint", 1024)
MsgBox % "Memory allocated: " obj.ptr
MemoryObject_Delete(obj)
{   ; Retrieve the resource.
    If p := obj.ptr
    {   ; 自動的に解放されないので、手動で開放する
        DllCall("GlobalFree", "uint", p)
        ; 解放されたことを示すためにポインタを0にする
        obj.ptr := 0
    }
    MsgBox % "Memory freed: " p
}

objに対する参照が全て無くなるときに、base.__Delete が呼び出され、第一引数に obj が渡される。
__Deleteが呼び出された時点でそのオブジェクトに対する参照は全て無くなっているので注意が必要。
関数の復帰時に何らかの参照があるような場合(関数内でstaticやglobalな変数に参照をコピーするなど)には、そのオブジェクトは削除されずにいて、その後の__Deleteの対象となる。

配列の配列[編集]

table[x, y] := content のように複数パラメタを渡すことで入れ子オブジェクトが自動生成されるが、通常は生成されるオブジェクトはどの base も持たず、また独自のメソッドや振る舞いを持たない。
以下のように __Set を利用することで初期化処理を行うことができる:

x := Object("base", Object("addr", "x_Addr", "__Set", "x_Setter"))
; 値の代入により、暗黙的に x_Setter が呼び出され入れ子オブジェクトが生成される
x[1,2,3] := "..."
; 値の取得とサンプルメソッドの呼び出しをする
MsgBox % x[1,2,3] "`n" x.addr() "`n" x[1].addr() "`n" x[1,2].addr()
x_Setter(x, p1, p2, p3) {
    x[p1] := Object("base", x.base)
}
x_Addr(x) {
    Return &x
}

x_Setterが4つの必須パラメタを持っているので、二つ以上のキーパラメタがある場合にのみ呼び出される。 上記のような割り当てを行う場合、以下のような挙動となる:

  • x[1]が存在しないので、x_Setter(x,1, 2,3)が呼ばれる。("..."は引数の数が足りないので受け渡しされない。)
    • x[1]には、xと同じbaseを持つ新規オブジェクト生成され、割り当てられる。
    • 戻り値を何も返さないので値の割り当ては続行する。
  • x[1][2]は存在しないので、x_Setter(x[1],2,3,"...")が呼び出される。
    • x[1][2]には、x[1]と同じbaseを持つ新規オブジェクトが生成され割り当てられる。
    • 戻り値を何も返さないので値の割り当ては続行する。
  • x[1][2][3]は存在しないが、x_Setterは少なくとも4つのパラメタを必要とするのに対し3つの(x[1][2], 3, "...")パラメタしか無いので、この関数の呼び出しはなされず通常の値の割り当てが行われる。

オブジェクトを関数として扱う[編集]

obj.func(param)のような呼び出しがなされた時、obj.funcは関数名かオブジェクトなはず。
obj.funcが何らかのオブジェクトを格納している時は、objはキーとして扱われる。例を挙げると、以下と等価となりる

ObjCall(obj.func, obj, param)

多くの場合obj.func[obj]というものは存在しないし、__Callメタ関数が代わりに呼び出される、それには上記と同じようなパラメタ(パラメタ数は増減する)を伴う。 これは関数呼び出しの振る舞いを変更したい場合に利用できる。以下に例を挙げる:

FuncType := Object("__Call", "FuncType_Call")
func := Object("base", FuncType, 1, "One", 2, "Two")
obj := Object("func", func, "name", "foo")
obj.func("bar")  ;  "One foo bar", "Two foo bar"と表示する
FuncType_Call(func, obj, param) {
    Loop % func._MaxIndex()
        ObjCall(func, A_Index, obj, param)
}
One(obj, param) {
    MsgBox % A_ThisFunc " " obj.name " " param
}
Two(obj, param) {
    MsgBox % A_ThisFunc " " obj.name " " param
}

デフォルトベース[編集]

非オブジェクト値に対してオブジェクトの文法を適応したり、ObjeGet, ObjeSet, ObjCallに引き渡した時は、デフォルトベースオブジェクトが呼び出される。
これはデバッグ目的として、または文字列型や数値型の値にオブジェクトの様な振る舞いを付与したい場合、などに利用できる。
デフォルトベースは非オブジェクト値に対して.baseを利用することでアクセスすることが出来る。 "".baseのようにする。
しかしながら、デフォルトベースに"".base := Object()のような割り当ては出来ない。
というのも、デフォルトベース自身"".base.base := Object()のようなベースを持っているため。

例1: 変数の自動初期化

代入操作の対象として空文字列が利用された場合は、__Setメタ関数に直接引き渡されるので、新規オブジェクトを変数に差し込むことが出来る。
制限事項として、第1パラメタは必ず ByRef でなくてはならないので、このメタ関数は対象が値で無い場合には呼び出されない。この例では複数パラメタをサポートしない。

"".base.__Set := "Default_Set_AutomaticVarInit"
empty_var.foo := "bar"
MsgBox % empty_var.foo
Default_Set_AutomaticVarInit(ByRef var, key, value)
{
    If var =
        var := Object(key, value)
}

例2: 擬似プロパティ

文字列型や数値型にオブジェクトの「糖衣構文」を適応できる。

"".base.__Get := "Default_Get_PseudoProperty"
"".base.is  := "Default_is"
MsgBox % A_AhkPath.length " == " StrLen(A_AhkPath)
MsgBox % A_AhkPath.length.is("integer")
Default_Get_PseudoProperty(nonobj, key)
{
    If key = length
        Return StrLen(nonobj)
}
Default_is(nonobj, type)
{
    If nonobj is %type%
        Return true
    Return false
}

注意: 組み込み関数も利用できるが、この場合は括弧を外すことは出来ない。

"".base.length := "StrLen"
MsgBox % A_AhkPath.length() " == " StrLen(A_AhkPath)

例3: デバッグ

非オブジェクト値をオブジェクトの様に扱うのが望ましくない場合には、以下のようにすると非オブジェクトをオブジェクトのように扱った際に警告を発することが出来る。

"".base.__Get  := "Default__Warn"
"".base.__Set  := "Default__Warn"
"".base.__Call := "Default__Warn"
empty_var.foo := "bar"
x := (1 + 1).is("integer")
Default__Warn(nonobj, p1="", p2="", p3="", p4="")
{
    ListLines
    MsgBox A non-object value was improperly invoked.`n`nSpecifically: %nonobj%
}