为什么Go中不经常使用列表?


82

我是Go的新手,对此感到很兴奋。但是,在我广泛使用的所有语言中:Delphi,C#,C ++,Python-列表非常重要,因为列表可以动态调整大小,而不是数组。

在Golang中,确实存在一个list.List结构,但是我很少看到有关它的文档-无论是在Go By Example还是我所拥有的三本Go书籍中,Summerfield,Chisnal和Balbaert都花了大量时间在数组和切片上,然后跳到地图。在源代码示例中,我也很少或根本没有使用list.List

看来,与Python不同,RangeList不支持Python,这是IMO的一大缺陷。我想念什么吗?

切片当然不错,但是它们仍然需要基于具有硬编码大小的数组。这就是List出现的地方。有没有一种方法可以在Go中创建数组/切片,而无需使用硬编码的数组大小?为什么列表被忽略?


9
请注意,Python的list类型不是使用链表实现的:它的行为类似于Go切片,偶尔需要扩展数据副本。
James Henstridge 2014年

@JamesHenstridge-正确记录并更正。
矢量

2
C ++并未广泛使用列表。std::list几乎总是一个坏主意。std::vector是您要管理一系列项目的对象。由于相同的原因而std::vector被优先考虑,Go slice也被优先考虑。
deft_code 2014年

@deft_code-了解。我的问题std::vector<T>之所以包含在list类别中,是因为它不需要用于初始化的常量值,并且可以动态调整大小。当我问这个问题时,我不清楚Go的slice用法是否可以类似地使用-当时我读到的所有内容都说明切片是“数组的视图”,并且像大多数其他语言一样,Go中的普通香草数组需要以恒定大小声明。(但感谢您的注意。)
2014年

Answers:


83

几乎总是在考虑列表时-在Go中使用切片代替。切片会动态调整大小。它们的基础是可以更改大小的连续内存片。

如果您阅读了SliceTricks Wiki页面,就会发现它们非常灵活。

这是节选:

复制

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团队本身有关切片博客文章的链接,该博客很好地解释了切片和数组以及切片内部之间的关系。


1
好的-这就是我想要的。我对切片有误解。您无需声明数组即可使用切片。您可以分配一个分片,然后分配后备存储。听起来类似于Delphi或C ++中的流。现在我明白了为什么所有的hoopla都是关于切片的。
矢量

2
@ComeAndGo,请注意,某些thimesimes创建指向“静态”数组的切片是一个有用的习惯用法。
kostix 2014年

2
@FelikZ,切片在其支持数组中创建“视图”。您通常经常事先知道函数将要操作的数据将具有固定的大小(或大小不超过已知的字节数;这对于网络协议来说是很常见的)。因此,您可以只声明一个数组以将这些数据保存在函数中,然后按需对其进行切片-将这些切片传递给调用的函数等。
kostix

52

几个月前,当我第一次开始研究Go时,我问了这个问题。从那时起,我每天都在阅读有关Go的内容,并在Go中进行编码。

因为我没有收到关于该问题的明确答案(尽管我已经接受了一个答案),所以我现在要根据我所学到的知识自行回答,因为我已经问过:

有没有一种方法可以在没有硬编码数组大小的情况下在Go中创建数组/切片?

是。切片不需要硬编码数组即可slice从:

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

此代码分配slice sl,其大小lencap-,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并非如此)。Golist.List似乎是一种“杂种”,由C ++vector<T>和Python产生 List(),并且在Go本身中可能有点不合适。

如果在不远的将来的某个时刻找到list,这也不会令我感到惊讶。Go中不推荐使用list,尽管它可能会保留下来,以容纳 稀有的东西。情况,即使使用良好的设计实践,也可以最好地解决问题与拥有各种类型的集合。或者,它可以为C系列开发人员提供一个“桥梁”,让他们在了解切片的细微差别之前熟悉Go,AFAIK是Go特有的。(在某些方面,切片似乎与C ++或Delphi中的流类相似,但不完全相同。)

尽管来自Delphi / C ++ / Python背景,但在初次接触Go时,我发现list.List自己比Go的slice更熟悉,因为我对Go越来越熟悉,所以我回去并将所有列表更改为slice。我还没有发现任何东西slice和/或map不允许我这样做,所以我需要使用list.List


@Alok Go是一种通用语言,设计时考虑了系统编程。它是强类型的...-他们也不知道他们在说什么吗?类型推断的使用并不意味着GoLang不是强类型的。我也清楚地说明了这一点:GoLang中不允许隐式类型转换,即使是向上转换也是如此。(感叹号并不能使您更加正确。请将其保存为儿童博客。)
2015年

@Alok-mods删除了您的评论,而不是我。只是简单地说一个人“不知道他们在说什么!” 除非您提供解释和证明,否则它是没有用的。此外,这应该是一个专业的场所,因此我们可以省略感叹号和夸张内容-将其保存在儿童博客中。如果您有问题,请说“我不知道如何说当我们有A,B和C时GoLang的字词是如此强烈”。OP可能会同意,或者解释为什么他们认为您错了。那将是一个有用且专业的意见,
2015年

4
静态检查的语言,它在代码运行之前强制执行一些规则。像C这样的语言为您提供了原始类型系统:您的代码可能会正确输入类型检查,但会在运行时崩溃。继续努力,就会得到Go语言,它比C语言给您更好的保证。但是,它距离像OCaml这样的语言的类型系统还很遥远(这也不是频谱的终结)。说“ Go可能是那里最强类型的语言”是完全错误的。对于开发人员来说,了解不同语言的安全性很重要,这样他们才能做出明智的选择。
Alok 2015年

4
Go中缺少的特定示例:缺少泛型会迫使您使用动态强制转换。缺乏枚举/检查交换机完整性的能力进一步暗示了动态检查,其他语言可以提供静态保证。
Alok 2015年

@ Alok-1我)也许是 2)我们正在谈论相当普遍使用的语言。这些天Go并不是很强大,但是Go标记了10545个问题,其中OCaml有3230个问题。3)Go you引用IMO中的不足之处与“强类型”无关(一个模糊的术语,不一定与编译时间检查相关)。4)“这很重要..”-抱歉,这没有意义-如果有人正在阅读此书,他们可能已经在使用Go。我怀疑有人在使用此答案来确定Go是否适合他们。海事组织,你应该找到更重要的事情来“深感困扰”……
矢量图片

11

我认为这是因为没有太多要谈论的内容,因为一旦您吸收了处理通用数据的主要Go习惯用法,该container/list程序包就很容易解释了。

在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,因此有一些解释将是有秩序的。但是,感谢类型断言/开关-直到现在我还是缺少这种东西。
矢量

@ComeAndGo,是的,它们不支持范围,因为这range是一种内置语言,仅适用于内置类型(数组,切片,字符串和映射),因为每次“调用”或range实际上会产生不同的机器代码来遍历它所处的容器应用于。
kostix

2
@ComeAndGo,关于索引...从软件包的文档中可以明显看出,它container/list提供了一个双向链接列表。这意味着索引是一项O(N)操作(您必须从头开始,然后遍历每个元素向尾部进行计数),而Go的基石设计范例之一就是没有隐藏的性能成本;另一个问题是,可以给程序员带来一些小的额外负担(为双向链接列表实现索引功能是十行的明智之举)。因此,该容器仅实现对其类型敏感的“规范”操作。
kostix 2014年

@ComeAndGo,请注意,在DelphiTList及其类似的组件中,其下面使用了动态数组,因此扩展这样的列表并不便宜,而对其进行索引则便宜。因此,尽管Delphi的“列表”看起来像抽象列表,但实际上它们是数组-在Go中将使用切片。我要强调的是,Go努力将事情布置得清晰而又不堆积“美丽的抽象”“隐藏”程序员的细节。Go的方法更像C,您可以清楚地知道数据的布局方式和访问方式。
kostix 2014年

3
@ComeAndGo,正是具有长度和容量的Go切片可以完成的工作。
kostix 2014年

6

请注意,可以通过append()内置函数扩展Go切片。尽管有时这需要复制后备阵列,但这不会每次都发生,因为Go会使新阵列过大,从而使其容量大于所报告的长度。这意味着可以在没有其他数据副本的情况下完成后续的追加操作。

尽管最终获得的数据副本多于通过链表实现的等效代码,但您无需分别分配列表中的元素,也无需更新Next指针。对于许多用途,基于数组的实现提供更好或足够好的性能,因此这是该语言所强调的。有趣的是,Python的标准list类型也支持数组,并且在附加值时具有类似的性能特征。

就是说,在某些情况下链表是更好的选择(例如,当您需要从长列表的开头/中间插入或删除元素时),这就是为什么提供标准库实现的原因。我猜他们没有添加任何特殊的语言功能来使用它们,因为这些案例比使用切片的案例少见。


不过,切片必须由具有硬编码大小的数组返回,对吗?那就是我不喜欢的东西。
矢量

3
如果是您的意思,那么切片的大小不会在程序源代码中进行硬编码。可以通过append()操作动态扩展它,如我所解释的(有时会涉及数据副本)。
James Henstridge 2014年

4

除非切片更新的频率太高(删除,在随机位置添加元素),否则切片的内存连续性与链接列表相比将提供出色的缓存命中率。

斯科特·迈耶(Scott Meyer)谈缓存的重要性。.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
当元素的顺序不重要时,
 使用切片和
 通过将其替换为切片中的最后一个元素来删除该元素,然后
 切片将len缩小1
 (如SliceTricks Wiki中所述)

因此,请
使用切片
1。如果列表中元素的顺序并不重要,并且您需要删除,只需
使用列表交换要删除的元素与最后一个元素,然后重新切片为(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。如果需要遍历速度

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.