Was Sie über Reflections in der Computertechnik wissen sollten.
Reflections (Computertechnik) beschreibt die Fähigkeit eines Programms, seine eigene Struktur zu untersuchen, insbesondere durch Typen. Es handelt sich dabei um eine Form der Metaprogrammierung. Reflections stiften in der Praxis häufig starke Verwirrungen.
In diesem Artikel werden wie Ihnen erklären, wie Reflections in Go funktionieren. Das Reflection-Modell jeder Sprache ist unterschiedlich, so dass wir im Folgenden unter Reflection „Reflection in Go“ meinen.
Typen und Interfaces.
Da die Reflection auf dem Typensystem aufbaut, beginnen wir mit einem Refresher über Typen in Go.
Go ist statisch typisiert. Jede Variable hat einen statischen Typ, d.h. genau einen Typ, der zur Kompilierungszeit bekannt und fixiert ist: int, float32, MyType, byte usw.
dann haben wir Typ int und j hat MyInt. Die Variablen i und j haben unterschiedliche statische Typen und können, obwohl sie den gleichen zugrundeliegenden Typ haben, nicht ohne Konvertierung einander zugeordnet werden.
Eine wichtige Kategorie von Typen sind Interface-Typen, die feste Mengen von Methoden repräsentieren. Eine Interface-Variable kann jeden konkreten (nicht-schnittstellenspezifischen) Wert speichern, solange dieser Wert die Methoden des Interfaces implementiert. Ein bekanntes Beispiel sind io.Reader und io.Writer, die Typen Reader und Writer aus dem io-Paket:
Jeder Typ, der eine Lese- (oder Schreib-)Methode mit dieser Signatur implementiert, soll io.Reader (oder io.Writer) implementieren. Für die Zwecke dieser Diskussion bedeutet das, dass eine Variable vom Typ io.Reader jeden Wert enthalten kann, dessen Typ eine Read-Methode hat:
Es ist wichtig, klarzustellen, dass der Typ von r, egal welchen konkreten Wert r hat, immer io.Reader ist: Go ist statisch typisiert und der statische Typ von r ist io.Reader.
Ein äußerst wichtiges Beispiel für einen Interface-Typ ist das leere Interface:
Es stellt den leeren Satz von Methoden dar und ist mit einem beliebigen Wert überhaupt zufrieden, da jeder Wert null oder mehr Methoden hat.
Einige Leute sagen, dass Go`s Interfaces dynamisch typisiert sind, aber das ist irreführend. Sie sind statisch typisiert: Eine Variable vom Typ Interface hat immer den gleichen statischen Typ, und obwohl zur Laufzeit der in der Interface-Variablen gespeicherte Wert den Typ ändern kann, wird dieser Wert immer dem Interface entsprechen.
Wir müssen das alles sehr genau wissen, denn Reflecion und Schnittstellen sind eng miteinander verbunden.
Die Darstellung einer Schnittstelle.
Russ Cox hat einen detaillierten Blogbeitrag über die Darstellung von Interface-Werten in Go geschrieben. Es ist nicht notwendig, die ganze Geschichte hier zu wiederholen, aber eine vereinfachte Zusammenfassung ist angebracht.
Eine Variable vom Typ Interface speichert ein Paar: den konkreten Wert, der der Variablen zugewiesen wurde, und den Typdeskriptor dieses Wertes. Genauer gesagt, ist der Wert das zugrunde liegende konkrete Datenelement, das die Schnittstelle implementiert, und der Typ beschreibt den vollständigen Typ dieses Elements. Beispiel:
r enthält schematisch das (Wert, Typ-) Paar (tty, *os.File). Beachten Sie, dass der Typ *os.File andere Methoden als Read implementiert. Obwohl der Interface-Wert nur Zugriff auf die Read-Methode bietet, trägt der Wert darin alle Typinformationen über diesen Wert. Deshalb können wir solche Dinge tun:
Der Ausdruck in dieser Zuweisung ist eine Typ-Assertion. Was er behauptet, ist, dass das Element in r auch io.Writer implementiert, und so können wir es w zuweisen. Nach der Zuweisung wird w das Paar enthalten (tty, *os.File). Das ist das gleiche Paar, das in r gehalten wurde. Der statische Typ des Interfaces bestimmt, welche Methoden mit einer Interface-Variablen aufgerufen werden können, auch wenn der konkrete Wert darin einen größeren Satz von Methoden haben kann.
Wenn wir weitermachen, können wir Folgendes erzeugen:
und unser leerer Interface-Wert empty wird wieder das gleiche Paar enthalten (tty, *os.File). Das ist praktisch: Eine leere Schnittstelle kann jeden Wert enthalten und enthält alle Informationen, die wir jemals über diesen Wert benötigen könnten.
(Wir benötigen hier keine Typ-Assertion, da es statisch bekannt ist, dass w das leere Interface erfüllt. In dem Beispiel, in dem wir einen Wert von einem Reader zu einem Writer verschoben haben, mussten wir explizit sein und eine Typ-Assertion verwenden, da die Methoden von Writer keine Teilmenge von Reader`s sind.)
Ein wichtiges Detail ist, dass das Paar innerhalb einer Schnittstelle immer die Form (Wert, konkreter Typ) und nicht die Form (Wert, Schnittstellentypen) hat. Schnittstellen enthalten keine Schnittstellenwerte.
Jetzt sind wir bereit zum Nachdenken.
Das erste Gesetz der Reflecion.
- Reflection wechselt vom Interface-Wert zum Reflection-Objekt.
Auf der grundlegenden Ebene ist die Reflecion nur ein Mechanismus, um das in einer Schnittstellenvariablen gespeicherte Typ- und Wertepaar zu untersuchen. Um zu beginnen, gibt es zwei Arten, die wir in Package Reflect kennen müssen: Typ und Wert. Diese beiden Typen geben Zugriff auf den Inhalt einer Interface-Variablen, und zwei einfache Funktionen, genannt reflect.TypeOf und reflect.ValueOf, holen reflect.Type und reflect.Value Pieces aus einem Interface-Wert. (Auch aus dem reflect.Value ist es einfach, zum reflect.Type zu gelangen, aber lassen Sie uns die Konzepte Value und Type vorerst getrennt halten.)
Beginnen wir mit TypeOf:
Dieses Programm druckt
Sie fragen sich vielleicht, wo sich das Interface hier befindet, da das Programm so aussieht, als würde es die float64-Variable x übergeben, nicht einen Interface-Wert, um reflect.TypeOf zu verwenden. Es ist vorhanden und die Signatur von reflect.TypeOf beinhaltet eine leere Schnittstelle.
Wenn wir reflect.TypeOf(x) aufrufen, wird x zuerst in einer leeren Schnittstelle gespeichert, die dann als Argument übergeben wird. Reflect.TypeOf entpackt diese leere Schnittstelle, um die Typinformationen wiederherzustellen.
Die reflect.ValueOf-Funktion stellt natürlich den Wert wieder her (von nun an werden wir die Boilerplate verschieben und uns nur noch auf den ausführbaren Code konzentrieren):
druckt
(Wir rufen die String-Methode explizit auf, weil das fmt-Paket standardmäßig in einen reflect.value gräbt, um den konkreten Wert darin anzuzeigen. Die String-Methode nicht.)
Sowohl reflect.Type als auch reflect.Value haben viele Methoden, um sie untersuchen und manipulieren zu können. Ein wichtiges Beispiel ist, dass Value eine Typ-Methode hat, die den Type eines reflect.value zurückgibt. Eine andere ist, dass sowohl Typ als auch Value eine Art Methode haben, die eine Konstante zurückgibt, die angibt, welche Art von Element gespeichert wird: Uint, Float64, Slice usw. Auch Methoden auf Value mit Namen wie Int und Float lassen und Werte (wie int64 und float64) greifen, die darin gespeichert sind:
druckt
Es gibt auch Methoden wie SetInt und SetFloat, aber um sie zu verwenden, müssen wir die Settability verstehen, das Thema des dritten Gesetzes der Reflecion, das im Folgenden behandelt wird.
Die Reflection Library verfügt über eine Reihe von Eigenschaften, die es wert sind, herausgegriffen zu werden. Erstens, um die API einfach zu halten, arbeiten die Methoden „Getter“ und „Setter“ von Value auf dem größten Typ, der den Wert halten kann: int64 für alle vorzeichenbehafteten ganzen Zahlen, zum Beispiel. Das heißt, die Int-Methode von Value gibt ein int64 zurück und der SetInt-Wert nimmt ein int64. Es kann notwendig sein, in den jeweiligen Typ zu konvertieren:
Die zweite Eigenschaft ist, dass die Art eines Reflection-Objekts den zugrunde liegenden Typ beschreibt, nicht den statischen Typ. Wenn ein Reflection-Objekt einen Wert eines benutzerdefinierten Integer-Typs enthält, wie in
die Art von v ist immer noch reflect.Int, obwohl der statische Typ von x MyInt ist, nicht int. Mit anderen Worten, die Art kann ein int nicht von einem MyInt unterscheiden, obwohl der Typ es kann.
Das zweite Gesetz der Reflexion.
- Reflection wechselt vom Reflection-Objekt zum Interface-Wert.
Wie die physische Reflecion erzeugt auch die Reflexion in Go ihren eigenen Umkehrschluss.
Mit einem reflect.value können wir einen Interface-Wert mit der Interface-Methode wiederherstellen. In der Tat packt die Methode die Typ- und Wertinformationen zurück in eine Interface-Darstellung und gibt das Ergebnis zurück:
Als Konsequenz daraus können wir sagen
um den float64-Wert zu drucken, der durch das Reflecion-Objekt v dargestellt wird.
Wir können es aber noch besser machen. Die Argumente an fmt.PrintIt, fmt.Printf usw. werden alle als leere Schnittstellenwerte übergeben, die anschließend vom fmt-Paket intern entpackt werden, wie wir es in den vorherigen Beispielen getan haben. Daher genügt es, das Ergebnis der Interface-Methode an die formatierte Druckroutine zu übergeben, um den Inhalt eines reflect.Value korrekt zu drucken:
(Warum nicht fmt.PrintIn (v)? Weil v ein reflect.value ist. Wir wollen den konkreten Wert, den es hat). Da unser Wert ein float64 ist, können wir sogar ein Gleitkommaformat verwenden, wenn wir wollen:
und in diesem Fall bekommen Sie
Auch hier ist es nicht notwendig, das Ergebnis von v.Interface() an float64 zu übergeben. Der leere Schnittstellenwert enthält die Typinformationen des konkreten Wertes und Printf wird sie wiederherstellen.
Kurz gesagt, die Interface-Methode ist die Umkehrung der ValueOf-Funktion, außer dass Ihr Ergebnis immer vom statischen Typ Interface {} ist.
Wiederholend: Reflection wechselt von Interface-Werten zu Reflection-Objekten und wieder zurück.
Das dritte Gesetz der Reflection.
- Um ein Reflection-Objekt zu modifizieren, muss der Wert einstellbar sein.
Das dritte Gesetz ist das subtileste und verwirrendste, aber es ist leicht genug zu verstehen, wenn wir von den ersten Prinzipien ausgehen.
Hier ist etwas Code, der nicht funktioniert, aber es wert ist, analysiert zu werden.
Wenn Sie diesen Code ausführen, kommt es zu einer kryptischen Meldung (Panic).
Das Problem ist nicht, dass der Wert 7.1 nicht adressierbar ist. Es ist, dass v nicht einstellbar ist. Die Setzungsfähigkeit ist eine Eigenschaft eines Reflexionswertes, und nicht alle Reflexionswerte haben ihn.
Die CanSet-Methode Value meldet die Einstellbarkeit eines Wertes. In unserem Fall:
druckt
Es ist ein Fehler, eine Set-Methode auf einem nicht einstellbaren Wert aufzurufen. Aber was ist Settability?
Die Settability ist ein wenig wie die Adressierbarkeit, aber strenger. Es ist die Eigenschaft, dass ein Reflection-Objekt den tatsächlichen Speicher modifizieren kann, der zum Erstellen des Reflection-Objekts verwendet wurde. Die Settability wird dadurch bestimmt, ob das Reflecion-Objekt das ursprüngliche Element enthält. Wenn wir sagen
übergeben wir eine Kopie von x an reflect. ValueOf, so dass der Interface-Wert, der als Argument für reflect.ValueOf erzeugt wird, eine Kopie von x ist, nicht x selbst. Wenn also die Anweisung
erfolgreich sein durfte, würde es x nicht aktualisieren, obwohl v so aussieht, als wäre es von x erstellt worden. Stattdessen würde es die Kopie von x aktualisieren, die innerhalb des Reflexionswertes gespeichert ist, und x selbst wäre davon unberührt. Das wäre verwirrend und nutzlos, also ist es verboten, und die Settability ist die Eigenschaft, mit der dieses Problem vermieden werden soll.
Wenn das bizarr erscheint, ist es das nicht. Es ist eigentlich eine vertraute Situation in ungewöhnlichem Gewand. Denken Sie daran, x an eine Funktion zu übergeben:
Wir würden nicht erwarten, dass f in der Lage sein wird, x zu ändern, weil wir eine Kopie des Wertes von x übergeben haben, nicht x selbst. Wenn wir f x direkt ändern wollen, müssen wir unserer Funktion die Adresse von x übergeben (d.h. einen Pointer auf x):
Das ist einfach und vertraut, und die Reflection funktioniert genauso. Wenn wir x durch Reflection ändern wollen, müssen wir die Reflection Library einen Pointer auf den Wert geben, den wir ändern wollen.
Lassen Sie uns das tun. Zuerst initialisieren wir x wie gewohnt und erstellen anschließend einen Reflecion-Wert, der auf diesen zeigt, genannt p.
Der bisherige Output ist
Das Reflection-Objekt p ist nicht setzbar, aber es ist nicht p, das wir setzen wollen, es ist (in Wirklichkeit) *p. Um zu dem zu gelangen, worauf p zeigt, nennen wir die Elem-Methode von Value, die durch den Pointer indirekt wird, und speichern das Ergebnis in einer Reflexion Value genannt v:
Nun ist v ein einsetzbares Reflection-Objekt, wie der Output zeigt,
und da es x darstellt, sind wir endlich in der Lage, mit v.SetFloat den Wert von x zu ändern:
Der Output ist (wie erwartet), wie folgt
Reflection kann schwer zu verstehen sein, aber sie tut genau das, was es aussagt, auch wenn durch Reflectiontypen und -werte das Geschehen verschleiert werden kann. Denken Sie nur daran, dass Reflecion-Werte die Adresse von etwas brauchen, um das, was sie repräsentieren, zu ändern.
Strukturen.
In unserem vorheringen Beispiel war v kein Pointer selbst, es wurde nur von einem abgeleitet. Ein gängiger Weg, wie diese Situation entstehen kann, ist die Verwendung von Reflection zur Modifikation der Felder einer Struktur. Solange wir die Adresse der Struktur haben, können wir Ihre Felder ändern.
Hier ist ein einfaches Beispiel, das einen struct-Wert analysiert, t. Wir erstellen das Reflection-Objekt mit der Adresse der Struct, weil wir es später ändern wollen. Dann setzen wir typeOfT auf seinen Typ und iterieren über die Felder mit einfachen Methodenaufrufen (siehe Package reflect für Details). Beachten Sie, dass wir die Namen der Felder aus dem Strukturtyp extrahieren, aber die Felder selbst sind reguläre reflect.value-Objekte.
Der Output dieses Programms ist
Es gibt noch einen weiteren Punkt zur Einstellbarkeit, der hier im Vorbeigehen eingeführt wurde. Die Feldnamen von T sind Großbuchstaben (exportiert), da nur exportierte Felder einer Struktur setzbar sind.
Da s ein setzbares Reflection-Objekt enthält, können wir die Felder der Struktur modifizieren.
Und hier ist das Ergebnis:
Wenn wir das Programm so modifiziert hätten, dass s aus t, nicht &t erstellt wurde, würden die Aufrufe von SetInt und SetString fehlschlagen, da die Felder von t nicht einstellbar wären.
Schlußfolgerungen.
Anbei noch einmal die Wiederholungen der Gesetze der Reflection:
- Reflection wechselt vom Interface-Wert zum Reflection-Objekt.
- Reflection wechselt vom Reflection-Objekt zum Interface-Wert.
- Um ein Reflection-Projekt zu modifizieren, muss der Wert einstellbar sein.
Sobald Sie diese Gesetze verstanden haben, wird die Reflection in Go viel einfacher zu bedienen sein, obwohl sie subtil bleibt. Es ist ein leistungsfähiges Werkzeug, das mit Sorgfalt verwendet und vermieden werden sollte, es sei denn, es ist unbedingt erforderlich.
Vielen Dank für Ihren Besuch.