среда, 3 июня 2020 г.

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

Роб Пайк

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

В этой статье мы попытаемся прояснить ситуацию, объяснив, как работает отражение в Go. Модель отражения каждого языка различна (и многие языки вообще ее не поддерживают), но эта статья посвящена Go, поэтому в остальной части этой статьи слово "отражение" (reflection) следует понимать как "отражение в 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 ни содержало, тип r всегда равен io.Reader: Go имеет статическую типизацию, а статический тип r - io.Reader.

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

interface{}

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

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

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

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

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

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

и наше пустое значение интерфейса 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:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var x float64 = 3.4
    fmt.Println("type:", reflect.TypeOf(x))
}

Эта программа печатает

type: float64

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

// TypeOf возвращает Type отражения значения в interface{}.
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 для отображения конкретного значения внутри. Метод String этого не делает.)

Оба reflect.Type и reflect.Value имеют много методов, позволяющих нам исследовать их и манипулировать ими. Одним из важных примеров является то, что Value имеет метод Type, который возвращает Type для reflect.Value. Другое состоит в том, что и Type, и Value имеют метод Kind, который возвращает константу, указывающую, какой тип элемента хранится: Uint, Float64, Slice и т. д. Также методы на 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), предмет третьего закона отражения, который обсуждается ниже.

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

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.

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

type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)

Kind v по-прежнему reflect.Int несмотря на то, что статическим типом x является MyInt, а не int. Другими словами, Kind не может отличить int от MyInt, хотя Type может.

Второй закон отражения

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

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

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

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

Как следствие мы можем сказать

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

чтобы распечатать значение float64, представленное объектом отражения 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; пустое значение интерфейса содержит информацию о типе конкретного значения внутри, и Printf восстановит его.

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

Повторение: Отражение переходит от значений интерфейса к отражающим объектам и обратно.

Третий закон отражения

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

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

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

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

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

panic: reflect.Value.SetFloat using unaddressable value

Проблема не в том, что значение 7.1 не адресуется; это то, что v не устанавливается. Устанавливаемость (settability) является свойством Value отражения, и не у всех Value отражения оно есть.

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

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

печатает

settability of v: false

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

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

var x float64 = 3.4
v := reflect.ValueOf(x)

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

v.SetFloat(7.1)

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

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

f(x)

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

f(&x)

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

Давайте сделаем это. Сначала мы инициализируем x как обычно, а затем создаем значение отражения, которое указывает на него, называемое 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

Объект отражения p не устанавливается, но мы хотим установить не p, а (в действительности) *p. Чтобы добраться до того, на что указывает p, мы вызываем метод Value элемента Elem, который перенаправляется через указатель, и сохраняем результат в значении отражения, называемом 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

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

Структуры

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

Вот простой пример, который анализирует значение структуры, t. Мы создаем объект отражения с адресом структуры, потому что мы хотим изменить его позже. Затем мы устанавливаем typeOfT на его тип и перебираем поля, используя прямые вызовы методов. Обратите внимание, что мы извлекаем имена полей из типа struct, но сами поля являются регулярными 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 содержит устанавливаемый объект отражения, мы можем изменять поля структуры.

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 не могли бы быть установлены.

Вывод

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

  1. Отражение переходит от значения интерфейса к объекту отражения.
  2. Отражение переходит от объекта отражения к значению интерфейса.
  3. Чтобы изменить объект отражения, значение должно быть настраиваемым.

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


Читайте также:


Комментариев нет:

Отправить комментарий