Objekte

Ein Objekt ist eine Kombination aus Eigenschaften und Methoden.

Verwandte Themen:

Mit IsObject kann festgestellt werden, ob ein Wert ein Objekt ist:

Ergebnis := IsObject(Ausdruck)

Eine Liste von Standardobjekttypen finden Sie unter Interne Klassen. Es gibt zwei Grundtypen:

Inhaltsverzeichnis

Grundlegende Verwendung

Arrays

Erstellen eines Arrays:

MeinArray := [Element1, Element2, ..., ElementN]
MeinArray := Array(Element1, Element2, ..., ElementN)

Abrufen eines Elements:

Wert := MeinArray[Index]

Ändern eines Elementwertes (Index muss zwischen 1 und Länge liegen oder ein äquivalenter umgekehrter Index sein):

MeinArray[Index] := Wert

Einfügen eines oder mehrerer Elemente ab einem bestimmten Index via InsertAt-Methode:

MeinArray.InsertAt(Index, Wert, Wert2, ...)

Anfügen eines oder mehrerer Elemente via Push-Methode:

MeinArray.Push(Wert, Wert2, ...)

Entfernen eines Elements via RemoveAt-Methode:

EntfernterWert := MeinArray.RemoveAt(Index)

Entfernen eines Elements via Pop-Methode:

EntfernterWert := MeinArray.Pop()

Length gibt die Anzahl der Elemente im Array zurück. Um die Elemente eines Arrays einzeln durchzugehen, verwenden Sie entweder A_Index oder eine For-Schleife. Zum Beispiel:

MeinArray := ["eins", "zwei", "drei"]

; Von 1 bis zum Ende des Arrays iterieren:
Loop MeinArray.Length
    MsgBox MeinArray[A_Index]

; Den Inhalt des Arrays enumerieren:
For Index, Wert in MeinArray
    MsgBox "Element " Index " ist '" Wert "'"
    
; Dasselbe noch einmal:
For Wert in MeinArray
    MsgBox "Element " A_Index " ist '" Wert "'"

Maps (assoziative Arrays)

Ein Map oder assoziatives Array ist ein Objekt, das eine Sammlung von eindeutigen Schlüsseln und eine Sammlung von Werten enthält, wobei jeder Schlüssel mit einem Wert verbunden ist. Schlüssel können Zeichenketten, Integer oder Objekte sein, während Werte von beliebigem Typ sein können. Ein assoziatives Array kann wie folgt erstellt werden:

MeinMap := Map("SchlüsselA", WertA, "SchlüsselB", WertB, ..., "SchlüsselZ", WertZ)

Abrufen eines Elements, wobei Schlüssel eine Variable oder ein Ausdruck ist:

Wert := MeinMap[Schlüssel]

Zuweisen eines Elements:

MeinMap[Schlüssel] := Wert

Entfernen eines Elements via Delete-Methode:

EntfernterWert := MeinMap.Delete(Schlüssel)

Enumerieren von Elementen:

MeinMap := Map("zehn", 10, "zwanzig", 20, "dreißig", 30)
For Schlüssel, Wert in MeinMap
    MsgBox Schlüssel ' = ' Wert

Objekte

Ein Objekt kann Eigenschaften und Elemente (z.B. Array-Elemente) haben. Der Zugriff auf Elemente erfolgt über [], wie in den vorherigen Abschnitten gezeigt. Der Zugriff auf Eigenschaften erfolgt durch Anfügen eines Punktes und einem Identifikator (einfach ein Name). Methoden sind Eigenschaften, die aufgerufen werden können.

Beispiele:

Abrufen oder Setzen einer Eigenschaft mit dem direkt geschriebenen Namen Eigenschaft:

Wert := MeinObjekt.Eigenschaft
MeinObjekt.Eigenschaft := Wert

Abrufen oder Setzen einer Eigenschaft mit einem via Ausdruck oder Variable ermittelten Namen:

Wert := MeinObjekt.%Ausdruck%
MeinObjekt.%Ausdruck% := Wert

Aufrufen einer Eigenschaft/Methode mit dem direkt geschriebenen Namen Methode:

RückgabeWert := MeinObjekt.Methode(Params)

Aufrufen einer Eigenschaft/Methode mit einem via Ausdruck oder Variable ermittelten Namen:

RückgabeWert := MeinObjekt.%Ausdruck%(Params)

Manchmal werden beim Abrufen oder Zuweisen von Eigenschaften Parameter akzeptiert:

Wert := MeinObjekt.Eigenschaft[Params]
MeinObjekt.Eigenschaft[Params] := Wert

Ein Objekt kann auch Indexierung unterstützen: MeinArray[Index] ruft indirekt die __Item-Eigenschaft von MeinArray auf und übergibt Index als Parameter.

Direkt geschriebenes Objekt

Ein direkt geschriebenes Objekt kann in einem Ausdruck verwendet werden, um ein improvisiertes Objekt zu erstellen. Ein direkt geschriebenes Objekt besteht aus zwei geschweiften Klammern ({}), die eine Liste von kommagetrennten Name-Wert-Paaren umschließen. Jedes Paar besteht aus einem direkt geschriebenen (anführungszeichenlosen) Eigenschaftsnamen und einem Wert (Teilausdruck), die mit einem Doppelpunkt (:) voneinander getrennt sind. Zum Beispiel:

Koord := {X: 13, Y: 240}

Folgendes ist äquivalent:

Koord := Object()
Koord.X := 13
Koord.Y := 240

Jedes Name-Wert-Paar definiert eine Werteigenschaft, mit der Ausnahme, dass Base gesetzt werden kann (mit den gleichen Einschränkungen wie bei einer normalen Zuweisung).

Namenssubstitution ermöglicht die Bestimmung eines Eigenschaftsnamens durch Auswertung eines Ausdrucks oder einer Variable. Zum Beispiel:

Teile := StrSplit("Schlüssel = Wert", "=", " ")
Teil := {%Teile[1]%: Teile[2]}
MsgBox Teil.Schlüssel

Objekte freigeben

Skripte geben Objekte nicht explizit frei. Sobald die letzte Referenz zu einem Objekt freigegeben ist, wird das Objekt automatisch freigegeben. Eine Referenz, die in einer Variable gespeichert ist, wird automatisch freigegeben, sobald dieser Variable ein anderer Wert zugewiesen wird. Zum Beispiel:

obj := {}  ; Erstellt ein Objekt.
obj := ""  ; Gibt die letzte Referenz frei, wodurch das Objekt freigegeben wird.

Gleichermaßen wird eine Referenz, die in einer Eigenschaft oder einem Array-Element gespeichert ist, freigegeben, wenn diese Eigenschaft oder dieses Array-Element einen anderen Wert zugewiesen bekommt oder aus dem Objekt entfernt wird.

arr := [{}]  ; Erstellt ein Array, das ein Objekt enthält.
arr[1] := {}  ; Erstellt ein zweites Objekt, wodurch das erste Objekt indirekt freigegeben wird.
arr.RemoveAt(1)  ; Entfernt das zweite Objekt und gibt es frei.

Da alle Referenzen zu einem Objekt freigegeben werden müssen, bevor das Objekt freigegeben werden kann, können Objekte mit Zirkelbezügen nicht automatisch freigegeben werden. Wenn z.B. x.child auf y und y.parent auf x verweist, genügt es nicht, x und y zu leeren, weil das Parent-Objekt noch eine Referenz zum Child-Objekt enthält, und umgekehrt. Um diese Situation zu lösen, entfernen Sie den Zirkelbezug.

x := {}, y := {}             ; Zwei Objekte erstellen.
x.child := y, y.parent := x  ; Einen Zirkelbezug erstellen.

y.parent := ""               ; Der Zirkelbezug muss entfernt werden, bevor die Objekte freigegeben werden können.
x := "", y := ""             ; Ohne die obige Zeile würde dies die Objekte nicht freigeben.

Eine ausführlichere Verwendung und Informationen finden Sie unter Referenzzählung.

Erweiterte Verwendung

Mehrdimensionale Arrays

Obwohl "mehrdimensionale" Arrays nicht unterstützt werden, kann ein Skript mehrere Arrays oder Maps kombinieren. Zum Beispiel:

Gitter := [[1,2,3],
           [4,5,6],
           [7,8,9]]
MsgBox Gitter[1][3] ; 3
MsgBox Gitter[3][2] ; 8

Ein benutzerdefiniertes Objekt kann mehrdimensionale Unterstützung durch Definition einer __Item-Eigenschaft implementieren. Zum Beispiel:

class Array2D extends Array {
    __new(x, y) {
        this.Length := x * y
        this.Breite := x
        this.Höhe := y
    }
    __Item[x, y] {
        get => super.Has(this.i[x, y]) ? super[this.i[x, y]] : false
        set => super[this.i[x, y]] := value
    }
    i[x, y] => this.Width * (y-1) + x
}

Gitter := Array2D(4, 3)
Gitter[4, 1] := "#"
Gitter[3, 2] := "#"
Gitter[2, 2] := "#"
Gitter[1, 3] := "#"
GitterText := ""
Loop Gitter.Höhe {
    y := A_Index
    Loop Gitter.Breite {
        x := A_Index
        GitterText .= Gitter[x, y] || "-"
    }
    GitterText .= "`n"
}
MsgBox GitterText

Ein echtes Skript sollte eine Fehlerprüfung durchführen und andere Methoden überschreiben, wie z.B. __Enum, um die Enumeration zu unterstützen.

Benutzerdefinierte Objekte

Es gibt zwei verschiedene Möglichkeiten, benutzerdefinierte Objekte zu erstellen:

Metafunktionen können verwendet werden, um das Verhalten eines Objekts genauer zu steuern.

Hinweis: In diesem Abschnitt ist ein Objekt eine Instanz der Object-Klasse. Dieser Abschnitt gilt nicht für COM-Objekte.

Ad Hoc

Grundsätzlich kann ein neues Objekt jederzeit um Eigenschaften und Methoden (aufrufbare Eigenschaften) erweitert werden. Das folgende Beispiel zeigt, wie ein Objekt mit einer Eigenschaft und einer Methode konstruiert werden kann:

; Ein Objekt erstellen.
obj := {}
; Einen Wert speichern.
obj.foo := "bar"
; Eine Methode definieren.
obj.test := obj_test
; Die Methode aufrufen.
obj.test()

obj_test(this) {
    MsgBox this.foo
}

Das obige Objekt kann auch mit obj := {foo: "bar"} erstellt werden. Bei der {Eigenschaft:Wert}-Schreibweise dürfen die Eigenschaften nicht in Anführungszeichen gesetzt werden.

Beim Aufruf von obj.test() wird obj automatisch an den Anfang der Parameterliste gesetzt. Konventionsgemäß setzt sich der Name der Funktion aus dem "Typ" des Objekts und dem Namen der Methode zusammen, kann aber auch anders lauten.

Im obigen Beispiel kann test, nachdem es definiert wurde, eine andere Funktion oder ein anderer Wert zugewiesen werden; in diesem Fall geht die ursprüngliche Funktion verloren und kann nicht über diese Eigenschaft aufgerufen werden. Alternativ kann eine schreibgeschützte Methode definiert werden:

obj.DefineProp 'test', {call: obj_test}

Siehe auch: DefineProp

Delegation

Objekte sind prototypenbasiert. Das heißt, dass alle Eigenschaften, die nicht im Objekt selbst definiert sind, stattdessen in der Basis des Objekts definiert sein können. Dies wird als Vererbung durch Delegation oder differentielle Vererbung bezeichnet, da ein Objekt nur die Teile implementieren kann, die sich vom Objekt unterscheiden, und den Rest an seine Basis delegiert.

Obwohl ein Basisobjekt allgemein auch als Prototyp bekannt ist, verwenden wir "Prototyp einer Klasse" für das Objekt, auf dem jede Instanz der Klasse basiert, und "Basis" für das Objekt, auf dem eine Instanz basiert.

Das Objektdesign von AutoHotkey wurde hauptsächlich von JavaScript und Lua und ein wenig von C# beeinflusst. Wir verwenden obj.base anstelle von JavaScripts obj.__proto__ und cls.Prototype anstelle von JavaScripts func.prototype. (Klassenobjekte werden anstelle von Konstruktorfunktionen verwendet.)

Die Basis eines Objekts wird auch verwendet, um seinen Typ oder seine Klasse zu identifizieren. Zum Beispiel erstellt x := [] ein Objekt basierend auf Array.Prototype, d.h. die Ausdrücke x is Array und x.HasBase(Array.Prototype) sind True und type(x) gibt "Array" zurück. Der Prototyp jeder Klasse basiert auf dem Prototyp ihrer Basisklasse, daher ist x.HasBase(Object.Prototype) ebenfalls True.

Jede Instanz von Object oder eine abgeleitete Klasse kann ein Basisobjekt sein, aber ein Objekt kann nur als Basis eines Objekts mit dem gleichen nativen Typ zugewiesen werden. Dadurch wird sichergestellt, dass interne Methoden immer den nativen Typ eines Objekts identifizieren können und dass sie nur mit Objekten arbeiten, die die korrekte binäre Struktur aufweisen.

Basisobjekte können auf zwei verschiedene Arten definiert werden:

Ein Basisobjekt kann der Base-Eigenschaft eines anderen Objekts zugewiesen werden, aber normalerweise wird die Basis eines Objekts implizit beim Erstellen festgelegt.

Basisobjekt erstellen

Jedes Objekt kann als Basis eines anderen Objekts mit demselben nativen Typ verwendet werden. Das folgende Beispiel baut auf dem vorherigen Beispiel unter Ad Hoc auf (kombinieren Sie beide vor der Ausführung):

anderesObj := {}
anderesObj.base := obj
anderesObj.test()

anderesObj erbt in diesem Fall foo und test von obj. Diese Vererbung ist dynamisch, d.h. wenn obj.foo geändert wird, wird diese Änderung durch anderesObj.foo widergespiegelt. Wenn das Skript anderesObj.foo einen Wert zuweist, wird dieser Wert in anderesObj gespeichert; weitere Änderungen an obj.foo hätten keinen Effekt auf anderesObj.foo. Beim Aufruf von anderesObj.test() enthält dessen this-Parameter eine Referenz zu anderesObj statt zu obj.

Klassen

Unter einer Klasse (auch Objekttyp genannt) versteht man in der objektorientierten Programmierung ein abstraktes Modell bzw. einen Bauplan für eine Reihe von ähnlichen Objekten. Wikipedia

Allgemein ausgedrückt ist eine Klasse eine Gruppe oder Kategorie von Dingen, die gemeinsame Eigenschaften oder Attribute haben. In AutoHotkey definiert class Eigenschaften, die von Instanzen der Klasse gemeinsam verwendet werden (und Methoden, die aufrufbare Eigenschaften sind). Eine Instanz ist schlicht ein Objekt, das Eigenschaften von der Klasse erbt und typischerweise auch als Teil dieser Klasse verstanden werden kann (z.B. mit dem Ausdruck Instanz is KlasseName). Instanzen werden typischerweise durch den Aufruf von KlasseName() erzeugt.

Da Objekte dynamisch und prototypenbasiert sind, besteht jede Klasse aus zwei Teilen:

Das Folgende zeigt die meisten Elemente einer Klassendefinition:

class KlasseName extends BasisklasseName
{
    InstanzVar := Ausdruck
    
    static KlasseVar := Ausdruck

    class VerschachtelteKlasse
    {
        ...
    }

    Methode()
    {
        ...
    }
    
    static Methode()
    {
        ...
    }

    Eigenschaft[Parameter]  ; Eckige Klammern nur verwenden, wenn Parameter vorhanden sind.
    {
        get {
            return Eigenschaftswert
        }
        set {
            Speichere oder verarbeite value
        }
    }
    
    KurzeEigenschaft
    {
        get => Ausdruck zum Ermitteln des Eigenschaftswertes
        set => Ausdruck zum Speichern oder Verarbeiten von value
    }
    
    KürzereEigenschaft => Ausdruck zum Ermitteln des Eigenschaftswertes
}

Dies bewirkt, dass beim Laden des Skripts ein Class-Objekt konstruiert und in eine globale Konstante (schreibgeschützte Variable) namens KlasseName gespeichert wird. Wenn extends BasisklasseName vorhanden ist, muss BasisklasseName der vollständige Name einer anderen Klasse sein. Der vollständige Name jeder Klasse ist in KlasseName.Prototype.__Class gespeichert.

Da der Zugriff auf die Klasse selbst über eine Variable erfolgt, kann der Klassenname nicht verwendet werden, um im selben Kontext sowohl die Klasse zu referenzieren als auch eine separate Variable zu erstellen (um z.B. eine Instanz der Klasse zu enthalten). Zum Beispiel wird box := Box() nicht funktionieren, da sowohl box als auch Box in dasselbe aufgelöst werden. Der Versuch, eine Top-Level-Klasse (nicht verschachtelte Klasse) auf diese Weise neu zuzuweisen, führt zu einem Ladezeitfehler.

In dieser Dokumentation bezieht sich das Wort "Klasse" in der Regel auf ein Klassenobjekt, das mit dem Schlüsselwort class erstellt wurde.

Klassendefinitionen können Variablendeklarationen, Methodendefinitionen und Unterklassendefinitionen enthalten.

Instanzvariablen

Eine Instanzvariable ist eine Variable, von der jede Instanz der Klasse eine eigene Kopie hat. Sie werden wie normale Zuweisungen deklariert und verhalten sich auch wie solche, aber ohne Angabe des Präfixes this. (nur direkt im Körper der Klasse):

InstanzVar := Ausdruck

Solche Deklarationen werden jedes Mal ausgewertet, wenn eine neue Instanz der Klasse mit KlasseName() erstellt wird, nachdem alle Basisklassendeklarationen ausgewertet wurden, aber bevor __New aufgerufen wird. Dies wird erreicht, indem automatisch eine Methode namens __Init erstellt wird, die einen Aufruf von super.__Init() enthält, und jede Deklaration in diese eingefügt wird. Daher darf eine einzelne Klassendefinition weder eine __Init-Methode noch eine Instanzvariablendeklaration enthalten.

Ausdruck kann mit this auf andere Instanzvariablen und Methoden zugreifen. Globale Variablen können gelesen, aber nicht zugewiesen werden. Eine zusätzliche Zuweisung (oder die Verwendung des Referenzoperators) innerhalb des Ausdrucks erzeugt in der Regel eine Variable, die lokal für die __Init-Methode ist. Zum Beispiel bewirkt x := y := 1, dass this.x und eine lokale Variable y gesetzt wird (die nach der Auswertung aller Initialisierungen wieder freigegeben wird).

Um auf eine Instanzvariable zuzugreifen (auch innerhalb einer Methode), geben Sie immer das Zielobjekt an, z.B. this.InstanzVar.

Deklarationen wie x.y := z werden ebenfalls unterstützt, sofern vorher x in dieser Klasse definiert wurde. Zum Beispiel bewirkt x := {}, x.y := 42, dass x deklariert und außerdem this.x.y initialisiert wird.

Statische bzw. Klassenvariablen

Statische bzw. Klassenvariablen gehören nur der Klasse selbst, aber ihre Werte können von Unterklassen geerbt werden. Solche Variablen müssen mit dem Schlüsselwort "static" deklariert werden:

static KlasseVar := Ausdruck

Diese Deklarationen werden nur einmal beim Initialisieren der Klasse ausgewertet. Zu diesem Zweck wird automatisch eine statische Methode namens __Init definiert.

Jede Deklaration fungiert wie eine normale Eigenschaftszuweisung, mit dem Klassenobjekt als Ziel. Ausdruck wird genauso interpretiert wie die Instanzvariablen, außer dass this auf die Klasse selbst verweist.

Um einer Klassenvariable an anderer Stelle etwas zuzuweisen, geben Sie immer das Klassenobjekt an, z.B. KlasseName.KlasseVar := Wert. Wenn eine Unterklasse keine Eigenschaft mit diesem Namen hat, kann auch mit Unterklasse.KlasseVar der Wert abgerufen werden, d.h. wenn der Wert eine Referenz zu einem Objekt ist, teilen sich die Unterklassen standardmäßig dieses Objekt. Allerdings würde Unterklasse.KlasseVar := y den Wert in Unterklasse speichern, nicht in KlasseName.

Deklarationen wie static x.y := z werden ebenfalls unterstützt, sofern vorher x in dieser Klasse definiert wurde. Zum Beispiel bewirkt static x := {}, x.y := 42, dass x deklariert und außerdem KlasseName.x.y initialisiert wird. Da Prototype implizit in jeder Klasse definiert ist, können mit static Prototype.geteilterWert := 1 Werte gesetzt werden, die dynamisch von allen Instanzen der Klasse geerbt werden (bis sie durch eine Eigenschaft der Instanz selbst "überschrieben" werden).

Verschachtelte Klassen

Verschachtelte Klassendefinitionen ermöglichen es, ein Klassenobjekt mit einer statischen bzw. Klassenvariable der äußeren Klasse zu assoziieren, anstatt mit einer separaten globalen Variablen. Im obigen Beispiel konstruiert class VerschachtelteKlasse ein Class-Objekt und speichert es in KlasseName.VerschachtelteKlasse. Unterklassen können VerschachtelteKlasse erben oder es mit ihrer eigenen verschachtelten Klasse überschreiben (in diesem Fall kann mit WelcheKlasse.VerschachtelteKlasse() die entsprechende Klasse instanziiert werden).

class VerschachtelteKlasse
{
    ...
}

Das Verschachteln einer Klasse impliziert keine besondere Beziehung zur äußeren Klasse. Weder wird die verschachtelte Klasse automatisch instanziiert, noch haben Instanzen der verschachtelten Klasse eine Verbindung zu einer Instanz der äußeren Klasse, es sei denn, das Skript stellt diese Verbindung explizit her.

Jede verschachtelte Klassendefinition erzeugt eine dynamische Eigenschaft mit Get- und Call-Akzessorfunktionen anstelle einer einfachen Werteigenschaft. Damit soll das folgende Verhalten unterstützt werden (wobei die Klasse X die verschachtelte Klasse Y enthält):

Methoden

Methodendefinitionen sehen genauso aus wie Funktionsdefinitionen. Jede Methodendefinition erstellt ein Func mit einem versteckten ersten Parameter namens this und definiert eine Eigenschaft, die zum Aufrufen der Methode oder zum Abrufen ihres Funktionsobjekts verwendet wird.

Es gibt zwei Arten von Methoden:

Die Methodendefinition unten erstellt eine Eigenschaft vom gleichen Typ wie Ziel.DefineProp('Methode', {call: funcObj}). Standardmäßig gibt Ziel.Methode funcObj zurück, und der Versuch, Ziel.Methode etwas zuzuweisen, löst einen Fehler aus. Diese Standardverhalten können durch Definieren einer Eigenschaft oder Aufrufen von DefineProp überschrieben werden.

Methode()
{
    ...
}

Mit der Fat-Arrow-Syntax kann eine einzeilige Methode definiert werden, die ein Ausdruck zurückgibt:

Methode() => Ausdruck

Super

Innerhalb einer Methode oder eines Eigenschafts-Getters/Setters kann das Schlüsselwort super anstelle von this verwendet werden, um auf Superklassenversionen von Methoden oder Eigenschaften zuzugreifen, die in einer abgeleiteten Klasse überschrieben werden. Zum Beispiel würde super.Methode() in der oben definierten Klasse typischerweise eine Version von Methode aufrufen, die innerhalb von BasisklasseName definiert wurde. Hinweis:

Nach dem Schlüsselwort super muss eines der folgenden Symbole folgen: .[(

super() ist äquivalent zu super.call().

Eigenschaften

Eine Eigenschaftsdefinition erzeugt eine dynamische Eigenschaft, die eine Methode aufruft, anstatt nur einen Wert zu speichern oder zurückzugeben.

Eigenschaft[Parameter]
{
    get {
        return Eigenschaftswert
    }
    set {
        Speichere oder verarbeite value
    }
}

Eigenschaft ist der Name der Eigenschaft, der für ihren indirekten Aufruf verwendet wird. Zum Beispiel bewirkt obj.Eigenschaft, dass get aufgerufen wird, und obj.Eigenschaft := Wert, dass set aufgerufen wird. Innerhalb von get oder set bezieht sich this auf das Objekt, das gerade indirekt aufgerufen wird. Innerhalb von set enthält value den Wert, der gerade zugewiesen wird.

Die Definition und Übergabe von Parametern erfolgt rechts vom Eigenschaftsnamen in eckigen Klammern - diese sollten jedoch weggelassen werden, wenn keine Parameter vorhanden sind (siehe unten). Abgesehen von der Verwendung eckiger Klammern werden die Parameter von Eigenschaften genauso definiert wie die Parameter von Methoden - optionale, variadische und ByRef-Parameter werden unterstützt.

Wenn eine Eigenschaft mit Parametern indirekt aufgerufen wird, aber keine definiert sind, werden Parameter automatisch an die __Item-Eigenschaft des von get zurückgegebenen Objekts weitergeleitet. Zum Beispiel hätte this.Eigenschaft[x] denselben Effekt wie (this.Eigenschaft)[x] oder y := this.Eigenschaft, y[x]. Leere Klammern (this.Eigenschaft[]) führen dazu, dass die __Item-Eigenschaft des Wertes von Eigenschaft immer indirekt aufgerufen wird, aber ein variadischer Aufruf wie this.Eigenschaft[Args*] hat diesen Effekt nur, wenn die Anzahl der Parameter ungleich Null ist.

Um statische Eigenschaften zu definieren, stellen Sie dem Eigenschaftsnamen das separate Schlüsselwort static voran. In diesem Fall verweist this auf die Klasse selbst oder auf eine Unterklasse.

Der Rückgabewert von set wird ignoriert. Zum Beispiel wird Wert := obj.Eigenschaft := 42 immer Wert := 42 zuweisen, egal was die Eigenschaft macht, es sei denn, sie löst eine Ausnahme aus oder beendet den Thread.

Jede Klasse kann eine oder beide Hälften einer Eigenschaft definieren. Wenn eine Klasse eine Eigenschaft überschreibt, kann sie super.Eigenschaft verwenden, um auf die von ihrer Basisklasse definierte Eigenschaft zuzugreifen. Wenn Get oder Set nicht definiert ist, kann es von einem Basisobjekt geerbt werden. Wenn Get undefiniert ist, kann die Eigenschaft einen Wert zurückgeben, der von einer Basis geerbt wurde. Wenn Set in diesem und allen Basisobjekten undefiniert ist (oder von einer geerbten Werteigenschaft verschleiert wird), löst der Versuch, die Eigenschaft zu setzen, eine Ausnahme aus.

Eine Eigenschaftsdefinition mit sowohl Get als auch Set erstellt faktisch zwei separate Funktionen, die weder lokale oder statische Variablen noch verschachtelte Funktionen gemeinsam nutzen. Analog zu den Methoden hat jede Funktion einen versteckten Parameter namens this, und Set hat einen zweiten versteckten Parameter namens value. Explizit definierte Parameter erfolgen danach.

Während eine Eigenschaftsdefinition die Get- und Set-Akzessorfunktionen für eine Eigenschaft auf die gleiche Weise wie DefineProp definiert, definiert eine Methodendefinition die Call-Akzessorfunktion. Jede Klasse kann eine Eigenschaftsdefinition und eine Methodendefinition gleichen Namens enthalten. Wenn eine Eigenschaft ohne Call-Akzessorfunktion (eine Methode) aufgerufen wird, wird Get ohne Parameter indirekt aufgerufen und das Ergebnis anschließend als Methode aufgerufen.

Fat-Arrow-Eigenschaften

Mit der Fat-Arrow-Syntax kann ein Eigenschaft-Getter oder -Setter definiert werden, der ein Ausdruck zurückgibt:

KurzeEigenschaft[Parameter]
{
    get => Ausdruck zum Ermitteln des Eigenschaftswertes
    set => Ausdruck zum Speichern oder Verarbeiten von value
}

Wenn nur ein Getter definiert wird, können die geschweiften Klammern und get weggelassen werden:

KürzereEigenschaft[Parameter] => Ausdruck zum Ermitteln des Eigenschaftswertes

In beiden Fällen müssen die eckigen Klammern weggelassen werden, es sei denn, Parameter sind definiert.

__Enum-Methode

__Enum(AnzahlVars)

Die __Enum-Methode wird aufgerufen, wenn das Objekt an eine For-Schleife übergeben wird. Diese Methode sollte einen Enumerator zurückgeben, der Elemente im Objekt zurückgibt, wie z.B. Array-Elemente. Wenn es undefiniert bleibt, kann das Objekt nicht direkt an eine For-Schleife übergeben werden, es sei denn, es hat eine Enumerator-kompatible Call-Methode.

AnzahlVars enthält die Anzahl der Variablen, die an die For-Schleife übergeben wurden. Wenn AnzahlVars 2 ist, wird erwartet, dass der Enumerator dem ersten Parameter den Schlüssel oder Index eines Elements und dem zweiten Parameter den Wert zuweist. Jeder Schlüssel oder Index sollte als Parameter der __Item-Eigenschaft akzeptiert werden. Dies befähigt DBGp-basierte Debugger, ein bestimmtes Element abzurufen oder zu setzen, nachdem sie durch indirekten Aufruf des Enumerators aufgelistet wurden.

__Item-Eigenschaft

Die __Item-Eigenschaft wird indirekt aufgerufen, wenn der Indexierungsoperator (Arraysyntax) in Verbindung mit dem Objekt verwendet wird. Im folgenden Beispiel wird die Eigenschaft als statisch deklariert, so dass der Indexierungsoperator auf die Env-Klasse selbst angewendet werden kann. Ein weiteres Beispiel finden Sie unter Array2D.

class Env {
    static __Item[name] {
        get => EnvGet(name)
        set => EnvSet(name, value)
    }
}

Env["PATH"] .= ";" A_ScriptDir  ; Betrifft nur dieses Skript und Unterprozesse.
MsgBox Env["PATH"]

__Item ist quasi ein Standardeigenschaftsname (falls eine solche Eigenschaft definiert wurde):

Zum Beispiel:

obj := {}
obj[] := Map()     ; Äquivalent zu obj.__Item := Map()
obj["base"] := 10
MsgBox obj.base = Object.prototype  ; True
MsgBox obj["base"]                  ; 10

Hinweis: Wenn ein expliziter Eigenschaftsname mit leeren eckigen Klammern kombiniert wird, wie in obj.prop[], wird dies als zwei separate Operationen behandelt: Zuerst obj.prop abrufen und dann die Standardeigenschaft des Ergebnisses indirekt aufrufen. Dies ist Teil der Sprachsyntax und somit objektunabhängig.

Konstruktion und Destruktion

Jedes Mal, wenn ein Objekt mit der Standardimplementierung von KlasseName() erstellt wird, wird die __New-Methode des neuen Objekts aufgerufen, um eine benutzerdefinierte Initialisierung zu ermöglichen. Alle Parameter, die an KlasseName() übergeben werden, werden an __New weitergeleitet, so dass sie den ursprünglichen Inhalt des Objekts oder die Art und Weise, wie es konstruiert wird, beeinflussen können. Bei Zerstörung des Objekts wird __Delete aufgerufen. Zum Beispiel:

m1 := GMem(0, 10)
m2 := {base: GMem.Prototype}, m2.__New(0, 30)

; Hinweis: Für allgemeine Speicherreservierungen stattdessen Buffer() verwenden.
class GMem
{
    __New(aFlags, aGröße)
    {
        this.ptr := DllCall("GlobalAlloc", "UInt", aFlags, "Ptr", aGröße, "Ptr")
        if !this.ptr
            throw MemoryError()
        MsgBox "Neues GMem von " aGröße " Bytes auf Adresse " this.ptr "."
    }

    __Delete()
    {
        MsgBox "GMem auf Adresse " this.ptr " löschen."
        DllCall("GlobalFree", "Ptr", this.ptr)
    }
}

__Delete wird nicht für Objekte aufgerufen, die eine Eigenschaft namens "__Class" haben. Prototypobjekte haben diese Eigenschaft standardmäßig.

Wenn ein Ausnahme- oder Laufzeitfehler bei der Ausführung von __Delete ausgelöst und nicht innerhalb von __Delete behandelt wird, verhält es sich so, als wäre __Delete von einem neuen Thread aufgerufen worden. Das heißt, dass ein Fehlerdialogfenster angezeigt wird und __Delete zurückkehrt, aber der Thread nicht beendet wird (es sei denn, er wurde bereits beendet).

Wenn das Skript auf irgendeine Weise direkt terminiert wird, z.B. über das Tray-Menü oder ExitApp, bekommen alle Funktionen, die noch nicht zu ihrem Aufrufer zurückgekehrt sind, keine Chance, dies jemals zu tun. Daher werden alle Objekte, die von lokalen Variablen dieser Funktionen referenziert werden, nicht freigegeben, was dazu führt, dass __Delete nicht aufgerufen wird.

Bei Beendigung des Skripts werden Objekte, die in globalen und statischen Variablen enthalten sind, automatisch in einer willkürlichen, von der Implementierung festgelegten Reihenfolge freigegeben. Wenn __Delete während dieses Vorgangs aufgerufen wird, können einige globale oder statische Variablen bereits freigegeben worden sein, aber alle Referenzen, die das Objekt selbst enthält, sind noch gültig. Daher ist es am besten, wenn __Delete völlig eigenständig ist und nicht auf globale oder statische Variablen angewiesen ist.

Klasseninitialisierung

Jede Klasse wird automatisch initialisiert, wenn eine Referenz zur Klasse erstmals ausgewertet wird. Wenn z.B. MeineKlasse noch nicht initialisiert wurde, würde MeineKlasse.MeineEigenschaft bewirken, dass die Klasse initialisiert wird, bevor die Eigenschaft abgerufen wird. Die Initialisierung umfasst den Aufruf von zwei statischen Methoden: __Init und __New.

static __Init wird automatisch für jede Klasse definiert und beginnt immer mit einer Referenz zur Basisklasse, falls angegeben, um sicherzustellen, dass sie initialisiert ist. Statische bzw. Klassenvariablen und verschachtelte Klassen werden in der Reihenfolge initialisiert, in der sie definiert wurden, außer wenn eine verschachtelte Klasse während der Initialisierung einer vorherigen Variable oder Klasse referenziert wird.

Wenn die Klasse eine static __New-Methode definiert oder erbt, wird diese unmittelbar nach __Init aufgerufen. Es ist wichtig zu beachten, dass __New einmal für die Klasse, in der es definiert ist, und einmal für jede Unterklasse, die keine eigene definiert (oder die super.__New() aufruft), aufgerufen werden kann. Dies kann verwendet werden, um allgemeine Initialisierungsaufgaben für jede Unterklasse durchzuführen oder um Unterklassen vor ihrer Verwendung in irgendeiner Weise zu modifizieren.

Wenn static __New nicht mit abgeleiteten Klassen arbeiten soll, kann dies durch die Überprüfung des Wertes von this vermieden werden. In einigen Fällen kann es ausreichend sein, dass die Methode sich selbst löscht, wie z.B. bei this.DeleteProp('__New'); allerdings könnte die erste Ausführung von __New für eine Unterklasse sein, wenn diese in der Basisklasse verschachtelt ist oder bei der Initialisierung einer statischen bzw. Klassenvariable referenziert wird.

Eine Klassendefinition bewirkt auch, dass die Klasse referenziert wird. Mit anderen Worten, wenn die Ausführung während der Startphase des Skripts eine Klassendefinition erreicht, werden __Init und __New automatisch aufgerufen, es sei denn, die Klasse wurde bereits vom Skript referenziert. Wenn die Ausführung jedoch daran gehindert wird, die Klassendefinition zu erreichen, z.B. durch return oder eine Endlosschleife, wird die Klasse erst initialisiert, wenn sie referenziert wird.

Wenn die automatische Initialisierung einmal begonnen hat, wird sie für dieselbe Klasse nicht erneut durchgeführt. Dies ist grundsätzlich kein Problem, es sei denn, mehrere Klassen referenzieren sich gegenseitig. Nehmen wir zum Beispiel die folgenden zwei Klassen. Wenn A zuerst initialisiert wird, bewirkt die Auswertung von B.SharedArray (A1), dass B initialisiert wird, bevor der Wert abgerufen und zurückgegeben wird, aber A.SharedValue (A3) ist undefiniert und bewirkt keine Initialisierung von A, da diese bereits im Gange ist. Mit anderen Worten, wenn A erstmals verwendet oder initialisiert wird, ist die Reihenfolge A1 bis A3, sonst B1 bis B4:

MsgBox A.SharedArray.Length
MsgBox B.SharedValue

class A {
    static SharedArray := B.SharedArray   ; A1          ; B3
    static SharedValue := 42                            ; B4
}

class B {
    static SharedArray := StrSplit("XYZ") ; A2          ; B1
    static SharedValue := A.SharedValue   ; A3 (Error)  ; B2
}

Metafunktionen

class KlasseName {
    __Get(Name, Params)
    __Set(Name, Params, Wert)
    __Call(Name, Params)
}
Name

Der Name der Eigenschaft oder Methode.

Params

Ein Array von Parametern. Dies umfasst nur die Parameter zwischen () oder [], kann also leer sein. Die Metafunktion soll Fälle wie x.y[z] behandeln, in denen x.y undefiniert ist.

Wert

Der Wert, der zugewiesen wird.

Metafunktionen definieren, was passieren soll, wenn eine undefinierte Eigenschaft oder Methode indirekt aufgerufen wird. Wenn z.B. obj.unk keinen Wert zugewiesen bekommen hat, ruft es indirekt die __Get-Metafunktion auf. obj.unk := Wert bewirkt dagegen den indirekten Aufruf von __Set und obj.unk() den indirekten Aufruf von __Call.

Eigenschaften und Methoden können im Objekt selbst oder in einem seiner Basisobjekte definiert werden. Damit für jede Eigenschaft eine Metafunktion aufgerufen wird, sollte die Definition von Eigenschaften grundsätzlich vermieden werden. Interne Eigenschaften wie Base können mit einer Eigenschaftsdefinition oder DefineProp überschrieben werden.

Wenn eine Metafunktion definiert ist, muss sie die gewünschte Standardaktion ausführen. Zum Beispiel kann Folgendes erwartet werden:

Jedes aufrufbare Objekt kann als Metafunktion verwendet werden, wenn es der entsprechenden Eigenschaft zugewiesen wird.

Metafunktionen werden in den folgenden Fällen nicht aufgerufen:

Dynamische Eigenschaften

Die Eigenschaftssyntax und DefineProp ermöglichen die Definition von Eigenschaften, die bei jeder Auswertung einen Wert ermitteln, allerdings muss jede Eigenschaft im Voraus definiert sein. Mit __Get und __Set können hingegen Eigenschaften implementiert werden, die erst zum Zeitpunkt ihres indirekten Aufrufs bekannt sind.

Zum Beispiel kann ein "Proxy-Objekt" mit Eigenschaften erstellt werden, die den aktuellen Wert über das Netzwerk (oder über einen anderen Kanal) abfragen. Ein Remote-Server sendet eine Antwort zurück, die den Wert der Eigenschaft enthält, und das Proxy-Objekt übergibt den Wert an seinen Aufrufer. Selbst wenn der Name jeder Eigenschaft im Voraus bekannt wäre, würde es keinen Sinn machen, jede Eigenschaft einzeln in der Proxy-Klasse zu definieren, da jede Eigenschaft dasselbe bewirkt (eine Netzwerkanfrage senden). Metafunktionen empfangen den Namen der Eigenschaft als Parameter und sind daher eine gute Lösung für dieses Problem.

Primitive Werte

Primitive Werte wie Zeichenketten und Zahlen können keine eigenen Eigenschaften und Methoden haben. Allerdings unterstützen primitive Werte die gleiche Art der Delegation wie Objekte. Das heißt, dass jeder Eigenschafts- oder Methodenaufruf bei einem primitiven Wert an ein vordefiniertes Prototypobjekt delegiert wird, auf das auch über die Prototype-Eigenschaft der entsprechenden Klasse zugegriffen werden kann. Die folgenden Klassen beziehen sich auf primitive Werte:

Obwohl die Überprüfung der Type-Zeichenkette in der Regel schneller ist, kann der Typ eines Wertes auch getestet werden, indem geprüft wird, ob der Wert eine Basis hat. Zum Beispiel ist n.HasBase(Number.Prototype) oder n is Number True, wenn n ein reiner Integer oder eine reine Floating-Point-Zahl ist, aber False, wenn n eine numerische Zeichenkette ist, da String nicht von Number ableitet. IsNumber(n) hingegen ist True, wenn n eine Zahl oder numerische Zeichenkette ist.

ObjGetBase und die Base-Eigenschaft geben ggf. eines der vordefinierten Prototypobjekte zurück.

Beachten Sie, dass x is Any normalerweise True für jeden Wert innerhalb der Typenhierarchie von AutoHotkey ist, aber False für COM-Objekte ist.

Eigenschaften und Methoden hinzufügen

Eigenschaften und Methoden können für alle Werte eines bestimmten Typs hinzugefügt werden, indem das Prototypobjekt dieses Typs geändert wird. Da jedoch ein primitiver Wert kein Object ist und keine eigenen Eigenschaften oder Methoden haben kann, werden die primitiven Prototypobjekte nicht von Object.Prototype ableiten. Mit anderen Worten, es ist standardmäßig nicht möglich, auf Methoden wie DefineProp und HasOwnProp zuzugreifen. Diese können indirekt aufgerufen werden. Zum Beispiel:

DefProp := {}.DefineProp
DefProp( "".base, "Length", { get: StrLen } )
MsgBox A_AhkPath.length " == " StrLen(A_AhkPath)

Obwohl primitive Werte Werteigenschaften von ihrem Prototyp erben können, wird eine Ausnahme ausgelöst, wenn das Skript versucht, eine Werteigenschaft bei einem primitiven Wert zu setzen. Zum Beispiel:

"".base.test := 1  ; Bitte nicht nachmachen.
MsgBox "".test  ; 1
"".test := 2  ; Fehler: Eigenschaft ist schreibgeschützt.

Obwohl __Set und Eigenschafts-Setter verwendet werden können, sind sie nicht nützlich, da primitive Werte als unveränderlich zu betrachten sind.

Implementierung

Referenzzählung

AutoHotkey verwendet einen einfachen Referenzzählmechanismus, um belegte Ressourcen eines Objekts automatisch freizugeben, wenn es nicht länger im Skript referenziert wird. Skriptautoren sollten diesen Mechanismus nicht explizit aufrufen, es sei denn, sie arbeiten direkt mit unverwalteten Objektpointern.

Temporäre Referenzen, die von Funktionen, Methoden oder Operatoren innerhalb eines Ausdrucks zurückgegeben werden, werden erst freigegeben, wenn die Auswertung dieses Ausdrucks abgeschlossen oder abgebrochen wurde. Im folgenden Beispiel wird das neue GMem-Objekt erst nach Abschluss von MsgBox freigegeben:

MsgBox DllCall("GlobalSize", "ptr", GMem(0, 20).ptr, "ptr")  ; 20

Hinweis: In diesem Beispiel kann .ptr weggelassen werden, da der Ptr-Argumenttyp Objekte mit einer Ptr-Eigenschaft zulässt. Allerdings funktioniert das oben gezeigte Muster auch mit anderen Eigenschaftsnamen.

Um Code beim Freigeben der letzten Referenz zu einem Objekt auszuführen, implementieren Sie die __Delete-Metafunktion.

Bekannte Einschränkungen:

Obwohl das Betriebssystem den vom Objekt belegten Speicher bei Beendigung des Programms zurückfordern wird, wird __Delete erst aufgerufen, wenn alle Referenzen zum Objekt freigegeben wurden. Dies kann wichtig sein, wenn es andere Ressourcen freigibt, die nicht automatisch vom Betriebssystem zurückgefordert werden, wie z.B. temporäre Dateien.

Objektpointer

In seltenen Fällen kann es erforderlich sein, dass ein Objekt via DllCall an einen externen Code übergeben werden muss oder dass ein Objekt in eine binäre Datenstruktur für späteren Gebrauch gespeichert werden muss. Die Adresse eines Objekts kann via Adresse := ObjPtr(meinObjekt) abgerufen werden, allerdings werden dadurch effektiv zwei Referenzen zum Objekt erzeugt, während das Programm selbst nur die eine Referenz in meinObjekt kennt. Das Objekt wird gelöscht, sobald die letzte bekannte Referenz zum Objekt freigegeben wird. Folglich muss das Skript dem Objekt mitteilen, dass es eine Referenz erhalten hat. Dies kann wie folgt erreicht werden (die folgenden zwei Zeilen sind äquivalent):

ObjAddRef(Adresse := ObjPtr(meinObjekt))
Adresse := ObjPtrAddRef(meinObjekt)

Außerdem muss das Objekt informiert werden, wenn das Skript mit dieser Referenz fertig ist:

ObjRelease(Adresse)

Generell sollte jede neue Kopie einer Objektadresse als separate Referenz zum Objekt behandelt werden, d.h. das Skript sollte ObjAddRef aufrufen, wenn es eine Kopie erhält, und sofort ObjRelease aufrufen, bevor es eine Kopie verliert. Zum Beispiel sollte immer ObjAddRef aufgerufen werden, wenn eine Adresse mit so etwas wie x := Adresse kopiert wird. Ebenso sollte das Skript ObjRelease aufrufen, wenn es mit x fertig ist (oder dabei ist, den Wert von x zu überschreiben).

Mit der ObjFromPtr-Funktion kann eine Adresse in eine geeignete Referenz umgewandelt werden:

meinObjekt := ObjFromPtr(Adresse)

ObjFromPtr vermutet, dass Adresse eine gezählte Referenz ist, und nimmt sie in Besitz. Mit anderen Worten, meinObjekt := "" bewirkt, dass die Referenz, ursprünglich durch Adresse repräsentiert, freigegeben wird. Danach muss Adresse als ungültig betrachtet werden. Um stattdessen eine neue Referenz zu erstellen, verwenden Sie eine der folgenden Möglichkeiten:

ObjAddRef(Adresse), meinObjekt := ObjFromPtr(Adresse)
meinObjekt := ObjFromPtrAddRef(Adresse)