英文:
How do you implement a container for different types in Go?
问题
以下代码实现了一个Go语言中的int列表:
package main
import "fmt"
type List struct {
Head int
Tail *List
}
func tail(list List) *List {
return list.Tail
}
func main() {
list := List{Head: 1, Tail:
&List{Head: 2, Tail:
&List{Head: 3, Tail:
nil}}}
fmt.Println(tail(list).Head)
}
问题是这个代码只适用于int
类型。如果我想要一个string
类型的列表,我需要重新实现每个列表方法(比如tail
)!这显然不实际,所以可以通过使用空接口来解决:
type List struct {
Head interface{} // 现在适用于任何类型!
Tail *List
}
问题是,1. 这似乎会因为类型转换而变得更慢,2. 它放弃了类型安全性,允许对任何类型进行类型检查:
// 这个会进行类型检查!
func main() {
list := List{Head: 123456789 , Tail:
&List{Head: "covfefe" , Tail:
&List{Head: nil , Tail:
&List{Head: []int{1,2}, Tail:
nil}}}}
fmt.Println(tail(list).Head)
}
显然,在静态类型语言中,这个程序不应该进行类型检查。
如何实现一个列表类型,不需要我为每个包含的类型重新实现所有列表方法,但仍保持期望的类型安全性和性能?
英文:
The following code implements a List of ints in Go:
package main
import "fmt"
type List struct {
Head int
Tail *List
}
func tail(list List) *List {
return list.Tail
}
func main() {
list := List{Head: 1, Tail:
&List{Head: 2, Tail:
&List{Head: 3, Tail:
nil}}}
fmt.Println(tail(list).Head)
}
Problem is this only works for int
. If I wanted a list of strings
, I'd need to re-implement every list method (such as tail
) again! That's obviously not practical, so, this can be solved by using an empty interface:
type List struct {
Head interface{} // Now works for any type!
Tail *List
}
Problem is, 1. this seems to be much slower because of type casts, 2. it throws aways type safety, allowing one to type-check anything:
// This type-checks!
func main() {
list := List{Head: 123456789 , Tail:
&List{Head: "covfefe" , Tail:
&List{Head: nil , Tail:
&List{Head: []int{1,2}, Tail:
nil}}}}
fmt.Println(tail(list).Head)
Obviously that program should not type-check in a statically typed language.
How can I implement a List type which doesn't require me to re-implement all List methods for each contained type, but which keeps the expected type safety and performance?
答案1
得分: 7
Go语言没有泛型类型,所以你只能使用你列出的选项。抱歉。
与此同时,Go内置的映射和切片,以及使用空接口构建容器(需要显式拆箱)的能力,意味着在许多情况下,可以编写能够实现泛型功能的代码,尽管不够灵活。
如果你对要存储在容器中的元素更了解,可以使用更专门的接口类型(而不是空接口interface{}
),它可以:
- 帮助你避免使用类型断言(保持良好的性能)
- 仍然保持类型安全
- 可以用于所有(隐式)实现你的接口的类型(代码的“可重用性”,不需要为多个类型重复编写代码)。
但仅限于此。在这里可以看到一个示例:https://stackoverflow.com/questions/39092925/why-are-interfaces-needed-in-golang/39100038#39100038
另外,如果你错过了,标准库中已经有一个双向链表的实现,位于container/list
包中(该实现也使用interface{}
类型作为值)。
英文:
Go doesn't have generic types, so you're stuck with the options you listed. Sorry.
> Meanwhile, Go's built-in maps and slices, plus the ability to use the empty interface to construct containers (with explicit unboxing) mean in many cases it is possible to write code that does what generics would enable, if less smoothly.
If you know more about the elements you want to store in the container, you may use a more specialized interface type (instead of the empty interface interface{}
), which
- could help you avoid using type assertions (keep good performance)
- and still keep type safety
- and it can be used for all types that (implicitly) implement your interface (code "re-usability", no need to duplicate for multiple types).
But that's about it. See an example of this here: https://stackoverflow.com/questions/39092925/why-are-interfaces-needed-in-golang/39100038#39100038
Also just in case you missed it, the standard library already has a doubly linked list implementation in the container/list
package (which also uses interface{}
type for the values).
答案2
得分: 4
重要的是要认识到,我们预计通用类型会比较慢,而不是更快。Java的优秀库fastutil通过使用具体实现来超越标准库中更灵活的类。
Go语言的作者之一Russ Cox在这篇文章中总结了这种情况:
- (C语言的方法)不支持泛型。
- 这会降低程序员的效率。
- (C++的方法)编译时特化或宏展开。
- 这会降低编译速度。
- (Java的方法)隐式装箱。
- 这会降低执行速度。
你可能对这份实时文档感兴趣,其中详细列出了许多优缺点。
正如另一个回答指出的,Go语言不支持你在这里尝试实现的设计。个人而言,我会简单地复制代码——这不会增加太多工作量,测试也很简单,并且大多数情况下你实际上并不需要你想要实现的通用行为。
英文:
It's important to acknowledge that we expect generic types to be slower, not faster. The excellent fastutil library for Java outperforms the more flexible classes from the standard library by using concrete implementations.
Russ Cox (one of Go's authors) summarises the situation like this:
> * (The C approach.) Leave them out.
- This slows programmers.
- (The C++ approach.) Compile-time specialization or macro expansion.
- This slows compilation.
- (The Java approach.) Box everything implicitly.
- This slows execution.
You may be interested in this living document which has a lot of the pros and cons outlined.
As the other answer points out, Go does not support the design you're trying to achieve here. Personally, I'd just duplicate the code - it's not much, the tests are cheap, and most of the time you don't actually need the generic behaviour you want to implement.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论