Leyes de reflexión en Go

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 :

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

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:

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

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

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

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:

 // Interface   v  interface{}. func (v Value) Interface() interface{} 
bvt
Como un ejemplo:

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

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

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 .

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


All Articles