1. 概述#
泛型は多くの言語で使用されており、適切に使用することで開発が容易になります。しかし、最初のリリース時点では Golang はジェネリックをサポートしていませんでした。Golang の開発者はジェネリックが重要ではあるが必須ではないと考えていたためです。しかし、今年、Go コミュニティの要望に応えて、Go1.17 ではジェネリックの体験版がリリースされ、Go1.18 の正式版に向けて準備が進められました。本文では、Go でのジェネリックとその使用方法について説明します。
2. ジェネリックとは何ですか#
ジェネリックに入る前に、まずポリモーフィズムについて理解しましょう。
例えば:
動物クラス
3 つのオブジェクト -> 犬、猫、鶏
共通のイベント -> 鳴く
3 つの異なる表現 -> ワンワン、ニャーニャー、コケコッコー
ポリモーフィズムには 2 つのタイプがあります:
- 一時的なポリモーフィズム(実引数の型に応じて対応するバージョンを呼び出し、呼び出しの数は非常に限られている。例:関数オーバーロード)
- パラメータ化ポリモーフィズム(実引数の型に応じて異なるバージョンを生成し、任意の数の呼び出しをサポートします。これがジェネリックです)
3. ジェネリックのジレンマ#
ジェネリックには利点もありますが、欠点もあります。ジェネリックの利点と欠点をどのようにバランスさせるかは、ジェネリックの設計者が直面する問題です。
ジェネリックのジレンマ:
要するに、低いコーディング効率(開発者の開発が遅い)、低いコンパイル効率(コンパイラのコンパイルが遅い)、低い実行効率(ユーザーエクスペリエンスが悪い)です。
4. Go1.17 でのジェネリックの要点#
-
関数は type キーワードを使用して追加の型パラメータ(type parameters)リストを導入できます:func F (type T)(p T) { ... }。
-
これらの型パラメータは、関数本体内で通常のパラメータと同様に使用できます。
-
型も型パラメータリストを持つことができます:type M (type T) [] T。
-
各型パラメータは制約を持つことができます:func F (type T Constraint)(p T) { ... }。
-
型の制約を記述するために interface を使用します。
-
型制約として使用される interface は、このインターフェースを実装するタイプの基本タイプを制限するための事前宣言されたタイプリストを持つことができます。
-
ジェネリックな関数や型を使用する場合、型引数を渡す必要があります。
-
一般的な場合、型推論により、ユーザーはジェネリックな関数を呼び出す際に型引数を省略することができます。
-
型パラメータに制約がある場合、型引数はインターフェースを実装する必要があります。
-
ジェネリックな関数では、制約で指定された操作のみが許可されます。
5. ジェネリックの使用方法#
5.1 ジェネリックの出力方法#
package main
import (
"fmt"
)
// 追加の型パラメータリストを保存するために[]を使用
// [T any]は型パラメータであり、この関数は任意のT型をサポートすることを示す
func printSlice[T any](s []T) {
for _, v := range s {
fmt.Printf("%v ", v)
}
fmt.Print("\n")
}
func main() {
printSlice[int]([]int{1, 2, 3, 4, 5})
printSlice[float64]([]float64{1.01, 2.02, 3.03, 4.04, 5.05})
// printSlice[string]([]string{"Hello", "World"})の呼び出しは、string型として推論されます
// コンパイラが型推論を行える場合、printSlice([]string{"Hello", "World"})と書くこともできます
printSlice([]string{"Hello", "World"})
printSlice[int64]([]int64{5, 4, 3, 2, 1})
}
ジェネリックを使用するには、Go1.17 以降のバージョンが必要です。
上記のコードを直接実行するとエラーが発生する可能性があります。
.\main.go:9:6: missing function body
.\main.go:9:16: syntax error: unexpected [, expecting (
正常に実行するには、パラメータ-gcflags=-G=3
を追加する必要があります。
5.2 ジェネリックの型範囲を制約する方法#
各型には型制約があり、通常のパラメータに型があるのと同じように扱われます。
Go1.17 では、ジェネリック関数は、型パラメータがサポートする操作をサポートする任意の型でのみ使用できます。
(以下のコードでは、add 関数の + はint, int8, int16, int32, int64,uint, uint8, uint16, uint32, uint64,uintptr,float32,float64, complex64, complex128,string
の操作をサポートします)
package main
import (
"fmt"
)
// ジェネリックな型の範囲を制約する
type Addable interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64, complex64, complex128,
string
}
func add[T Addable] (a, b T) T {
return a + b
}
func main() {
fmt.Println(add(1,2))
fmt.Println(add("hello","world"))
}
制約のない型パラメータインスタンスに対して許可される操作は次のとおりです:
- これらの型の変数を宣言する。
- 同じ型の値を使用してこれらの変数に値を割り当てる。
- これらの型の変数を関数に実引数として渡すか、関数から返す。
- これらの変数のアドレスを取得する。
- これらの型の値を interface {} 型の変数に変換または割り当てる。
- 型アサーションを使用して、この型の変数にインターフェース値を割り当てる。
- type スイッチブロック内の case ブランチとして使用する。
- この型で構成される複合型(スライスなど)を定義および使用する。
- この型を new などの組み込み関数に渡す。
5.21 comparable 制約#
すべての型が==
を使用して比較できるわけではありません。Go には比較可能な制約が組み込まれており、比較可能な型を表します。
package main
import (
"fmt"
)
func findFunc[T comparable](a []T, v T) int {
for i, e := range a {
if e == v {
return i
}
}
return -1
}
func main() {
fmt.Println(findFunc([]int{1, 2, 3, 4, 5, 6}, 5))
}
5.3 ジェネリックスライス#
ジェネリック関数と同様に、ジェネリック型を使用する場合は、まずインスタンス化する必要があります(明示的に型パラメータに型を割り当てること):vs:=slice{5,4,2,1}
package main
import (
"fmt"
)
type slice[T any] []T
/*
type any interface {
type int, string
}*/
func printSlice[T any](s []T) {
for _, v := range s {
fmt.Printf("%v ", v)
}
fmt.Print("\n")
}
func main() {
// 注意1:インスタンス化されていないジェネリック型slice[T interface{}]は使用できません
// 注意2:インスタンス化されていないジェネリック型slice[T any]は使用できません
vs := slice[int]{5, 4, 2, 1}
printSlice(vs)
5.4 ジェネリックポインタ#
package main
import (
"fmt"
)
func pointerOf[T any](v T) *T {
return &v
}
func main() {
sp := pointerOf("foo")
fmt.Println(*sp)
ip := pointerOf(123)
fmt.Println(*ip)
*ip = 234
fmt.Println(*ip)
}
5.5 ジェネリックマップ#
package main
import (
"fmt"
)
func mapFunc[T any, M any](a []T, f func(T) M) []M {
n := make([]M, len(a), cap(a))
for i, e := range a {
n[i] = f(e)
}
return n
}
func main() {
vi := []int{1, 2, 3, 4, 5, 6}
vs := mapFunc(vi, func(v int) string {
return "<" + fmt.Sprint(v*v) + ">"
})
fmt.Println(vs)
}
5.6 ジェネリックキュー#
package main
import (
"fmt"
)
type queue[T any] []T
func (q *queue[T]) enqueue(v T) {
*q = append(*q, v)
}
func (q *queue[T]) dequeue() (T, bool) {
if len(*q) == 0 {
var zero T
return zero, false
}
r := (*q)[0]
*q = (*q)[1:]
return r, true
}
func main() {
q := new(queue[int])
q.enqueue(5)
q.enqueue(6)
fmt.Println(q)
fmt.Println(q.dequeue())
fmt.Println(q.dequeue())
fmt.Println(q.dequeue())
}
6. 結論#
本文では、Go でのジェネリックの使用方法について簡単に説明しました。適切にジェネリックを使用することで、開発効率が向上します。