ホームページ >バックエンド開発 >Golang >golang のジェネリックについての深い理解 (Generic)

golang のジェネリックについての深い理解 (Generic)

青灯夜游
青灯夜游転載
2023-04-11 19:20:461799ブラウズ

この記事は、golang のジェネリックスについての深い理解をもたらします。ジェネリック医薬品の使い方は?一定の参考値があるので、困っている友人は参考にしていただければ幸いです。

golang のジェネリックについての深い理解 (Generic)

ジェネリックスとは

ジェネリックス (ジェネリック) はプログラミング テクノロジです。厳密に型指定された言語では、後で指定される型を使用してコードを記述し、インスタンス化時に対応する型を指定できます。

ジェネリックでは、特定のデータ型の代わりに型パラメーターを使用できます。これらの型パラメーターは、クラス、メソッド、またはインターフェイスで宣言でき、これらの宣言で使用できます。ジェネリックスを使用するコードでは、実行時に実際の型パラメーターを指定できるため、コードをさまざまな種類のデータに適用できます。

ジェネリックにより、コードの可読性、保守性、再利用性が向上します。これにより、コードの冗長性が軽減され、型安全性とコンパイル時の型チェックが向上します。

ジェネリクスがコードの冗長性を削減できる理由を具体的な例を使用して説明します:

a と b の最小値を返す関数を提供します。それぞれが必要です。 a に対して関数を作成します。特定のデータ型「int、float...」、またはインターフェイスを使用します。「パラメータの型アサーションが必要ですが、これは実行パフォーマンスに影響を及ぼし、渡されるパラメータを制約できません。」

func minInt(a, b int) int {
    if a > b {
        return b
    }
    return a
}

func minFloat(a, b float64) float64 {
    if a > b {
        return b
    }
    return a
}

func minItf(a, b interface{}) interface{} {
    switch a.(type) {
    case int:
        switch b.(type) {
        case int:
            if a.(int) > b.(int) {
                return b
            }
            return a
        }
    case float64:
        switch b.(type) {
        case float64:
            if a.(float64) > b.(float64) {
                return b
            }
            return a
        }
    }
    return nil
}

上記のメソッドから、パラメータと返される結果のタイプが異なることを除けば、minInt と minFloat は同じコードであることがわかります。特定の型を指定せずに関数が呼び出されたときに渡される型を判断する方法はありますか?ここでは、ジェネリックと呼ばれる概念が導入されています。これは、単純に広範な型または不特定の特定の型として理解できます。ジェネリックスを導入することで、特定のデータ型を指定する必要がなくなりました。min 関数は次のように使用できます:

##
// T 为类型参数, 在调用时确定参数的具体值, 可以为 int, 也可以为 float64;它与 a, b 一样也是参数, 需要调用时传入具体的值;不同的是,T 为类型参数,值为具体的类型, a,b 为函数参数,值为具体类型对应的值
func minIntAndFloat64[T int | float64](a, b T) T { 
    if a < b {
        return a
    }
    return b
}

minIntAndFloat64[int](1, 2) // 实例化/调用时指定具体的类型
#Generics in go

# go はバージョン 1.8 でのみジェネリックを導入しました。 go のバージョンが 1.8 より前の場合は、ジェネリックを使用できません。この記事のコードではバージョン 1.9 を使用します。バージョン 1.8 では、ジェネリックをサポートするために多くの変更が加えられました。

型パラメーターは関数と型の宣言で導入されます

    型のコレクションは、メソッドのない型も含め、インターフェイスを通じて定義できます
  • 一部のシナリオでは型の派生型パラメータが推定され、型パラメータの値を指定せずに関数を呼び出すことができます。
  • 仮パラメータ、実パラメータ、型パラメータ、型実パラメータ、インスタンス化

まず、一般的な add

関数を見てみましょう。

add は関数名、 x, y は仮パラメータ、 (x, y int) はパラメータ リストです。関数呼び出しが発生した場合、add(2, 3) 2, 3 が実際のパラメータになります。 ジェネリックスと同様に、型パラメーターが必要です。関数呼び出しが発生すると、対応する型パラメーターが渡されます。型パラメーターを持つ関数は、ジェネリック関数と呼ばれます。

[T int | int64]

は型パラメータのリスト、golang のジェネリックについての深い理解 (Generic) T は型パラメータ、 int | int64 は型セット/型制約です。関数呼び出し add[int](2,3) が発生すると、int が型実パラメータになります。この呼び出しはインスタンス化とも呼ばれ、型実パラメータが決定されます。

#型パラメータは、構造体の宣言時に指定することもできます。

MyStruct[T] golang のジェネリックについての深い理解 (Generic) はジェネリック構造体であり、ジェネリック構造体に対してメソッドを定義できます。

型コレクション、インターフェイスgolang のジェネリックについての深い理解 (Generic)

基本型では、uint8 は 0 ~ 255 のコレクションを表します。次に、型パラメータについては、基本型と同様に型のコレクションを定義する必要があります。上の例の int | string は型のコレクションです。では、型のコレクションを再利用するにはどうすればよいでしょうか?ここではインターフェイスを定義に使用します。以下は型コレクションの定義です。したがって、汎用関数

add[T Signed](x, y T) T

1.8 に移行する前に、インターフェイスの定義を定義できます。これはメソッドのコレクションです。つまり、インターフェイスに対応するメソッドが実装されていれば、対応するインターフェイスに変換できます。次の例では、

MyIntgolang のジェネリックについての深い理解 (Generic) 型は Add メソッドを実装しているため、

MyInterface

に変換できます。 <pre class="brush:js;toolbar:false;">type MyInterface interface { Add(x, y int) int } type MyInt int func (mi myInt) Add(x, y int) int { return x + y } func main() { var mi MyInterface = myInt(1) fmt.Println(mi.Add(1, 2)) }</pre>別の角度から考えると、MyInterface は、

add

メソッドを実装するすべての型を含む型のコレクションとみなすことができます。その後、MyInterface を型のコレクションとして使用できます。たとえば、次のようにジェネリック関数を定義できます。 <pre class="brush:js;toolbar:false;">func I[T MyInterface](x, y int, i T) int { return i.Add(x, y) }</pre><p>在泛型中, 我们的类型集合不仅仅是实现接口中定义方法的类型, 还需要包含基础的类型。因此, 我们可以对接口的定义进行延伸, 使其支持基础类型。为了保证向前兼容, 我们需要对接口类型进行分类:</p> <h4 data-id="heading-4"><strong>基础接口类型</strong></h4> <p>只包含方法的集合, 既可以当作类型集合, 又可以作为数据类型进行声明。如下面的 <code>MyInterface。还有一个特殊的接口类型 interface{}, 它可以用来表示任意类型, 即所有的类型都实现了它的空方法。在 1.8 之后可以使用 any 进行声明。

type any = interface{}

type MyInterface interface {
    Add(x, y int) int
    String() string
    String() string  // 非法: String 不能重复声明
    _(x int)         // 非法: 必须要有一个非空的名字
}

接口组合

可以通过接口组合的形式声明新的接口, 从而尽可能的复用接口。从下面的例子可以看出, ReadWriterReaderWrite 的类型集合的交集。

type Reader interface {
        Read(p []byte) (n int, err error)
        Close() error
}

type Writer interface {
        Write(p []byte) (n int, err error)
        Close() error
}

// ReadWriter&#39;s methods are Read, Write, and Close.
type ReadWriter interface {
        Reader  // includes methods of Reader in ReadWriter&#39;s method set
        Writer  // includes methods of Writer in ReadWriter&#39;s method set
}

通用接口

上面说的接口都必须要实现具体的方法, 但是类型集合中无法包含基础的数据类型。如: int, float, string...。通过下面的定义, 可以用来表示包含基础数据类型的类型集合。在 golang.org/x/exp/constraints 中定义了基础数据类型的集合。我们可以看到 符号, 它表示包含潜在类型为 int | int8 | int16 | int32 | int64 的类型, | 表示取并集。Singed 就表示所有类型为 int 的类型集合。

// Signed is a constraint that permits any signed integer type.
// If future releases of Go add new predeclared signed integer types,
// this constraint will be modified to include them.
type Signed interface {
     ~int | ~int8 | ~int16 | ~int32 | ~int64
}

type myInt int // 潜在类型为 int

func add[T constraints.Integer](x, y T) T {
        return x + y
}

func main() {
        var x, y myInt = 1, 2
        fmt.Println(add[myInt](x, y))
}

下面来看一些特殊的定义

// 潜在类型为 int, 并且实现了 String 方法的类型
type E interface {
    ~int
    String() string
}

type mInt int // 属于 E 的类型集合
func (m mInt) String() string {
    return fmt.Sprintf("%v", m)
}

// 潜在类型必须是自己真实的类型
type F interface {
    ~int
    // ~mInt  invalid use of ~ (underlying type of mInt is int)
    // ~error illegal: error is an interface
}

// 基础接口可以作为形参和类型参数类型, 通用类型只能作为类型参数类型, E 只能出现在类型参数中 [T E]
var x E                    // illegal: cannot use type E outside a type constraint: interface contains type constraints
var x interface{} = E(nil) // illegal: cannot use interface E in conversion (contains specific type constraints or is comparable)

类型推导

由于泛型使用了类型参数, 因此在实例化泛型时我们需要指定类型实参。 看下面的 case, 我们在调用函数的时候并没有指定类型实参, 这里是编译器进行了类型推导, 推导出类型实参, 不需要显性的传入。

func add[T constraints.Integer](x, y T) T {
    return x + y
}

func main() {
    fmt.Println(add(1, 1)) // add[int](1,1)
}

有时候, 编译器无法推导出具体类型。则需要指定类型, 或者更换写法, 也许可以推断出具体类型。

// 将切片中的值扩大
func Scale[E constraints.Integer](s []E, c E) []E {
    r := make([]E, len(s))
    for i, v := range s {
        r[i] = v * c
    }
    return r
}

func ScaleAndPrint(p Point) {
    r := Scale(p, 2)
    r.string() // 非法, Scale 返回的是 []int32
}

type Point []int32

func (p Point) string() {
    fmt.Println(p)
}

// 方法更新,这样传入的是 Point 返回的也是 Point
func Scale[T ~[]E, E constraints.Integer](s T, c E) T {
    r := make([]E, len(s))
    for i, v := range s {
        r[i] = v * c
    }
    return r
}

泛型的使用

go 是在 1.8 版本中开始引入泛型的。下面主要介绍一下什么时候使用泛型:

内置容器类型

在 go 中, 提供以下容器类型:map, slice, channel。当我们用到容器类型时, 且逻辑与容器具体的类型无关, 这个时候可以考虑泛型。这样我们可以在调用时指定具体的类型实参, 从而避免了类型断言。例如,下面的例子, 返回 map 中的 key。

// comparable 是一个内置类型, 只能用于对类型参数的约束。在 map 中, key 必须是可比较类型。
func GetKeys[K comparable, V any](m map[K]V) []K {
    res := make([]K, 0, len(m))
    for k := range m {
        res = append(res, k)
    }
    return res
}

通用的结构体

对于一些通用的结构体, 我们应该使用泛型。例如, 栈、队列、树结构。这些都是比较通用的结构体, 且逻辑都与具体的类型无关, 因此需要使用泛型。下面是一个栈的例子:

type Stack[T any] []T

func (s *Stack[T]) Push(item T) {
    *s = append(*s, item)
}

func (s *Stack[T]) Pop() T {
    if len(*s) == 0 {
        panic("can not pop item in emply stack")
    }
    lastIndex := len(*s) - 1
    item := (*s)[lastIndex]
    *s = (*s)[:lastIndex]
    return item
}

func main() {
    var s Stack[int]
    s.Push(9)
    fmt.Println(s.Pop())
    s.Push(9)
    s.Push(8)
    fmt.Println(s.Pop(), s.Pop())
}

通用的函数

有些类型会实现相同的方法, 但是对于这些类型的处理逻辑又与具体类型的实现无关。例如: 两个数比大小, 只要实现 Ordered 接口即可进行大小比较:

func Min[T constraints.Ordered](x, y T) T {
    if x < y {
        return x
    }

    return y
}

func main() {
    fmt.Println(Min(5, 6))
    fmt.Println(Min(6.6, 9.9))
}

总结

go 在引入泛型算是一次较大的改动。我们只有弄清楚类型参数、类型约束、类型集合、基础接口、通用接口、泛型函数、泛型类型、泛型接口等概念, 才能不会困惑。核心改动点还是引入了类型参数, 使用接口来定义类型集合。

当然,也不能为了使用泛型而使用泛型。还是要具体的 case 具体来分析。 简单的指导原则就是, 当你发现你的代码除了类型不同外, 其余代码逻辑都相同; 或者你写了许多重复代码, 仅仅是为了支持不同类型; 那么你可以考虑使用泛型。

推荐学习:Golang教程

以上がgolang のジェネリックについての深い理解 (Generic)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。