Go 泛型:为什么 switch case *A[any] 与 *A[int] 不匹配

Cod*_*ker 8 generics go

刚开始学习泛型。我正在制作一个命令处理器,老实说我不知道​​如何表达,所以我只想展示一个示例问题:

var ErrInvalidCommand = errors.New("invalid command")

type TransactionalFn[T any] func(ctx context.Context, db T) error

func NewTransactionalCommand[T any](fn TransactionalFn[T]) *TransactionalCommand[T] {
    return &TransactionalCommand[T]{
        fn: fn,
    }
}

type TransactionalCommand[T any] struct {
    fn TransactionalFn[T]
}

func (cmd *TransactionalCommand[T]) StartTransaction() error {
    return nil
}

func (cmd *TransactionalCommand[T]) Commit() error {
    return nil
}

func (cmd *TransactionalCommand[T]) Rollback() error {
    return nil
}

type CMD interface{}

type CommandManager struct{}

func (m *CommandManager) Handle(ctx context.Context, cmd CMD) error {
    switch t := cmd.(type) {
    case *TransactionalCommand[any]:
        return m.handleTransactionalCommand(ctx, t)
    default:
        fmt.Printf("%T\n", cmd)
        return ErrInvalidCommand
    }
}

func (m *CommandManager) handleTransactionalCommand(ctx context.Context, cmd *TransactionalCommand[any]) error {
    if err := cmd.StartTransaction(); err != nil {
        return err
    }

    if err := cmd.fn(ctx, nil); err != nil {
        if err := cmd.Rollback(); err != nil {
            return err
        }
    }

    if err := cmd.Commit(); err != nil {
        return err
    }

    return nil
}

// tests
type db struct{}

func (*db) Do() {
    fmt.Println("doing stuff")
}

func TestCMD(t *testing.T) {
    ctx := context.Background()
    fn := func(ctx context.Context, db *db) error {
        fmt.Println("test cmd")
        db.Do()
        return nil
    }
    tFn := bus.NewTransactionalCommand(fn)

    mng := &bus.CommandManager{}
    err := mng.Handle(ctx, tFn)
    if err != nil {
        t.Fatal(err)
    }
}

Run Code Online (Sandbox Code Playgroud)

mng.handle返回ErrInvalidCommand,因此测试失败,因为cmd*TransactionalCommand[*db]而不是*TransactionalCommand[any]

让我举另一个更抽象的例子:

type A[T any] struct{}

func (*A[T]) DoA() { fmt.Println("do A") }

type B[T any] struct{}

func (*B[T]) DoB() { fmt.Println("do B") }

func Handle(s interface{}) {
    switch x := s.(type) {
    case *A[any]:
        x.DoA()
    case *B[any]:
        x.DoB()
    default:
        fmt.Printf("%T\n", s)
    }
}



func TestFuncSwitch(t *testing.T) {
    i := &A[int]{}

    Handle(i) // expected to print "do A"
}

Run Code Online (Sandbox Code Playgroud)

为什么这个 switch 语句大小写不*A[any]匹配*A[int]?如何让CommandManager.Handle(...)接受通用命令?

zan*_*ngw 13

为什么泛型类型开关无法编译?

  • 这其实是Go团队有意决定的结果。事实证明,允许参数化类型上的类型切换可能会导致混乱

  • 在此设计的早期版本中,我们允许对类型为类型参数或类型基于类型参数的变量使用类型断言和类型开关。我们删除了此功能,因为始终可以将任何类型的值转换为空接口类型,然后对其使用类型断言或类型开关。此外,有时会令人困惑的是,在使用近似元素的类型集的约束中,类型断言或类型开关将使用实际类型参数,而不是类型参数的基础类型(差异在识别部分中进行了解释)匹配的预声明类型)

    来自类型参数提案

让我把强调的语句变成代码。如果类型约束使用类型近似(注意波浪号)...

func PrintStringOrInt[T ~string | ~int](v T)

...如果还有一个自定义类型作为int基础类型...

type Seconds int

...并且如果PrintOrString()使用Seconds参数调用...

PrintStringOrInt(Seconds(42))

...那么该switch块将不会进入int case而是直接进入default case,因为Seconds不是int。开发人员可能期望它也case int:与类型匹配。Seconds

要允许case语句匹配两者Seconds,则int需要新的语法,例如,

case ~int:

截至撰写本文时,讨论仍在进行中,也许它将产生一个用于打开类型参数(例如,switch type T)的全新选项。

更多细节请参考提案:spec: generics: type switch on parametric types


技巧:将类型转换为“any”

幸运的是,我们不需要等待这个提案在未来的版本中得到实施。现在有一个超级简单的解决方法。

与其打开v.(type),不如打开any(v).(type)

switch any(v).(type) {
    ...
Run Code Online (Sandbox Code Playgroud)

这个技巧转换v成一个空的interface{}(又名any),并switch愉快地进行类型匹配。


来源:使用泛型时的提示和技巧


bla*_*een 6

*A[any]不匹配,*A[int]因为它any是静态类型,而不是通配符。因此,用不同类型实例化通用结构会产生不同的类型

为了正确匹配类型开关中的泛型结构,您必须使用类型参数实例化它:

func Handle[T any](s interface{}) {
    switch x := s.(type) {
    case *A[T]:
        x.DoA()
    case *B[T]:
        x.DoB()
    default:
        panic("no match")
    }
}
Run Code Online (Sandbox Code Playgroud)

尽管 infer 没有其他函数参数T,但您必须Handle使用显式实例化进行调用。T不会单独从结构中推断出来。

func main() {
    i := &A[int]{}
    Handle[int](i) // expected to print "do A"
}
Run Code Online (Sandbox Code Playgroud)

游乐场:https://go.dev/play/p/2e5E9LSWPmk


然而,whenHandle实际上是一个方法,就像在数据库代码中一样,这具有在实例化接收器时选择类型参数的缺点。

为了改进这里的代码,您可以创建Handle一个顶级函数:

func Handle[T any](ctx context.Context, cmd CMD) error {
    switch t := cmd.(type) {
    case *TransactionalCommand[T]:
        return handleTransactionalCommand(ctx, t)
    default:
        fmt.Printf("%T\n", cmd)
        return ErrInvalidCommand
    }
}
Run Code Online (Sandbox Code Playgroud)

db T然后你就会遇到如何向命令函数提供参数的问题。为此,您可以:

  • 只需向and传递一个附加*db参数,这也有助于类型参数推断。调用为. 游乐场:https://go.dev/play/p/6WESb86KN5DHandlehandleTransactionalCommandHandle(ctx, &db{}, tFn)

  • 传递一个实例CommandManager(类似于上面的解决方案,但*db被包装)。更加冗长,因为它需要到处显式实例化。游乐场:https://go.dev/play/p/SpXczsUM5aW

  • 请改用参数化接口(如下所示)。所以你甚至不需要类型转换。游乐场:https://go.dev/play/p/EgULEIL6AV5

type CMD[T any] interface {
    Exec(ctx context.Context, db T) error
}
Run Code Online (Sandbox Code Playgroud)