Hallo Habr! Ich präsentiere Ihnen die Übersetzung des Artikels
"Die Gesetze der Reflexion" vom Schöpfer der Sprache.
Reflexion ist die Fähigkeit eines Programms, seine eigene Struktur zu erkunden, insbesondere durch Typen. Dies ist eine Form der Metaprogrammierung und eine große Quelle der Verwirrung.
In Go wird Reflexion beispielsweise in den Test- und FMT-Paketen häufig verwendet. In diesem Artikel werden wir versuchen, "Magie" loszuwerden, indem wir erklären, wie Reflexion in Go funktioniert.
Typen und Schnittstellen
Da die Reflexion auf einem Typsystem basiert, aktualisieren wir unser Wissen über Typen in Go.
Go ist statisch typisiert. Jede Variable hat einen und nur einen statischen Typ, der zur Kompilierungszeit festgelegt wurde:
int, float32, *MyType, []byte
... Wenn wir deklarieren:
type MyInt int var i int var j MyInt
dann ist
i
vom Typ
int
und
j
vom Typ
MyInt
. Die Variablen
i
und
j
haben unterschiedliche statische Typen, und obwohl sie denselben Basistyp haben, können sie ohne Konvertierung nicht einander zugewiesen werden.
Eine der wichtigen Typkategorien sind Schnittstellen, bei denen es sich um feste Methodensätze handelt. Eine Schnittstelle kann einen bestimmten Wert (ohne Schnittstelle) speichern, solange dieser Wert die Methoden der Schnittstelle implementiert. Ein bekanntes Beispielpaar sind
io.Reader und io.Writer , die
Reader- und Writer-Typen aus dem
io-Paket :
Es wird gesagt, dass jeder Typ, der die
Read()
oder
Write()
-Methode mit dieser Signatur implementiert,
io.Reader
bzw.
io.Writer
implementiert. Dies bedeutet, dass eine Variable vom Typ
io.Reader
einen beliebigen Wert vom Typ Read () enthalten kann:
var r io.Reader r = os.Stdin r = bufio.NewReader(r) r = new(bytes.Buffer)
Es ist wichtig zu verstehen, dass
r
jedem Wert zugewiesen werden kann, der
io.Reader
implementiert. Go ist statisch typisiert und der statische Typ
r
ist
io.Reader
.
Ein äußerst wichtiges Beispiel für einen Schnittstellentyp ist die leere Schnittstelle:
interface{}
Es ist eine leere Menge von ∅ Methoden und wird durch einen beliebigen Wert implementiert.
Einige sagen, Go-Schnittstellen seien dynamisch typisierte Variablen, aber dies ist ein Irrtum. Sie sind statisch typisiert: Eine Variable mit einem Schnittstellentyp hat immer denselben statischen Typ, und obwohl zur Laufzeit der in der Schnittstellenvariablen gespeicherte Wert den Typ ändern kann, erfüllt dieser Wert immer die Schnittstelle. (Keine
undefined
,
NaN
oder andere Dinge, die die Programmlogik brechen.)
Dies muss verstanden werden - Reflexion und Schnittstellen sind eng miteinander verbunden.
Interne Darstellung der Schnittstelle
Russ Cox schrieb einen ausführlichen
Blog-Beitrag über das Einrichten einer Benutzeroberfläche in Go. Nicht weniger guter Artikel
ist auf Habr'e . Es ist nicht nötig, die ganze Geschichte zu wiederholen, die Hauptpunkte werden erwähnt.
Eine Schnittstellestypvariable enthält ein Paar: den der Variablen zugewiesenen spezifischen Wert und einen Typdeskriptor für diesen Wert. Genauer gesagt ist der Wert das grundlegende Datenelement, das die Schnittstelle implementiert, und der Typ beschreibt den vollständigen Typ dieses Elements. Zum Beispiel nach
var r io.Reader tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) if err != nil { return nil, err } r = tty
r
enthält schematisch ein Paar
(, ) --> (tty, *os.File)
. Beachten Sie, dass der Typ
*os.File
andere Methoden als
Read()
implementiert. Selbst wenn der Schnittstellenwert nur Zugriff auf die Read () -Methode bietet, enthält der darin enthaltene Wert alle Informationen über den Typ dieses Werts. Deshalb können wir solche Dinge tun:
var w io.Writer w = r.(io.Writer)
Der Ausdruck in dieser Zuweisung ist eine Typanweisung. es behauptet, dass das Element in
r
auch
io.Writer
implementiert, und deshalb können wir es
w
zuweisen. Nach der Zuweisung enthält
w
ein Paar
(tty, *os.File)
. Dies ist das gleiche Paar wie in
r
. Der statische Typ der Schnittstelle bestimmt, welche Methoden für die Schnittstellenvariable aufgerufen werden können, obwohl ein größerer Satz von Methoden einen bestimmten Wert enthalten kann.
Weiter können wir Folgendes tun:
var empty interface{} empty = w
und der leere Wert des leeren Feldes enthält wieder dasselbe Paar
(tty, *os.File)
. Dies ist praktisch: Eine leere Schnittstelle kann einen beliebigen Wert und alle Informationen enthalten, die wir jemals benötigen werden.
Wir brauchen hier keine Typzusicherung, da bekannt ist, dass
w
eine leere Schnittstelle erfüllt. In dem Beispiel, in dem wir den Wert vom
Reader
zum
Writer
, mussten wir explizit eine Typzusicherung verwenden, da die
Writer
Methoden keine Teilmenge der
Reader
Writer
Methoden sind. Der Versuch, einen Wert zu konvertieren, der nicht mit der Schnittstelle übereinstimmt, führt zu Panik.
Ein wichtiges Detail ist, dass ein Paar innerhalb einer Schnittstelle immer ein Formular (Wert, bestimmter Typ) hat und kein Formular (Wert, Schnittstelle) haben kann. Schnittstellen unterstützen keine Schnittstellen als Werte.
Jetzt sind wir bereit zu reflektieren.
Das erste Gesetz der Reflexion reflektieren
- Die Reflexion erstreckt sich von der Schnittstelle bis zur Reflexion des Objekts.
Auf einer grundlegenden Ebene ist Reflect nur ein Mechanismus zum Untersuchen eines Paares von Typ und Wert, das in einer Schnittstellenvariablen gespeichert ist. Zu Beginn müssen wir zwei Typen kennen:
reflect.Type
und
reflect.Value
. Diese beiden Typen bieten Zugriff auf den Inhalt der Schnittstellenvariablen und werden von den einfachen Funktionen Reflect.TypeOf () bzw. Reflect.ValueOf () zurückgegeben. Sie extrahieren Teile aus der Bedeutung der Schnittstelle. (Außerdem ist
reflect.Value
leicht zu
reflect.Type
, aber lassen Sie uns die Konzepte von
Value
und
Type
im Moment nicht mischen.)
Beginnen wir mit
TypeOf()
:
package main import ( "fmt" "reflect" ) func main() { var x float64 = 3.4 fmt.Println("type:", reflect.TypeOf(x)) }
Das Programm wird ausgegeben
type: float64
Das Programm ähnelt der Übergabe einer einfachen Variablen
float64 x
an
reflect.TypeOf()
. Sehen Sie die Schnittstelle? Und es ist -
reflect.TypeOf()
akzeptiert eine leere Schnittstelle gemäß der Funktionsdeklaration:
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 Typinformationen wiederherzustellen.
Die Funktion
reflect.ValueOf()
stellt natürlich den Wert wieder her (im Folgenden werden wir die Vorlage ignorieren und uns auf den Code konzentrieren):
var x float64 = 3.4 fmt.Println("value:", reflect.ValueOf(x).String())
wird gedruckt
value: <float64 Value>
(Wir rufen die
String()
-Methode explizit auf, da das fmt-Paket standardmäßig
reflect.Value
, um den Wert zu
reflect.Value
Wert und druckt einen bestimmten Wert.)
Sowohl
reflect.Type
als auch
reflect.Value
verfügen über viele Methoden, mit denen Sie sie untersuchen und ändern können. Ein wichtiges Beispiel ist das
reflect.Value
verfügt über eine
Type()
-Methode, die den
reflect.Value
zurückgibt.
reflect.Type
und
reflect.Value
haben eine
Kind()
-Methode, die eine Konstante
Uint, Float64, Slice
welches primitive Element gespeichert ist:
Uint, Float64, Slice
... Diese Konstanten werden in der Aufzählung im
Uint, Float64, Slice
Paket deklariert.
Value
mit Namen wie
Int()
und
Float()
ermöglichen es uns, darin enthaltene Werte (wie int64 und float64) herauszuholen:
var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) fmt.Println("kind is float64:", v.Kind() == reflect.Float64) fmt.Println("value:", v.Float())
wird gedruckt
type: float64 kind is float64: true value: 3.4
Es gibt auch Methoden wie
SetInt()
und
SetFloat()
, aber um sie zu verwenden, müssen wir die Einstellbarkeit verstehen, das Thema des dritten Reflexionsgesetzes.
Die Reflect-Bibliothek verfügt über einige Eigenschaften, die Sie hervorheben müssen.
int64
die API einfach zu halten, wirken die
int64
"getter" und "setter" auf den größten Typ, der einen Wert enthalten kann:
int64
für alle
int64
Ganzzahlen. Das heißt, die
Int()
-Methode des
Value
Werts gibt
int64
, und der
SetInt()
-Wert nimmt
int64
. Möglicherweise ist eine Konvertierung in den tatsächlichen Typ erforderlich:
var x uint8 = 'x' v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) x = uint8(v.Uint())
wird sein
type: uint8 kind is uint8: true
Hier gibt
v.Uint()
uint64
, eine explizite
uint64
ist erforderlich.
Die zweite Eigenschaft ist, dass die
Kind()
Reflektion des Objekts den Basistyp und nicht den statischen Typ beschreibt. Wenn das Reflektionsobjekt einen Wert eines benutzerdefinierten Ganzzahltyps enthält, wie in
type MyInt int var x MyInt = 7 v := reflect.ValueOf(x)
v.Kind() == reflect.Int
, obwohl der statische Typ von
x
MyInt
, nicht
int
. Mit anderen Worten,
Kind()
kann im
MyInt
Type()
int
von
MyInt
unterscheiden.
Kind
kann nur Werte von integrierten Typen akzeptieren.
Das zweite Reflexionsgesetz reflektiert
- Die Reflexion erstreckt sich vom reflektierenden Objekt zur Schnittstelle.
Wie bei der physischen Reflexion erzeugt das Reflektieren in Go das Gegenteil.
Mit
reflect.Value
können wir den Wert der Schnittstelle mithilfe der
Interface()
-Methode wiederherstellen. Die Methode packt die Typ- und Wertinformationen zurück in die Schnittstelle und gibt das Ergebnis zurück:
bvt
Als Beispiel:
y := v.Interface().(float64)
float64
den Wert von
float64
der durch das
float64
Objekt
v
.
Wir können es jedoch noch besser machen. Die Argumente in
fmt.Println()
und
fmt.Printf()
werden als leere Schnittstellen übergeben, die dann wie in den vorherigen Beispielen vom fmt-Paket intern entpackt werden. Daher ist alles, was zum korrekten Drucken des Inhalts von
reflect.Value
erforderlich ist, das
reflect.Value
des Ergebnisses der
Interface()
-Methode an die formatierte Ausgabefunktion:
fmt.Println(v.Interface())
(Warum nicht
fmt.Println(v)
? Da
v
vom Typ
reflect.Value
, möchten wir den darin enthaltenen Wert erhalten.) Da unser Wert
float64
, können wir sogar das Gleitkommaformat verwenden, wenn wir möchten:
fmt.Printf("value is %7.1e\n", v.Interface())
wird in einem bestimmten Fall ausgegeben
3.4e+00
Auch hier muss
v.Interface()
Ergebnistyp
v.Interface()
in
float64
. Ein leerer Schnittstellenwert enthält Informationen zu einem bestimmten Wert und wird von
fmt.Printf()
wiederhergestellt.
Kurz gesagt, die
Interface()
-Methode ist die Umkehrung der
ValueOf()
-Funktion, mit der Ausnahme, dass das Ergebnis immer vom statischen Typ
interface{}
.
Wiederholen: Die Reflexion erstreckt sich von Schnittstellenwerten zu Reflexionsobjekten und umgekehrt.
Drittes Gesetz der Reflexionsreflexion
- Um das Reflexionsobjekt zu ändern, muss der Wert einstellbar sein.
Das dritte Gesetz ist das subtilste und verwirrendste. Wir beginnen mit den ersten Prinzipien.
Dieser Code funktioniert nicht, verdient jedoch Aufmerksamkeit.
var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat(7.1)
Wenn Sie diesen Code ausführen, stürzt er vor Panik mit einer kritischen Meldung ab:
panic: reflect.Value.SetFloat
Das Problem ist nicht, dass das Literal
7.1
nicht angesprochen wird; Dies ist, was
v
nicht installierbar ist.
reflect.Value
ist eine Eigenschaft von
reflect.Value
, und nicht jeder
reflect.Value
hat sie.
Die
reflect.Value.CanSet()
-Methode gibt
reflect.Value.CanSet()
an. in unserem Fall:
var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("settability of v:", v.CanSet())
wird drucken:
settability of v: false
Beim Aufrufen der
Set()
-Methode für einen nicht verwalteten Wert ist ein Fehler aufgetreten. Aber was ist Installierbarkeit?
Nachhaltigkeit ist ein bisschen wie Adressierbarkeit, aber strenger. Dies ist eine Eigenschaft, bei der das Reflexionsobjekt den gespeicherten Wert ändern kann, der zum Erstellen des Reflexionsobjekts verwendet wurde. Die Nachhaltigkeit wird dadurch bestimmt, ob das Reflexionsobjekt das Quellelement oder nur eine Kopie davon enthält. Wenn wir schreiben:
var x float64 = 3.4 v := reflect.ValueOf(x)
Wir übergeben eine Kopie von
x
an
reflect.ValueOf()
, daher wird die Schnittstelle als Argument für
reflect.ValueOf()
- dies ist eine Kopie von
x
, nicht von
x
selbst. Also, wenn die Aussage:
v.SetFloat(7.1)
Wenn es ausgeführt würde, würde es
x
nicht aktualisieren, obwohl
v
aussieht, als wäre es aus
x
. Stattdessen würde er die Kopie von
x
aktualisieren, die im Wert von
v
gespeichert ist, und
x
selbst wäre nicht betroffen. Dies ist verboten, um keine Probleme zu verursachen, und die Installierbarkeit ist eine Eigenschaft, mit der ein Problem verhindert wird.
Das sollte nicht seltsam erscheinen. Dies ist eine häufige Situation in ungewöhnlichen Kleidern. Überlegen Sie, ob Sie
x
an eine Funktion übergeben
x
:
f(x)
Wir erwarten nicht, dass
f()
x
ändern kann, da wir eine Kopie des Werts von
x
, nicht
x
selbst. Wenn
f()
x
direkt ändern soll, müssen wir einen Zeiger auf
x
an unsere Funktion übergeben:
f(&x)
Dies ist unkompliziert und vertraut, und die Reflexion funktioniert ähnlich. Wenn wir
x
mithilfe der Reflexion ändern möchten, müssen wir der Reflexionsbibliothek einen Zeiger auf den Wert bereitstellen, den wir ändern möchten.
Lass es uns tun. Zuerst initialisieren wir
x
wie gewohnt und erstellen dann ein
reflect.Value p
, das darauf zeigt.
var x float64 = 3.4 p := reflect.ValueOf(&x)
wird ausgegeben
type of p: *float64
settability of p: false
Das Reflexionsobjekt
p
kann nicht gesetzt werden, aber es ist nicht das
p
, das wir setzen wollen, es ist der Zeiger
*p
. Um
Value.Elem()
, auf was
p
zeigt, rufen wir die
Value.Elem()
-Methode auf, die den Wert indirekt über den Zeiger
reflect.Value v
und das Ergebnis in
reflect.Value v
speichert:
v := p.Elem() fmt.Println("settability of v:", v.CanSet())
Jetzt ist
v
ein installierbares Objekt.
settability of v: true
und da es
x
, können wir endlich
v.SetFloat()
, um den Wert von
x
zu ändern:
v.SetFloat(7.1) fmt.Println(v.Interface()) fmt.Println(x)
Schlussfolgerung wie erwartet
7.1
7.1
Reflektieren mag schwer zu verstehen sein, aber es macht genau das, was die Sprache macht, wenn auch mit Hilfe von
reflect.Type
und
reflection.Value
reflect.Type
, der verbergen kann, was passiert. Denken Sie daran, dass
reflection.Value
die Adresse einer Variablen benötigt, um sie zu ändern.
Strukturen
In unserem vorherigen Beispiel war
v
kein Zeiger, sondern wurde nur daraus abgeleitet. Ein üblicher Weg, um diese Situation zu schaffen, besteht darin, mithilfe von Reflexion Strukturfelder zu ändern. Solange wir die Adresse der Struktur haben, können wir ihre Felder ändern.
Hier ist ein einfaches Beispiel, das den Wert der Struktur
t
analysiert. Wir erstellen ein Reflexionsobjekt mit der Adresse der Struktur, um es später zu ändern. Setzen Sie dann typeOfT auf seinen Typ und durchlaufen Sie die Felder mit einfachen Methodenaufrufen (
eine detaillierte Beschreibung finden Sie im
Paket ). Beachten Sie, dass wir Feldnamen aus dem Strukturtyp extrahieren, die Felder selbst jedoch regelmäßig
reflect.Value
.
type T struct { A int B string } t := T{23, "skidoo"} s := reflect.ValueOf(&t).Elem() typeOfT := s.Type() for i := 0; i < s.NumField(); i++ { f := s.Field(i) fmt.Printf("%d: %s %s = %v\n", i, typeOfT.Field(i).Name, f.Type(), f.Interface()) }
Das Programm wird ausgegeben
0: A int = 23
1: B string = skidoo
Ein weiterer Punkt zur Installierbarkeit wird hier gezeigt: Die Namen der
T
Felder in Großbuchstaben (exportiert), da nur exportierte Felder einstellbar sind.
Da
s
ein installierbares Reflexionsobjekt enthält, können wir das Strukturfeld ändern.
s.Field(0).SetInt(77) s.Field(1).SetString("Sunset Strip") fmt.Println("t is now", t)
Ergebnis:
t is now {77 Sunset Strip}
Wenn wir das Programm so ändern, dass
s
aus
t
anstelle von
&t
,
SetInt()
die Aufrufe von
SetInt()
und
SetString()
in Panik, da die Felder
t
nicht einstellbar wären.
Fazit
Erinnern Sie sich an die Gesetze der Reflexion:
- Die Reflexion erstreckt sich von der Schnittstelle bis zur Reflexion des Objekts.
- Die Reflexion erstreckt sich von der Reflexion eines Objekts bis zur Schnittstelle.
- Um das Reflexionsobjekt zu ändern, muss der Wert festgelegt werden.
Gepostet von
Rob Pike .