在 Go 中如何使用泛型 入门 – 使用泛型的集合

在上一篇文章 没有泛型的Go集合 中,我们介绍了泛型的概念。这里我们来介绍带有泛型的集合。在上一篇中,我们使用一个 interface{} 类型切片创建了一个集合。 但是要使用这些值,我们需要做一些额外的工作来将 interface{} 中的值转换为这些值的实际类型。 但是,使用泛型,我们可以创建一个或多个类型参数,它们的行为几乎类似于函数参数,但它们可以将类型作为值而不是数据来保存。 这样,泛型提供了一种在每次使用泛型类型时用不同类型替换类型参数的方法。 这就是泛型类型得名的地方。 由于泛型类型可以与多种类型一起使用,而不仅仅是像 io.Reader 或 interface{} 这样的特定类型,它的泛型足以适应多个用例。

在本节中,我们将在创建 Deck 实例而不是使用 interface{} 时将我们的 Deck 类型更新为可以使用任何特定类型卡的泛型类型。

要进行第一次更新,请打开 main.go 文件并删除 os 包导入:

package main

import (
    "fmt"
    "math/rand"
    // "os" package import is removed
    "time"
)

正如我们将在以后的更新中看到的,我们不再需要使用 os.Exit 函数,因此删除此导入是安全的。

接下来,将我们的 Deck 结构更新为泛型类型:

type Deck[C any] struct {
    cards []C
}

此更新引入了泛型用于在结构声明中创建占位符类型或类型参数的新语法。我们几乎可以将这些类型参数视为类似于我们将包含在函数中的参数。调用函数时,我们为每个函数参数提供值。同样,在创建泛型类型值时,为类型参数提供类型。

在结构名称 Deck 之后看到我们在方括号 [] 内添加了一条语句。这些方括号允许我们为结构定义一个或多个这些类型参数。

对于你的牌组类型,你只需要一个名为 C 的类型参数来表示牌组中牌的类型。通过在类型参数中声明 C any ,我们的代码会说,“创建一个名为 C 的泛型类型参数,我可以在我的结构中使用它,并允许它是任何类型”。在幕后,any 类型实际上是 interface{} 类型的别名。这使泛型更易于阅读,并且无需使用 C interface{}。我们的牌组只需要一种通用类型来表示卡片,但如果我们需要其他通用类型,则可以使用逗号分隔的语法添加它们,例如 C any、F any。如果没有保留,我们用于类型参数的名称可以是自己喜欢的任何名称,但它们通常很短且大写。

最后,在对 Deck 声明的更新中,我们更新了结构中卡片切片的类型以使用 C 类型参数。使用泛型时,我们可以在通常放置特定类型的任何地方使用类型参数。在这种情况下,我们希望 C 参数代表切片中的每张卡片,因此您将 [] 片类型声明放在 C 参数声明之后。

接下来,更新 Deck 类型的 AddCard 方法来使用定义的泛型类型。现在,我们将跳过更新 NewPlayingCardDeck 函数,但很快就会回来:

func (d *Deck[C]) AddCard(card C) {
    d.cards = append(d.cards, card)
}

在对 Deck 的 AddCard 方法的更新中,我们首先将 [C]d 泛型类型参数添加到方法的接收器。 这让 Go 知道我们将在方法声明的其他地方使用的类型参数的名称,并遵循与 struct 声明类似的方括号语法。 但是,在这种情况下,我们不需要提供 any 约束,因为它已经在 Deck 的声明中提供。 然后,您更新了 card 函数参数以使用 C 占位符类型而不是原始 interface{} 类型。 这使得使用特定类型的方法最终会变成 C。

更新 AddCard 方法后,更新 RandomCard 方法以使用 C 泛型类型:

func (d *Deck[C]) RandomCard() C {
    r := rand.New(rand.NewSource(time.Now().UnixNano()))

    cardIdx := r.Intn(len(d.cards))
    return d.cards[cardIdx]
}

这一次,我们更新了方法以返回值 C 而不是 interface{},而不是使用 C 泛型类型作为函数参数。 除了更新接收器以包含 [C] 之外,这是我们需要对该函数进行的唯一更新。 由于 Deck 上的 cards 字段已经在 struct 声明中进行了更新,因此当此方法从卡中返回值时,它返回的是 C 类型的值。

现在我们的 Deck 类型已更新为使用泛型,返回到我们的 NewPlayingCardDeck 函数并更新它以使用 *PlayingCard 类型的泛型 Deck 类型:

func NewPlayingCardDeck() *Deck[*PlayingCard] {
    suits := []string{"Diamonds", "Hearts", "Clubs", "Spades"}
    ranks := []string{"A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"}

    deck := &Deck[*PlayingCard]{}
    for _, suit := range suits {
        for _, rank := range ranks {
            deck.AddCard(NewPlayingCard(suit, rank))
        }
    }
    return deck
}

NewPlayingCardDeck 中的大部分代码保持不变,但现在我们使用的是通用版本的 Deck,我们需要在使用 Deck 时指定要用于 C 的类型。你可以像往常一样引用你的 Deck 类型,无论是 Deck 还是像 *Deck 这样的引用,然后使用最初声明类型参数时使用的方括号提供应该替换 C 的类型。

对于 NewPlayingCardDeck 返回类型,我们仍然像以前一样使用 *Deck,但是这一次,还包括方括号和 *PlayingCard。通过为 type 参数提供 [*PlayingCard],这是说我们希望在 Deck 声明和方法中使用 *PlayingCard 类型来替换 C 的值。这意味着 Deck 上的卡牌字段的类型基本上从 [ ]C 到 []*PlayingCard

类似地,当创建一个新的 Deck 实例时,您还需要提供替换 C 的类型。您通常可以使用 &Deck{} 对 Deck 进行新引用,而是将类型包含在方括号内以结束 &Deck[*PlayingCard]{}

现在我们的类型已更新为使用泛型,我们可以更新 main 函数来利用它们:

func main() {
    deck := NewPlayingCardDeck()

    fmt.Printf("--- drawing playing card ---\n")
    playingCard := deck.RandomCard()
    fmt.Printf("drew card: %s\n", playingCard)
    // Code removed
    fmt.Printf("card suit: %s\n", playingCard.Suit)
    fmt.Printf("card rank: %s\n", playingCard.Rank)
}

这次我们的更新是删除代码,因为不再需要将 interface{} 值断言为 *PlayingCard 值。 当我们更新 Deck 的 RandomCard 方法以返回 C 并更新 NewPlayingCardDeck 以返回 *Deck[*PlayingCard] 时,它更改了 RandomCard 以返回 *PlayingCard 值而不是 interface{}。 当 RandomCard 返回 *PlayingCard 时,表示 playCard 的类型也是 *PlayingCard 而不是 interface{},我们可以立即访问 Suit 或 Rank 字段。

要在将更改保存到 main.go 后查看程序运行,请再次使用 go run 命令:

$ go run main.go

我们应该会看到类似于以下输出的输出,但抽出的卡片可能会有所不同:

--- drawing playing card ---
drew card: 8 of Hearts
card suit: Hearts
card rank: 8

尽管输出与使用 interface{} 的程序的先前版本相同,但代码本身更简洁一些,并且避免了潜在的错误。我们不再需要对 *PlayingCard 类型进行断言,从而避免了额外的错误处理。此外,通过说我们的 Deck 实例只能包含 *PlayingCard,不可能将 *PlayingCard 以外的值添加到卡片切片中。

在本节中,我们将 Deck 结构更新为泛型类型,从而更好地控制您的牌组每个实例可以包含的卡片类型。我们还更新了 AddCard 和 RandomCard 方法以接受通用参数或返回通用值。然后,您更新了 NewPlayingCardDeck 以返回包含 *PlayingCard 牌的 *Deck。最后,我们删除了 main 函数中的错误处理,因为我们不再需要它。

现在我们的套牌已更新为通用套牌,我们可以使用它来放置自己想要的任何类型的卡牌。

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » 在 Go 中如何使用泛型 入门 – 使用泛型的集合