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

时间:2016-03-05 05:57:35

标签: go iterator

我有一个自定义类型,内部有一段数据。

通过实现一些函数或范围操作符所需的接口,是否可以在我的自定义类型上迭代(使用范围)?

4 个答案:

答案 0 :(得分:37)

简短的回答是否定的。

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

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

方法1:切片

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

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

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

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

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

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

故事的寓意是频道对迭代很可爱,但你可能不想真正使用它们。

方法3:内部迭代器

hobbs感谢getting to this before me,但为了完整起见,我会在这里介绍它(因为我想多说一点)。

这里的想法是创建一个迭代器对象(或者让对象一次只支持一个迭代器,并直接迭代它),就像在更直接支持它的语言中一样。那么,你所做的是调用Next()方法,a)将迭代器推进到下一个元素,并且b)返回一个布尔值,指示是否还有剩余的东西。然后,您需要一个单独的Get()方法来实际获取当前元素的值。使用它实际上并不使用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%的方法,因为它是最干净和最少的黑客(毕竟,标准库包括这种模式)。

答案 1 :(得分:8)

不,不使用rangerange接受数组,切片,字符串,地图和渠道,以及它。

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

iter := NewIterator(...)
for iter.More() {
    item := iter.Item()
    // do something with item
}

但是没有通用接口(无论如何都不会对类型系统非常有用),实现模式的不同类型通常会为More和{{1}设置不同的名称方法(例如ItemScan用于Text

答案 2 :(得分:6)

joshlf给出了一个很好的答案,但我想补充几点:

使用频道

频道迭代器的一个典型问题是你必须在整个数据结构范围内进行搜索,否则goroutine将为频道提供永久的悬挂。但这可以很容易地规避,这是一种方式:

break

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

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
    }
}

非常重要的是,您不要简单地{for}循环iter := s.cloIter() for i, ok := iter(); ok; i, ok = iter() { fmt.Println(i) } 。您可以中断,但您必须必须首先写入该频道以确保goroutine将退出。

使用闭包

我经常倾向于使用迭代的方法是使用迭代器闭包。在这种情况下,迭代器是一个函数值,当重复调用时,它返回下一个元素并指示迭代是否可以继续:

iter

像这样使用:

public class HighScores extends Activity {

    private TextView thighscore1;
    private TextView thighscore2;
    private TextView thighscore3;
    public TextView name;
    public   int highscore1 =0;
    public   int highscore2 =0;
    public   int highscore3 =0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_high);

        name = (TextView) findViewById(R.id.Names);
        thighscore1 = (TextView) findViewById(R.id.highscore1);
        thighscore2 = (TextView) findViewById(R.id.highscore2);
        thighscore3 = (TextView) findViewById(R.id.highscore3);

        SharedPreferences prefs = this.getSharedPreferences("myPrefsKey",
                Context.MODE_PRIVATE);

        int score = prefs.getInt("key", 0); //0 is the default value
        thighscore1.setText("" + score);
        if(score > highscore2) {
            highscore1 = score;
            thighscore1.setText("" + highscore1);
        }else{
            highscore1=highscore1;
        }
        if(score > highscore3 && score < highscore1){
            highscore2 = score;
            thighscore2.setText("" + highscore2);
        }else{
            highscore2 = highscore2;
        }
        if (score > 0 && score < highscore2){
            highscore3 = score;
            thighscore3.setText("" + highscore3);
        }else{
            highscore3 = highscore3;
        }

        SharedPreferences sp = this.getSharedPreferences("MyKey",0);
        String data = sp.getString("tag", "");
        name.setText(""+ data);
    }}

在这种情况下,完全摆脱循环是完全可以的,calloc最终将被垃圾收集。

游乐场

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

答案 3 :(得分:0)

还有一个未提及的选择。

您可以定义 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