Почему списки в Go используются нечасто?

85

Я новичок в Go и очень рад этому. Но на всех языках, с которыми я активно работал: Delphi, C #, C ++, Python - списки очень важны, потому что их можно динамически изменять размер, в отличие от массивов.

В Golang действительно есть list.Listструктура, но я вижу очень мало документации по ней - будь то в Go By Example или в трех моих книгах по Go - Summerfield, Chisnal и Balbaert - все они тратят много времени на массивы, срезы и затем переходите к картам. В примерах исходного кода я также практически не использую list.List.

Также кажется, что, в отличие от Python, RangeList не поддерживается - большой недостаток IMO. Я что-то упускаю?

Срезы, безусловно, хороши, но они все равно должны основываться на массиве с жестко заданным размером. Вот где на помощь приходит List. Есть ли способ создать массив / фрагмент в Go без жестко заданного размера массива? Почему список игнорируется?

Вектор
источник
10
Обратите внимание, что listтип Python не реализуется с использованием связанного списка: он ведет себя аналогично слайсу Go, иногда требуя расширения копий данных.
Джеймс Хенстридж
@JamesHenstridge - должным образом отмечено и исправлено.
Vector
2
C ++ не использует списки широко. std::listпочти всегда плохая идея. std::vectorэто то, что вы хотите управлять последовательностью элементов. По тем же причинам std::vectorпредпочтительным является также Go slice.
deft_code
@deft_code - понял. В моем вопросе std::vector<T>был включен в listкатегорию, потому что он не требует постоянного значения для инициализации и может быть изменен динамически. Когда я задал вопрос, мне было неясно, что Go sliceможно использовать аналогичным образом - все, что я читал в то время, объясняло, что срез был «представлением массива», и, как и в большинстве других языков, простые ванильные массивы в Go должны быть объявлены с постоянным размером. (Но спасибо за внимание.)
Вектор

Ответы:

88

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

Они очень гибкие, как вы увидите, если прочитаете вики-страницу SliceTricks .

Вот отрывок: -

Копировать

b = make([]T, len(a))
copy(b, a) // or b = append([]T(nil), a...)

Порез

a = append(a[:i], a[j:]...)

удалять

a = append(a[:i], a[i+1:]...) // or a = a[:i+copy(a[i:], a[i+1:])]

Удалить без сохранения порядка

a[i], a = a[len(a)-1], a[:len(a)-1]

Поп

x, a = a[len(a)-1], a[:len(a)-1]

От себя

a = append(a, x)

Обновление : вот ссылка на сообщение в блоге о срезах от самой команды go, которое хорошо объясняет взаимосвязь между срезами и массивами, а также внутреннее устройство срезов.

Ник Крейг-Вуд
источник
2
ОК - это то, что я искал. У меня было недопонимание насчет срезов. Для использования среза необязательно объявлять массив. Вы можете выделить срез, и он выделяет резервное хранилище. Похоже на потоки в Delphi или C ++. Теперь я понимаю, почему так много шума по поводу ломтиков.
Vector
2
@ComeAndGo, обратите внимание, что иногда создание среза, указывающего на «статический» массив, является полезной идиомой.
kostix
2
@FelikZ, срезы создают "представление" в своем массиве поддержки. Часто вы заранее знаете, что данные, с которыми будет работать функция, будут иметь фиксированный размер (или будут иметь размер не более известного количества байтов; это довольно часто встречается для сетевых протоколов). Таким образом, вы можете просто объявить массив для хранения этих данных в своей функции, а затем нарезать его по своему желанию - передав эти фрагменты вызываемым функциям и т. Д.
kostix
53

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

Поскольку я не получил четкого ответа на этот вопрос (хотя я принял один ответ), я сейчас отвечу на него сам, основываясь на том, что я узнал, поскольку я его задал:

Есть ли способ создать массив / фрагмент в Go без жестко заданного размера массива?

Да. Для срезов не требуется жестко запрограммированный массив sliceиз:

var sl []int = make([]int,len,cap)

Этот код выделяет срез slразмером lenс емкостью cap- lenи capпредставляет собой переменные, которые могут быть назначены во время выполнения.

Почему list.Listигнорируется?

Похоже, что основными причинами list.List, по которым в Go уделяется мало внимания, являются:

  • Как было объяснено в ответе @Nick Craig-Wood, со списками практически ничего нельзя сделать, что нельзя сделать с помощью срезов, часто более эффективно и с более чистым и элегантным синтаксисом. Например, конструкция диапазона:

    for i:=range sl {
      sl[i]=i
    }
    

    не может использоваться со списком - требуется стиль C для цикла. И во многих случаях синтаксис стиля коллекции C ++ должен использоваться со списками: push_backи т. Д.

  • Возможно, что еще более важно, list.Listне является строго типизированным - он очень похож на списки и словари Python, которые позволяют смешивать различные типы вместе в коллекции. Кажется, это противоречит подходу Go к вещам. Go - это язык с очень строгой типизацией - например, неявные преобразования типов никогда не допускаются в Go, даже upCast из intв int64должен быть явным. Но все методы для list.List принимают пустые интерфейсы - все идет.

    Одна из причин, по которой я отказался от Python и перешел на Go, - это слабость такого рода в системе типов Python, хотя Python утверждает, что он «строго типизирован» (IMO это не так). Go list.Listкажется чем-то вроде «дворняги», порожденным C ++ vector<T>и Python List(), и, возможно, немного неуместен в самом Go.

Меня не удивит, если в какой-то момент в не столь отдаленном будущем мы обнаружим list.List, который устарел в Go, хотя, возможно, он останется, чтобы приспособиться к тем редким ситуациям, когда даже с использованием хороших методов проектирования проблема может быть решена лучше всего. с коллекцией, содержащей различные типы. Или, возможно, он нужен для того, чтобы предоставить разработчикам семейства C «мост», чтобы они могли освоиться с Go, прежде чем они узнают нюансы срезов, которые уникальны для Go, AFAIK. (В некоторых отношениях срезы кажутся похожими на классы потоков в C ++ или Delphi, но не полностью.)

Несмотря на то, что я пришел из среды Delphi / C ++ / Python, при первом знакомстве с Go я обнаружил, list.Listчто он более знаком, чем фрагменты Go, поскольку я стал более комфортно работать с Go, я вернулся и изменил все свои списки на фрагменты. Я еще ничего не нашел sliceи / или mapне разрешаю мне делать то, что мне нужно использовать list.List.

Вектор
источник
@Alok Go - это язык общего назначения, разработанный с учетом системного программирования. Он строго типизирован ... - Они тоже не понимают, о чем говорят? Использование вывода типа не означает, что GoLang не является строго типизированным. Я также четко проиллюстрировал этот момент: неявные преобразования типов в GoLang не разрешены, даже при восходящем преобразовании. (Восклицательные знаки не сделают вас более правильным. Сохраните их для детских блогов.)
Вектор
@Alok - ваш комментарий удалили моды, а не я. Просто сказать, что кто-то «не знает, о чем говорит!» бесполезен, если вы не предоставите объяснение и доказательство. Вдобавок это должно быть профессиональное место встречи, поэтому мы можем опустить восклицательные знаки и преувеличения - оставьте их для детских блогов. Если у вас возникла проблема, просто скажите: «Я не понимаю, как вы можете сказать, что GoLang так строго типизирован, когда у нас есть A, B и C, которые, кажется, противоречат этому». Возможно, ОП согласится или объяснит, почему они думают, что вы ошибаетесь. Это был бы полезный и профессиональный комментарий,
Vector
4
статически проверенный язык, который применяет некоторые правила перед запуском кода. Языки, такие как C, предоставляют вам примитивную систему типов: ваш код может правильно проверять тип, но взрывается во время выполнения. Продолжая следовать этому спектру, вы получаете Go, который дает вам лучшие гарантии, чем C. Однако он далек от систем типов в таких языках, как OCaml (что тоже не является концом спектра). Сказать, что «Go - это, пожалуй, самый строго типизированный язык», совершенно неверно. Разработчикам важно понимать свойства безопасности различных языков, чтобы они могли сделать осознанный выбор.
Alok
4
Конкретные примеры того, чего не хватает в Go: отсутствие универсальных типов вынуждает использовать динамическое приведение типов. Отсутствие перечислений / возможности проверки полноты переключения также подразумевает динамические проверки, когда другие языки могут предоставлять статические гарантии.
Алок,
@ Alok-1 I) сказал, что возможно 2) Мы говорим о языках, которые используются довольно часто. Go в наши дни не очень силен, но Go имеет 10545 тегов, здесь OCaml - 3230. 3) Недостатки Go, на которые вы ссылаетесь, IMO не имеют ничего общего с «строго типизированным» (расплывчатый термин, который не обязательно коррелирует с проверками времени компиляции). 4) «Это важно ...» - извините, но это не имеет смысла - если кто-то читает это, он, вероятно, уже использует Go. Я сомневаюсь, что кто-то использует этот ответ, чтобы решить, подходит ли им Go. ИМО, вы должны найти что-то более важное, о чем следует "глубоко беспокоиться" ...
Вектор
11

Я думаю, это потому, что о них особо нечего сказать, поскольку container/listпакет довольно очевиден, если вы усвоили главную идиому Go для работы с общими данными.

В Delphi (без дженериков) или в C вы должны хранить указатели или TObjects в списке, а затем возвращать их к их реальным типам при получении из списка. В C ++ списки STL являются шаблонами и, следовательно, параметризованы по типу, а в C # (в наши дни) списки являются общими.

В Go container/listхранит значения типа, interface{}который является особым типом, способным представлять значения любого другого (реального) типа - путем сохранения пары указателей: один на информацию о типе содержащегося значения и указатель на значение (или значение напрямую, если его размер не превышает размер указателя). Поэтому, когда вы хотите добавить элемент в список, вы просто делаете это, поскольку параметры функции типа interface{}принимают значения любого типа. Но когда вы извлекаете значения из списка и что работать с их реальными типами, вам нужно либо ввести их тип, либо переключить их - оба подхода - это просто разные способы сделать, по сути, одно и то же.

Вот пример, взятый отсюда :

package main

import ("fmt" ; "container/list")

func main() {
    var x list.List
    x.PushBack(1)
    x.PushBack(2)
    x.PushBack(3)

    for e := x.Front(); e != nil; e=e.Next() {
        fmt.Println(e.Value.(int))
    }
}

Здесь мы получаем значение элемента, используя, e.Value()а затем утверждаем его как intтип исходного вставленного значения.

Вы можете прочитать об утверждениях типов и переключателях типов в «Effective Go» или в любой другой вводной книге. В container/listдокументации пакета перечислены все поддерживаемые списки методов.

костикс
источник
Ну, поскольку списки Go не действуют как другие списки или векторы: они не могут быть проиндексированы (List [i]) AFAIK (возможно, мне что-то не хватает ...), а также они не поддерживают Range, некоторые пояснения будет в порядке. Но спасибо за утверждения / переключатели типов - это то, чего мне до сих пор не хватало.
Vector
@ComeAndGo, да, они не поддерживают диапазоны, потому что rangeэто встроенный язык, который применим только к встроенным типам (массивы, срезы, строки и карты), потому что каждый «вызов» или rangeфактически будет создавать другой машинный код для обхода контейнера, в котором он применительно к.
kostix
2
@ComeAndGo, насчет индексации ... Из документации пакета ясно, что container/listэто список с двойной связью. Это означает, что индексация - это O(N)операция (вы должны начинать с головы и перемещаться по каждому элементу к хвосту, считая), и одна из краеугольных парадигм дизайна Go не имеет скрытых затрат на производительность; с другой стороны, небольшая дополнительная нагрузка на программиста (реализация функции индексации для двусвязного списка - это простая задача из 10 строк). Таким образом, контейнер реализует только «канонические» операции, разумные для своего вида.
kostix
@ComeAndGo, обратите внимание, что в Delphi TListи ему подобных используют динамический массив внизу, поэтому расширение такого списка недешево, а индексирование - дешево. Итак, хотя «списки» Delphi выглядят как абстрактные списки, на самом деле они представляют собой массивы - для чего вы бы использовали срезы в Go. Что я хочу подчеркнуть, так это то, что Go стремится прояснить ситуацию без нагромождения «красивых абстракций», «скрывающих» детали от программиста. Подход Go больше похож на C, где вы явно знаете, как расположены ваши данные и как вы к ним обращаетесь.
kostix
3
@ComeAndGo, именно то, что можно сделать с кусочками Go, которые имеют как длину, так и емкость.
kostix
6

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

Хотя в результате получается больше копий данных, чем в эквивалентном коде, реализованном со связанными списками, вы устраняете необходимость выделять элементы в списке по отдельности и обновлять Nextуказатели. Для многих применений реализация на основе массива обеспечивает лучшую или достаточно хорошую производительность, так что это то, что подчеркивается в языке. Интересно, что стандартный listтип Python также поддерживается массивом и имеет аналогичные характеристики производительности при добавлении значений.

Тем не менее, есть случаи, когда связанные списки являются лучшим выбором (например, когда вам нужно вставить или удалить элементы из начала / середины длинного списка), и поэтому предоставляется стандартная реализация библиотеки. Я предполагаю, что они не добавляли никаких специальных языковых функций для работы с ними, потому что эти случаи менее распространены, чем те, где используются срезы.

Джеймс Хенстридж
источник
Тем не менее, срезы должны возвращаться массивом с жестко заданным размером, верно? Вот что мне не нравится.
Vector
3
Размер среза жестко не задан в исходном коде программы, если вы это имеете в виду. Как append()я объяснил, его можно динамически расширить с помощью операции (которая иногда включает в себя копию данных).
Джеймс Хенстридж
4

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

Выступление Скотта Мейера о важности кеширования .. https://www.youtube.com/watch?v=WDIkqP4JbkE

Манохар
источник
4

list.Listреализован в виде двусвязного списка. Списки на основе массивов (векторы в C ++ или фрагменты в golang) являются лучшим выбором, чем связанные списки в большинстве случаев, если вы не часто вставляете их в середину списка. Амортизированная временная сложность для добавления составляет O (1) как для списка массивов, так и для связанного списка, даже если список массивов должен увеличивать емкость и копировать существующие значения. Списки массивов имеют более быстрый произвольный доступ, меньший объем памяти и, что более важно, удобны для сборщика мусора из-за отсутствия указателей внутри структуры данных.

леса
источник
3

От: https://groups.google.com/forum/#!msg/golang-nuts/mPKCoYNwsoU/tLefhE7tQjMJ

Это во многом зависит от количества элементов в ваших списках,
 будет ли более эффективным истинный список или срез
 когда вам нужно сделать много удалений в «середине» списка.

# 1
Чем больше элементов, тем менее привлекательным становится ломтик. 

# 2
Когда порядок элементов не важен,
 наиболее эффективно использовать срез и
 удаление элемента путем замены его последним элементом в срезе и
 перерезать ломтик, чтобы уменьшить линзу на 1
 (как описано в вики SliceTricks)

Поэтому
используйте срез
1. Если порядок элементов в списке не важен, и вам нужно удалить, просто
используйте List, замените элемент, который нужно удалить, последним элементом и повторно срежьте срез на (длина-1)
2. когда элементов больше ( что бы то ни было еще)


There are ways to mitigate the deletion problem --
e.g. the swap trick you mentioned or
just marking the elements as logically deleted.
But it's impossible to mitigate the problem of slowness of walking linked lists.

Поэтому
используйте срез
1. Если вам нужна скорость обхода

Манохар Редди Поредди
источник