Go'da neden listeler nadiren kullanılıyor?


85

Go'da yeniyim ve bu konuda oldukça heyecanlıyım. Ancak, yoğun olarak çalıştığım tüm dillerde: Delphi, C #, C ++, Python - Listeler çok önemlidir çünkü dizilerin aksine dinamik olarak yeniden boyutlandırılabilirler.

Golang'da gerçekten bir list.Listyapı var, ancak bununla ilgili çok az belge görüyorum - Örneğe Göre Go'da ya da sahip olduğum üç Go kitabında - Summerfield, Chisnal ve Balbaert - hepsi diziler ve dilimler üzerinde çok zaman harcıyor ve ardından haritalara atlayın. Sos kodu örneklerinde de çok az veya hiç kullanmıyorum list.List.

Ayrıca Python'dan farklı Rangeolarak List için desteklenmediği anlaşılıyor - büyük dezavantaj IMO. Bir şey mi kaçırıyorum?

Dilimler kesinlikle iyidir, ancak yine de sabit kodlanmış boyuta sahip bir diziye dayanmaları gerekir. İşte List burada devreye giriyor. Go'da sabit kodlanmış dizi boyutu olmadan dizi / dilim oluşturmanın bir yolu var mı? Liste neden göz ardı ediliyor?


10
Python'un listtürünün bağlantılı bir liste kullanılarak uygulanmadığını unutmayın : Go dilimine benzer davranır ve bazen veri kopyalarının genişletilmesini gerektirir.
James Henstridge

@JamesHenstridge - usulüne uygun olarak not edildi ve düzeltildi.
Vektör

2
C ++ listeleri yoğun bir şekilde kullanmaz. std::listneredeyse her zaman kötü bir fikirdir. std::vectorbir dizi öğeyi yönetmek istediğiniz şeydir. Aynı nedenlerden dolayı std::vectortercih edilirse Go dilimi de tercih edilmektedir.
deft_code

@deft_code - anlaşıldı. Sorum std::vector<T>, listbaşlatma için sabit bir değer gerektirmediği ve dinamik olarak yeniden boyutlandırılabildiği için kategoriye dahil edildi . Soruyu sorduğumda, Go'ların slicebenzer şekilde kullanılabileceği net değildi - o sırada okuduğum her şey bir dilimin "bir dizinin görünümü" olduğunu ve diğer birçok dilde olduğu gibi Go'daki sade vanilya dizilerinin sabit bir boyutta beyan edilmesi gerekir. (Ama uyarılar için teşekkürler.)
Vektör

Yanıtlar:


88

Hemen hemen her zaman bir liste düşündüğünüzde - Go yerine bir dilim kullanın. Dilimler dinamik olarak yeniden boyutlandırılır. Bunların temelinde, boyutu değiştirebilen bitişik bir bellek dilimi vardır.

SliceTricks wiki sayfasını okursanız göreceğiniz için çok esnektirler .

İşte bir alıntı: -

Kopyala

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

Kesmek

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

Sil

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

Sırayı korumadan sil

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

Pop

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

it

a = append(a, x)

Güncelleme : Burada, tümüyle go ekibinin kendi dilimleriyle ilgili bir blog gönderisinin bağlantısı var ; bu, dilimler ve diziler ve dilim iç bileşenleri arasındaki ilişkiyi açıklamak için iyi bir iş çıkarıyor.


2
Tamam - aradığım buydu. Dilimler hakkında bir yanlış anlaşılma yaşadım. Bir dilim kullanmak için bir dizi bildirmek zorunda değilsiniz. Bir dilim tahsis edebilirsiniz ve bu, destek deposunu tahsis eder. Delphi veya C ++ 'daki akışlara benziyor. Şimdi neden dilimlerle ilgili tüm olayları anlıyorum.
Vektör

2
@ComeAndGo, bazen "statik" bir diziye işaret eden bir dilim oluşturmanın yararlı bir deyim olduğuna dikkat edin.
kostix

2
@FelikZ, dilimler destek dizilerine bir "görünüm" oluşturur. Genellikle, bir işlevin üzerinde çalışacağı verilerin sabit boyutta olacağını (veya boyutu bilinen bayt miktarından daha uzun olmayacağını; bu ağ protokolleri için oldukça yaygındır) önceden bilirsiniz. Yani bu veriyi fonksiyonunuzda tutacak bir dizi
tanımlayabilir

53

Bu soruyu birkaç ay önce Go'yu araştırmaya başladığımda sordum. O zamandan beri her gün Go hakkında okuyorum ve Go'da kod yazıyorum.

Bu soruya net bir cevap alamadığım için (bir cevabı kabul etmiş olsam da), şimdi sorduğumdan beri öğrendiklerime dayanarak cevaplayacağım:

Go'da sabit kodlanmış dizi boyutu olmadan dizi / dilim oluşturmanın bir yolu var mı?

Evet. Dilimler, aşağıdakilerden sabit kodlanmış bir dizi gerektirmez slice:

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

Bu kod ayırdığı dilim slboyutu, lenkapasiteli cap- lenve capzamanında atanabilir değişkenlerdir.

Neden list.Listgörmezden geliniyor?

Görünüşe göre list.ListGo'da çok az ilgi gören ana nedenler :

  • @Nick Craig-Wood'un cevabında açıklandığı gibi, dilimlerle, genellikle daha verimli ve daha temiz, daha zarif bir sözdizimi ile yapılamayan listelerle yapılabilecek neredeyse hiçbir şey yoktur. Örneğin, aralık yapısı:

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

    listeyle kullanılamaz - döngü için C stili gereklidir. Ve çoğu durumda, C ++ koleksiyon stili sözdizimi listelerle birlikte kullanılmalıdır: push_backvb.

  • Belki daha da önemlisi, list.Listgüçlü bir şekilde yazılmamış - koleksiyonda çeşitli türleri karıştırmaya izin veren Python'un listeleri ve sözlüklerine çok benziyor. Bu, şeylere yönelik Go yaklaşımının tam tersi gibi görünüyor. Go, çok güçlü bir şekilde yazılmış bir dildir - örneğin, Go'da örtük tür dönüşümlerine hiçbir zaman izin verilmez, hatta bir upCast from intto int64açık olmalıdır. Ancak list.List için tüm yöntemler boş arayüzler alır - her şey gider.

    Python'u terk edip Go'ya geçmemin nedenlerinden biri, Python'un "güçlü yazılmış" olduğunu iddia etmesine rağmen (IMO değil) Python'un tür sistemindeki bu tür zayıflıktır. Go list.List, C ++ 'lar vector<T>ve Python'lardan doğan bir tür "melez" gibi görünüyor List()ve belki de Go'nun kendisinde biraz yerinde değil.

Çok uzak olmayan bir gelecekte bir noktada liste bulmamız beni şaşırtmaz. Liste, belki de kalmaya devam edecek olsa da, iyi tasarım uygulamaları kullanılsa bile bir sorunun en iyi şekilde çözülebileceği nadir durumlara uyum sağlamak için Go'da kullanımdan kaldırılmıştır. çeşitli türleri barındıran bir koleksiyonla. Ya da belki de C ailesi geliştiricilerinin, Go'ya özgü AFAIK'e özgü dilim nüanslarını öğrenmeden önce Go ile rahat olmaları için bir "köprü" sağlamak için oradadır. (Bazı açılardan dilimler C ++ veya Delphi'deki akış sınıflarına benziyor, ancak tamamen değil.)

Delphi / C ++ / Python arka planından gelmesine rağmen, Go ile ilk karşılaşmamda list.ListGo'nun dilimlerinden daha tanıdık buldum , Go ile daha rahat hale geldikçe, geri döndüm ve tüm listelerimi dilimlere dönüştürdüm. Henüz kullanmam gereken sliceve / veya mapyapmama izin vermeyen bir şey bulamadım list.List.


@Alok Go, sistem programlaması düşünülerek tasarlanmış genel amaçlı bir dildir. Kesinlikle yazılmış ... - Ne hakkında konuştukları hakkında da hiçbir fikirleri yok mu? GoLang anlamına gelmez tür kesmesi kullanımı olduğu değil kesinlikle yazılı. Ayrıca bu noktanın net bir açıklamasını da verdim: GoLang'da, yukarı yayın yaparken bile örtük tür dönüşümlerine izin verilmez. (Ünlem işaretleri sizi daha doğru yapmaz. Bunları çocuk blogları için saklayın.)
Vektör

@Alok - modlar yorumunuzu sildi, ben değil. Basitçe "neden bahsettiğini bilmeyen" demek! bir açıklama ve kanıt sağlamadığınız sürece işe yaramaz. Ayrıca buranın profesyonel bir mekan olması gerekiyor, bu yüzden ünlem işaretlerini ve abartıyı dışarıda bırakabiliriz - bunları çocuk blogları için saklayabiliriz. Eğer bir sorununuz varsa, "GoLang'ın A, B ve C'ye sahip olduğumuzda bununla çelişiyor gibi göründüğünde nasıl güçlü bir şekilde yazıldığını nasıl söyleyeceğinizi anlamıyorum" deyin. Belki de OP aynı fikirde olacak veya neden yanıldığınızı düşündüklerini açıklayacaktır. Bu kullanışlı ve profesyonel bir seslendirme olabilir,
Vektör

5
kod çalıştırılmadan önce bazı kuralları uygulayan statik olarak kontrol edilen dil. C gibi diller size ilkel bir tür sistemi sağlar: kodunuz kontrol yazabilir ancak çalışma zamanında patlayabilir. Bu spektrumda devam edersiniz, Go elde edersiniz, bu da size C'den daha iyi garantiler verir. Ancak OCaml gibi dillerdeki (spektrumun sonu da değildir) tür sistemlerine hiçbir yerde yakın değildir. "Git, belki de en kuvvetli yazılmış dildir" demek düpedüz yanlıştır. Geliştiricilerin bilinçli bir seçim yapabilmeleri için farklı dillerin güvenlik özelliklerini anlamaları önemlidir.
Alok

4
Go'da eksik olan şeylerin belirli örnekleri: jenerik eksikliği sizi dinamik yayınlar kullanmaya zorlar. Numaralandırma eksikliği / anahtarın eksiksizliğini kontrol etme yeteneği, diğer dillerin statik garantiler sağlayabildiği dinamik kontroller anlamına gelir.
Alok

@ Alok-1 Ben) belki 2) Oldukça yaygın kullanılan dillerden bahsediyoruz. Go bugünlerde çok güçlü değil, ancak Go'nun etiketlenmiş 10545 sorusu var, burada OCaml'de 3.230 var. 3) IMO'nun "güçlü bir şekilde yazılmış" ( derleme zamanı kontrolleri ile ille de ilintili olmayan belirsiz bir terim) ile pek ilgisi olmadığını söylediğiniz Go'daki eksiklikler . 4) "Önemli .." - üzgünüm ama bu mantıklı değil - birisi bunu okuyorsa, muhtemelen Go'yu zaten kullanıyordur. Go'nun onlar için olup olmadığına karar vermek için kimsenin bu cevabı kullandığından şüpheliyim. IMO hakkında "derinden rahatsız" olacağınız daha önemli bir şey bulmalısınız ...
Vektör

11

Bunun nedeni , jenerik verilerle çalışmanın başlıca Go deyiminin ne olduğunu öğrendikten sonra , container/listpaket oldukça açıklayıcı olduğu için onlar hakkında söylenecek pek bir şey olmadığı için.

Delphi'de (jenerikler olmadan) veya C'de işaretçileri veya s'leri TObjectlistede saklayabilir ve ardından listeden alırken gerçek türlerine geri döndürebilirsiniz. C ++ 'da STL listeleri şablonlardır ve bu nedenle türe göre parametreleştirilmiştir ve C #' da (bu günlerde) listeler geneldir.

Go'da, herhangi bir başka (gerçek) türdeki değerleri temsil edebilen özel bir container/listtür interface{}olan türdeki değerleri bir çift işaretçi depolayarak depolar : biri içerilen değerin tür bilgisine ve değere (veya doğrudan, boyutu bir işaretçinin boyutundan büyük değilse). Bu nedenle, listeye bir eleman eklemek istediğinizde, bunu sadece türündeki fonksiyon parametreleri interface{}değerleri kabul ettikçe yaparsınız . Ancak listeden değerleri çıkardığınızda ve gerçek türleriyle ne çalışmanız gerektiğini ya yazmanız gerekir ya da üzerlerinde bir tür anahtarı yapmanız gerekir - her iki yaklaşım da aslında aynı şeyi yapmanın farklı yollarıdır.

İşte buradan alınan bir örnek :

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))
    }
}

Burada kullanarak bir elemanın değerini elde ederiz e.Value()ve sonra onu intorijinal eklenen değerin bir türü olarak yazın-assert ederiz .

"Etkili Git" veya başka bir giriş kitabında tür iddialarını okuyabilir ve anahtarları yazabilirsiniz. container/listPaketin dokümantasyon özetleri tüm yöntemler listeleri desteği.


Pekala, Go listeleri diğer listeler veya vektörler gibi davranmadığı için: indekslenemezler (Liste [i]) AFAIK (belki bir şeyi kaçırıyorum ...) ve ayrıca Range'i desteklemiyorlar, bazı açıklamalar sırayla olurdu. Ancak tip iddiaları / anahtarları için teşekkürler - bu şimdiye kadar eksik olan bir şeydi.
Vektör

@ComeAndGo, evet, aralıkları desteklemiyorlar çünkü rangeyalnızca yerleşik türler (diziler, dilimler, dizeler ve haritalar) için geçerli olan bir dil yerleşiktir çünkü her "çağırma" veya rangeaslında kapsayıcıda gezinmek için farklı makine kodu üretecektir. uygulanan.
kostix

2
@ComeAndGo, indekslemeyle ilgili olarak ... Paketin belgelerine göre container/listçift ​​bağlantılı bir liste sağladığı açıktır . Bu, indekslemenin bir O(N)işlem olduğu (baştan başlamanız ve her öğenin üzerinden kuyruğa doğru ilerlemeniz, saymanız gerektiği) ve Go'nun temel tasarım paradigmalarından birinin hiçbir gizli performans maliyetine sahip olmadığı anlamına gelir ; bir diğeri ise programcıya biraz fazladan yük koymak (çift bağlantılı bir liste için bir indeksleme işlevi uygulamak 10 satırlık bir beyinsizdir) sorun değil. Dolayısıyla, konteyner sadece kendi türüne göre mantıklı "kanonik" işlemleri uygular.
kostix

@ComeAndGo, Delphi o notu TListve onun ilk diğerleri dinamik bir dizi derini kullanıyorum o yüzden dizine eklerken böyle bir liste ucuz değil uzanan bir ucuz. Yani Delphi'nin "listeleri" soyut listeler gibi görünseler de aslında dizilerdir - Go'da dilimleri ne için kullanırsınız. Vurgulamak istediğim şey, Go'nun ayrıntıları programcıdan "güzel soyutlamalar" "gizlemeden" "netleştirmeye" çalıştığıdır. Go'nun yaklaşımı, verilerinizin nasıl yerleştirildiğini ve ona nasıl eriştiğinizi açıkça bildiğiniz C'lerin yaklaşımı gibidir.
kostix

3
@ComeAndGo, Go'nun hem uzunluk hem de kapasiteye sahip dilimleriyle tam olarak neler yapılabilir?
kostix

6

Go dilimlerinin append()yerleşik işlevi ile genişletilebileceğini unutmayın . Bu bazen arka dizinin bir kopyasını almayı gerektirse de, Go yeni diziyi fazla boyutlandıracağından bu diziye bildirilen uzunluktan daha büyük bir kapasite vereceği için bu her seferinde gerçekleşmez. Bu, sonraki bir ekleme işleminin başka bir veri kopyası olmadan tamamlanabileceği anlamına gelir.

Bağlantılı listelerle uygulanan eşdeğer koda göre daha fazla veri kopyası elde ederken, listedeki öğeleri ayrı ayrı tahsis etme ihtiyacını ve Nextişaretçileri güncelleme ihtiyacını ortadan kaldırırsınız . Birçok kullanım için dizi tabanlı uygulama daha iyi veya yeterince iyi performans sağlar, bu nedenle dilde vurgulanan şey budur. İlginç bir şekilde, Python'un standart listtürü de dizi desteklidir ve değerleri eklerken benzer performans özelliklerine sahiptir.

Bununla birlikte, bağlantılı listelerin daha iyi bir seçim olduğu durumlar vardır (örneğin, uzun bir listenin başlangıcından / ortasından öğeler eklemeniz veya çıkarmanız gerektiğinde) ve bu nedenle standart bir kitaplık uygulaması sağlanır. Sanırım onlarla çalışmak için herhangi bir özel dil özelliği eklemediler çünkü bu durumlar dilimlerin kullanıldığı durumlardan daha az yaygındır.


Yine de, dilimler sabit kodlanmış boyutta bir dizi ile geri dönmelidir, değil mi? Sevmediğim şey bu.
Vektör

3
Bir dilimin boyutu, program kaynak kodunda sabit kodlanmamıştır, eğer kastettiğiniz buysa. Açıkladığım append()gibi (bazen bir veri kopyasını içerecek olan) , işlem boyunca dinamik olarak genişletilebilir .
James Henstridge

4

Dilim çok sık güncellenmedikçe (silin, rastgele konumlarda öğeler ekleyin), dilimlerin bellek bitişikliği, bağlantılı listelere kıyasla mükemmel önbellek isabet oranı sunacaktır.

Scott Meyer'in önbelleğin önemi hakkındaki konuşması .. https://www.youtube.com/watch?v=WDIkqP4JbkE


4

list.Listçift ​​bağlantılı bir liste olarak uygulanmaktadır. Dizi tabanlı listeler (C ++ 'daki vektörler veya golang'daki dilimler), listenin ortasına sık sık eklemezseniz, çoğu durumda bağlantılı listelerden daha iyi bir seçimdir. Ekleme için amortize edilmiş zaman karmaşıklığı, dizi listesinin kapasiteyi genişletmesi ve mevcut değerleri kopyalaması gerekmesine rağmen, hem dizi listesi hem de bağlantılı liste için O (1) 'dir. Dizi listeleri daha hızlı rasgele erişime, daha küçük bellek ayak izine ve daha da önemlisi veri yapısında işaretçiler bulunmadığından çöp toplayıcıya daha kolay sahiptir.


3

Gönderen: https://groups.google.com/forum/#!msg/golang-nuts/mPKCoYNwsoU/tLefhE7tQjMJ

Listelerinizdeki öğelerin sayısına çok bağlıdır,
 gerçek bir liste mi yoksa bir dilim mi daha verimli olacak
 Listenin 'ortasında' birçok silme işlemi yapmanız gerektiğinde.

# 1
Ne kadar çok eleman, bir dilim o kadar az çekici hale gelir. 

# 2
Öğelerin sıralaması önemli olmadığında,
 bir dilim kullanmak en verimlidir ve
 bir öğeyi dilimdeki son öğe ile değiştirerek silmek ve
 dilimi, len'i 1 küçültmek için yeniden biçimlendirme
 (SliceTricks wiki'sinde açıklandığı gibi)

Öyleyse
dilim
1'i kullanın . Listedeki öğelerin sırası önemli değilse ve silmeniz gerekiyorsa,
List'i kullanarak silmek için öğeyi son öğeyle takas edin ve
öğeler daha fazla olduğunda (uzunluk-1) 2'ye yeniden dilimleyin ( daha fazla anlamı ne olursa olsun)


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.

Öyleyse
dilim
1'i kullanın . Çaprazlamada hıza ihtiyacınız varsa

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.