亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

為了賬號安全,請及時綁定郵箱和手機立即綁定
已解決430363個問題,去搜搜看,總會有你想問的

在 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 關注
  • 168 瀏覽
慕課專欄
更多

添加回答

舉報

0/150
提交
取消
微信客服

購課補貼
聯系客服咨詢優惠詳情

幫助反饋 APP下載

慕課網APP
您的移動學習伙伴

公眾號

掃描二維碼
關注慕課網微信公眾號