技術共有

Go ジェネリックの詳しい説明

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

導入

2 つの整数と浮動小数点数のサイズをそれぞれ比較する関数を作成したい場合は、2 つの関数を作成する必要があります。次のように:

func Min(x, y float64) float64 {
    if x < y {
        return x
    }
    return y
}

func MinInt(x, y int) int {
	if x < y {
		return x
	}
	return y
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

2 つの関数は、データ型が異なることを除いて、まったく同じ処理ロジックを持ちます。上記の機能を 1 つの関数で実現する方法はありますか?はい、それは一般的なものです。

func min[T int | float64](x, y T) T {
	if x < y {
		return x
	}
	return y
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

ジェネリック

公式ウェブサイトのドキュメント:https://go.dev/blog/intro-generics
ジェネリックスは、言語に 3 つの重要な新機能を追加します。

  • 関数と型の型パラメータ。
  • インターフェイス型を、メソッドのない型を含む型のセットとして定義します。
  • 型推論では、多くの場合、関数を呼び出すときに型パラメーターを省略できます。

型パラメータ

関数と型は型パラメータを持つことができるようになりました。型パラメータ リストは、丸括弧の代わりに角括弧を使用することを除いて、通常のパラメータ リストと似ています。
ここに画像の説明を挿入します

package main

import (
	"fmt"
	"golang.org/x/exp/constraints"
)

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

func main() {
	x := GMin[int](2, 3)
	fmt.Println(x) // 输出结果为2
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

このうち、constraints.Ordered はカスタム タイプです (ソース コードはここには示されていません)。
理解できない場合は、制約を一時的に置き換えることができます。int | float64

GMin に型パラメータ (この場合は int) を与えることをインスタンス化と呼びます。インスタンス化は 2 つのステップで行われます。

  • まず、コンパイラは、ジェネリック関数またはジェネリック型全体にわたって、すべての型引数をそれぞれの型パラメータに置き換えます。
  • 次に、コンパイラは、各型パラメータがそれぞれの制約を満たしていることを検証します。
    これが何を意味するかは後ほど説明しますが、2 番目のステップが失敗すると、インスタンス化は失敗し、プログラムは無効になります。

インスタンス化が成功すると、他の関数と同様に呼び出すことができる非ジェネリック関数が得られます。たとえば、次のようなコードでは、

fmin := GMin[float64]
m := fmin(2.71, 3.14)
  • 1
  • 2

すべてのコードは、

package main

import (
	"fmt"
	"golang.org/x/exp/constraints"
)

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

func main() {
	fmin := GMin[float64] // 相当于func GMin(x, y float64) float64{...}
	m := fmin(2.71, 3.14)
	fmt.Println(m) // 输出结果为2.71
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

GMin[float64] をインスタンス化すると、実際にはオリジナルの浮動小数点 Min 関数が生成され、関数呼び出しで使用できます。

型パラメータは型とともに使用することもできます。

type Tree[T interface{}] struct {
    left, right *Tree[T]
    value       T
}

func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }

var stringTree Tree[string]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

ここで、ジェネリック型 Tree には型パラメーター T の値が格納されます。ジェネリック型には、この例の Lookup などのメソッドを含めることができます。ジェネリック型を使用するには、インスタンス化する必要があります; Tree[string] は、型パラメータ文字列を使用して Tree をインスタンス化する例です。

タイプセット

型パラメータ リスト内の各型パラメータには型があります。型パラメーター自体が型であるため、型パラメーターの型によって型のセットが定義されます。このメタタイプは次のように呼ばれます型制約
ジェネリック メソッド GMin では、型制約が制約パッケージからインポートされます。 順序付き制約は、順序付けできる、つまり &lt; 演算子 (または &lt;=、&gt; など) と比較できる値を持つすべてのタイプのコレクションを記述します。この制約により、ソート可能な値を持つ型のみが GMin に渡されることが保証されます。これは、GMin 関数本体で、この型パラメーターの値を &lt; 演算子との比較に使用できることも意味します。
Go では、型制約はインターフェイスでなければなりません 。つまり、インターフェイス タイプは値タイプまたはメタタイプとして使用できます。インターフェイスはメソッドを定義するため、特定のメソッドの存在を必要とする型制約を表現できることは明らかです。ただし、constraints.Ordered もインターフェイス型であり、&lt; 演算子はメソッドではありません。
インターフェイス型の二重の目的は、確かに Go 言語における重要な概念です。 「インターフェイス型は値型としてもメタタイプとしても使用できる」というステートメントを深く理解し、例を示して説明しましょう [1][2][3][4][5]。

  1. 値型としてのインターフェイス:

インターフェイスが値の型として使用される場合、インターフェイスは一連のメソッドを定義し、これらのメソッドを実装する任意の型をインターフェイス変数に割り当てることができます。これはインターフェイスの最も一般的な使用法です。

例えば:

type Stringer interface {
    String() string
}

type Person struct {
    Name string
}

func (p Person) String() string {
    return p.Name
}

var s Stringer = Person{"Alice"} // Person 实现了 Stringer 接口
fmt.Println(s.String()) // 输出: Alice
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

この例では、Stringer インターフェイスは値の型として使用されます。Person 型は実装しますString() メソッドなので、それに割り当てることができますStringer 型変数。

  1. メタタイプとしてのインターフェース:

インターフェイスをメタタイプとして使用すると、汎用プログラミングで使用するための型制約のセットが定義されます。これは、Go 1.18 でのジェネリックの導入後の新しい使用法です。

例えば:

type Ordered interface {
    int | float64 | string
}

func Min[T Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

fmt.Println(Min(3, 5))       // 输出: 3
fmt.Println(Min(3.14, 2.71)) // 输出: 2.71
fmt.Println(Min("a", "b"))   // 输出: a
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

この例では、Ordered インターフェイスは、比較可能な型 (整数、浮動小数点数、文字列) のセットを定義するメタタイプとして使用されます。Min 関数はこのインターフェイスを型制約として使用し、任意の型制約を受け入れることができます。Ordered 引数としての制約のタイプ。

この 2 つの目的により、Go のインターフェイスは汎用プログラミングに対して非常に強力かつ柔軟になります。オブジェクトの動作を (値の型として) 定義できるだけでなく、型のコレクションを (メタタイプとして) 定義することもできるため、言語の単純さを維持しながら、コードの表現力と再利用性が大幅に向上します。

最近まで、Go 仕様では、インターフェイスはメソッド セットを定義すると述べていました。メソッド セットとは、インターフェイス内で列挙されるメソッドのセットのことを指します。これらのメソッドをすべて実装する型は、このインターフェイスを実装します。
ここに画像の説明を挿入します
しかし、これを別の見方で見ると、インターフェイスは型のセット、つまりこれらのメソッドを実装する型を定義すると言えます。この観点から、インターフェイス型セットの要素である型はすべて、そのインターフェイスを実装します。
ここに画像の説明を挿入します
どちらのビューでも同じ結果が得られます。メソッドのセットごとに、これらのメソッドを実装する対応する型のセット、つまりインターフェイスによって定義された型のセットを想像できます。

ただし、ここでの目的としては、タイプ セット ビューにはメソッド セット ビューよりも優れた点が 1 つあります。それは、コレクションに型を明示的に追加することで、新しい方法で型セットを制御できることです。

これを実現するために、インターフェイス タイプの構文を拡張しました。たとえば、interface{ int|string|bool } は、int、string、bool 型を含む型セットを定義します。
ここに画像の説明を挿入します
これを別の言い方で言えば、インターフェイスは int、string、または bool によってのみ満たされるということです。

次に、constraints.Ordered の実際の定義を見てみましょう。

type Ordered interface {
    Integer|Float|~string
}
  • 1
  • 2
  • 3

この宣言は、Ordered インターフェイスがすべての整数、浮動小数点、および文字列型のコレクションであることを示します。垂直バーは型の結合 (この場合は型のセット) を表します。 Integer と Float は、同様に制約パッケージで定義されたインターフェイス タイプです。 Ordered インターフェイスではメソッドが定義されていないことに注意してください。

型制約の場合、通常、文字列などの特定の型は気にしません。すべての文字列型に関心があります。これは ~ トークンの目的。表現~string 基になる型が文字列であるすべての型のコレクションを表します。これには、型文字列自体と、定義で宣言されたすべての型が含まれます。type MyString string

もちろん、インターフェイス内でメソッドを指定する必要はありますし、下位互換性も維持したいと考えています。 Go 1.18 では、以前と同様にインターフェイスにメソッドと埋め込みインターフェイスを含めることができますが、非インターフェイス型、共用体、および基礎となる型のセットを埋め込むこともできます。

制約として使用されるインターフェイスは、名前を付けることも (Ordered など)、型パラメーター リストにインラインでリテラル インターフェイスにすることもできます。例えば:

[S interface{~[]E}, E interface{}]
  • 1

ここで、S はスライス タイプである必要があり、その要素タイプは任意のタイプにすることができます。

これは一般的な状況であるため、位置を制約するインターフェースの場合、閉じたインターフェースを省略して、単純に書くことができます(Go 言語のジェネリックスの構文糖と型制約の簡略化された記述)。

[S ~[]E, E interface{}]
  • 1

空のインターフェイスは通常の Go コードだけでなく型パラメーター リストでも一般的であるため、Go 1.18 では、空のインターフェイス型のエイリアスとして、新しい事前宣言された識別子 any が導入されています。したがって、次のような慣用的なコードが得られます。

[S ~[]E, E any]
  • 1

型推論

型パラメーターを使用する場合は、型パラメーターを渡す必要があるため、コードが冗長になる可能性があります。一般的な GMin 関数に戻ります。