Go/ジェネリクス

出典: フリー教科書『ウィキブックス(Wikibooks)』
< Go

ジェネリクス[編集]

Go言語では、ジェネリクスが導入される前は、インターフェースや具体的な型に依存した実装が主流でした。 ジェネリクスが導入されても、従来の方法も引き続き使用できます。

interfaceを使った実装[編集]

以下はその一例として、ジェネリクスを使わずにスタックを実装する方法を示します。

非ジェネリクス版スタック
package main

import (
	"fmt"
)

// スタックの構造体
type Stack struct {
	data []interface{}
}

// スタックに要素を追加
func (s *Stack) Push(item interface{}) {
	s.data = append(s.data, item)
}

// スタックから要素を取り出す
func (s *Stack) Pop() interface{} {
	item := s.data[len(s.data)-1]
	s.data = s.data[:len(s.data)-1]
	return item
}

// スタックの先頭の要素を取得(削除はしない)
func (s *Stack) Peek() interface{} {
	return s.data[len(s.data)-1]
}

// スタックが空かどうかをチェックするメソッド
func (s *Stack) IsEmpty() bool {
	return len(s.data) == 0
}

func main() {
	// 空のスタックを作成
	stack := Stack{}

	// スタックに値を追加
	stack.Push(10)
	stack.Push("Hello")
	stack.Push(3.14)

	// スタックの内容を表示
	for !stack.IsEmpty() {
		item := stack.Pop()
		fmt.Println("Popped:", item)
	}
}
実行結果
Popped: 3.14
Popped: Hello 
Popped: 10

この例では、Stackという構造体を定義し、Pushで要素を追加し、Popで要素を取り出します。interface{}型を使用することで、任意の型の要素をスタックに追加できます。ただし、実行時に型アサーションを使用して型を確認する必要があります。

型パラメータを使った実装[編集]

Go言語におけるジェネリクス(型パラメータ)を利用したスタックの実装は次のようになります。

ジェネリクス版スタック
package main

import (
	"fmt"
)

// ジェネリクス化されたスタックの定義
type Stack[T any] []T

// スタックに要素を追加するメソッド
func (s *Stack[T]) Push(item T) {
	*s = append(*s, item)
}

// スタックから要素を取り出すメソッド
func (s *Stack[T]) Pop() T {
	item := (*s)[len(*s)-1]
	*s = (*s)[:len(*s)-1]
	return item
}

// スタックが空かどうかをチェックするメソッド
func (s *Stack[T]) IsEmpty() bool {
	return len(*s) == 0
}

func main() {
	// int型のスタックを作成
	var intStack Stack[int]

	// スタックに値を追加
	intStack.Push(1)
	intStack.Push(2)
	intStack.Push(3)

	// スタックから値を取り出して表示
	for !intStack.IsEmpty() {
		item := intStack.Pop()
		fmt.Println(item)
	}

	// 文字列型のスタックを作成
	var stringStack Stack[string]

	// スタックに値を追加
	stringStack.Push("apple")
	stringStack.Push("banana")
	stringStack.Push("cherry")

	// スタックから値を取り出して表示
	for !stringStack.IsEmpty() {
		item := stringStack.Pop()
		fmt.Println(item)
	}
}
実行結果
3
2
1
cherry
banana 
apple

このコードは、Go言語のジェネリクスを使ってスタックを実装しています。Stack[T any]という定義は、Tという任意の型の要素を持つスタックを表します。

Pushメソッドはスタックに要素を追加し、Popメソッドはスタックから要素を取り出します。IsEmptyメソッドはスタックが空かどうかを確認します。

main関数では、intStackstringStackという異なる型のスタックを作成し、それぞれのスタックに値を追加してから、要素を取り出して表示しています。これにより、異なる型の要素を持つジェネリックなスタックが正しく機能していることが示されています。

このコードを実行すると、intStackには1、2、3が、stringStackには"apple"、"banana"、"cherry"が順番に追加され、それらがスタックから取り出されて画面に表示されます。

型制約[編集]

1.18で追加された予約済識別子 anyinterface{} の alias で全ての型と一致します。

ただ、何でもかんでも any にすると必要なメソッドを備えていない型のインスタンスまで受け付けてしまいます。 この問題を解決するために型制約が用意されています。

Go言語における型制約は、主にインターフェースを通じて行われます。Goにはジェネリクス制約を直接指定する仕組みはありませんが、インターフェースを使って特定の振る舞いを持つ型を制約することができます。

例えば、fmt.StringerString()メソッドを持つ型を表すインターフェースです。これは標準パッケージの中で多く利用されます。これにより、fmt.Printなどの関数はfmt.Stringerインターフェースを満たす型を引数に受け取ることができます。

型制約
package main

import (
	"fmt"
)

type Bin int

func (b Bin) String() string {
	return fmt.Sprintf("%#b", int(b))
}

func Print[T fmt.Stringer](s []T) {
	for _, v := range s {
		fmt.Print(v, "\n")
	}
}

func main() {
	Print([]Bin{0b1101, 0b1010_0101})
}
実行結果
0b1101 
0b10100101

このコードは、Go言語でのジェネリクスに型制約を加える例です。Binという新しい型を定義し、Bin型に対してString()メソッドを実装しています。String()メソッドは、Bin型を2進数文字列として表現するためのものです。

Print関数は、fmt.Stringerインターフェースを実装した任意の型のスライスを受け取り、その要素をfmt.Printを使って画面に出力する関数です。fmt.Stringerインターフェースは、String()メソッドを持つ型に適用されます。

main関数では、Print関数を使ってBin型のスライスを表示しています。Print関数はfmt.Stringerインターフェースを満たすため、Bin型のスライスを受け取ることができ、Bin型の各要素はString()メソッドを介して2進数文字列として出力されます。

最終的に、main関数ではPrint関数を使って[]Bin{0b1101, 0b1010_0101}を表示しています。これにより、Bin型の値が2進数文字列として正しく出力されることが確認できます。

Goのジェネリクスの特徴
Go言語におけるジェネリクスの導入は、バージョン1.18で行われました。ジェネリクスの導入により、より柔軟で再利用可能なコードを書くことができるようになりました。以下に、Goのジェネリクスの特徴を解説します。
  1. ユーザー定義のジェネリクス型
    ジェネリクスを使用して、関数や型をパラメータ化できます。例えば、スライス、マップ、チャネルなどの標準ライブラリのデータ構造をジェネリックにすることが可能です。
  2. 型安全なコード
    ジェネリクスを使うことで、異なる型のデータを扱う場合でも、静的な型チェックが行われます。これにより、型安全なコードを保ちながら汎用性を持たせることができます。
  3. コードの再利用性
    同じような機能を持つ関数やデータ構造を複数の型で使い回すことができます。これにより、冗長なコードを減らし、メンテナンス性を高めることができます。
  4. パフォーマンス向上
    ジェネリクスは、型を具体化することで効率的なコードを生成します。これにより、ジェネリックなアルゴリズムを使っても、実行時のオーバーヘッドを最小限に抑えることができます。
  5. インターフェースとの組み合わせ
    ジェネリクスは、インターフェースと組み合わせることで柔軟性を高めることができます。ジェネリックな型をインターフェースを介して制約することが可能です。

Goのジェネリクスは、これまでのバージョンよりも柔軟性と再利用性を向上させ、型安全性を保ちながら汎用性の高いコードを書くことができるようにしました。これにより、より洗練された、効率的で堅牢なプログラムを開発することができます。

脚註[編集]