为了账号安全,请及时绑定邮箱和手机立即绑定

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

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

Go
慕尼黑5688855 2021-12-20 15:08:08
我有一个自定义类型,它内部有一段数据。是否有可能通过实现范围运算符需要的某些函数或接口来迭代(使用范围)我的自定义类型?
查看完整描述

3 回答

?
慕森王

TA贡献1777条经验 获得超3个赞

最简洁的答案是不。


长答案仍然是否定的,但有可能以某种有效的方式破解它。但需要明确的是,这肯定是一次黑客攻击。


有几种方法可以做到,但它们之间的共同主题是您希望以某种方式将数据转换为 Go 能够覆盖的类型。


方法一:切片

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


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


mm := NewMyType()

for i, v := range mm.Iterate() {

    ...

}

这里有一些问题。首先,分配 - 除非您想公开对内部数据的引用(通常,您可能不会这样做),否则您必须创建一个新切片并复制所有元素。从大 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() {

    ...

}

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


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


这个故事的寓意是通道很适合迭代,但你可能不想实际使用它们。


方法 3:内部迭代器

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


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


mm := MyNewType()

for mm.Next() {

    v := mm.Get()

    ...

}

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


mm := MyNewType()

for mm.Next() {

    v := mm.Get()

    ...

}

if err := mm.Error(); err != nil {

    ...

}

结论

Go 不支持覆盖任意数据结构——或自定义迭代器——但你可以破解它。如果您必须在生产代码中执行此操作,则第三种方法是 100% 可行的方法,因为它既是最干净的又是最少的 hack(毕竟,标准库包含此模式)。


查看完整回答
反对 回复 2021-12-20
?
一只斗牛犬

TA贡献1784条经验 获得超2个赞

不,不使用range. range接受数组、切片、字符串、映射和通道,仅此而已。


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


iter := NewIterator(...)

for iter.More() {

    item := iter.Item()

    // do something with item

}

但是没有通用接口(无论如何,鉴于类型系统都不会很有用),并且实现该模式的不同类型通常具有不同的名称More和Item方法(例如Scan和Texta bufio.Scanner)


查看完整回答
反对 回复 2021-12-20
?
慕工程0101907

TA贡献1887条经验 获得超5个赞

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


查看完整回答
反对 回复 2021-12-20
  • 3 回答
  • 0 关注
  • 127 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信