Golang的流式程式碼 - 0x46

banq發表於2022-03-18

Go 1.18 剛剛釋出,這意味著 Go 現在正式支援泛型。出於好奇,我決定研究建立一個實現類似於 Java 流的庫。我的簡單實現的目標是支援使用兩個操作處理切片的元素:對映和過濾。
如果您只想檢視程式碼,則可以在此處找到儲存庫
我沒有在本文中展示實現,坦率地說,它並不那麼有趣。
我們將只看使用這個庫產生的程式碼。這樣我們就可以評估使用類似的庫是否會使 Go 程式更具可讀性。
  

過濾
在本節中,我們將嘗試過濾一個整數切片,在結果切片中只留下偶數。首先讓我們看一下使用帶有for迴圈的標準方法執行此操作的一段程式碼:

func OnlyEven(slice []int) []int {
        var result []int

        for _, element := range slice {
            if element%2 == 0 {
                    result = append(result, element)
            }
        }

        return result
}

我相信大多數人在使用 Go 程式設計時編寫了數百個類似的函式。這樣簡單的函式編寫起來非常重複,但很容易識別,並且通常與我剛剛介紹的函式一樣簡單。
現在讓我們看看使用我編寫的庫實現的相同功能:

func OnlyEven(slice []int) []int {
    return streams.New(slice).
        Filter(onlyEven).
        Collect()
}

func onlyEven(v int) bool {
    return v%2 == 0
}


為了便於閱讀,我用命名函式替換了匿名函式。我相信這段程式碼相對容易閱讀,但我很難判斷它是否比之前的函式更具可讀性。然而,它似乎寫起來更快,並且包含的​​樣板較少,這可能會損害可讀性。
總的來說,我相信for在某些情況下,過濾器呼叫鏈可能比迴圈內的許多語句更具可讀性。話雖如此,我認為普通for迴圈不會令人討厭或難以閱讀,因此很難判斷這是否解決了任何實際問題。
 

過濾和對映
現在讓我們試著讓這個例子更復雜一些。首先我們將過濾這些值,然後將它們從 對映int到string,然後過濾掉超過一位數的值。為了簡單len起見,我們將只使用類似RuneCountInString.
同樣,首先讓我們嘗試一個傳統的for迴圈:

func OnlyEvenAsStrings(slice []int) []string {
        var result []int

        for _, element := range slice {
            if element%2 != 0 {
                continue
            }

            s := strconv.Itoa(element)
            if len(s) > 1 {
                continue
            }

            result = append(result, s)
        }

        return result

如您所見,我試圖以一種避免巢狀條件語句的方式構造程式碼。儘管可以透過多種方式編寫此函式,但我認為這種方法使控制流更易於遵循。所有條件語句都可以清楚地識別為過濾元素的條件。
現在讓我們嘗試對我的庫做同樣的事情:

func OnlyEvenAsStrings(slice []int) []string {
    return streams.Map(
        streams.New(slice).Filter(onlyEven),
        strconv.Itoa,
    ).
        Filter(onlyOneByte).
        Collect()
}

func onlyEven(v int) bool {
    return v%2 == 0
}

func onlyOneByte(v string) bool {
    return len(v) == 1
}

不幸的是,正如您所看到的,程式碼突然變得不那麼可讀了。在我看來,函式應該是這樣的:

func OnlyEvenAsStrings(slice []int) []string {
    return streams.New(slice).
        Filter(onlyEven).
        Map(strconv.Itoa).
        Filter(onlyOneByte).
        Collect()
}

不幸的是,以Go中泛型的工作方式,目前這是不可能的。
真實的例子看起來很糟糕,因為方法不能用額外的型別引數進行引數化。
由於這個原因,我不得不使用一個頂層函式。這是很不幸的,因為我認為我的理想化例子會更有意義。
目前產生的程式碼在可讀性方面肯定是完全失敗的。
 
關於效能的簡短說明
出於好奇,我對第一節中的過濾函式進行了基準測試。一個簡單的迴圈的效能比試圖用我的庫來執行同樣的任務要好。雖然我的實現可能過於天真,但我不認為這與我的程式碼有必然聯絡。我假設這是由編譯器在使用簡單迴圈時進行的各種最佳化造成的。因此,一個更復雜的實現很可能總是失敗。
 

相關文章