Привет, Хабр! Представляю вашему вниманию перевод статьи «The Laws of Reflection» от создателя языка.
Рефлексия — способность программы исследовать собственную структуру, в особенности через типы. Это форма метапрограммирования и отличный источник путаницы.
В Go рефлексия широко используется, например, в пакетах test и fmt. В этой статье попытаемся избавиться от «магии», объяснив, как рефлексия работает в Go.
Так как рефлексия основывается на системе типов, давайте освежим знания о типах в Go.
Go статически типизирован. Каждая переменная имеет один и только один статический тип, зафиксированный во время компиляции:
то
Одной из важных категорий типа являются интерфейсы, которые представляют собой фиксированные множества методов. Интерфейс может хранить любое конкретное (неинтерфейсное) значение, пока это значение реализует методы интерфейса. Известной парой примеров является io.Reader и io.Writer, типы Reader и Writer из пакета io:
Говорят, что любой тип, который реализует метод
Важно понять, что
Чрезвычайно важным примером типа интерфейса является пустой интерфейс:
Он представляет собой пустое множество ∅ методов и реализуется любым значением.
Некоторые говорят, что интерфейсы Go являются переменными с динамической типизацией, но это заблуждение. Они статически типизированы: переменная с типом интерфейс всегда имеет один и тот же статический тип, и хотя во время выполнения значение, хранящееся в переменной интерфейса, может изменять тип, это значение всегда будет удовлетворять интерфейсу. (Никаких
Это надо понять — отражение и интерфейсы тесно связаны.
Russ Cox написал подробное сообщение в блоге о утройстве интерфейса в Go. Не менее хорошая статья есть на Habr'е. Здесь нет необходимости повторять всю историю, основные моменты упомянуты.
Переменная типа интерфейса хранит пару: конкретное значение, присвоенное переменной, и дескриптор типа этого значения. Точнее, значение — базовый элемент данных, который реализует интерфейс, а тип описывает полный тип этого элемента. Например, после
Выражение в этом присваивании является утверждением типа; оно утверждает, что элемент внутри
Продолжая, мы можем сделать следующее:
и пустое значение пустого поля снова будет содержать ту же пару
Нам здесь не нужно утверждение типа, потому что известно, что
Одна важная деталь заключается в том, что пара внутри интерфейса всегда имеет форму (значение, конкретный тип) и не может иметь форму (значение, интерфейс). Интерфейсы не поддерживают интерфейсы как значения.
Теперь мы готовы изучить reflect.
Первый закон
На базовом уровне reflect является всего лишь механизмом для изучения пары тип и значение, хранящейся внутри переменной интерфейса. Чтобы начать работу, есть два типа, о которых нам нужно знать:
Начнем с
Программа выведет
Программа похожа на передачу простой переменной
Когда мы вызываем
Функция
напечатает
(Мы вызываем метод
И
напечатает
Существуют также методы, такие как
Библиотека reflect имеет пару свойств, которые нужно выделить. Во-первых, чтобы API был прост, «getter» и «setter» методы
будет
Здесь
Второе свойство состоит в том, что
Второй закон
Как и физическое отражение, reflect в Go создаёт свою противоположность.
Имея
Как пример:
напечатает значение
Однако мы можем сделать еще лучше. Аргументы в
(Почему бы не
выведет в конкретном случае
Опять же, нет необходимости приводить тип результата
Короче говоря, метод
Повторим: Reflection распространяется от значений интерфейса к объектам reflection и обратно.
Третий закон
Третий закон является самым тонким и запутанным. Начинаем с первых принципов.
Вот такой код не работает, но заслуживает внимания.
Если вы запустите этот код, он упадёт с panic с критическим сообщением:
Проблема не в том, что литерал
Метод
напечатает:
Ошибка вызова метода
Устанавливаемость немного напоминает адресуемость, но строже. Это свойство, при котором reflection объект может изменить хранимое значение, которое было использовано при создании reflection объекта. Устанавливаемость определяется тем, содержит ли reflection объект исходный элемент, или только его копию. Когда мы пишем:
мы передаем копию
было бы выполнено, оно бы не обновило
Это не должно казаться странным. Это обычная ситуация в необычной одежде. Подумайте о передаче
Мы не ожидаем, что
Это прямолинейно и знакомо, и reflection работает сходно. Если мы хотим изменить
Давайте сделаем это. Сначала мы инициализируем
выведет
Reflection объект
Теперь
и поскольку он представляет
вывод, как ожидалось
Reflect может быть трудно понять, но он делает именно то, что делает язык, хотя и с помощью
В нашем предыдущем примере
Вот простой пример, который анализирует значение структуры
Программа выведет
Здесь проявляется еще один пункт об устанавливаемости: имена полей
Поскольку
Результат:
Если мы изменим программу так, чтобы
Вспомним законы reflection:
Автор Rob Pike.
Рефлексия — способность программы исследовать собственную структуру, в особенности через типы. Это форма метапрограммирования и отличный источник путаницы.
В Go рефлексия широко используется, например, в пакетах test и fmt. В этой статье попытаемся избавиться от «магии», объяснив, как рефлексия работает в Go.
Типы и Интерфейсы
Так как рефлексия основывается на системе типов, давайте освежим знания о типах в Go.
Go статически типизирован. Каждая переменная имеет один и только один статический тип, зафиксированный во время компиляции:
int, float32, *MyType, []byte
… Если мы объявляем:type MyInt int
var i int
var j MyInt
то
i
имеет тип int
и j
имеет тип MyInt
. Переменные i
и j
имеют разные статические типы и, хотя они имеют один и тот же базовый тип, они не могут быть присвоены друг другу без преобразования.Одной из важных категорий типа являются интерфейсы, которые представляют собой фиксированные множества методов. Интерфейс может хранить любое конкретное (неинтерфейсное) значение, пока это значение реализует методы интерфейса. Известной парой примеров является io.Reader и io.Writer, типы Reader и Writer из пакета 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)
}
Говорят, что любой тип, который реализует метод
Read()
или Write()
с этой сигнатурой, реализует io.Reader
или io.Writer
соответственно. Это означает, что переменная типа io.Reader
может содержать любое значение, тип которого имеет метод Read():var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
Важно понять, что
r
может присваиваться любое реализующее io.Reader
значение. Go статически типизирован, а статический тип r
— io.Reader
.Чрезвычайно важным примером типа интерфейса является пустой интерфейс:
interface{}
Он представляет собой пустое множество ∅ методов и реализуется любым значением.
Некоторые говорят, что интерфейсы Go являются переменными с динамической типизацией, но это заблуждение. Они статически типизированы: переменная с типом интерфейс всегда имеет один и тот же статический тип, и хотя во время выполнения значение, хранящееся в переменной интерфейса, может изменять тип, это значение всегда будет удовлетворять интерфейсу. (Никаких
undefined
, NaN
и прочих ломающих логику программы вещей.)Это надо понять — отражение и интерфейсы тесно связаны.
Внутреннее представление интерфейса
Russ Cox написал подробное сообщение в блоге о утройстве интерфейса в Go. Не менее хорошая статья есть на Habr'е. Здесь нет необходимости повторять всю историю, основные моменты упомянуты.
Переменная типа интерфейса хранит пару: конкретное значение, присвоенное переменной, и дескриптор типа этого значения. Точнее, значение — базовый элемент данных, который реализует интерфейс, а тип описывает полный тип этого элемента. Например, после
var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return nil, err
}
r = tty
r
содержит, схематически, пару (значение, тип) --> (tty, *os.File)
. Обратите внимание, что тип *os.File
реализует методы, отличные от Read()
; даже если значение интерфейса обеспечивает доступ только к методу Read(), значение внутри несет всю информацию о типе этого значения. Вот почему мы можем делать такие вещи:var w io.Writer
w = r.(io.Writer)
Выражение в этом присваивании является утверждением типа; оно утверждает, что элемент внутри
r
также реализует io.Writer
, и поэтому мы можем назначить его w
. После назначения w
будет содержать пару (tty, *os.File)
. Это та же пара, что и в r
. Статический тип интерфейса определяет, какие методы могут быть вызваны у интерфейсной переменной, хотя конкретное значение внутри может иметь более широкий набор методов.Продолжая, мы можем сделать следующее:
var empty interface{}
empty = w
и пустое значение пустого поля снова будет содержать ту же пару
(tty, *os.File)
. Это удобно: пустой интерфейс может содержать любое значение и всю информацию, которая нам когда-либо понадобится от него.Нам здесь не нужно утверждение типа, потому что известно, что
w
удовлетворяет пустому интерфейсу. В примере, где мы перенесли значение из Reader
в Writer
, нам нужно было явно использовать утверждение типа, потому что методы Writer
'а не являтся подмножеством Reader
'а. Попытка преобразовать значение, которое не соответствует интерфейсу, вызовет панику.Одна важная деталь заключается в том, что пара внутри интерфейса всегда имеет форму (значение, конкретный тип) и не может иметь форму (значение, интерфейс). Интерфейсы не поддерживают интерфейсы как значения.
Теперь мы готовы изучить reflect.
Первый закон отражения reflect
- Reflection распространяется от интерфейса до reflection объекта.
На базовом уровне reflect является всего лишь механизмом для изучения пары тип и значение, хранящейся внутри переменной интерфейса. Чтобы начать работу, есть два типа, о которых нам нужно знать:
reflect.Type
и reflect.Value
. Эти два типа предоставляют доступ к содержимому интерфейсной переменной и возвращаются простыми функциями, reflect.TypeOf() и reflect.ValueOf() соответственно. Они выделяют части из значения интерфейса. (Кроме того, из reflect.Value
легко получить reflect.Type
, но давайте не будем смешивать концепции Value
и Type
на данный момент.)Начнем с
TypeOf()
:package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
}
Программа выведет
type: float64
Программа похожа на передачу простой переменной
float64 x
в reflect.TypeOf()
. Вы видете интерфейс? А он есть — reflect.TypeOf()
принимает пустой интерфейс, согласно объявлению функции:// TypeOf() возвращает reflect.Type переменной в пустой интерфейс.
func TypeOf(i interface{}) Type
Когда мы вызываем
reflect.TypeOf(x)
, x
сначала сохраняется в пустом интерфейсе, который затем передается в качестве аргумента; reflect.TypeOf()
распаковывает этот пустой интерфейс для восстановления информации о типе.Функция
reflect.ValueOf()
, конечно же, восстанавливает значение (далее мы будем игнорировать шаблон и сосредоточимся на коде):var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())
напечатает
value: <float64 Value>
(Мы вызываем метод
String()
явно, потому что по умолчанию пакет fmt распаковывает в reflect.Value
и выводит конкретное значение.)И
reflect.Type
, и reflect.Value
имеют много методов, что позволяет исследовать и изменять их. Одним из важных примеров является то, что reflect.Value
имеет метод Type()
, который возвращает тип значения. reflect.Type
и reflect.Value
имеют метод Kind()
, который возвращает константу, указывающую, какой примитивный элемент хранится: Uint, Float64, Slice
… Эти константы объявлены в перечислении в пакете reflect. Методы Value
с такими именами, как Int()
и Float()
, позволяют нам вытащить значения (как int64 и float64), заключённые внутри: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())
напечатает
type: float64
kind is float64: true
value: 3.4
Существуют также методы, такие как
SetInt()
и SetFloat()
, но для их использования нам необходимо понять устанавливаемость (settability), тему третьего закона отражения.Библиотека reflect имеет пару свойств, которые нужно выделить. Во-первых, чтобы API был прост, «getter» и «setter» методы
Value
действуют на самый большой тип, который может содержать значение: int64
для всех целых чисел со знаком. То есть метод Int()
значения Value
возвращает int64
, а значение SetInt()
принимает int64
; может потребоваться преобразование в фактический тип: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.
будет
type: uint8
kind is uint8: true
Здесь
v.Uint()
вернёт uint64
, необходимо явное утверждение типа.Второе свойство состоит в том, что
Kind()
reflect объекта описывает базовый тип, а не статический тип. Если объект отражения содержит значение определяемого пользователем целочисленного типа, как вtype MyInt int
var x MyInt = 7
v := reflect.ValueOf(x) // v имеет тип Value.
v.Kind() == reflect.Int
, хотя статический тип x
является MyInt
, а не int
. Другими словами, Kind()
не может различать int
из MyInt
, в отличае от Type()
. Kind
может принимать только значения встроенных типов.Второй закон отражения reflect
- Reflection распространяется от reflect объекта до интерфейса.
Как и физическое отражение, reflect в Go создаёт свою противоположность.
Имея
reflect.Value
, мы можем восстановить значение интерфейса с помощью метода Interface()
; метод упаковывает информацию о типе и значении обратно в интерфейс и возвращает результат:// Interface вернёт значение v как interface{}.
func (v Value) Interface() interface{}
bvtКак пример:
y := v.Interface().(float64) // y имеет тип float64.
fmt.Println(y)
напечатает значение
float64
, представленного reflect объектом v
.Однако мы можем сделать еще лучше. Аргументы в
fmt.Println()
и fmt.Printf()
передаются как пустые интерфейсы, которые затем распаковываются пакетом fmt внутри, как и в предыдущих примерах. Поэтому все, что требуется для печати содержимого reflect.Value
правильно — передать результат метода Interface()
в функцию отформатированного вывода:fmt.Println(v.Interface())
(Почему бы не
fmt.Println(v)
? Потому что v
имеет тип reflect.Value
; мы же хотим получить содержащееся внутри значение.) Поскольку наше значение — float64
, мы можем даже использовать формат с плавающей запятой, если хотим:fmt.Printf("value is %7.1e\n", v.Interface())
выведет в конкретном случае
3.4e+00
Опять же, нет необходимости приводить тип результата
v.Interface()
в float64
; пустое значение интерфейса содержит информацию о конкретном значении внутри, а fmt.Printf()
восстановит его.Короче говоря, метод
Interface()
является инверсией функции ValueOf()
, за исключением того, что его результат всегда имеет статический тип interface{}
.Повторим: Reflection распространяется от значений интерфейса к объектам reflection и обратно.
Третий закон отражения reflection
- Чтобы изменить объект отражения, значение должно быть устанавливаемым.
Третий закон является самым тонким и запутанным. Начинаем с первых принципов.
Вот такой код не работает, но заслуживает внимания.
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Ошибка
Если вы запустите этот код, он упадёт с panic с критическим сообщением:
panic: reflect.Value.SetFloat использует неадресуемое значение
Проблема не в том, что литерал
7.1
не адресуется; это то, что v
не устанавливаемо. Устанавливаемость — свойство reflect.Value
, и не каждое reflect.Value
имеет его.Метод
reflect.Value.CanSet()
сообщает о устанавливаемости Value
; в нашем случае:var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())
напечатает:
settability of v: false
Ошибка вызова метода
Set()
на неустанавливаемом значении. Но что такое устанавливаемость?Устанавливаемость немного напоминает адресуемость, но строже. Это свойство, при котором reflection объект может изменить хранимое значение, которое было использовано при создании reflection объекта. Устанавливаемость определяется тем, содержит ли reflection объект исходный элемент, или только его копию. Когда мы пишем:
var x float64 = 3.4
v := reflect.ValueOf(x)
мы передаем копию
x
в reflect.ValueOf()
, поэтому интерфейс создается как аргумент для reflect.ValueOf()
— это копия x
, а не сам x
. Таким образом, если бы утверждение:v.SetFloat(7.1)
было бы выполнено, оно бы не обновило
x
, хотя v
выглядит так, как будто оно было создано из x
. Вместо этого он обновил бы копию x
, хранящуюся внутри значения v
, a сам x
не был бы затронут. Это запрещено, что бы не порождать проблем, а устанавливаемость — свойство, используемое для предотвращения проблемы.Это не должно казаться странным. Это обычная ситуация в необычной одежде. Подумайте о передаче
x
в функцию:f(x)
Мы не ожидаем, что
f()
сможет изменить x
, потому что мы передали копию значения x
, а не сам x
. Если мы хотим, чтобы f()
непосредственно меняла x
, мы должны передать нашей функции указатель на x
:f(&x)
Это прямолинейно и знакомо, и reflection работает сходно. Если мы хотим изменить
x
с помощью reflection, мы должны предоставить библиотеке reflection указатель на значение, которое мы хотим изменить.Давайте сделаем это. Сначала мы инициализируем
x
как обычно, а затем создаем reflect.Value p
, которое указывает на него.var x float64 = 3.4
p := reflect.ValueOf(&x) // Берём адрес x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())
выведет
type of p: *float64
settability of p: false
Reflection объект
p
не может быть установлен, но это не p
, который мы хотим установить, это указатель *p
. Чтобы получить то, на что указывает p
, мы вызываем метод Value.Elem()
, который берёт значение косвенным образом через указатель, и сохраняем результат в reflect.Value v
:v := p.Elem()
fmt.Println("settability of v:", v.CanSet())
Теперь
v
является устанавливаемым объектом, резутьтат:settability of v: true
и поскольку он представляет
x
, мы, наконец, можем использовать v.SetFloat()
для изменения значения x
:v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)
вывод, как ожидалось
7.1
7.1
Reflect может быть трудно понять, но он делает именно то, что делает язык, хотя и с помощью
reflect.Type
и reflection.Value
, которые могут скрывать, что происходит. Просто имейте в виду, что reflection.Value
нужен адрес переменной, чтобы изменить её.Структуры
В нашем предыдущем примере
v
не был указателем, он был просто получен из него. Общим способом возникновения этой ситуации является использование reflection для изменения полей структуры. До тех пор, пока у нас есть адрес структуры, мы можем изменять её поля.Вот простой пример, который анализирует значение структуры
t
. Мы создаем reflection объект с адресом структуры, чтобы изменять его позже. Затем устанавливаем typeOfT в его тип и итерируемся по полям, используя простые вызовы методов (см. Подробное описание пакета). Обратите внимание, что мы извлекаем имена полей из типа структуры, но сами поля являются обычными 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())
}
Программа выведет
0: A int = 23
1: B string = skidoo
Здесь проявляется еще один пункт об устанавливаемости: имена полей
T
в верхнем регистре (экспортируемы), потому что только экспортируемые поля устанавливаемы.Поскольку
s
содержит устанавливаемый reflection объект, мы можем изменить поле структуры.s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)
Результат:
t is now {77 Sunset Strip}
Если мы изменим программу так, чтобы
s
был создан из t
, а не &t
, вызовы SetInt()
и SetString()
завершились бы паникой, поскольку поля t
не были бы устанавливаемыми.Заключение
Вспомним законы reflection:
- Reflection распространяется от интерфейса до reflection объекта.
- Reflection распространяется от reflection объекта до интерфейса.
- Чтобы изменить reflection объект, значение должно быть устанавливаемым.
Автор Rob Pike.