【发布时间】:2016-06-19 01:14:05
【问题描述】:
我有一个自定义类型,它内部有一段数据。
是否有可能通过实现范围运算符需要的一些函数或接口来迭代(使用范围)我的自定义类型?
【问题讨论】:
我有一个自定义类型,它内部有一段数据。
是否有可能通过实现范围运算符需要的一些函数或接口来迭代(使用范围)我的自定义类型?
【问题讨论】:
不,不使用range。 range 接受数组、切片、字符串、映射和通道,仅此而已。
可迭代事物的常用习语(例如bufio.Scanner)似乎是
iter := NewIterator(...)
for iter.More() {
item := iter.Item()
// do something with item
}
但是没有通用接口(考虑到类型系统,无论如何都不会很有用)并且实现该模式的不同类型通常具有不同的名称,用于它们的More 和Item 方法(例如Scan 和@ 987654328@bufio.Scanner)
【讨论】:
简短的回答是否定的。
答案仍然是否定的,但有可能以某种可行的方式破解它。但需要明确的是,这肯定是一种 hack。
有几种方法可以做到这一点,但它们之间的共同主题是您希望以某种方式将数据转换为 Go 能够覆盖的类型。
由于您提到您在内部有一个切片,因此这对于您的用例来说可能是最简单的。这个想法很简单:您的类型应该有一个Iterate() 方法(或类似方法),其返回值是适当类型的切片。调用时,将创建一个新切片,其中包含数据结构的所有元素,按照您希望它们被迭代的任何顺序。所以,例如:
func (m *MyType) Iterate() []MyElementType { ... }
mm := NewMyType()
for i, v := range mm.Iterate() {
...
}
这里有一些问题。首先,分配 - 除非您想公开对内部数据的引用(通常您可能不会这样做),否则您必须创建一个新切片并复制所有元素。从 big-O 的角度来看,这并没有那么糟糕(无论如何,您都在做线性数量的工作来迭代所有内容),但出于实际目的,这可能很重要。
此外,这不处理对变异数据的迭代。大多数时候这可能不是问题,但如果您真的想支持并发更新和某些类型的迭代语义,您可能会关心。
频道也是可以在 Go 中进行范围划分的东西。这个想法是让您的Iterate() 方法生成一个 goroutine,它将遍历数据结构中的元素,并将它们写入通道。然后,当迭代完成时,可以关闭通道,这将导致循环结束。例如:
func (m *MyType) Iterate() <-chan MyElementType {
c := make(chan MyElementType)
go func() {
for _, v := range m.elements {
c <- v
}
close(c)
}()
return c
}
mm := NewMyType()
for v := range mm.Iterate() {
...
}
与 slice 方法相比,此方法有两个优点:首先,您不必分配线性数量的内存(尽管出于性能原因,您可能希望让您的通道有一点缓冲区),以及其次,如果你喜欢这种事情,你可以让你的迭代器很好地处理并发更新。
这种方法的大缺点是,如果你不小心,你可能会泄漏 goroutine。解决这个问题的唯一方法是让你的通道有一个足够深的缓冲区来保存数据结构中的所有元素,以便 goroutine 可以填充它然后返回,即使没有从通道中读取任何元素(然后通道可以后来被垃圾收集)。这里的问题是,a)你现在回到线性分配,b)你必须预先知道你要写多少元素,这会阻止整个并发更新的事情.
这个故事的寓意是频道很适合迭代,但您可能不想实际使用它们。
感谢hobbs 提供getting to this before me,但为了完整起见,我会在这里介绍(因为我想多说一点)。
这里的想法是创建一个迭代器对象(或者让你的对象一次只支持一个迭代器,并直接对其进行迭代),就像在更直接支持它的语言中一样。然后,您要做的是调用Next() 方法,该方法a) 将迭代器前进到下一个元素,b) 返回一个布尔值,指示是否还有任何东西。然后你需要一个单独的Get() 方法来实际获取当前元素的值。 this 的用法实际上并没有使用 range 关键字,但它看起来很自然:
mm := MyNewType()
for mm.Next() {
v := mm.Get()
...
}
与前两种技术相比,这种技术有一些优点。首先,它不涉及预先分配内存。其次,它非常自然地支持错误。虽然它不是真正的迭代器,但这正是bufio.Scanner 所做的。基本上,这个想法是有一个Error() 方法,您在迭代完成后调用该方法,以查看迭代是否因为完成而终止,或者是因为在中途遇到错误。对于纯粹的内存数据结构,这可能无关紧要,但对于涉及 IO 的结构(例如,遍历文件系统树、迭代数据库查询结果等),这真的很好。所以,完成上面的sn-p代码:
mm := MyNewType()
for mm.Next() {
v := mm.Get()
...
}
if err := mm.Error(); err != nil {
...
}
Go 不支持任意数据结构的范围 - 或自定义迭代器 - 但您可以破解它。如果您必须在生产代码中执行此操作,则第三种方法是 100% 可行的方法,因为它既是最干净的,也是最少的 hack(毕竟,标准库包含这种模式)。
【讨论】:
select,一个defer close,还有一点点小心。
joshlf 给出了一个很好的答案,但我想补充几点:
通道迭代器的一个典型问题是您必须遍历整个数据结构,否则为通道提供数据的 goroutine 将永远挂起。但这很容易被规避,这里有一种方法:
func (s intSlice) chanIter() chan int {
c := make(chan int)
go func() {
for _, i := range s {
select {
case c <- i:
case <-c:
close(c)
return
}
}
close(c)
}()
return c
}
在这种情况下写回到迭代器通道会提前中断迭代:
s := intSlice{1, 2, 3, 4, 5, 11, 22, 33, 44, 55}
c := s.chanIter()
for i := range c {
fmt.Println(i)
if i > 30 {
// Send to c to interrupt
c <- 0
}
}
这里非常重要的是,您不要简单地将break 排除在for 循环之外。你可以中断,但你必须必须先写入通道以确保goroutine退出。
我经常喜欢的一种迭代方法是使用迭代器闭包。在这种情况下,迭代器是一个函数值,当被重复调用时,它会返回下一个元素并指示迭代是否可以继续:
func (s intSlice) cloIter() func() (int, bool) {
i := -1
return func() (int, bool) {
i++
if i == len(s) {
return 0, false
}
return s[i], true
}
}
像这样使用它:
iter := s.cloIter()
for i, ok := iter(); ok; i, ok = iter() {
fmt.Println(i)
}
在这种情况下,尽早跳出循环是完全可以的,iter 最终会被垃圾回收。
这是上述实现的链接:http://play.golang.org/p/JC2EpBDQKA
【讨论】:
还有一个没有提到的选项。
您可以定义一个 Iter(fn func(int)) 函数,该函数接受一些将为您的自定义类型中的每个项目调用的函数。
type MyType struct {
data []int
}
func (m *MyType) Iter(fn func(int)) {
for _, item := range m.data {
fn(item)
}
}
而且可以这样使用:
d := MyType{
data: []int{1,2,3,4,5},
}
f := func(i int) {
fmt.Println(i)
}
d.Iter(f)
游乐场
链接到工作实现:https://play.golang.org/p/S3CTQmGXj79
【讨论】: