Чому списки рідко використовуються в Go?


85

Я новачок у Go, і дуже схвильований цим. Але у всіх мовах, з якими я багато працював: Delphi, C #, C ++, Python - Списки дуже важливі, оскільки їх можна динамічно змінювати, на відміну від масивів.

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

Також виявляється, що, на відміну від Python, Rangeне підтримується List - великий недолік IMO. Мені чогось не вистачає?

Зрізи, безумовно, приємні, але вони все одно повинні базуватися на масиві з жорстким кодованим розміром. Ось де з’являється List. Чи є спосіб створити масив / зріз у Go без жорстко закодованого розміру масиву? Чому Список ігнорується?


10
Зверніть увагу, що listтип Python не реалізований за допомогою пов'язаного списку: він поводиться подібно до фрагмента Go, іноді вимагає розширення копій даних.
James Henstridge

@JamesHenstridge - належним чином зазначено та виправлено.
Вектор

2
C ++ широко не використовує списки. std::listмайже завжди погана ідея. std::vectorце те, що ви хочете керувати послідовністю елементів. З тих самих причин std::vectorперевагу надають також фрагменту Go.
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 ++. Тепер я розумію, навіщо всі шуми про скибочки.
Вектор

2
@ComeAndGo, зауважимо, що іноді створення фрагмента, який вказує на "статичний" масив, є корисною ідіомою.
kostix

2
@FelikZ, зрізи створюють "вигляд" у своєму масиві підкладки. Часто ви заздалегідь знаєте, що дані, над якими функціонуватиме функція, матимуть фіксований розмір (або матимуть розмір не більший за відомий обсяг байт; це досить часто для мережевих протоколів). Тож ви можете просто оголосити масив для зберігання цих даних у вашій функції, а потім нарізати їх як потрібно - передаючи ці фрагменти викликаним функціям тощо
kostix

53

Я поставив це питання кілька місяців тому, коли вперше розпочав розслідування щодо Го. Відтоді щодня я читаю про 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 з intto int64повинен бути явним. Але всі методи для list.List беруть порожні інтерфейси - все йде.

    Однією з причин того, що я відмовився від Python і перейшов до Go, є така слабкість у системі типів Python, хоча Python стверджує, що вона "сильно набрана" (IMO це не так). Go's, list.Listздається, є свого роду "дворняжкою", народженою C ++ vector<T>та Python List(), і, можливо, трохи недоречна в самому Go.

Мене не здивує, якщо в якийсь момент у не надто віддаленому майбутньому ми знайдемо список. Список застарілих в 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, які, здається, суперечать цьому". Можливо, ОП погодиться або пояснить, чому вони вважають, що ви помиляєтесь. Це був би корисний та професійний звучачий коментар,
Вектор

5
статично перевірена мова, яка застосовує деякі правила до запуску коду. Мови, такі як C, дають вам примітивну систему типу: ваш код може вводити перевірку правильно, але підірвати під час виконання. Ви продовжуєте цей спектр, отримуєте Go, що дає вам кращі гарантії, ніж C. Однак, це ніде не наближається до систем типу на таких мовах, як OCaml (що також не закінчується в спектрі). Висловлювання "Go - це, мабуть, найсильніше набрана мова", - це просто неправильно. Для розробників важливо розуміти властивості безпеки різних мов, щоб вони могли зробити обгрунтований вибір.
Alok

4
Конкретні приклади речей, яких не вистачає в Go: відсутність дженериків змушує вас використовувати динамічні трансляції. Відсутність переліків / можливості перевірити повноту комутатора додатково передбачає динамічні перевірки, де інші мови можуть надати статичні гарантії.
Алок

@ Alok-1 I) сказав, мабуть, 2) Ми говоримо про мови, що використовуються досить широко. Go сьогодні не дуже сильний, але Go має 10545 запитань, тут OCaml має 3230. 3) Недоліки Go, на які ви посилаєтесь, мають багато спільного з "сильно набраним" (туманний термін, який не обов'язково співвідноситься з компіляцією перевірок часу). 4) "Це важливо .." - вибачте, але це не має сенсу - якщо хтось читає це, можливо, він вже використовує Go. Сумніваюся, хтось використовує цю відповідь, щоб вирішити, чи Go для них. ІМО ви повинні знайти щось більш важливе, щоб вас "глибоко турбували" ...
Вектор

11

Я думаю, це тому, що про них не так багато що можна сказати, оскільки container/listпакет досить зрозумілий, як тільки ви зрозуміли, що є головною ідіомою Go для роботи із загальними даними.

У Delphi (без дженериків) або на C ви б зберігали покажчики або TObjects у списку, а потім повертали їх до їх реальних типів при отриманні зі списку. У C ++ списки STL є шаблонами і, отже, параметризовані за типом, а в C # (в наші дні) списки є загальними.

У Go, container/list зберігає значення типу, interface{}який є спеціальним типом, здатним представляти значення будь-якого іншого (реального) типу - зберігаючи пару покажчиків: один на інформацію про тип, що міститься значення, і вказівник на значення (або значення безпосередньо, якщо його розмір не перевищує розмір покажчика). Отже, коли ви хочете додати елемент до списку, ви просто робите це, оскільки параметри функції типу interface{}приймають значення coo будь-якого типу. Але коли ви витягуєте значення зі списку, і що працювати з їх реальними типами, вам доводиться або вставляти їх, або робити перемикання типів на них - обидва підходи - це просто різні способи зробити по суті одне і те ж.

Ось приклад, взятий звідси :

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, деякі пояснення буде в порядку. Але дякую твердженням / перемикачам типу - цього я досі бракував.
Вектор

@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покажчиків. Для багатьох застосувань реалізація, що базується на масиві, забезпечує кращу або достатньо хорошу продуктивність, тому саме це наголошується в мові. Цікаво, що стандарт Pythonlist тип також підтримується масивом і має подібні характеристики продуктивності при додаванні значень.

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


Тим не менше, фрагменти повинні повертатися масивом із жорстким закодованим розміром, чи не так? Це те, що мені не подобається.
Вектор

3
Розмір фрагмента не кодується жорстко у вихідному коді програми, якщо ви маєте на увазі це. Як append()я пояснив, його можна динамічно розширювати за допомогою операції (яка іноді передбачає копіювання даних).
James Henstridge

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 swap element, щоб видалити останнім елементом, і повторно наріжте (length-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. Якщо вам потрібна швидкість у траверсі

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.