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 :
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:
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())
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.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:
bvt
A titre d'exemple:
y := v.Interface().(float64)
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)
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 .