英文:
Enforcing type in "generic" code with empty interfaces
问题
抱歉标题不够明确。
我正在阅读这本书http://algs4.cs.princeton.edu/home/,我认为用Go实现书中的示例作为学习练习会很好,但是这本书使用Java来描述代码。
其中的一章讨论了设置一些核心数据类型/容器样式类以便以后重用,但是我在尝试将它们应用到Go环境中时遇到了麻烦,主要是因为这些数据类型似乎在使用Java泛型。
例如,我写了以下代码
package bag
type T interface{}
type Bag []T
func (a *Bag) Add(t T) {
*a = append(*a, t)
}
func (a *Bag) IsEmpty() bool {
return len(*a) == 0
}
func (a *Bag) Size() int {
return len(*a)
}
这个代码原则上是可以工作的,我可以向Bag
中添加项目并检查其大小等。然而,这也意味着以下代码是合法的
a := make(bag.Bag,0,0)
a.Add(1)
a.Add("Hello world!")
a.Add(5.6)
a.Add(time.Now())
我只是想知道是否有任何方法可以强制类型,使其符合类似于
Bag<T> bagOfMyType = new Bag<T>()
例如
Bag<Integer> bagOfInts = new Bag<Integer>()
我知道Go没有泛型,它们也不是Go的方式,但我只是想知道是否有可能在编译时“强制”任何东西(可能不行)
对不起,帖子太长了
**编辑:**好的,我进一步研究了一下,我基本上放弃了泛型方面的东西(我知道这不在Go的路线图上),所以我正在考虑使用类似于Haskell类型类的接口,例如
type T interface{}
type Bag interface {
Add(t T)
IsEmpty() bool
Size() int
}
type IntSlice []int
func (i *IntSlice) Add(t T) {
*i = append(*i, t.(int)) // 如果用户尝试添加除int以外的任何内容,将引发运行时异常
}
func (i *IntSlice) IsEmpty() bool {
return len(*i) == 0
}
func (i *IntSlice) Size() int {
return len(*i)
}
这种方法的问题是类型强制只在运行时强制执行。
有人有任何改进这个的想法吗?
英文:
Sorry for the ambiguous title.
I'm reading this book http://algs4.cs.princeton.edu/home/ and I thought it would be good to implement the examples in Go as a learning exercise, however the book uses Java as its language to describe code in.
One of the first chapters discusses setting up some core datatypes/container style classes to re-use later on but I'm having trouble trying to hammer these into a Go setting, mainly because these datatypes seem to be enjoying the use of Java generics.
For example, I've written the following code
package bag
type T interface{}
type Bag []T
func (a *Bag) Add(t T) {
*a = append(*a, t)
}
func (a *Bag) IsEmpty() bool {
return len(*a) == 0
}
func (a *Bag) Size() int {
return len(*a)
}
This works in principle in the sense that I can add items to a Bag
and check its size and everything. However this also means that the following code is legal
a := make(bag.Bag,0,0)
a.Add(1)
a.Add("Hello world!")
a.Add(5.6)
a.Add(time.Now())
I was just wondering if there was any way of enforcing the type so it conforms to a contract similar to
Bag<T> bagOfMyType = new Bag<T>()
e.g.
Bag<Integer> bagOfInts = new Bag<Integer>()
I know Go doesn't have generics and they're not really The Go Way, but I was just wondering if it is a possible to "enforce" anything at compile time (probably not)
Sorry for the long post
EDIT: OK so I've been looking into this a little further, I've pretty much given up with the generics side of things (I understand this is not on the roadmap for Go) so I'm thinking of doing something similar to Haskell typeclasses with interfaces, e.g.
type T interface{}
type Bag interface {
Add(t T)
IsEmpty() bool
Size() int
}
type IntSlice []int
func (i *IntSlice) Add(t T) {
*i = append(*i, t.(int)) // will throw runtime exception if user attempts to add anything other than int
}
func (i *IntSlice) IsEmpty() bool {
return len(*i) == 0
}
func (i *IntSlice) Size() int {
return len(*i)
}
The problem with this is the type enforcement is only enforced at runtime.
Anyone got any ideas how to improve on this?
答案1
得分: 5
我对Go还不太熟悉,所以我很好奇是否有人能给出更好的答案,但是我能给你提供我的看法:
你想要在IntSlice
上调用Add()
时,在编译时强制参数为int
类型。这是如何实现的:
func (i *IntSlice) Add(t int) {
*i = append(*i, t)
}
由于Go没有泛型,Add()
方法对于每种类型的Bag
都是不同的,所以Bag
接口(如果你需要的话)只需包含以下方法:
type Bag interface {
IsEmpty() bool
Size() int
}
对我来说,这是有意义的,因为你不能将一个Bag
传递并随意往里面放任何东西。仅仅知道某个东西是Bag
还不足以知道如何调用它的Add()
方法;你必须知道你正在处理的Bag
的类型。
你可以将接口具体化,比如IntBag
,但是由于只有一种类型实际上能满足该接口,你可以将接口去掉,并将IntSlice
的名称改为IntBag
。
基本上,这意味着完全放弃类似泛型的东西,只需创建一个具有一些方法的类型,以实现你想要的功能:
type IntBag []int
func (b *IntBag) Add(i int) {
*b = append(*b, i)
}
func (b IntBag) IsEmpty() bool {
return len(b) == 0
}
func (b IntBag) Size() int {
return len(b)
}
**更新:**有时候泛型确实非常方便。在我看来,我们只能根据具体问题来选择最佳解决方案。通过使用空接口和反射,你可以获得一些类似泛型的行为,但是这往往很丑陋,并且会放弃一些编译时类型检查。或者你放弃泛型,但是会有一些代码重复。或者你完全用一种不同的方式来解决问题。
几周前,我问了一个关于如何使用Go处理看起来需要类层次结构的问题的问题。答案基本上是没有通用解决方案;都是具体问题具体分析。我认为泛型也是一样的:Go中没有泛型,也没有将基于泛型的解决方案转化为Go的通用解决方案。
在其他语言中,有很多情况下你可能会使用泛型,但是在Go中,接口通常已经足够好用(或者真正闪耀)。你的例子是一个接口并不能真正替代的情况。另请参阅:Go Vs. Generics。
英文:
I'm new to Go myself, so I'm curious if someone will have a better answer, but here's how I see it:
You want compile-time enforcement that when Add()
is called on an IntSlice
, its parameter is an int
. Well, here's how you do that:
func (i *IntSlice) Add(t int) {
*i = append(*i, t)
}
Since there aren't generics, the Add()
method is going to be different for every type of Bag
, so the Bag
interface, assuming you need it, becomes just:
type Bag interface {
IsEmpty() bool
Size() int
}
That makes sense to me, because you can't pass a Bag
around and throw just anything in it. Knowing that something is a Bag
isn't enough to know how to call Add()
on it; you must know what type of Bag
you're dealing with.
You could make the interface specific to the type, like IntBag
, but since only one type is actually going to satisfy that interface, you might as well get rid of the interface and change the name of IntSlice
to IntBag
.
Basically that means giving up entirely on anything generic-like, and just creating a type with some methods that do what you want:
type IntBag []int
func (b *IntBag) Add(i int) {
*b = append(*b, i)
}
func (b IntBag) IsEmpty() bool {
return len(b) == 0
}
func (b IntBag) Size() int {
return len(b)
}
Update: Sometimes generics really would come in handy. It seems to me we're left choosing on a case-by-case basis what exactly is best for a given problem. With empty interfaces and reflection, you can get some generic-like behavior, but it tends to be ugly and you give up some compile-time type checking. Or you give up on generics and have some code-duplication. Or you just do it a totally different way.
I asked a question a few weeks ago about how I should use Go to handle problems that look to me like they need class hierarchies. The answer was basically that there is no general solution; it's all case-by-case. I think the same applies for generics: there are no generics in Go, and there's no general solution for translating generics-based solutions to Go.
There are many cases where you might use generics in another language but interfaces are perfectly adequate (or truly shine) in Go. Your example here is one where interfaces aren't really a proper replacement. See also: Go Vs. Generics.
答案2
得分: 1
我对Go语言非常熟悉。泛型是一个备受争议的话题,目前还没有类似于Java泛型或C++模板的东西。目前的约定是使用空接口实现一个“泛型”类型,然后用特定类型的实现包装它,以确保只使用该类型的元素。这里有一个使用Go标准库中的container/list
的示例。
package main
import (
"container/list"
"fmt"
)
type IntList struct {
innerList *list.List
}
func NewIntList() *IntList {
return &IntList{list.New()}
}
func (l *IntList) Add(i int) {
// 这是向列表中添加元素的唯一方法,
// Add()方法只接受int类型,所以只能添加int类型
l.innerList.PushBack(i)
}
func (l *IntList) Last() int {
lastElem := l.innerList.Back()
// 我们可以安全地将其类型断言为int,因为Add()保证我们不能将非int放入列表中
return lastElem.Value.(int)
}
func main() {
l := NewIntList()
l.Add(5)
l.Add(4)
l.Add(3)
l.Add(2)
l.Add(1)
fmt.Println("期望输出1;实际输出:", l.Last())
}
英文:
I'm pretty well-versed with Go. Generics are a hotly-debated topic, and there is currently nothing analogous to Java generics or C++ templates. The convention at the moment is to implement a "generic" type with an empty interface and then wrap it with a specific type implementation that makes sure only elements of that type are used. Here's an example with container/list
from the Go standard library.
http://play.golang.org/p/9w9H1EPHKR
package main
import (
"container/list"
"fmt"
)
type IntList struct {
innerList *list.List
}
func NewIntList() *IntList {
return &IntList{list.New()}
}
func (l *IntList) Add(i int) {
// this is the only way to add an element to the list,
// and the Add() method only takes ints, so only ints
// can be added
l.innerList.PushBack(i)
}
func (l *IntList) Last() int {
lastElem := l.innerList.Back()
// We can safely type-assert to an int, because Add()
// guarantees that we can't put a non-int into our list
return lastElem.Value.(int)
}
func main() {
l := NewIntList()
l.Add(5)
l.Add(4)
l.Add(3)
l.Add(2)
l.Add(1)
fmt.Println("Expecting 1; got:", l.Last())
}
答案3
得分: 0
Go现在支持泛型(从版本1.18开始):https://go.dev/doc/tutorial/generics
这是一个基于问题中示例的说明:
import "time"
type Bag[T any] []T
func (a *Bag[T]) Add(t T) {
*a = append(*a, t)
}
func (a *Bag[T]) IsEmpty() bool {
return len(*a) == 0
}
func (a *Bag[T]) Size() int {
return len(*a)
}
func main() {
indiscriminateBag := make(Bag[any], 0, 0)
indiscriminateBag.Add(1)
indiscriminateBag.Add("Hello world!")
indiscriminateBag.Add(5.6)
indiscriminateBag.Add(time.Now())
selectiveBag := make(Bag[int], 0, 0)
selectiveBag.Add(1)
selectiveBag.Add(1.25) // 编译器报错:无法将1.25(无类型浮点常量)作为int类型的参数传递给selectiveBag.Add
}
我在这里添加这个答案,以防其他刚接触Go的人遇到这个问题。
顺便说一下,我在使用Go实现普林斯顿算法课程时也开始思考Go泛型。这里有一些通用符号表。
英文:
Go now supports generics (from version 1.18 onwards): https://go.dev/doc/tutorial/generics
Here's an illustration based the example in the question:
import "time"
type Bag[T any] []T
func (a *Bag[T]) Add(t T) {
*a = append(*a, t)
}
func (a *Bag[T]) IsEmpty() bool {
return len(*a) == 0
}
func (a *Bag[T]) Size() int {
return len(*a)
}
func main() {
indiscriminateBag := make(Bag[any], 0, 0)
indiscriminateBag.Add(1)
indiscriminateBag.Add("Hello world!")
indiscriminateBag.Add(5.6)
indiscriminateBag.Add(time.Now())
selectiveBag := make(Bag[int], 0, 0)
selectiveBag.Add(1)
selectiveBag.Add(1.25) // Compiler complains: cannot use 1.25 (untyped float constant) as int value in argument to selectiveBag.Add
}
I'm adding this answer here just in case anyone else new to Go stumbles across this question.
Incidentally, I also started thinking about Go generics while implementing the Princeton algorithms course in Go. Here are some generic symbol tables
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论