Leis de reflexão em Go

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 :

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

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:

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

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

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

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:

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

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

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

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 .

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


All Articles