xfeng

xfeng

健身 技术 阅读 思考 记录
tg_channel
tg_channel
github
bilibili
tg_channel

Golangジェネリックの概要

image

1. 概述#

泛型は多くの言語で使用されており、適切に使用することで開発が容易になります。しかし、最初のリリース時点ではジェネリックをサポートしていなかったため、Golang はジェネリックをサポートしていませんでした。今年、Go コミュニティの要望に応えて、Go1.17 ではジェネリックの体験版がリリースされ、Go1.18 の正式版ではジェネリックが導入されました。本文では、Go でのジェネリックとその使用方法について説明します。

2. ジェネリックとは何ですか#

ジェネリックに入る前に、まずポリモーフィズムについて理解しましょう。

ポリモーフィズムとは何ですか?
ポリモーフィズムは、異なるオブジェクトが同じイベントに対して異なる振る舞いをすることを指します。

例えば:

動物クラス

3 つのオブジェクト -> 犬、猫、鶏

1 つの共通のイベント -> 鳴く

3 つの異なる振る舞い -> ワンワン、ニャーニャー、コケコッコー

ポリモーフィズムは 2 つのタイプに分けられます:

  • 一時的なポリモーフィズム(実引数の型に基づいて対応するバージョンを呼び出すことができますが、呼び出し可能な数には制限があります。例:関数のオーバーロード)
  • パラメータ化されたポリモーフィズム(実引数の型に基づいて異なるバージョンを生成し、任意の数の呼び出しをサポートします。これがジェネリックです)

ジェネリックとは何ですか?
要するに、要素の型をパラメータに変えることです。

3. ジェネリックのジレンマ#

ジェネリックには利点もありますが、欠点もあります。ジェネリックの利点と欠点をバランスさせる方法は、ジェネリックの設計者が直面する問題です。

ジェネリックのジレンマ:

image

簡単に言えば、低いコーディング効率(開発者の開発速度が遅い)、低いコンパイル効率(コンパイラのコンパイル速度が遅い)、低い実行効率(ユーザーエクスペリエンスが悪い)です。

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 には比較可能な型を表す comparable 制約が組み込まれています。

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 でのジェネリックの使用方法について簡単に説明しました。適切にジェネリックを使用することで、開発効率を向上させることができます。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。