Menu
CoddyTech

泛型方法的变通方案

本课属于 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。此限制只适用于在方法签名中引入额外的类型参数。

需要这种灵活性时,可以使用一个以结构体作为第一个参数的通用函数来实现同样的效果。

challenge icon

挑战

简单

让我们构建一个数据转换工具包,演示如何绕过 Go 对泛型方法的限制!由于方法无法引入新的类型参数,你将创建独立的泛型函数,实现同样的灵活性。

你将把代码分放在两个文件中:

  • wrapper.go:定义泛型容器和转换函数。

    创建一个泛型结构体 Wrapper[T any],其中包含一个类型为 T 的字段 Value。

    为 Wrapper[T] 添加一个方法 Get,用于返回包装的值。这个方法使用结构体的类型参数,这是允许的。

    创建一个独立的泛型函数 Transform[T, U any](w Wrapper[T], fn func(T) U) U,将转换函数应用于包装器中的值并返回结果。这个函数需要自己的类型参数 U 来表示输出类型,因此它必须是独立函数,而不能是方法。

    再创建一个独立函数 TransformToString[T any](w Wrapper[T]) string,使用 fmt.Sprintf("%v", ...) 将包装的值转换为字符串。

  • main.go:使用不同的转换来演示这种绕过限制的模式。

    读取类型指示符(int 或 string),然后读取一个值。创建相应类型的 Wrapper。

    对于 int 输入:创建一个 Wrapper[int],然后使用 Transform 将值加倍(返回一个 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

关键在于,Transform 将 Wrapper[T] 作为第一个参数,并为返回类型引入一个新的类型参数 U。如果将它定义为结构体的方法,就无法做到这一点。这种模式让你在遵守 Go 类型系统限制的同时,也能灵活地进行泛型转换。

亲手试试

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 将值翻倍(提示:传入一个将值翻倍的函数)
		
		// 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自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Go 在线编译器