Menu
Coddy logo textTech

ジェネリックメソッドの回避策

CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 71/107。

Goには注目すべき制限があります。構造体に対して、独自の型パラメータを持つメソッドを定義することはできません。ジェネリックな構造体はうまく機能しますが、新しい型パラメータを導入するメソッドを追加することは許可されていません。

このコードはコンパイルできません:

type Box[T any] struct {
    Value T
}

// エラー:メソッドには型パラメータを指定できません
func (b Box[T]) Convert[U any](fn func(T) U) U {
    return fn(b.Value)
}

回避策は、メソッドの代わりにスタンドアロンのジェネリック関数を使用することです。

type Box[T any] struct {
    Value T
}

// 通常のメソッド - 構造体の型パラメータを使用します
func (b Box[T]) Get() T {
    return b.Value
}

// スタンドアロン関数 - 独自の型パラメータを持つことができます
func Convert[T, U any](b Box[T], fn func(T) U) U {
    return fn(b.Value)
}

func main() {
    intBox := Box[int]{Value: 42}
    
    // メソッドを使用する
    fmt.Println(intBox.Get())  // 42
    
    // スタンドアロン関数を使用する
    strResult := Convert(intBox, func(n int) string {
        return fmt.Sprintf("Number: %d", n)
    })
    fmt.Println(strResult)  // Number: 42
}

ジェネリック構造体のメソッドでは、引き続きその構造体の型パラメータTを使用できます。この制限は、メソッドシグネチャ内で追加の型パラメータを導入する場合にのみ適用されます。

そのような柔軟性が必要な場合、構造体を第1引数として受け取るジェネリック関数によって、同じ結果が得られます。

challenge icon

チャレンジ

簡単

Goのジェネリックメソッドに関する制限を回避するためのワークアラウンドを実証する、データ変換ツールキットを作成しましょう!メソッドでは新しい型パラメータを導入できないため、同じ柔軟性を実現するスタンドアロンのジェネリック関数を作成します。

コードは2つのファイルに分けて構成します:

  • wrapper.go: ジェネリックコンテナと変換関数を定義します。

    T の単一フィールド Value を持つジェネリック構造体 Wrapper[T any] を作成します。

    ラップされた値を返すメソッド GetWrapper[T] に追加します。このメソッドは構造体の型パラメータを使用しますが、これは許可されています。

    ラップされた値に変換関数を適用して結果を返す、スタンドアロンのジェネリック関数 Transform[T, U any](w Wrapper[T], fn func(T) U) U を作成します。この関数は出力型のために独自の型パラメータ U を必要とするため、メソッドではなくスタンドアロン関数である必要があります。

    fmt.Sprintf("%v", ...) を使用してラップされた値を文字列に変換する、別のスタンドアロン関数 TransformToString[T any](w Wrapper[T]) string を作成します。

  • main.go: さまざまな変換を使用して、ワークアラウンドパターンを実証します。

    型インジケータ(int または string)を読み取り、次に値を読み取ります。適切な型の Wrapper を作成します。

    int 入力の場合:Wrapper[int] を作成し、Transform を使用して値を2倍にし(intを返す)、TransformToString を使用して文字列形式を取得します。

    string 入力の場合:Wrapper[string] を作成し、Transform を使用して文字列の長さを取得し(intを返す)、TransformToString を使用して文字列形式を取得します。

    結果を次の形式で出力します:

    Original: [value]
    Transformed: [transformed value]
    As String: [string representation]

以下の入力が提供されます:

  • 1行目:型インジケータ(int または string
  • 2行目:値

例えば、以下が与えられた場合:

int
25

出力は以下のようになります:

Original: 25
Transformed: 50
As String: 25

また、以下が与えられた場合:

string
Hello World

出力は以下のようになります:

Original: Hello World
Transformed: 11
As String: Hello World

重要なポイントは、TransformWrapper[T] を最初の引数として受け取り、戻り値の型として新しい型パラメータ U を導入していることです。これは構造体のメソッドとしては不可能なことです。このパターンにより、Goの型システムの制約内で作業しながら、ジェネリックな変換の柔軟性が得られます。

チートシート

Goでは、ジェネリック構造体のメソッドが独自の型パラメータを導入することは許可されていません。メソッドは、構造体の既存の型パラメータのみを使用できます。

このコードはコンパイルされません。

type Box[T any] struct {
    Value T
}

// ERROR: method must have no type parameters
func (b Box[T]) Convert[U any](fn func(T) U) U {
    return fn(b.Value)
}

回避策は、メソッドの代わりに独立したジェネリック関数を使用することです。

type Box[T any] struct {
    Value T
}

// Regular method - uses the struct's type parameter
func (b Box[T]) Get() T {
    return b.Value
}

// Standalone function - can have its own type parameters
func Convert[T, U any](b Box[T], fn func(T) U) U {
    return fn(b.Value)
}

func main() {
    intBox := Box[int]{Value: 42}
    
    // Use the method
    fmt.Println(intBox.Get())  // 42
    
    // Use the standalone function
    strResult := Convert(intBox, func(n int) string {
        return fmt.Sprintf("Number: %d", n)
    })
    fmt.Println(strResult)  // Number: 42
}

ジェネリック構造体のメソッドは、構造体の型パラメータを使用できます。この制限は、メソッドのシグネチャに新しく型パラメータを追加する場合にのみ適用されます。その柔軟性が必要な場合は、構造体を最初の引数として受け取るジェネリック関数を使用してください。

自分で試してみよう

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)
	
	// 型インジケーターを読み込む
	typeIndicator, _ := reader.ReadString('\n')
	typeIndicator = strings.TrimSpace(typeIndicator)
	
	// 値を読み込む
	value, _ := reader.ReadString('\n')
	value = strings.TrimSpace(value)
	
	if typeIndicator == "int" {
		// 整数値をパースする
		num, _ := strconv.Atoi(value)
		
		// TODO: パースされた数値を使用して Wrapper[int] を作成する
		
		// TODO: Transform を使用して値を2倍にする(ヒント:2倍にする関数を渡す)
		
		// TODO: TransformToString を使用して文字列表現を取得する
		
		// TODO: 必要な形式で結果を出力する:
		// Original: [value]
		// Transformed: [transformed value]
		// As String: [string representation]
		
		_ = num // num を使用する際にこの行を削除する
	} else if typeIndicator == "string" {
		// TODO: 値を使用して Wrapper[string] を作成する
		
		// TODO: Transform を使用して文字列の長さを取得する(ヒント:len() を返す関数を渡す)
		
		// TODO: TransformToString を使用して文字列表現を取得する
		
		// TODO: 必要な形式で結果を出力する:
		// Original: [value]
		// Transformed: [transformed value]
		// As String: [string representation]
		
		_ = value // value を使用する際にこの行を削除する
	}
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン