Olá Habr! Apresento a você a tradução do artigo
"As Leis da Reflexão" do criador da língua.
Reflexão é a capacidade de um programa explorar sua própria estrutura, especialmente através de tipos. Esta é uma forma de metaprogramação e uma grande fonte de confusão.
No Go, a reflexão é amplamente usada, por exemplo, nos pacotes test e fmt. Neste artigo, tentaremos nos livrar da "mágica" explicando como a reflexão funciona no Go.
Tipos e interfaces
Como a reflexão é baseada em um sistema de tipos, vamos atualizar nosso conhecimento de tipos no Go.
Go é digitado estaticamente. Cada variável possui um e apenas um tipo estático fixo em tempo de compilação:
int, float32, *MyType, []byte
... Se declararmos:
type MyInt int var i int var j MyInt
então
i
é do tipo
int
e
j
é do tipo
MyInt
. As variáveis iej têm tipos estáticos diferentes e, embora tenham o mesmo tipo básico, não podem ser atribuídas uma à outra sem conversão.
Uma das categorias de tipo importantes são as interfaces, que são conjuntos fixos de métodos. Uma interface pode armazenar qualquer valor específico (sem interface), desde que esse valor implemente os métodos da interface. Um par conhecido de exemplos é
io.Reader e io.Writer , os tipos Reader e Writer do
pacote io :
Diz-se que qualquer tipo que implemente o método
Read()
ou
Write()
com esta assinatura implementa
io.Reader
ou
io.Writer
respectivamente. Isso significa que uma variável do tipo
io.Reader
pode conter qualquer valor do tipo Read ():
var r io.Reader r = os.Stdin r = bufio.NewReader(r) r = new(bytes.Buffer)
É importante entender que
r
pode ser atribuído a qualquer valor que implemente
io.Reader
. Go é digitado estaticamente e o tipo estático
r
é
io.Reader
.
Um exemplo extremamente importante de um tipo de interface é a interface vazia:
interface{}
É um conjunto vazio de métodos e é implementado por qualquer valor.
Alguns dizem que as interfaces Go são variáveis de tipo dinâmico, mas isso é uma falácia. Eles são digitados estaticamente: uma variável com um tipo de interface sempre tem o mesmo tipo estático e, embora em tempo de execução, o valor armazenado na variável da interface possa alterar o tipo, esse valor sempre satisfará a interface. (Não é
undefined
,
NaN
ou outras coisas que quebram a lógica do programa.)
Isso deve ser entendido - a reflexão e as interfaces estão intimamente relacionadas.
Representação interna da interface
Russ Cox escreveu um
post detalhado sobre como configurar uma interface no Go. Nenhum artigo menos bom
é sobre Habr'e . Não há necessidade de repetir toda a história, os principais pontos são mencionados.
Uma variável de tipo de interface contém um par: o valor específico atribuído à variável e um descritor de tipo para esse valor. Mais precisamente, o valor é o elemento de dados básico que implementa a interface e o tipo descreve o tipo completo desse elemento. Por exemplo, depois
var r io.Reader tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) if err != nil { return nil, err } r = tty
r
contém, esquematicamente, um par
(, ) --> (tty, *os.File)
. Observe que o tipo
*os.File
implementa métodos diferentes de
Read()
; mesmo que o valor da interface forneça acesso apenas ao método Read (), o valor interno carregará todas as informações sobre o tipo desse valor. É por isso que podemos fazer essas coisas:
var w io.Writer w = r.(io.Writer)
A expressão nesta atribuição é uma declaração de tipo; afirma que o elemento dentro de
r
também implementa
io.Writer
e, portanto, podemos atribuí-lo a
w
. Uma vez atribuído,
w
conterá um par
(tty, *os.File)
. Este é o mesmo par que em
r
. O tipo estático da interface determina quais métodos podem ser chamados na variável da interface, embora um conjunto mais amplo de métodos possa ter um valor específico.
Continuando, podemos fazer o seguinte:
var empty interface{} empty = w
e o valor vazio do campo vazio conterá novamente o mesmo par
(tty, *os.File)
. Isso é conveniente: uma interface vazia pode conter qualquer valor e todas as informações que precisaremos dela.
Não precisamos de uma asserção de tipo aqui, porque é sabido que
w
satisfaz uma interface vazia. No exemplo em que transferimos o valor do
Reader
para o
Writer
, precisamos usar explicitamente uma asserção de tipo, porque
Writer
métodos
Writer
não são um subconjunto dos do
Reader
. Tentar converter um valor que não corresponda à interface causará pânico.
Um detalhe importante é que um par dentro de uma interface sempre tem um formulário (valor, tipo específico) e não pode ter um formulário (valor, interface). As interfaces não suportam interfaces como valores.
Agora estamos prontos para estudar refletir.
A primeira lei da reflexão reflete
- A reflexão se estende da interface para a reflexão do objeto.
Em um nível básico, refletir é apenas um mecanismo para examinar um par de tipo e valor armazenado dentro de uma variável de interface. Para começar, existem dois tipos que precisamos conhecer:
reflect.Type
e
reflect.Value
. Esses dois tipos fornecem acesso ao conteúdo da variável de interface e são retornados por funções simples, reflect.TypeOf () e reflect.ValueOf (), respectivamente. Eles extraem partes do significado da interface. (Além disso,
reflect.Value
fácil de obter
reflect.Type
, mas não vamos misturar os conceitos de
Value
e
Type
no momento.)
Vamos começar com
TypeOf()
:
package main import ( "fmt" "reflect" ) func main() { var x float64 = 3.4 fmt.Println("type:", reflect.TypeOf(x)) }
O programa produzirá
type: float64
O programa é semelhante a passar uma variável simples
float64 x
para
reflect.TypeOf()
. Você vê a interface? E é -
reflect.TypeOf()
aceita uma interface vazia, de acordo com a declaração da função:
Quando chamamos
reflect.TypeOf(x)
,
x
armazenado primeiro em uma interface vazia, que é passada como argumento;
reflect.TypeOf()
descompacta essa interface vazia para restaurar informações de tipo.
A função
reflect.ValueOf()
, é claro, restaura o valor (a seguir ignoraremos o modelo e focaremos no código):
var x float64 = 3.4 fmt.Println("value:", reflect.ValueOf(x).String())
irá imprimir
value: <float64 Value>
(Chamamos o método
String()
explicitamente porque, por padrão, o pacote fmt é descompactado para
reflect.Value
e imprime um valor específico.)
Ambos
reflect.Type
e
reflect.Value
têm muitos métodos, o que permite explorar e modificá-los. Um exemplo importante é que
reflect.Value
possui um método
Type()
que retorna o tipo de valor.
reflect.Type
e
reflect.Value
têm um método
Kind()
que retorna uma constante indicando qual elemento primitivo está armazenado:
Uint, Float64, Slice
... Essas constantes são declaradas na enumeração no pacote reflect. Métodos de
Value
com nomes como
Int()
e
Float()
nos permitem extrair valores (como int64 e float64) colocados 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())
irá imprimir
type: float64 kind is float64: true value: 3.4
Existem também métodos como
SetInt()
e
SetFloat()
, mas para usá-los, precisamos entender a configurabilidade, o tópico da terceira lei da reflexão.
A biblioteca de reflexão possui algumas propriedades que você precisa destacar. Primeiro, para manter a API simples, os métodos
Value
"getter" e "setter" atuam no maior tipo que pode conter um valor:
int64
para todos os números inteiros
int64
. Ou seja, o método
Int()
do valor
Value
retorna
int64
e o valor
SetInt()
recebe
int64
; pode ser necessária a conversão para o tipo real:
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
Aqui
v.Uint()
retornará
uint64
, uma declaração de tipo explícita é necessária.
A segunda propriedade é que o
Kind()
reflete o objeto descreve o tipo base, não o tipo estático. Se o objeto de reflexão contiver um valor de um tipo inteiro definido pelo usuário, como em
type MyInt int var x MyInt = 7 v := reflect.ValueOf(x)
v.Kind() == reflect.Int
, embora o tipo estático de
x
seja
MyInt
, não
int
. Em outras palavras,
Kind()
não pode distinguir
int
de
MyInt
,
MyInt
Type()
.
Kind
só pode aceitar valores de tipos internos.
A segunda lei da reflexão reflete
- A reflexão se estende do objeto de reflexão para a interface.
Como a reflexão física, refletir em Go cria seu oposto.
Tendo
reflect.Value
, podemos restaurar o valor da interface usando o método
Interface()
; O método empacota as informações de tipo e valor de volta na interface e retorna o resultado:
bvt
Como exemplo:
y := v.Interface().(float64)
imprime o valor de
float64
representado pelo objeto de reflexão
v
.
No entanto, podemos fazer ainda melhor. Os argumentos em
fmt.Println()
e
fmt.Printf()
são transmitidos como interfaces vazias, que são descompactadas internamente pelo pacote fmt, como nos exemplos anteriores. Portanto, tudo o que é necessário para imprimir o conteúdo de
reflect.Value
corretamente é passar o resultado do método
Interface()
para a função de saída formatada:
fmt.Println(v.Interface())
(Por que não
fmt.Println(v)
? Como
v
é do tipo
reflect.Value
; queremos obter o valor contido
float64
.) Como nosso valor é
float64
, podemos usar o formato de ponto flutuante, se desejar:
fmt.Printf("value is %7.1e\n", v.Interface())
produzirá em um caso específico
3.4e+00
Novamente, não há necessidade de
v.Interface()
tipo de resultado
v.Interface()
em
float64
; um valor de interface vazio contém informações sobre um valor específico, e
fmt.Printf()
restaurará.
Em resumo, o método
Interface()
é o inverso da função
ValueOf()
, exceto que seu resultado é sempre da
interface{}
tipo estático
interface{}
.
Repita: a reflexão se estende dos valores da interface aos objetos de reflexão e vice-versa.
Terceira lei da reflexão reflexão
- Para alterar o objeto de reflexão, o valor deve ser configurável.
A terceira lei é a mais sutil e confusa. Começamos com os primeiros princípios.
Este código não funciona, mas merece atenção.
var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat(7.1)
Se você executar esse código, ele entrará em pânico com uma mensagem crítica:
panic: reflect.Value.SetFloat
O problema não é que o literal
7.1
não
7.1
abordado; é isso que
v
não
v
instalável.
reflect.Value
é uma propriedade de
reflect.Value
, e nem todos os
reflect.Value
possuem.
O método
reflect.Value.CanSet()
está sendo definido; no nosso caso:
var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("settability of v:", v.CanSet())
irá imprimir:
settability of v: false
Ocorreu um erro ao chamar o método
Set()
em um valor não gerenciado. Mas o que é instalabilidade?
A sustentabilidade é um pouco como endereçamento, mas mais rigorosa. Esta é uma propriedade em que o objeto de reflexão pode alterar o valor armazenado que foi usado para criar o objeto de reflexão. A sustentabilidade é determinada se o objeto de reflexão contém o elemento de origem ou apenas uma cópia dele. Quando escrevemos:
var x float64 = 3.4 v := reflect.ValueOf(x)
passamos uma cópia de
x
para
reflect.ValueOf()
, para que a interface seja criada como um argumento para
reflect.ValueOf()
- esta é uma cópia de
x
, não
x
si. Assim, se a afirmação:
v.SetFloat(7.1)
se fosse executado, não atualizaria
x
, embora
v
pareça que ele foi criado a partir de
x
. Em vez disso, ele atualizaria a cópia de
x
armazenada dentro do valor de
v
ex não seria afetado. Isso é proibido para não causar problemas, e a instalabilidade é uma propriedade usada para evitar um problema.
Isso não deve parecer estranho. Esta é uma situação comum em roupas incomuns. Considere passar
x
para uma função:
f(x)
Não esperamos que
f()
possa alterar
x
, porque passamos uma cópia do valor de
x
, e não
x
si. Se queremos que
f()
mude diretamente
x
, devemos passar um ponteiro para
x
para nossa função:
f(&x)
Isso é direto e familiar, e a reflexão funciona da mesma forma. Se queremos alterar
x
usando reflexão, devemos fornecer à biblioteca de reflexão um ponteiro para o valor que queremos alterar.
Vamos fazer isso. Primeiro, inicializamos
x
como de costume e depois criamos um
reflect.Value p
que aponta para ele.
var x float64 = 3.4 p := reflect.ValueOf(&x)
irá produzir
type of p: *float64
settability of p: false
O objeto Reflection
p
não pode ser definido, mas não é o
p
que queremos definir, é o ponteiro
*p
. Para obter o que
p
aponta, chamamos o método
Value.Elem()
, que leva o valor indiretamente pelo ponteiro e armazena o resultado em
reflect.Value v
:
v := p.Elem() fmt.Println("settability of v:", v.CanSet())
Agora
v
é um objeto instalável;
settability of v: true
e como representa
x
, podemos finalmente usar
v.SetFloat()
para alterar o valor de
x
:
v.SetFloat(7.1) fmt.Println(v.Interface()) fmt.Println(x)
conclusão como esperado
7.1
7.1
Refletir pode ser difícil de entender, mas faz exatamente o que a linguagem faz, embora com a ajuda de
reflection.Value
.
reflect.Type
e
reflection.Value
, que pode ocultar o que está acontecendo. Lembre-se de que o
reflection.Value
precisa do endereço de uma variável para alterá-lo.
Estruturas
No nosso exemplo anterior,
v
não
v
um ponteiro, apenas derivava dele. Uma maneira comum de criar essa situação é usar a reflexão para alterar os campos da estrutura. Enquanto tivermos o endereço da estrutura, podemos alterar seus campos.
Aqui está um exemplo simples que analisa o valor da estrutura
t
. Criamos um objeto de reflexão com o endereço da estrutura para modificá-lo posteriormente. Em seguida, defina typeOfT para seu tipo e itere sobre os campos usando chamadas de método simples (consulte o
pacote para obter uma descrição detalhada ). Observe que estamos extraindo nomes de campos do tipo de estrutura, mas os próprios campos são regulares
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()) }
O programa produzirá
0: A int = 23
1: B string = skidoo
Mais um ponto sobre a instalabilidade é mostrado aqui: os nomes dos campos
T
em maiúsculas (exportados), porque apenas os campos exportados são configuráveis.
Como
s
contém um objeto de reflexão instalável, podemos alterar o campo da estrutura.
s.Field(0).SetInt(77) s.Field(1).SetString("Sunset Strip") fmt.Println("t is now", t)
Resultado:
t is now {77 Sunset Strip}
Se mudarmos o programa para que
s
criado de
t
vez de
&t
, as chamadas para
SetInt()
e
SetString()
terminariam em pânico, uma vez que os campos
t
não seriam configuráveis.
Conclusão
Lembre-se das leis da reflexão:
- A reflexão se estende da interface para a reflexão do objeto.
- A reflexão se estende da reflexão de um objeto à interface.
- Para alterar o objeto de reflexão, o valor deve ser definido.
Postado por
Rob Pike .