Для многих программистов, которые используют или желали бы использовать Go на практике, отсутсвие механизмов параметрического полиморфизма в языке является большой печалью. Но не все так плохо как может показаться на первый взгляд.
Конечно в Go нельзя писать обобщенные программы, например в стиле C++ templates, которые бы практически не влияли на затраты процессорного времени. Такого механизма в языке нет и, вполне возможно, что не предвидится.
С другой стороны, язык представляет довольно мощный встроенный пакет reflect
, которой позволяет производить рефлексию как объектов, так и функций. Если не ставить быстродействие во главу угла, то с помощью этого пакета можно достигать интересных и гибких решений.
В этой статье я покажу как реализовать for each
в виде типонезависимой рефлексивной функции.
Проблема
В языке Go для перебора элементов коллекции (Array
, Slice
, String
) используется конструкция for range
:
for i, item := range items {
// do something
}
Аналогичным образом можно выбрать элементы из Channel
:
for item := range queue {
// do something
}
В общем-то это перекрывает 80% потребностей в цикле for each. Но у встроенной конструкции for range
есть подводные камни, которые легко продемонстрировать на небольшом примере.
Допустим мы имеем две структуры Car
и Bike
(представим что пишем код для автомобильного магазина):
type Car struct{
Name string
Count uint
Price float64
}
type Bike struct{
Name string
Count uint
Price float64
}
Нам нужно подсчитать стоимость всех автомобилей и мотоциклов которые у нас есть в наличии.
Что бы это можно было сделать одним циклом в Go требуется новый тип, который обобщает доступ к полям:
type Vehicle interface{
GetCount() uint
GetPrice() float64
}
func (c Car) GetCount() uint { return c.Count; }
func (c Car) GetPrice() float64 { return c.Price; }
func (b Bike) GetCount() uint { return b.Count; }
func (b Bike) GetPrice() float64 { return b.Price; }
Теперь суммарную стоимость можно подсчитать организуя обход vehicles
с помощью for range
:
vehicles := []Vehicle{
Car{"Banshee ", 1, 10000},
Car{"Enforcer ", 3, 15000},
Car{"Firetruck", 4, 20000},
Bike{"Sanchez", 2, 5000},
Bike{"Freeway", 2, 5000},
}
total := float64(0)
for _, vehicle := range vehicles {
total += float64(vehicle.GetCount()) * vehicle.GetPrice()
}
fmt.Println("total", total)
// $ total 155000
Что бы не писать цикл каждый раз мы можем написать функцию которая принимает на вход тип []Vehicle
и возвращает численный результат:
func GetTotalPrice(vehicles []Vehicle) float64 {
var total float64
for _, vehicle := range vehicles {
total += float64(vehicle.GetCount()) * vehicle.GetPrice()
}
return total
}
Выделяя этот код в отдельную функциюю, как ни странно, мы теряем в гибкости, т.к. появляется следующие проблемы:
- Ограничение строгой типизации элементов. Т.к. конструкция for/range строго типизирована и не производит приведение типов элементов, то приходится явно указывать ожидаемый тип элементов в сигнатуре функции. Как следствие нет возможности передать срез
[]Car
или[]Bike
напрямую, хотя оба типа — иCar
, иBike
, удовлетворяют условиям интерфейсаVehicle
:
cars := []Car{
Car{"Banshee ", 1, 10000},
Car{"Enforcer ", 3, 15000},
Car{"Firetruck", 4, 20000},
}
fmt.Println("total", GetTotalPrice(cars))
// Compilation error: cannot use cars (type []Car) as type []Vehicle in argument to GetTotalPrice
- Ограничение строгой типизации коллекции. Например, нет возмоности передать вместо среза
[]Vehicle
словарьmap[int]Vehicle
:
cars := map[int]Vehicle{
1: Car{"Banshee ", 1, 10000},
2: Car{"Enforcer ", 3, 15000},
3: Car{"Firetruck", 4, 20000},
}
fmt.Println("total", GetTotalPrice(cars))
// Compilation error: cannot use vehicles (type map[int]Vehicle) as type []Vehicle in argument to GetTotalPrice
Другими словами, for/range не позволяет выбрать произвольную часть кода и обернуть её в функцию не теряя гибкости.
Решение
Описанная проблема во многих языках со строгой типизацией решается привлечением механизма параметрического полиморфизма (generics, templates). Но взамен параметрического полиформизма авторы Go представили встроенный пакет reflect
реализующий механизм рефлексии.
С одной стороны рефлексия является более затратным по ресурсам решением, но с другой, она позволяет создавать более гибкие и интеллектуальные алгоритмы.
Рефлексия типа (reflect.Type)
По сути в пакете reflect
существует два вида рефлексии — это рефлексия типа reflect.Type
и рефлексия значения reflect.Value
. Рефлексия типа описывает исключительно свойства типа, поэтому две разные переменные с одним типом будут иметь одну и ту же рефлексию типа.
var i, j int
var k float32
fmt.Println(reflect.TypeOf(i) == reflect.TypeOf(j)) // true
fmt.Println(reflect.TypeOf(i) == reflect.TypeOf(k)) // false
Т.к. в Go типы конструируются на основе базовых типов, то для классификации существует специальное перечисление с типом Kind:
const (
Invalid Kind = iota
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr
Float32
Float64
Complex64
Complex128
Array
Chan
Func
Interface
Map
Ptr
Slice
String
Struct
UnsafePointer
)
Таким образом, имея доступ к рефлексии типа reflect.Type
, можно всегда узнать род типа, который позволяет произвести диспетчеризацию без определения полного типа переменной. Например, достаточно знать что переменная является функцией, не вдаваясь в подробности какой конкретный тип имеет эта функция:
valueType := reflect.TypeOf(value)
switch valuteType.Kind() {
case reflect.Func:
fmt.Println("It's a function")
default:
fmt.Println("It's something else")
}
Для удобства записи будем именовать рефлексию типа некоторой переменной тем же именем, но с суффиксом Type
:
callbackType := reflect.TypeOf(callback)
collectionType := reflect.TypeOf(collection)
Кроме рода к которому принадлежит тип, с помощью рефлексии типа можно узнать остальную статическую информацию о типе (т.е. ту информацию которая не меняется во время выполнения). Например, можно узнать количество аргументов функции и тип ожидаемого аргумента на некоторой позиции:
if callbackType.NumIn() > 0 {
keyType := callbackType.In(0) // expected argument type at zeroth position
}
Аналогичным образом можно получить доступ к описанию членов структуры:
type Person struct{
Name string
Email string
}
structType := reflect.TypeOf(Person{})
fmt.Println(structType.Field(0).Name) // Name
fmt.Println(structType.Field(1).Name) // Email
Размер массива так же можно узнать через рефлексию типа:
array := [3]int{1, 2, 3}
arrayType := reflect.TypeOf(array)
fmt.Println(arrayType.Len()) // 3
Но размер среза через рефлексию типа уже узнать нельзя, т.к. эта информация меняется во время выполнения.
slice := []int{1, 2, 3}
sliceType := reflect.TypeOf(slice)
fmt.Println(sliceType.Len()) // panic!
Рефлексия значения (reflect.Value)
Аналогично рефлексии типа, в Go существует рефлексия значения reflect.Value
, которая отражает свойства конкретного значения хранящегося в переменной. Может показаться, что это довольно тривиальная рефлексия, но т.к. в Go переменная с типом interface{}
может хранить всё что угодно — функцию, число, структуру и т.д., то и рефлексия значения вынуждена представлять в более–менее безопасном виде доступ ко всем вероятным возможностям объекта. Что, конечно же, порождает довольно длинный список методов.
Например, рефлексия функции может использоваться для вызова — достаточно передать список аргументов приведённый к типу reflect.Value
:
_callback := reflect.ValueOf(callback)
_callback.Call([]reflect.Value{ values })
Рефлексию коллекции (среза, массива, строки и т.д.) можно использовать для доступа к элементам:
_collection := reflect.ValueOf(collection)
for i := 0; i < _collection.Len(); i++ {
fmt.Println(_collection.Index(i))
}
Аналогичным образом работает рефлексия словаря — для обхода нужно получить список ключей через метод MapKeys
и выбрать элементы через MapIndex
:
for _, k := range _collection.MapKeys() {
keyValueCallback(k, _collection.MapIndex(k))
}
С помощью рефлексии структуры можно получить значения членов. При этом названия и типы членов следует получать из рефлексии типа структуры:
_struct := reflect.ValueOf(aStructIstance)
for i := 0; i < _struct.NumField(); i++ {
name := structType.Field(i).Name
fmt.Println(name, _struct.Field(i))
}
Рефлексивный цикл for each
Итак, если вернуться к for each, то желательно получить функцию которая принимала бы коллекцию и функцию обратного вызова произвольного типа, таким образом ответственность за согласование типов лежала бы на пользователе.
Т.к. единственная возможность в Go передать произвольный тип функции это указать тип interface{}
, то в теле функции необходимо произвести проверки на основе информация содержащейся в рефлексии типа callbackType
:
- убедиться что функция обратного вызова действительно является функцией (через метод
calbackType.Kind()
) - выяснить количество ожидаемых аргументов (метод
callbackType.NumIn()
) - в случае провала вызвать
panic()
В итоге получается примерно такой код:
func ForEach(collection, callback interface{}) {
callbackType := reflect.TypeOf(callback)
_callback := reflect.ValueOf(callback)
if callbackType.Kind() != reflect.Func {
panic("foreach: the second argument should be a function")
}
switch callbackType.NumIn() {
case 1:
// Callback expects only value
case 2:
// Callback expects key-value pair
default:
panic("foreach: the function should have 1 or 2 input arguments")
}
}
Теперь требуется спроектировать вспомогательную функцию которая будет производить обход по коллекции.
В неё удобнее передавать обратный вызов не в бестиповом виде, а в виде функции с двумя аргументами принимающую рефлексии ключа и элемента:
func eachKeyValue(collection interface{}, keyValueCallback func(k, v reflect.Value)) {
_collection := reflect.ValueOf(collection)
collectionType := reflect.TypeOf(collection)
switch collectionType.Kind() {
// loops
}
}
Т.к. алгоритм прохода коллекциии зависит от рода который можно получить через метод Kind()
рефлексии типа, то для диспетчеризации удобно воспользоваться конструкцией switch-case
:
switch collectionType.Kind() {
case reflect.Array: fallthrough
case reflect.Slice: fallthrough
case reflect.String:
for i := 0; i < _collection.Len(); i++ {
keyValueCallback(reflect.ValueOf(i), _collection.Index(i))
}
case reflect.Map:
for _, k := range _collection.MapKeys() {
keyValueCallback(k, _collection.MapIndex(k))
}
case reflect.Chan:
i := 0
for {
elementValue, ok := _collection.Recv()
if !ok {
break
}
keyValueCallback(reflect.ValueOf(i), elementValue)
i += 1
}
case reflect.Struct:
for i := 0; i < _collection.NumField(); i++ {
name := collectionType.Field(i).Name
keyValueCallback(reflect.ValueOf(name), _collection.Field(i))
}
default:
keyValueCallback(reflect.ValueOf(nil), _collection)
}
Как видно из кода, обход массива, среза и строки происходит одинаково. Словарь, канал и структура имеют свой собственный алгоритм обхода. В случае, если род коллекции не подпадает ни под один из перечисленных, алгоритм пытается передать в обратный вызов саму коллекцию, прим этом в качестве ключа указывается рефлексия указателя nil
(которая на вызов метод IsValid()
возвращает false
).
Теперь, имея функцию производяющую беcтиповый обход коллекции, можно адаптирвоать её к вызову из функции ForEach
обернув в замыкание. Это и есть окончательное решение:
func ForEach(collection, callback interface{}) {
callbackType := reflect.TypeOf(callback)
_callback := reflect.ValueOf(callback)
if callbackType.Kind() != reflect.Func {
panic("foreach: the second argument should be a function")
}
switch callbackType.NumIn() {
case 1:
eachKeyValue(collection, func(_key, _value reflect.Value){
_callback.Call([]reflect.Value{ _value })
})
case 2:
keyType := callbackType.In(0)
eachKeyValue(collection, func(_key, _value reflect.Value){
if !_key.IsValid() {
_callback.Call([]reflect.Value{reflect.Zero(keyType), _value })
return
}
_callback.Call([]reflect.Value{ _key, _value })
})
default:
panic("foreach: the function should have 1 or 2 input arguments")
}
}
Надо заметить, что в случае когда функция обратного вызова ожидает передачу двух аргументов (пары ключ/значение) необходимо производить проверку корректности ключа, т.к. он может оказаться невалидным. В последнем случае на основе типа ключа конструируется нулевой объект.
Примеры
Теперь пришло время продемонстироровать что же даёт наш подход. Если вернуться к проблеме, мы теперь можем решить её таким путём:
func GetTotalPrice(vehicles interface{}) float64 {
var total float64
ForEach(vehicles, func(vehicle Vehicle) {
total += float64(vehicle.GetCount()) * vehicle.GetPrice()
})
return total
}
Эта функция, в отличии от приведённой в начале статьи, гораздо гибче, т.к. позволяет подсчитывать сумму вне зависимости от типа коллекции и не обязывает приводить тип элементов к интерфейсу Vehicle
:
vehicles := []Vehicle{
Car{"Banshee ", 1, 10000},
Bike{"Sanchez", 2, 5000},
}
cars := []Car{
Car{"Enforcer ", 3, 15000},
Car{"Firetruck", 4, 20000},
}
vehicleMap := map[int]Vehicle{
1: Car{"Banshee ", 1, 10000},
2: Bike{"Sanchez", 2, 5000},
}
vehicleQueue := make(chan Vehicle, 2)
vehicleQueue <- Car{"Banshee ", 1, 10000}
vehicleQueue <- Bike{"Sanchez", 2, 5000}
close(vehicleQueue)
garage := struct{
MyCar Car
MyBike Bike
}{
Car{"Banshee ", 1, 10000},
Bike{"Sanchez", 1, 5000},
}
fmt.Println(GetTotalPrice(vehicles)) // 20000
fmt.Println(GetTotalPrice(cars)) // 125000
fmt.Println(GetTotalPrice(vehicleMap)) // 20000
fmt.Println(GetTotalPrice(vehicleQueue)) // 20000
fmt.Println(GetTotalPrice(garage)) // 15000
И небольшой бенчмарк для двух идентичных циклов, который наглядно показывает за счёт чего достигается гибкость:
// BenchmarkForEachVehicles1M
total := 0.0
for _, v := range vehicles {
total += v.GetPrice()
}
//BenchmarkForRangeVehicles1M
total := 0.0
ForEach(vehicles, func(v Vehicle) {
total += v.GetPrice()
})
PASS
BenchmarkForEachVehicles1M-2 2000000000 0.20 ns/op
BenchmarkForRangeVehicles1M-2 2000000000 0.01 ns/op
Заключение
Да, в Go нет параметрического полиформизма. Но зато есть пакет reflect
, который предоставляет обширные возможности в области метапрограммирования. Код с использованием reflect
конечно же выглядит намного сложнее, чем типичный код на Go. С другой стороны, рефлексивные функции позволяют создавать более гибкие решения. Это очень важно при написании прикладных библиотек, например, при реализации концепции Active Record
.
Так что, если вы заранее не знаете каким образом другие программисты будут использовать вашу библиотеку и предельное быстродействием для вас не главная цель, то, вполне возможно, рефлексивное метапрограммирвоание будет наилучшим выбором.
Комментарии (34)
ZurgInq
24.07.2016 23:44Более идиоматическим решением будет кодогенерация. Наряду с пакетом reflect, существует возможность спарсить исходники и получить доступ к ast. Правда такая реализация будет пожалуй даже посложнее, чем пляски с reflect.
arvitaly
25.07.2016 04:09+2Массивы в go — низкоуровневый тип. Он не предназачен для обобщения. Создавайте свою коллекцию.
type VehicleCollection struct {
vehicles []Vehicle
}
https://play.golang.org/p/2sfhGl5YlCdeniskreshikhin
25.07.2016 10:31+1Хорошо, у меня есть массив []Car, как его добавить в вашу коллекцию не делая цикл for-range?
arvitaly
25.07.2016 12:06+1Откуда он взялся? Я же написал, что нужно оперировать сущностями вашей модели, а не типами Go. Т.е. массив []Car не должен существовать отдельно.
Приведите бизнес-кейс (не абстрактный), тогда я опишу, как вижу решение без рефлексии.
А вообще, есть простое правило, рефлексия нужна только для удобства работы с внешними источниками: базы данных, сетевые запросы и т.д. И никогда не нужна для описания вашей модели.deniskreshikhin
25.07.2016 12:37+1Да это не важно откуда он взялся, если следовать Dependency inversion principle, то:
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.
Т.е. что бы рассчитать сумму, вы вынуждены подгонять все под массив []Vehicle, который у вас находится в реализации (low-level module), и не важно каким образом вы это делаете в цикле for-range, в методе `Add`, или как-то еще. Это не лучшая практика с позиции DI.
Рефлексивный foreach производит приведение типов самостоятельно, поэтому архитектурно это более правильно решение, т.к. главное что бы ваша коллекция удовлетворяла двум требованиям: имела семантику коллекции (массив, срез, словарь, канал, поля структуры и т.д.) и хранила элементы реализующие интерфейс Vehicle.
>> А вообще, есть простое правило, рефлексия нужна только для удобства работы с внешними источниками: базы данных, сетевые запросы и т.д. И никогда не нужна для описания вашей модели.
В статье рефлексия для описания модели не используется соврешенно. Она испольуется внутри рефлексивной функции.arvitaly
25.07.2016 13:23-1Еще как важно, он не существует отдельно от структуры моей модели.
«Т.е. что бы рассчитать сумму, вы вынуждены подгонять все под массив []Vehicle»
Что означает эта фраза? Обобщение Vehicle придумали вы, а теперь хотите избавиться от автоматической проверки его же интерфейсов с помощью рефлексии. У меня же есть абстракция VehicleCollection, которая никак не зависит от реализации, можете хранить там все в строке, вообще не используя массивы. Или вычислять AllPrice сразу при добавлении и не хранить элементы вообще. Все по одной простой причине, мне не нужно перебирать Vehicle, как массив, мне нужны методы для работы конкретно с этой коллекцией.
А вам кажется не нужен Go. В любом динамически-типизированном языке уже есть все, что вы хотите.
Одним из основных преимуществ Go является возможность понимать чужой код. Это достигается за счет Ограниченного числа типов, возможностей их преобразования и синтаксиса. Как только вы вводите новую конструкцию, вы теряете эту возможность.
Мало того, я утверждаю, что в вашем примере это совершенно не нужно, а рассуждения об абстрактных задачах всегда приводят к overhead ненужных абстракций.deniskreshikhin
25.07.2016 16:39У меня же есть абстракция VehicleCollection, которая никак не зависит от реализации, можете хранить там все в строке, вообще не используя массивы. Или вычислять AllPrice сразу при добавлении и не хранить элементы вообще. Все по одной простой причине, мне не нужно перебирать Vehicle, как массив, мне нужны методы для работы конкретно с этой коллекцией.
У вас понятие абстракции сводися по сути к понятию класс из ООП. Но Go не ООП язык программирования, и если бы люди имели ввиду под понятием абстракция класс, то они и писали бы слово класс. Понятие абстракция более широкоее нежели понятие класс.
А вам кажется не нужен Go. В любом динамически-типизированном языке уже есть все, что вы хотите.
Не всё там есть, например, во всех динамически-типизированных языках программирования огромная проблема с подключением нативных библиотек через хедеры .h. Например, нормальный биндинг к OpenGL есть только у Python. С OpenCL вообще беда у всех «скриптовых» языков. А в Go это делается очень просто.
Вообще, странно что люди стали рассматривать Go как некоторую замену C по быстродействию. В этом плане есть гораздо более удачные языки C++, D, Rust etc. Там столько этого быстродействия, что утонуть можно.
Мало того, я утверждаю, что в вашем примере это совершенно не нужно, а рассуждения об абстрактных задачах всегда приводят к overhead ненужных абстракций.
Я несколько раз описывал контекст в котором этот пример надо воспринимать, жаль что все это игнорируют. Видимо надо было как-то это выделить.
Вы предлагаете заменить общее решение (рефлексивный ForEach) на частное решение (VehicleCollection). Но я привел эту частную проблему лишь как иллюстрацию общей проблемы. В Go из-за строгой типизации и отсутсвия параметрического полиморфизма нет возможности разрешить эту проблему кроме как через рефлексию. Статья об этом.
В частном случае, возможно VehicleCollection будет лучшим решением, все зависит от контекста задачи. В статье мне было интересно продемонстрировать рефлексию. А как обертывать массивы в класс я думаю итак все знают.arvitaly
25.07.2016 17:24+1Go — еще какой ООП язык, и да, структуры и интерфейсы (заметьте, не классы) являются его основными инструментами. Подозреваю, что вы склоняетесь к какому-нибудь Haskell или Scala, но опять же зачем вам тогда Go?
Вы говорите, потому что можно подключить OpenGL, тогда используйте C++, раз остальные преимущества Go вы предпочитаете переписывать.
И я ничего не писал про быстродействие, я не против рефлексии, речь именно о композиции, именно это сильная сторона Go. Как только вам хочется рефлексии, значит где-то лишняя абстракция, если речь не идет о внешних источниках.
Именно в замене общих решений на частные и заключается простота Go и проблемы тут нет, это просто другой язык.
Я ничего не имею против статей о рефлексии в Go (хотя я тоже могу сказать, что там все просто, достаточно разобраться с указателями), но не могу согласиться с попыткой уничтожения достоинств языка (если это не шутка) без аргументов. Поэтому я попросил описать бизнес-кейс, вдруг действительно существует ситуация, в которой это необходимо, и нужно менять язык? Но пока таких примеров я не видел.
Это равносильно подмене нативных прототипов в JavaScript, или перегрузке оператора плюс на минус где-нибудь еще.deniskreshikhin
25.07.2016 17:38Вы говорите, потому что можно подключить OpenGL, тогда используйте C++, раз остальные преимущества Go вы предпочитаете переписывать.
В C++ нет рефлексии, допустим такие вещи как маршалинг (который базируется на рефлексии) там являются большой головной болью. В Go это все идет «из коробки».
Как только вам хочется рефлексии, значит где-то лишняя абстракция, если речь не идет о внешних источниках.
В системных библиотека рефлексии используются сплошь и рядом. Получается, у разработчиков системных библиотек Go тоже где-то лишнии абстракции? Ну думаю.
Именно в замене общих решений на частные и заключается простота Go и проблемы тут нет, это просто другой язык.
Тут каждый сам выбирает. В тех приложениях которые я пишу Go прекрасно себе показывает и с рефлексиями. Я вполне доволен. Поэтому и делюсь опытом.arvitaly
25.07.2016 18:10Системные библиотеки поэтому так и называются, что работают с внешними источниками (через системные вызовы) И, в десятый раз, я не против рефлексии!
По поводу «прекрасно показывает», вы так и не привели конкретный кейс, где хорошо показывает себя рефлексия. Пока вы просто не пользуетесь языком, а пытаетесь переписать его, чтобы использовать свой личный предыдущий опыт. Ок, ваш подход подойдет тем, кто перешел из другого языка и кому лень разбираться в best practices Go.deniskreshikhin
25.07.2016 18:46+1Окей, спасибо за отзыв.
Постараюсь учесть ваши замечания при подготовке следующей статьи про рефлексию.
youROCK
25.07.2016 08:15+1Вы теряете type safety и вносите возможность легко получить панику во время работы вместо того, чтобы получить ее во время компиляции. Это делает неочевидным профит от использования языков со статической типизацией.
deniskreshikhin
25.07.2016 10:38-2Старая как мир тема. Безопасность типов обеспечивает отлов довольно узкого класс багов, так что нельзя сказать что в целом приложеие в чем-то теряет при хорошего покрытия тестами.
The general argument for static types is that it catches bugs that are otherwise hard to find. But I discovered that in the presence of SelfTestingCode, most bugs that static types would have were found just as easily by the tests. Since the tests found much more than type errors, you needed them in either a static or dynamically typed language, so having the static typing gave you little gain.
http://martinfowler.com/bliki/DynamicTyping.html
Stronix
25.07.2016 12:36Или я неверно понял задачу, или зачем всё усложнять?
https://play.golang.org/p/ZX9kkuqQyedeniskreshikhin
25.07.2016 12:50Если кратко. У вас есть некоторая коллекция (что это конкретно — массив, срез, словарь, канал вы не знаете). В этой коллекции хранятся элементы с типом реализующим интерефейс Vehicle. Нужно рассчитать сумму. Рефлексивный foreach позволяет это сделать так:
total := float64(0) ForEach(collection, func(v Vehicle){ total += v.GetPrice() })
Stronix
25.07.2016 13:03Возможно, кодогенерация для этого лучше подойдёт, насколько я знаю, именно так и работают шаблоны в С++, только там это делает компилятор, а не отдельная утилита.
deniskreshikhin
25.07.2016 13:08Да, безусловно шаблоны разруливают эту ситуацию наилучшем образом. Но кодогенерация в Go это не совсем тоже самое что и шаблоны, с ней нужно сильно попотеть что бы разрулить такое.
ulvham
26.07.2016 06:42С точки зрения среднестатистического программиста — читабельность всегда актуальнее. А вот, если вы делаете пакет для Go, то тут, наверное, «изкоробки» будет лучше.
ardente
26.07.2016 11:21Мне кажется, для иллюстрации рефлексии пример с коллекциями выбран не совсем удачно: обработка типа коллекции зашита в eachKeyValue, что может создать проблемы при расширении ПО (новый тип коллекции, реализация Fold/Map/etc). Задачи перебора коллекций в Go лучше решать через итераторы, например: play.golang.org/p/FPm0vTKYaJ или через каналы, например: play.golang.org/p/seSoJfz6kH
deniskreshikhin
26.07.2016 11:26Виды коллекции в Golang фиксированы, так что проблемы в расширении нет. (В любом случае коллекцию можно обернуть в channel и передать как channel.)
Я не имею ничего против частных решений, но статья про общее решение без привязки к конкретным типам.
snuk182
26.07.2016 13:01Для даункастинга слайсов есть еще более другое колдунство
cars := *(*[]Car)(unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&vehicles)))) //cars []Car <= vehicles []Vehicle
Очень помогло в свое время написать сортировку массивов разного типа, которые реализуют один общий интерфейс (по результатам вызова которого происходит сортировка)deniskreshikhin
26.07.2016 13:16Интересное решение)
Сортировка без `sort.Sort`, напрямую как я понял?snuk182
26.07.2016 13:49Почему же, вот:
type sortvehicles []Vehicle func (this sortvehicles) Len() int { //sort.Sort return len(this) } func (this sortvehicles) Less(i, j int) bool {//sort.Sort return this[i].ID() < this[j].ID() } func (this sortvehicles) Swap(i, j int) {//sort.Sort this[i], this[j] = this[j], this[i] }
А потом это все счастье вызывается как-то так:
sort.Sort(sortvehicles(*(*[]Vehicle)(unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&cars))))))
при условии, что нам надо сортировать по Vehicle.ID(), но cars у нас это []Car, лежащий вообще в отдельном месте
VGrabko
Надо делать в 2 цикла. А то нагородили рефлексии тут. Если люди начнут юзать рефлексию повсеместно то какой профит от Go вообще? Код же там не очевидный. У меня на работе там где заюзана рефлексия около 300 строк комментариев и один фиг с наскоку не понять что за магия.
deniskreshikhin
Спасибо за критическое мнение -)
Статья больше не про цикл, а про проблему с которой сталкиваются программисты на Go. Эта проблема, как правило, возникает чаще всего именно с for-range.
Что касается быстродействия, то если бы быстродействие всегда было ключевым критерием, то люди бы писали только на C (или даже asm'е). Но кроме быстродействия порой нужно иметь гибкость, так в общем-то появилось куча языков с динамической типизацией (perl, python, ruby, php, javascript, lua etc). По мне Go тем и отличается, что не только быстр, но еще и представляет кучу плюшек, в том числе и рефлексию.
В любом случае, даже код с рефлексией выполняется довольно быстро, если сравнивать с многими чисто динамически типизированными языками.
К тому же есть большое множество задач, когда наносекундные затраты на рефлексию не сравнимы с затратами на запросы по http, обращению к жесткому диску, к базе данных, межпроцессному взаимодействию и т.д. Так что область применения рефлексии без проседания быстродействия довольно обширна.
Что касается сложности кода с рефлекскией. Да он сложен внутри, но зато прост извне. Это важно допустим когда есть желание предоставить гибкий однообразный API. Здесь многое зависит от вкуса. Я люблю что бы была одна пусть сложная функция в 100 строчек, чем тысячи строчек однообразного кода.
Не надо забывать еще, что уменьшение дублирование повышает связность кода (т.н. cohesion), а это в целом повышает качество проекта. Например, одну сложную функцию проще тестировать и поддерживать, чем десятки однообразных.
VGrabko
создатели языка говорят что рефлексию юзать это плохой тон.
deniskreshikhin
Что касается рефлексии, то Роб Пайк писал, что:
«It's a powerful tool that should be used with care and avoided unless strictly necessary.»
Не думаю, что если бы он считал рефлексии дурным тоном, то использовал бы словосочетание «powerfool tool».
ufm
ящик с динамитом тоже очень «powerfool tool». Но использовать его везде не думая — явный дурной тон.
deniskreshikhin
Больше конструктива пожалуйста)
RomanArzumanyan
Offtopic: powerfool tool — это прекрасная игра слов. Думаю, может быть использовано в качестве мема. Как нечто, что при попытке выстрела в ногу, отрывает весь пояс нижних конечностей.
VGrabko
а дублирование побороть функцией в 2 аргументами :D
Aleksi
Это проблема, с которой сталкиваются начинающие программисты на Go, пришедшии с JavaScript, Python и Ruby. Не нужно писать на Go как на этих языках. Не нужно бороться с языком.
deniskreshikhin
Не согласен, по мне эта проблема возникает у людей которые до Go использовали C++, Java etc где есть дженерики, шаблоны и т.п. инструменты, и в то же время есть строгая типизация. Т.е. я не видел что бы кто-то задавал вопросы типа «Как смапить массив в lodash стиле»? В основном звучит вопрос «Где дежнерики?»
Боротья с языком никто не предлагает, рефлексия это часть стандартной библиотеки.