在Go中是否可以迭代自定义类型?

Koa*_*la3 32 iterator go

我有一个自定义类型,内部有一片数据.

是否可以通过实现范围运算符所需的某些函数或接口来迭代(使用范围)我的自定义类型?

jos*_*hlf 45

最简洁的答案是不.

答案仍然很长,但是有可能以一种有效的方式破解它.但要明确的是,这肯定是一个黑客攻击.

有几种方法可以做到,但它们之间的共同主题是你想以某种方式将你的数据转换为Go能够超越的类型.

方法1:切片

既然你提到你内部有一个切片,这对你的用例来说可能是最简单的.这个想法很简单:你的类型应该有一个Iterate()方法(或类似的),其返回值是适当类型的一个切片.调用时,会创建一个新切片,其中包含数据结构的所有元素,无论您希望迭代它们的顺序如何.所以,例如:

func (m *MyType) Iterate() []MyElementType { ... }

mm := NewMyType()
for i, v := range mm.Iterate() {
    ...
}
Run Code Online (Sandbox Code Playgroud)

这里有一些问题.首先,分配 - 除非你想公开对内部数据的引用(通常,你可能不这样做),你必须创建一个新的切片并复制所有元素.从大O角度来看,这并不是那么糟糕(无论如何,你正在做一系列线性的迭代工作),但出于实际目的,它可能很重要.

此外,这不会处理迭代变异数据.这在大多数情况下可能不是问题,但如果您真的想要支持并发更新和某些类型的迭代语义,您可能会关心.

方法2:渠道

频道也可以在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() {
    ...
}
Run Code Online (Sandbox Code Playgroud)

这种方法相对于切片方法有两个优点:首先,您不必分配线性数量的内存(尽管出于性能原因,您可能希望让通道具有一些缓冲区),其次,您如果你有这样的事情,可以让你的迭代器很好地与并发更新.

这种方法的一个重大缺点是,如果你不小心,你可以泄漏goroutines.解决这个问题的唯一方法是让你的通道有一个足够深的缓冲区来容纳数据结构中的所有元素,这样goroutine就可以填充它然后返回即使没有从通道中读取元素(然后通道可以以后被垃圾收集).这里的问题是,a)你现在回到线性分配,并且b)你必须事先知道你要编写多少元素,哪种类型会阻止整个并发更新的事情.

这个故事的寓意是频道可以迭代,但你可能不想真正使用它们.

方法3:内部迭代器

感谢霍布斯我之前得到这个,但我会在这里介绍它的完整性(因为我想多说一点关于它).

这里的想法是创建一个迭代器对象(或者让对象一次只支持一个迭代器,并直接迭代它),就像在更直接支持它的语言中一样.那么,你所做的是调用一个Next()方法,a)将迭代器推进到下一个元素,并且b)返回一个布尔值,指示是否还有剩下的东西.然后,您需要一个单独的Get()方法来实际获取当前元素的值.使用它实际上并不使用range关键字,但它看起来很自然:

mm := MyNewType()
for mm.Next() {
    v := mm.Get()
    ...
}
Run Code Online (Sandbox Code Playgroud)

与前两种技术相比,这种技术有一些优点.首先,它不涉及预先分配内存.其次,它非常自然地支持错误.虽然它不是真正的迭代器,但这正是bufio.Scanner它的作用.基本上,我们的想法是Error()在迭代完成后调用一个方法来查看迭代是否因为完成而终止,或者因为中途遇到了错误.对于纯粹的内存数据结构,这可能无关紧要,但对于涉及IO的那些(例如,遍历文件系统树,迭代数据库查询结果等),它真的很好.因此,要完成上面的代码段:

mm := MyNewType()
for mm.Next() {
    v := mm.Get()
    ...
}
if err := mm.Error(); err != nil {
    ...
}
Run Code Online (Sandbox Code Playgroud)

结论

Go不支持任意数据结构 - 或自定义迭代器 - 但你可以破解它.如果你必须在生产代码中执行此操作,第三种方法是100%的方法,因为它是最干净和最少的黑客(毕竟,标准库包括这种模式).

  • #2很像其他地方的发电机.但是,不是那么容易使用.为了避免没有缓冲通道的泄漏需要两个通道,一个"选择",一个"延迟关闭"和一些小心. (2认同)
  • 那个,并且客户愿意明确表示他们已经完成了测距. (2认同)
  • 一个出色的答案joshif,谢谢.我将采用方法3. (2认同)

hob*_*bbs 11

不,不使用range.range接受数组,切片,字符串,映射和通道,就是这样.

bufio.Scanner似乎是可迭代事物(例如a )的惯用语

iter := NewIterator(...)
for iter.More() {
    item := iter.Item()
    // do something with item
}
Run Code Online (Sandbox Code Playgroud)

但是没有通用接口(无论如何都不会给出类型系统非常有用),实现模式的不同类型通常具有不同的名称MoreItem方法(例如ScanTexta bufio.Scanner)


Lu.*_*mec 7

还有一个没有提到的选项。

您可以定义一个Iter(fn func(int))函数,该函数接受将为自定义类型中的每个项目调用的某个函数。

type MyType struct {
    data []int
}

func (m *MyType) Iter(fn func(int)) {
    for _, item := range m.data {
        fn(item)
    }
}
Run Code Online (Sandbox Code Playgroud)

它可以这样使用:

d := MyType{
    data: []int{1,2,3,4,5},
}

f := func(i int) {
    fmt.Println(i)
}
d.Iter(f)
Run Code Online (Sandbox Code Playgroud)

Playground
工作实现链接:https://play.golang.org/p/S3CTQmGXj79


Lem*_*eId 6

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
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,写回迭代器通道会中断迭代:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

这里非常重要的是你不要简单地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
    }
}
Run Code Online (Sandbox Code Playgroud)

像这样使用它:

iter := s.cloIter()
for i, ok := iter(); ok; i, ok = iter() {
    fmt.Println(i)
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,完全可以提前摆脱循环,iter最终将被垃圾收集.

操场

以下是上述实现的链接:http://play.golang.org/p/JC2EpBDQKA