迹忆客 专注技术分享

当前位置:主页 > 学无止境 > 编程语言 > Go >

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

作者:迹忆客 最近更新:2022/10/31 浏览次数:

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

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

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

转载请发邮件至 1244347461@qq.com 进行申请,经作者同意之后,转载请以链接形式注明出处

本文地址:

相关文章

Golang 中的零值 Nil

发布时间:2023/04/27 浏览次数:166 分类:Go

本篇文章介绍 nil 在 Golang 中的含义,nil 是 Go 编程语言中的零值,是众所周知且重要的预定义标识符。

Golang 中的 Lambda 表达式

发布时间:2023/04/27 浏览次数:93 分类:Go

本篇文章介绍如何在 Golang 中创建 lambda 表达式。Lambda 表达式似乎不存在于 Golang 中。 函数文字、lambda 函数或闭包是匿名函数的另一个名称。

Go 中的深度复制

发布时间:2023/04/27 浏览次数:90 分类:Go

当我们尝试生成对象的副本时,深层副本会准确复制原始对象的所有字段。 此外,如果它有任何对象作为字段,也会制作这些对象的副本。本篇文章介绍如何在 Golang 中进行深度复制。

在 Go 中捕获 Panics

发布时间:2023/04/27 浏览次数:66 分类:Go

像错误一样,Panic 发生在运行时。 换句话说,当您的 Go 程序中出现意外情况导致执行终止时,就会发生 Panics。让我们看一些例子来捕捉 Golang 中的Panics。

Go 中的日志级别

发布时间:2023/04/27 浏览次数:199 分类:Go

本篇文章介绍如何在 Golang 中创建和使用日志级别。Go 中的日志级别。Golang提供了一个日志包,名为log,是一个简单的日志包。 这个包不提供分级日志; 如果我们想要分级日志记录,我们必须

在 Go 中使用断言

发布时间:2023/04/27 浏览次数:181 分类:Go

本篇文章介绍了 assert 在 GoLang 中的使用。在 Go 语言中使用断言:GoLang 不提供对断言的任何内置支持,但我们可以使用来自 Testify API 的广泛使用的第三方包断言。

Go 中的随机数生成

发布时间:2023/04/27 浏览次数:114 分类:Go

本篇文章介绍如何在 Go 语言中使用随机数生成功能。Go 中的随机数生成 Go 语言为随机数生成功能提供内置支持。 内置包 math 有方法 rand(),用于随机数生成。

GoLang 电子邮件验证器

发布时间:2023/04/27 浏览次数:195 分类:Go

本篇文章介绍如何在 Go 语言中验证电子邮件。电子邮件需要特定格式; 否则,它们将无法工作。

扫一扫阅读全部技术教程

社交账号
  • https://www.github.com/onmpw
  • qq:1244347461

最新推荐

教程更新

热门标签

扫码一下
查看教程更方便