繁体   English   中英

switch 语句中的泛型类型

[英]Generic type in a switch statement

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

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)
    }
}

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"
}

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

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

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

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

尽管在没有其他 function arguments 来推断T的情况下,您将不得不使用显式实例化调用Handle T不会单独从结构中推断出来。

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

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


但是,当Handle实际上是一个方法时,如在您的数据库代码中,这具有在实例化接收器时选择类型参数的缺点。

为了改进这里的代码,您可以将Handle设置为顶级 function:

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
    }
}

然后你就有了如何将参数db T提供给命令 function 的问题。 为此,您可以:

  • 只需将额外的*db参数传递给HandlehandleTransactionalCommand ,这也有助于类型参数推断。 调用为Handle(ctx, &db{}, tFn) 游乐场: https://go.dev/play/p/6WESb86KN5D

  • 传递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
}

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

  • 这实际上是 Go 团队有意决定的结果。 事实证明,允许在参数化类型上进行类型切换会导致混淆

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

    来自类型参数提案

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

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

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

type Seconds int

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

PrintStringOrInt(Seconds(42))

...然后switch块不会进入int case ,而是 go 进入default case ,因为Seconds不是int 开发人员可能期望case int:也匹配Seconds类型。

要允许case语句同时匹配Secondsint将需要新的语法,例如,

case ~int:

在撰写本文时,讨论仍处于开放状态,也许它会带来一个全新的选项来打开类型参数(例如, switch type T )。

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


技巧:将类型转换为 'any'

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

不要打开v.(type) ,而是打开any(v).(type)

switch any(v).(type) {
    ...

这个技巧将v转换为一个空的interface{} (又名any ), switch很乐意为此进行类型匹配。


资料来源: 使用 generics 时的提示和技巧

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM