The Go Blog

Законы отражения

Роб Пайк
6 сентября 2011

Введение

Отражение (reflection) в вычислительной технике — это способность программы изучать собственную структуру, особенно через типы; это форма метапрограммирования. Это также отличный источник путаницы.

В этой статье мы пытаемся прояснить ситуацию, объяснив, как работает отражение в Go. Модель отражения каждой языковой среды различна (и многие языки вообще не поддерживают отражение), но эта статья посвящена Go, поэтому для остальной части статьи слово «отражение» должно пониматься как «отражение в Go».

Примечание, добавленное в январе 2022 года: Эта статья была написана в 2011 году и предшествует параметрической полиморфности (также известной как дженерики) в Go. Хотя ничего важного в статье не стало неверным в результате этого развития языка, в ней были внесены некоторые изменения, чтобы избежать путаницы для тех, кто знаком с современным Go.

Типы и интерфейсы

Поскольку отражение строится на системе типов, начнем с повторения основных понятий о типах в Go.

Go — это язык со статической типизацией. У каждой переменной есть статический тип, то есть точно один тип, известный и фиксированный во время компиляции: int, float32, *MyType, []byte и так далее. Если мы объявим

<code>type MyInt int
var i int
var j MyInt
</code>

то i имеет тип int, а j имеет тип MyInt. Переменные i и j имеют разные статические типы, и хотя они имеют одинаковый базовый тип, их нельзя присвоить друг другу без преобразования.

Одной из важных категорий типов являются интерфейсные типы, которые представляют собой фиксированные наборы методов. (При обсуждении отражения можно игнорировать использование определений интерфейсов как ограничений внутри полиморфного кода.) Переменная интерфейса может хранить любое конкретное (не интерфейсное) значение, если только это значение реализует методы интерфейса. Хорошо известная пара примеров — io.Reader и io.Writer, типы Reader и Writer из пакета io:

<code>// Reader — это интерфейс, который оборачивает базовый метод Read.
type Reader interface {
  Read(p []byte) (n int, err error)
}
// Writer — это интерфейс, который оборачивает базовый метод Write.
type Writer interface {
  Write(p []byte) (n int, err error)
}
</code>

Любой тип, реализующий метод Read (или Write) с этой сигнатурой, считается реализующим io.Reader (или io.Writer). В целях этого обсуждения это означает, что переменная типа io.Reader может содержать любое значение, тип которого имеет метод Read:

<code>var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// и так далее
</code>

Важно понимать, что независимо от того, какое конкретное значение может хранить r, тип r всегда будет io.Reader: Go является статически типизированным языком, и статический тип rio.Reader.

Очень важным примером типа интерфейса является пустой интерфейс:

<code>interface{}
</code>

или его эквивалентный псевдоним,

<code>any
</code>

Он представляет собой пустое множество методов и удовлетворяется любым значением, поскольку у каждого значения есть ноль или более методов.

Некоторые люди говорят, что интерфейсы Go являются динамически типизированными, но это вводит в заблуждение. Они являются статически типизированными: переменная типа интерфейс всегда имеет один и тот же статический тип, и даже если во время выполнения значение, хранимое в переменной интерфейса, может менять тип, это значение всегда будет удовлетворять интерфейсу.

Нам нужно быть точными во всём этом, потому что рефлексия и интерфейсы тесно связаны.

Представление интерфейса

Расс Ко́кс написал подробную статью в блоге о представлении значений интерфейсов в Go. Нет необходимости повторять всю историю здесь, но будет уместно краткое резюме.

Переменная типа интерфейс хранит пару: конкретное значение, присвоенное переменной, и дескриптор типа этого значения. Более точно, значение — это базовый конкретный элемент данных, реализующий интерфейс, а тип описывает полный тип этого элемента. Например, после

<code>var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
  return nil, err
}
r = tty
</code>

r содержит, условно, пару (tty, *os.File). Обратите внимание, что тип *os.File реализует методы, отличные от Read; несмотря на то, что значение интерфейса предоставляет доступ только к методу Read, значение внутри содержит всю информацию о типе этого значения. Поэтому мы можем делать такие вещи:

<code>var w io.Writer
w = r.(io.Writer)
</code>

Выражение в этой инструкции — это утверждение типа; оно утверждает, что элемент внутри r также реализует io.Writer, и поэтому мы можем присвоить его переменной w. После присвоения w будет содержать пару (tty, *os.File). Это та же пара, что и была в r. Статический тип интерфейса определяет, какие методы могут быть вызваны с использованием переменной интерфейса, даже если конкретное значение внутри может иметь более широкий набор методов.

Продолжая, мы можем сделать следующее:

<code>var empty interface{}
empty = w
</code>

и наше значение пустого интерфейса empty снова будет содержать ту же пару, (tty, *os.File). Это удобно: пустой интерфейс может хранить любое значение и содержит всю информацию, которую мы когда-либо могли бы нуждаться об этом значении.

Здесь утверждение типа не требуется, поскольку статически известно, что w удовлетворяет пустому интерфейсу. В примере, где мы переместили значение из Reader в Writer, нам пришлось быть явными и использовать утверждение типа, потому что методы Writer не являются подмножеством методов Reader.

Одним из важных моментов является то, что пара внутри переменной интерфейса всегда имеет вид (значение, конкретный тип) и не может иметь вид (значение, интерфейсный тип). Интерфейсы не содержат интерфейсных значений.

Теперь мы готовы перейти к отражению.

Первый закон отражения

1. Отражение переходит от значения интерфейса к объекту отражения.

В базовом понимании, отражение — это просто механизм для проверки типа и пары значений, хранящихся внутри переменной интерфейса. Для начала необходимо знать два типа из пакета reflect: Type и Value. Эти два типа предоставляют доступ к содержимому переменной интерфейса, а две простые функции, называемые reflect.TypeOf и reflect.ValueOf, извлекают из значения интерфейса части типа reflect.Type и reflect.Value. (Кроме того, из reflect.Value легко получить соответствующий reflect.Type, но пока давайте сохраним концепции Value и Type отдельными.)

Начнем с TypeOf:

<code>package main
import (
  "fmt"
  "reflect"
)
func main() {
  var x float64 = 3.4
  fmt.Println("type:", reflect.TypeOf(x))
}
</code>

Эта программа выводит

<code>type: float64
</code>

Может возникнуть вопрос, где же интерфейс в этом примере, ведь программа выглядит так, как будто передается переменная float64 x, а не значение интерфейса, в функцию reflect.TypeOf. Но оно там есть; как указано в godoc, сигнатура функции reflect.TypeOf включает пустой интерфейс:

<code>// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type
</code>

Когда мы вызываем reflect.TypeOf(x), сначала значение x сохраняется в пустой интерфейс, который затем передается как аргумент; reflect.TypeOf распаковывает этот пустой интерфейс, чтобы восстановить информацию о типе.

Функция reflect.ValueOf, конечно же, восстанавливает значение (далее мы будем опускать стандартный шаблон и сосредоточимся только на исполняемом коде):

<code>var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())
</code>

выводит

<code>value: <float64 Value>
</code>

(Мы явно вызываем метод String, поскольку по умолчанию пакет fmt раскрывает reflect.Value, чтобы показать внутреннее конкретное значение. Метод String этого не делает.)

Как reflect.Type, так и reflect.Value содержат множество методов, позволяющих нам исследовать и манипулировать ими. Одним из важных примеров является то, что у Value есть метод Type, который возвращает Type объекта reflect.Value. Другой пример — это то, что и Type, и Value имеют метод Kind, который возвращает константу, указывающую, какой тип элемента хранится: Uint, Float64, Slice и так далее. Также методы у Value с именами вроде Int и Float позволяют извлекать значения (в виде int64 и float64), которые хранятся внутри:

<code>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())
</code>

выводит

<code>type: float64
kind is float64: true
value: 3.4
</code>

Существуют также методы вроде SetInt и SetFloat, но чтобы использовать их, необходимо понимать возможность присваивания (settability), тему третьего закона отражения, о котором говорится ниже.

В библиотеке отражения есть несколько свойств, которые стоит выделить. Во-первых, чтобы сохранить простоту API, методы «getter» и «setter» у Value работают с наибольшим типом, который может содержать значение: например, int64 для всех знаковых целых чисел. То есть метод Int у Value возвращает int64, а метод SetInt принимает int64; возможно, потребуется преобразование к фактическому типу:

<code>var x uint8 = 'x'
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())                            // uint8.
fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
x = uint8(v.Uint())                                       // v.Uint возвращает uint64.
</code>

Второе свойство заключается в том, что Kind объекта отражения описывает базовый тип, а не статический тип. Если объект отражения содержит значение пользовательского целочисленного типа, как в

<code>type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
</code>

то Kind объекта v по-прежнему будет reflect.Int, несмотря на то, что статический тип xMyInt, а не int. Иными словами, Kind не может отличить int от MyInt, даже если Type может.

Второе правило отражения

2. Отражение идёт от объекта отражения к значению интерфейса.

Как и физическое отражение, отражение в Go порождает собственную обратную операцию.

Имея reflect.Value, можно восстановить значение интерфейса с помощью метода Interface; по сути, метод упаковывает информацию о типе и значении обратно в интерфейсную форму и возвращает результат:

<code>// Interface возвращает значение v как interface{}.
func (v Value) Interface() interface{}
</code>

В результате мы можем написать

<code>y := v.Interface().(float64) // y будет иметь тип float64.
fmt.Println(y)
</code>

для печати значения float64, представленного объектом отражения v.

Можно сделать ещё лучше. Аргументы функций fmt.Println, fmt.Printf и т. д. передаются все как пустые значения интерфейсов, которые затем распаковываются пакетом fmt внутренне так же, как мы делали это в предыдущих примерах. Следовательно, всё, что требуется для корректной печати содержимого reflect.Value, — это передать результат метода Interface в форматированную функцию печати:

<code>fmt.Println(v.Interface())
</code>

(Поскольку эта статья была написана впервые, в пакет fmt была внесена правка, так что он автоматически распаковывает reflect.Value подобным образом, поэтому можно было бы просто написать

<code>fmt.Println(v)
</code>

для получения того же результата, но ради ясности мы оставим вызовы .Interface() здесь.)

Поскольку наше значение имеет тип float64, мы даже можем использовать формат с плавающей точкой, если захотим:

<code>fmt.Printf("value is %7.1e\n", v.Interface())
</code>

и получить в этом случае

<code>3.4e+00
</code>

Опять же, нет необходимости утверждать тип результата v.Interface() как float64; значение пустого интерфейса содержит внутри информацию о типе конкретного значения, и Printf восстановит его.

Короче говоря, метод Interface является обратным к функции ValueOf, за исключением того, что его результат всегда имеет статический тип interface{}.

Повторим: отражение работает по пути от значений интерфейсов к объектам отражения и обратно.

Третье правило отражения

3. Чтобы изменить объект отражения, значение должно быть устанавливаемым.

Третье правило является самым тонким и запутанным, но его легко понять, если начать с основ.

Вот код, который не работает, но заслуживает внимательного изучения.

<code>var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Ошибка: вызов panic.
</code>

Если выполнить этот код, произойдет паника с загадочным сообщением

<code>panic: reflect.Value.SetFloat using unaddressable value
</code>

Проблема не в том, что значение 7.1 не является адресуемым; проблема в том, что v не является устанавливаемым. Свойство устанавливаемости является свойством отражения Value, и не все значения отражения Value обладают этим свойством.

Метод CanSet типа Value сообщает о возможности установки значения Value; в нашем случае,

<code>var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())
</code>

выводит

<code>settability of v: false
</code>

Вызов метода Set для недженерикового Value является ошибкой. Но что такое settability (возможность установки)?

Settability (возможность установки) похожа на addressability (возможность получения адреса), но более строгая. Это свойство отражённого объекта, которое позволяет изменять фактическое хранилище, использованное для создания отражённого объекта. Settability определяется тем, содержит ли отражённый объект исходный элемент. Когда мы говорим

<code>var x float64 = 3.4
v := reflect.ValueOf(x)
</code>

мы передаём копию x в reflect.ValueOf, поэтому интерфейсное значение, созданное как аргумент для reflect.ValueOf, является копией x, а не самим x. Следовательно, если бы инструкция

<code>v.SetFloat(7.1)
</code>

была разрешена, она не обновила бы x, несмотря на то, что v выглядит так, будто была создана из x. Вместо этого она обновила бы копию x, хранящуюся внутри отражённого значения, и x остался бы неизменным. Это было бы запутанным и бесполезным, поэтому это запрещено, и settability — это свойство, используемое для предотвращения такой ситуации.

Если это кажется странным, то это не так. Это на самом деле знакомая ситуация, но в необычном виде. Подумайте о передаче x в функцию:

<code>f(x)
</code>

Мы не ожидаем, что f сможет изменить x, потому что мы передали копию значения x, а не сам x. Если мы хотим, чтобы f напрямую изменяла x, необходимо передать функции адрес x (то есть указатель на x):

<code>f(&x)
</code>

Это просто и знакомо, и отражение работает так же. Если мы хотим изменить x с помощью отражения, необходимо передать библиотеке отражения указатель на значение, которое мы хотим изменить.

Сделаем это. Сначала инициализируем x обычным образом, а затем создаём отражённое значение, которое указывает на него, назовём его p.

<code>var x float64 = 3.4
p := reflect.ValueOf(&x) // Примечание: берём адрес x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())
</code>

Вывод до сих пор выглядит так:

<code>type of p: *float64
settability of p: false
</code>

Отражённый объект p не является settable (не может быть установлен), но мы не хотим установить p, мы хотим установить (по сути) *p. Чтобы получить доступ к тому, на что указывает p, мы вызываем метод Elem типа Value, который разыменовывает указатель, и сохраняем результат в отражённом Value под названием v:

<code>v := p.Elem()
fmt.Println("settability of v:", v.CanSet())
</code>

Теперь v является settable отражённым объектом, как показывает вывод,

<code>settability of v: true
</code>

и поскольку он представляет x, мы наконец можем использовать v.SetFloat для изменения значения x:

<code>v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)
</code>

Вывод, как и ожидалось, выглядит следующим образом

<code>7.1
7.1
</code>

Отражение (reflection) может быть трудным для понимания, но оно делает именно то же самое, что и язык программирования, только через отражение Types и Values, которые могут скрывать происходящее. Просто помните, что отражение Values требует адрес чего-либо, чтобы изменить то, что они представляют.

Структуры

В нашем предыдущем примере v сам по себе не был указателем, он просто был получен из указателя. Часто такая ситуация возникает при использовании отражения для изменения полей структуры. Пока у нас есть адрес структуры, мы можем изменять её поля.

Вот простой пример, который анализирует значение структуры t. Мы создаём объект отражения с адресом структуры, потому что позже планируем его изменить. Затем мы устанавливаем typeOfT в её тип и перебираем поля с использованием простых методов (см. пакет reflect для подробностей). Обратите внимание, что мы извлекаем имена полей из типа структуры, но сами поля представляют собой обычные объекты reflect.Value.

<code>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())
}
</code>

Вывод этого программного кода:

<code>0: A int = 23
1: B string = skidoo
</code>

Ещё один момент, касающийся возможности установки значений, который был упомянут здесь: имена полей T записаны с заглавной буквы (экспортированы), поскольку только экспортированные поля структуры могут быть изменены.

Поскольку s содержит изменяемый объект отражения, мы можем изменять поля структуры.

<code>s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)
</code>

И вот результат:

<code>t is now {77 Sunset Strip}
</code>

Если бы мы изменили программу так, чтобы s создавался из t, а не из &t, вызовы SetInt и SetString завершились бы ошибкой, поскольку поля t не были бы изменяемыми.

Заключение

Вот снова законы отражения:

Как только вы освоите эти законы, отражение в Go становится намного проще в использовании, хотя оно остаётся тонким. Это мощный инструмент, который следует использовать с осторожностью и избегать, если это не строго необходимо.

В отражении есть ещё многое, о чём мы не говорили — отправка и получение по каналам, выделение памяти, работа со срезами и картами, вызов методов и функций — но эта статья уже достаточно длинная. Мы рассмотрим некоторые из этих тем в следующих статьях.

Следующая статья: Пакет Go image
Предыдущая статья: Две презентации по Go: "Лексический анализ в Go" и "Cuddle: демо для App Engine"
Индекс блога

GoRu.dev Golang на русском

На сайте представлена адаптированная под русский язык документация языка программирования Golang