Hola Habr! Les presento la traducción del artículo
"Las leyes de la reflexión" del creador del lenguaje.
La reflexión es la capacidad de un programa para explorar su propia estructura, especialmente a través de los tipos. Esta es una forma de metaprogramación y una gran fuente de confusión.
En Go, la reflexión se usa ampliamente, por ejemplo, en los paquetes test y fmt. En este artículo, intentaremos deshacernos de la "magia" explicando cómo funciona la reflexión en Go.
Tipos e interfaces
Dado que la reflexión se basa en un sistema de tipos, vamos a actualizar nuestro conocimiento de los tipos en Go.
Go está estáticamente escrito. Cada variable tiene un único tipo estático fijo en tiempo de compilación:
int, float32, *MyType, []byte
... Si declaramos:
type MyInt int var i int var j MyInt
entonces
i
es de tipo
int
y
j
es de tipo
MyInt
. Las variables
i
y
j
tienen diferentes tipos estáticos y, aunque tienen el mismo tipo básico, no pueden asignarse entre sí sin conversión.
Una de las categorías de tipo importantes son las interfaces, que son conjuntos fijos de métodos. Una interfaz puede almacenar cualquier valor específico (no interfaz) siempre que este valor implemente los métodos de la interfaz. Un par de ejemplos bien conocidos son
io.Reader y io.Writer , los tipos Reader y Writer del
paquete io :
Se dice que cualquier tipo que implemente el método
Read()
o
Write()
con esta firma implementa
io.Reader
o
io.Writer
respectivamente. Esto significa que una variable de tipo
io.Reader
puede contener cualquier valor de tipo Read ():
var r io.Reader r = os.Stdin r = bufio.NewReader(r) r = new(bytes.Buffer)
Es importante comprender que a
r
se le puede asignar cualquier valor que implemente
io.Reader
. Go se escribe estáticamente, y el tipo estático
r
es
io.Reader
.
Un ejemplo extremadamente importante de un tipo de interfaz es la interfaz vacía:
interface{}
Es un conjunto vacío de métodos ∅ y se implementa con cualquier valor.
Algunos dicen que las interfaces Go son variables de tipo dinámico, pero esto es una falacia. Están tipificados estáticamente: una variable con un tipo de interfaz siempre tiene el mismo tipo estático, y aunque en el tiempo de ejecución el valor almacenado en la variable de interfaz puede cambiar el tipo, este valor siempre satisfará la interfaz. (No hay elementos
undefined
,
NaN
u otras cosas que rompan la lógica del programa).
Esto debe entenderse: la reflexión y las interfaces están estrechamente relacionadas.
Representación interna de la interfaz.
Russ Cox escribió una
publicación de blog detallada sobre la configuración de una interfaz en Go. No menos buen artículo
es sobre Habr'e . No es necesario repetir toda la historia, se mencionan los puntos principales.
Una variable de tipo de interfaz contiene un par: el valor específico asignado a la variable y un descriptor de tipo para ese valor. Más precisamente, el valor es el elemento de datos básico que implementa la interfaz, y el tipo describe el tipo completo de este elemento. Por ejemplo, después
var r io.Reader tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) if err != nil { return nil, err } r = tty
r
contiene, esquemáticamente, un par
(, ) --> (tty, *os.File)
. Tenga en cuenta que el tipo
*os.File
implementa métodos distintos de
Read()
; incluso si el valor de la interfaz proporciona acceso solo al método Read (), el valor dentro contiene toda la información sobre el tipo de este valor. Es por eso que podemos hacer tales cosas:
var w io.Writer w = r.(io.Writer)
La expresión en esta asignación es una declaración de tipo; afirma que el elemento dentro de
r
también implementa
io.Writer
, y por lo tanto podemos asignarlo a
w
. Una vez asignado,
w
contendrá un par
(tty, *os.File)
. Este es el mismo par que en
r
. El tipo estático de la interfaz determina qué métodos se pueden invocar en la variable de la interfaz, aunque un conjunto más amplio de métodos puede tener un valor específico en su interior.
Continuando, podemos hacer lo siguiente:
var empty interface{} empty = w
y el valor vacío del campo vacío nuevamente contendrá el mismo par
(tty, *os.File)
. Esto es conveniente: una interfaz vacía puede contener cualquier valor y toda la información que necesitemos de ella.
No necesitamos una aserción de tipo aquí porque se sabe que
w
satisface una interfaz vacía. En el ejemplo donde transferimos el valor de
Reader
a
Writer
, necesitábamos usar explícitamente una aserción de tipo, porque
Writer
métodos
Writer
no son un subconjunto de
Reader
's. Intentar convertir un valor que no coincida con la interfaz provocará pánico.
Un detalle importante es que un par dentro de una interfaz siempre tiene un formulario (valor, tipo específico) y no puede tener un formulario (valor, interfaz). Las interfaces no admiten interfaces como valores.
Ahora estamos listos para estudiar reflexionar.
La primera ley de reflexión refleja
- La reflexión se extiende desde la interfaz hasta la reflexión del objeto.
En un nivel básico, reflexionar es solo un mecanismo para examinar un par de tipos y valores almacenados dentro de una variable de interfaz. Para comenzar, hay dos tipos que debemos conocer:
reflect.Type
y
reflect.Value
. Estos dos tipos proporcionan acceso a los contenidos de la variable de interfaz y son devueltos por funciones simples, reflect.TypeOf () y reflect.ValueOf (), respectivamente. Extraen partes del significado de la interfaz. (Además,
reflect.Value
fácil de obtener
reflect.Type
, pero no
reflect.Type
los conceptos de
Value
y
Type
en este momento).
Comencemos con
TypeOf()
:
package main import ( "fmt" "reflect" ) func main() { var x float64 = 3.4 fmt.Println("type:", reflect.TypeOf(x)) }
El programa saldrá
type: float64
El programa es similar a pasar una variable simple
float64 x
para
reflect.TypeOf()
.
reflect.TypeOf()
. ¿Ves la interfaz? Y lo es:
reflect.TypeOf()
acepta una interfaz vacía, de acuerdo con la declaración de función:
Cuando llamamos a
reflect.TypeOf(x)
,
x
almacena primero en una interfaz vacía, que luego se pasa como argumento;
reflect.TypeOf()
desempaqueta esta interfaz vacía para restaurar la información de tipo.
La función
reflect.ValueOf()
, por supuesto, restaura el valor (en adelante ignoraremos la plantilla y nos centraremos en el código):
var x float64 = 3.4 fmt.Println("value:", reflect.ValueOf(x).String())
imprimirá
value: <float64 Value>
(Llamamos al método
String()
explícitamente porque, por defecto, el paquete fmt se desempaqueta para
reflect.Value
e imprime un valor específico).
Tanto
reflect.Type
como
reflect.Value
tienen muchos métodos, que le permiten explorarlos y modificarlos. Un ejemplo importante es que
reflect.Value
tiene un método
Type()
que devuelve el tipo de valor.
reflect.Type
y
reflect.Value
tienen un método
Kind()
que devuelve una constante que indica qué elemento primitivo está almacenado:
Uint, Float64, Slice
... Estas constantes se declaran en la enumeración en el paquete
Uint, Float64, Slice
.
Value
métodos de
Value
con nombres como
Int()
y
Float()
nos permiten extraer valores (como int64 y float64) encerrados dentro:
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())
imprimirá
type: float64 kind is float64: true value: 3.4
También hay métodos como
SetInt()
y
SetFloat()
, pero para usarlos necesitamos comprender la capacidad de configuración, el tema de la tercera ley de reflexión.
La biblioteca de reflejos tiene un par de propiedades que debe resaltar. Primero, para mantener la API simple, los métodos de
Value
"getter" y "setter" actúan sobre el tipo más grande que puede contener un valor:
int64
para todos los enteros con
int64
. Es decir, el método
Int()
del valor
Value
devuelve
int64
y el valor
SetInt()
toma
int64
; La conversión al tipo real puede ser necesaria:
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())
será
type: uint8 kind is uint8: true
Aquí
v.Uint()
devolverá
uint64
, se necesita una declaración de tipo explícito.
La segunda propiedad es que el reflejo
Kind()
del objeto describe el tipo base, no el tipo estático. Si el objeto de reflexión contiene un valor de un tipo entero definido por el usuario, como en
type MyInt int var x MyInt = 7 v := reflect.ValueOf(x)
v.Kind() == reflect.Int
, aunque el tipo estático de
x
es
MyInt
, no
int
. En otras palabras,
Kind()
no puede distinguir
int
de
MyInt
, a
MyInt
Type()
.
Kind
solo puede aceptar valores de tipos incorporados.
La segunda ley de reflexión refleja
- La reflexión se extiende desde el objeto reflejado hasta la interfaz.
Al igual que la reflexión física, reflexionar en Go crea su opuesto.
Habiendo
reflect.Value
, podemos restaurar el valor de la interfaz usando el método
Interface()
; El método empaqueta la información de tipo y valor en la interfaz y devuelve el resultado:
bvt
Como un ejemplo:
y := v.Interface().(float64)
imprime el valor de
float64
representado por el objeto reflejado
v
.
Sin embargo, podemos hacerlo aún mejor. Los argumentos en
fmt.Println()
y
fmt.Printf()
se pasan como interfaces vacías, que luego el paquete fmt desempaqueta internamente, como en los ejemplos anteriores. Por lo tanto, todo lo que se requiere para imprimir el contenido de
reflect.Value
correctamente es pasar el resultado del método
Interface()
a la función de salida formateada:
fmt.Println(v.Interface())
(¿Por qué no
fmt.Println(v)
? Debido a que
v
es de tipo
reflect.Value
; queremos obtener el valor que contiene). Dado que nuestro valor es
float64
, incluso podemos usar el formato de coma flotante si queremos:
fmt.Printf("value is %7.1e\n", v.Interface())
saldrá en un caso específico
3.4e+00
Nuevamente, no es necesario
v.Interface()
tipo de resultado
v.Interface()
en
float64
; un valor de interfaz vacío contiene información sobre un valor específico en su interior, y
fmt.Printf()
restaurará.
En resumen, el método
Interface()
es el inverso de la función
ValueOf()
, excepto que su resultado es siempre de la
interface{}
tipo estático
interface{}
.
Repita: la reflexión se extiende desde los valores de la interfaz hasta los objetos de reflexión y viceversa.
Tercera ley de reflexión reflexión
- Para cambiar el objeto de reflexión, el valor debe ser configurable.
La tercera ley es la más sutil y confusa. Comenzamos con los primeros principios.
Este código no funciona, pero merece atención.
var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat(7.1)
Si ejecuta este código, se bloqueará del pánico con un mensaje crítico:
panic: reflect.Value.SetFloat
El problema no es que no se aborde el literal
7.1
; Esto es lo que
v
no
v
instalable.
reflect.Value
es una propiedad de
reflect.Value
, y no todo
reflect.Value
tiene.
El método
reflect.Value.CanSet()
se establece; en nuestro caso:
var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("settability of v:", v.CanSet())
imprimirá:
settability of v: false
Se produjo un error al llamar al método
Set()
en un valor no administrado. Pero, ¿qué es la instalabilidad?
La sostenibilidad es un poco como la direccionabilidad, pero más estricta. Esta es una propiedad donde el objeto de reflexión puede cambiar el valor almacenado que se usó para crear el objeto de reflexión. La sostenibilidad está determinada por si el objeto de reflexión contiene el elemento fuente o solo una copia del mismo. Cuando escribimos:
var x float64 = 3.4 v := reflect.ValueOf(x)
le pasamos una copia de
x
a
reflect.ValueOf()
, por lo que la interfaz se crea como un argumento para
reflect.ValueOf()
: esta es una copia de
x
, no
x
sí misma. Por lo tanto, si la declaración:
v.SetFloat(7.1)
si se ejecutara, no actualizaría
x
, aunque
v
parece que se creó a partir de
x
. En cambio, actualizaría la copia de
x
almacenada dentro del valor de
v
, y la
x
misma no se vería afectada. Esto está prohibido para no causar problemas, y la instalabilidad es una propiedad utilizada para evitar un problema.
Esto no debería parecer extraño. Esta es una situación común en ropa inusual. Considere pasar
x
a una función:
f(x)
No esperamos que
f()
pueda cambiar
x
, porque pasamos una copia del valor de
x
, no
x
sí. Si queremos que
f()
cambie directamente
x
, debemos pasar un puntero a
x
a nuestra función:
f(&x)
Esto es sencillo y familiar, y la reflexión funciona de manera similar. Si queremos cambiar
x
usando la reflexión, debemos proporcionar a la biblioteca de reflexión un puntero al valor que queremos cambiar.
Hagámoslo Primero, inicializamos
x
como de costumbre, y luego creamos un
reflect.Value p
que apunta a él.
var x float64 = 3.4 p := reflect.ValueOf(&x)
saldrá
type of p: *float64
settability of p: false
El objeto Reflection
p
no se puede establecer, pero no es la
p
que queremos establecer, es el puntero
*p
. Para obtener lo que señala
p
, llamamos al método
Value.Elem()
, que toma el valor indirectamente a través del puntero y almacena el resultado en
reflect.Value v
:
v := p.Elem() fmt.Println("settability of v:", v.CanSet())
Ahora
v
es un objeto instalable;
settability of v: true
y dado que representa
x
, finalmente podemos usar
v.SetFloat()
para cambiar el valor de
x
:
v.SetFloat(7.1) fmt.Println(v.Interface()) fmt.Println(x)
conclusión como se esperaba
7.1
7.1
Reflexionar puede ser difícil de entender, pero hace exactamente lo que hace el lenguaje, aunque con la ayuda de
reflection.Value
reflect.Type
y
reflection.Value
reflect.Type
, que puede ocultar lo que está sucediendo. Solo tenga en cuenta esa
reflection.Value
necesita la dirección de una variable para cambiarlo.
Estructuras
En nuestro ejemplo anterior,
v
no
v
un puntero, solo se derivaba de él. Una forma común de crear esta situación es usar la reflexión para cambiar los campos de la estructura. Mientras tengamos la dirección de la estructura, podemos cambiar sus campos.
Aquí hay un ejemplo simple que analiza el valor de la estructura
t
. Creamos un objeto de reflexión con la dirección de la estructura para modificarlo más tarde. Luego establezca typeOfT en su tipo e itere sobre los campos utilizando llamadas a métodos simples (consulte el
paquete para obtener una descripción detallada ). Tenga en cuenta que estamos extrayendo nombres de campo del tipo de estructura, pero los campos en sí mismos son
reflect.Value
regulares.
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()) }
El programa saldrá
0: A int = 23
1: B string = skidoo
Aquí se muestra un punto más sobre la instalabilidad: los nombres de los campos
T
en mayúsculas (exportados), porque solo los campos exportados son configurables.
Como
s
contiene un objeto de reflexión instalable, podemos cambiar el campo de estructura.
s.Field(0).SetInt(77) s.Field(1).SetString("Sunset Strip") fmt.Println("t is now", t)
Resultado:
t is now {77 Sunset Strip}
Si cambiamos el programa para que
s
cree desde
t
lugar de
&t
, las llamadas a
SetInt()
y
SetString()
terminarían en pánico, ya que los campos
t
no serían configurables.
Conclusión
Recordemos las leyes de la reflexión:
- La reflexión se extiende desde la interfaz hasta la reflexión del objeto.
- La reflexión se extiende desde la reflexión de un objeto hasta la interfaz.
- Para cambiar el objeto de reflexión, se debe establecer el valor.
Publicado por
Rob Pike .