Lois de réflexion dans Go

Bonjour, Habr! Je vous présente la traduction de l'article "Les lois de la réflexion" du créateur de la langue.

La réflexion est la capacité d'un programme à explorer sa propre structure, en particulier à travers les types. Il s'agit d'une forme de métaprogrammation et d'une grande source de confusion.
Dans Go, la réflexion est largement utilisée, par exemple, dans les packages test et fmt. Dans cet article, nous allons essayer de nous débarrasser de la «magie» en expliquant comment fonctionne la réflexion dans Go.

Types et interfaces


La réflexion étant basée sur un système de types, rafraîchissons nos connaissances sur les types dans Go.
Go est tapé de façon statique. Chaque variable a un et un seul type statique fixé au moment de la compilation: int, float32, *MyType, []byte ... Si nous déclarons:

 type MyInt int var i int var j MyInt 

alors i est de type int et j est de type MyInt . Les variables i et j ont des types statiques différents et, bien qu'elles aient le même type de base, elles ne peuvent pas être affectées l'une à l'autre sans conversion.

L'une des catégories de types importantes sont les interfaces, qui sont des ensembles de méthodes fixes. Une interface peut stocker n'importe quelle valeur spécifique (non-interface) tant que cette valeur implémente les méthodes de l'interface. Une paire bien connue d'exemples est io.Reader et io.Writer , les types Reader et Writer du package io :

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

Il est dit que tout type qui implémente la méthode Read() ou Write() avec cette signature implémente respectivement io.Reader ou io.Writer . Cela signifie qu'une variable de type io.Reader peut contenir n'importe quelle valeur de type Read ():

 var r io.Reader r = os.Stdin r = bufio.NewReader(r) r = new(bytes.Buffer) 

Il est important de comprendre que r peut recevoir n'importe quelle valeur qui implémente io.Reader . Go est de type statique et le type statique r est io.Reader .

Un exemple extrêmement important d'un type d'interface est l'interface vide:

 interface{} 

Il s'agit d'un ensemble vide de méthodes ∅ et est implémenté par n'importe quelle valeur.
Certains disent que les interfaces Go sont des variables typées dynamiquement, mais c'est une erreur. Ils sont typés statiquement: une variable avec un type d'interface a toujours le même type statique, et bien qu'au moment de l'exécution la valeur stockée dans la variable d'interface puisse changer le type, cette valeur satisfera toujours l'interface. (Pas d' undefined , de NaN ou d'autres choses qui cassent la logique du programme.)

Cela doit être compris - la réflexion et les interfaces sont étroitement liées.

Représentation interne de l'interface


Russ Cox a écrit un article de blog détaillé sur la configuration d'une interface dans Go. Pas moins bon article est sur Habr'e . Il n'est pas nécessaire de répéter toute l'histoire, les principaux points sont mentionnés.

Une variable de type d'interface contient une paire: la valeur spécifique affectée à la variable et un descripteur de type pour cette valeur. Plus précisément, la valeur est l'élément de données de base qui implémente l'interface, et le type décrit le type complet de cet élément. Par exemple, après

 var r io.Reader tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) if err != nil { return nil, err } r = tty 

r contient, schématiquement, une paire (, ) --> (tty, *os.File) . Notez que le type *os.File implémente des méthodes autres que Read() ; même si la valeur d'interface ne donne accès qu'à la méthode Read (), la valeur à l'intérieur contient toutes les informations sur le type de cette valeur. C'est pourquoi nous pouvons faire de telles choses:

 var w io.Writer w = r.(io.Writer) 

L'expression dans cette affectation est une instruction de type; il prétend que l'élément à l'intérieur de r implémente également io.Writer , et donc nous pouvons l'assigner à w . Une fois attribué, w contiendra une paire (tty, *os.File) . C'est la même paire qu'en r . Le type statique de l'interface détermine quelles méthodes peuvent être appelées sur la variable d'interface, bien qu'un ensemble plus large de méthodes puisse avoir une valeur spécifique à l'intérieur.

En continuant, nous pouvons faire ce qui suit:

 var empty interface{} empty = w 

et la valeur vide du champ vide contiendra à nouveau la même paire (tty, *os.File) . C'est pratique: une interface vide peut contenir n'importe quelle valeur et toutes les informations dont nous aurons besoin.

Nous n'avons pas besoin d'une assertion de type ici, car il est connu que w satisfait une interface vide. Dans l'exemple où nous avons transféré la valeur de Reader à Writer , nous devions utiliser explicitement une assertion de type, car Writer méthodes Writer ne sont pas un sous-ensemble de Reader . Tenter de convertir une valeur qui ne correspond pas à l'interface provoquera la panique.

Un détail important est qu'une paire à l'intérieur d'une interface a toujours une forme (valeur, type spécifique) et ne peut pas avoir de forme (valeur, interface). Les interfaces ne prennent pas en charge les interfaces en tant que valeurs.

Nous sommes maintenant prêts à étudier la réflexion.

La première loi de réflexion reflète


  • La réflexion s'étend de l'interface à la réflexion de l'objet.

Au niveau de base, la réflexion n'est qu'un mécanisme pour examiner une paire de type et de valeur stockée dans une variable d'interface. Pour commencer, il y a deux types que nous devons connaître: reflect.Type et reflect.Value . Ces deux types donnent accès au contenu de la variable d'interface et sont retournés par des fonctions simples, reflect.TypeOf () et reflect.ValueOf (), respectivement. Ils extraient des parties de la signification de l'interface. (De plus, reflect.Value facile à obtenir reflect.Type , mais ne mélangeons pas les concepts de Value et de Type pour le moment.)

Commençons par TypeOf() :

 package main import ( "fmt" "reflect" ) func main() { var x float64 = 3.4 fmt.Println("type:", reflect.TypeOf(x)) } 

Le programme affichera
type: float64

Le programme est similaire à la transmission d'une variable simple float64 x à reflect.TypeOf() . Voyez-vous l'interface? Et c'est - reflect.TypeOf() accepte une interface vide, selon la déclaration de fonction:

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

Lorsque nous appelons reflect.TypeOf(x) , x abord stocké dans une interface vide, qui est ensuite passée en argument; reflect.TypeOf() décompresse cette interface vide pour restaurer les informations de type.

La fonction reflect.ValueOf() , bien sûr, restaure la valeur (ci-après, nous ignorerons le modèle et nous concentrerons sur le code):

 var x float64 = 3.4 fmt.Println("value:", reflect.ValueOf(x).String()) 

imprimera
value: <float64 Value>
(Nous appelons la méthode String() explicitement car, par défaut, le package fmt se reflect.Value pour reflect.Value et imprime une valeur spécifique.)
Les deux reflect.Type et reflect.Value ont de nombreuses méthodes qui vous permettent de les explorer et de les modifier. Un exemple important est celui de reflect.Value qui a une méthode Type() qui renvoie le type de valeur. reflect.Type et reflect.Value ont une méthode Kind() qui retourne une constante indiquant quel élément primitif est stocké: Uint, Float64, Slice ... Ces constantes sont déclarées dans l'énumération dans le package reflect. Value méthodes de Value avec des noms comme Int() et Float() nous permettent d'extraire des valeurs (comme int64 et float64) enfermées à l'intérieur:

 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()) 

imprimera

 type: float64 kind is float64: true value: 3.4 

Il existe également des méthodes telles que SetInt() et SetFloat() , mais pour les utiliser, nous devons comprendre la settability, le sujet de la troisième loi de la réflexion.

La bibliothèque de réflexion a quelques propriétés que vous devez mettre en évidence. Premièrement, pour garder l'API simple, les méthodes Value «getter» et «setter» agissent sur le plus grand type pouvant contenir une valeur: int64 pour tous les entiers int64 . Autrement dit, la méthode Int() de la valeur Value renvoie int64 et la valeur SetInt() prend int64 ; une conversion en type réel peut être nécessaire:

 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. 

sera

 type: uint8 kind is uint8: true 

Ici, v.Uint() renverra uint64 , une instruction de type explicite est nécessaire.

La deuxième propriété est que la réflexion Kind() de l'objet décrit le type de base, pas le type statique. Si l'objet de réflexion contient une valeur d'un type entier défini par l'utilisateur, comme dans

 type MyInt int var x MyInt = 7 v := reflect.ValueOf(x) // v   Value. 

v.Kind() == reflect.Int , bien que le type statique de x soit MyInt , pas int . En d'autres termes, Kind() ne peut pas distinguer int de MyInt , MyInt Type() . Kind ne peut accepter que les valeurs des types intégrés.

La deuxième loi de la réflexion reflète


  • La réflexion s'étend de l'objet réfléchi à l'interface.

Comme la réflexion physique, la réflexion dans Go crée son contraire.

Ayant reflect.Value , nous pouvons restaurer la valeur de l'interface en utilisant la méthode Interface() ; La méthode regroupe les informations de type et de valeur dans l'interface et renvoie le résultat:

 // Interface   v  interface{}. func (v Value) Interface() interface{} 
bvt
A titre d'exemple:

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

float64 la valeur de float64 représentée par reflet object v .
Mais nous pouvons faire encore mieux. Les arguments de fmt.Println() et fmt.Printf() sont passés en tant fmt.Printf() vides, qui sont ensuite décompressées par le package fmt en interne, comme dans les exemples précédents. Par conséquent, tout ce qui est nécessaire pour imprimer correctement le contenu de reflect.Value est de transmettre le résultat de la méthode Interface() à la fonction de sortie formatée:

 fmt.Println(v.Interface()) 

(Pourquoi pas fmt.Println(v) ? Parce que v est de type reflect.Value ; nous voulons obtenir la valeur contenue à l'intérieur.) Puisque notre valeur est float64 , nous pouvons même utiliser le format à virgule flottante si nous voulons:

 fmt.Printf("value is %7.1e\n", v.Interface()) 

affichera dans un cas spécifique
3.4e+00

Encore une fois, il n'est pas nécessaire de v.Interface() type de résultat v.Interface() dans float64 ; une valeur d'interface vide contient des informations sur une valeur spécifique à l'intérieur, et fmt.Printf() restaurera.
En bref, la méthode Interface() est l'inverse de la fonction ValueOf() , sauf que son résultat est toujours du type interface{} statique interface{} .

Répéter: la réflexion s'étend des valeurs d'interface aux objets de réflexion et vice versa.

Troisième loi de réflexion réflexion


  • Pour modifier l'objet de réflexion, la valeur doit être réglable.

La troisième loi est la plus subtile et la plus déroutante. Nous commençons par les premiers principes.
Ce code ne fonctionne pas, mais il mérite attention.

 var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat(7.1) //  

Si vous exécutez ce code, il se bloquera de panique avec un message critique:
panic: reflect.Value.SetFloat
Le problème n'est pas que le littéral 7.1 pas traité; c'est ce que v pas installable. reflect.Value est une propriété de reflect.Value , et pas tous les reflect.Value ont.
La méthode reflect.Value.CanSet() définie; dans notre cas:

 var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("settability of v:", v.CanSet()) 

imprimera:
settability of v: false

Une erreur s'est produite lors de l'appel de la méthode Set() sur une valeur non gérée. Mais qu'est-ce que l'installabilité?

La durabilité est un peu comme l'adressabilité, mais plus stricte. Il s'agit d'une propriété dans laquelle l'objet de réflexion peut modifier la valeur stockée utilisée pour créer l'objet de réflexion. La durabilité est déterminée par le fait que l'objet de réflexion contient l'élément source ou seulement une copie de celui-ci. Quand on écrit:

 var x float64 = 3.4 v := reflect.ValueOf(x) 

nous transmettons une copie de x à reflect.ValueOf() , de sorte que l'interface est créée en tant qu'argument pour reflect.ValueOf() - c'est une copie de x , pas x lui-même. Ainsi, si la déclaration:

 v.SetFloat(7.1) 

s'il était exécuté, il ne mettrait pas à jour x , bien que v semble avoir été créé à partir de x . Au lieu de cela, il mettrait à jour la copie de x stockée dans la valeur de v , et x lui-même ne serait pas affecté. Ceci est interdit afin de ne pas causer de problèmes, et l'installabilité est une propriété utilisée pour éviter un problème.

Cela ne devrait pas paraître étrange. Il s'agit d'une situation courante dans les vêtements inhabituels. Pensez à passer x à une fonction:
f(x)

Nous ne nous attendons pas à ce que f() puisse changer x , car nous avons passé une copie de la valeur de x , pas x lui-même. Si nous voulons que f() change directement x , nous devons passer un pointeur vers x à notre fonction:
f(&x)

C'est simple et familier, et la réflexion fonctionne de la même manière. Si nous voulons changer x utilisant la réflexion, nous devons fournir à la bibliothèque de réflexion un pointeur sur la valeur que nous voulons changer.

Faisons-le. Tout d'abord, nous initialisons x comme d'habitude, puis créons un reflect.Value p qui pointe vers lui.

 var x float64 = 3.4 p := reflect.ValueOf(&x) //   x. fmt.Println("type of p:", p.Type()) fmt.Println("settability of p:", p.CanSet()) 

affichera
type of p: *float64
settability of p: false


L'objet Reflection p ne peut pas être défini, mais ce n'est pas le p que nous voulons définir, c'est le pointeur *p . Pour obtenir ce vers quoi p pointe, nous appelons la méthode Value.Elem() , qui prend la valeur indirectement via le pointeur et stocke le résultat dans reflect.Value v :

 v := p.Elem() fmt.Println("settability of v:", v.CanSet()) 

Maintenant, v est un objet installable;
settability of v: true
et comme il représente x , nous pouvons enfin utiliser v.SetFloat() pour changer la valeur de x :

 v.SetFloat(7.1) fmt.Println(v.Interface()) fmt.Println(x) 

conclusion comme prévu
7.1
7.1

Réfléchir peut être difficile à comprendre, mais il fait exactement ce que fait le langage, bien qu'avec l'aide de reflection.Value . reflect.Type et reflection.Value . reflect.Type , ce qui peut cacher ce qui se passe. Gardez à l'esprit cette reflection.Value besoin de l'adresse d'une variable pour la changer.

Structures


Dans notre exemple précédent, v pas un pointeur, il en était juste dérivé. Une manière courante de créer cette situation consiste à utiliser la réflexion pour modifier les champs de structure. Tant que nous avons l'adresse de la structure, nous pouvons changer ses champs.

Voici un exemple simple qui analyse la valeur de la structure t . Nous créons un objet de réflexion avec l'adresse de la structure afin de la modifier ultérieurement. Ensuite, définissez typeOfT sur son type et parcourez les champs à l'aide d'appels de méthode simples (voir le package pour une description détaillée ). Notez que nous reflect.Value noms de champs du type de structure, mais les champs eux-mêmes sont des reflect.Value réguliers.

 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()) } 

Le programme affichera
0: A int = 23
1: B string = skidoo

Un autre point sur l'installabilité est affiché ici: les noms des champs T en majuscules (exportés), car seuls les champs exportés sont paramétrables.
Puisque s contient un objet de réflexion installable, nous pouvons changer le champ de structure.

 s.Field(0).SetInt(77) s.Field(1).SetString("Sunset Strip") fmt.Println("t is now", t) 

Résultat:
t is now {77 Sunset Strip}
Si nous changeons le programme pour que s créé à partir de t plutôt que &t , les appels à SetInt() et SetString() se termineraient en panique, car les champs t ne seraient pas paramétrables.

Conclusion


Rappelons les lois de la réflexion:

  • La réflexion s'étend de l'interface à la réflexion de l'objet.
  • La réflexion s'étend de la réflexion d'un objet à l'interface.
  • Pour modifier l'objet de réflexion, la valeur doit être définie.

Publié par Rob Pike .

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


All Articles