Reflexionsgesetze in Go

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 :

 // Reader -  ,    Read(). type Reader interface { Read(p []byte) (n int, err error) } // Writer -  ,    Write(). type Writer interface { Write(p []byte) (n int, err error) } 

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:

 // TypeOf()  reflect.Type    . func TypeOf(i interface{}) Type 

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()) // v.Uint  uint64. 

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   Value. 

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:

 // Interface   v  interface{}. func (v Value) Interface() interface{} 
bvt
Als Beispiel:

 y := v.Interface().(float64) // y   float64. fmt.Println(y) 

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) //   x. fmt.Println("type of p:", p.Type()) fmt.Println("settability of p:", p.CanSet()) 

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 .

Source: https://habr.com/ru/post/de415171/


All Articles