Syntax for using a generic type as struct field

huangapple go评论78阅读模式
英文:

Syntax for using a generic type as struct field

问题

我正在尝试使用结构体在Golang中定义一个表类型。这是我目前的代码:

type Column[T any] struct {
    Name   string
    Values map[int]T
}

我想使用这个列类型来定义一个表,代码如下:

type Table struct {
    Columns map[string]Column

Go的编译器报错说我需要实例化泛型类型Column。有人可以帮我解决创建它的语法吗?

英文:

I am trying to define a table type in Golang using structs. This is what I have at the moment.

type Column[T any] struct {
    Name   string
    Values map[int]T
}

I would like to use this column type to define a table like so,

type Table struct {
//Following line returns an error
    Columns map[string]Column

Go's compiler is throwing an error that I need to instantiate the generic type Column.

Can anyone help me with the syntax for creating it?

答案1

得分: 3

你需要从顶层结构传播类型:

type Table[T any] struct {
    Columns map[string]Column[T]
}

请参考playground

英文:

You need to propagate the type from top level structure:

type Table[T any] struct {
    Columns map[string]Column[T]
}

See playground

答案2

得分: 2

所以你想做的事情实际上是有多个列,这些列具有不同类型的值,并将它们全部收集到一个Table类型中。使用普通的泛型,你可以像Ado Ren建议的那样做:

type Column[T any] struct {
    Name   string
    Values map[int]T
}

type Table[T any] struct {
    Columns map[string]Column[T]
}

然而,正如我所怀疑的,你希望Table能够包含多个不同类型的列,所以像下面这样的代码是行不通的:

sCol := Column[string]{
    Name: "strings",
}
uiCol := Column[uint64]{
    Name: "Uints",
}
tbl := Table[any]{
    Columns: map[string]any{
        sCol.Name:  sCol,
        uiCol.Name: uiCol,
    },
}

如果你看一下这意味着什么,原因就会显而易见了,与你分配的值相比,tbl的类型是不合理的。any(或interface{})类型意味着tbl的初始化和预期的类型是这样的(我用一个匿名结构体替换了Column类型,以便更容易看出类型不匹配):

Table[any]{
    Columns: map[string]struct{
        Name   string
        Values map[int]any // 或者 interface{}
    }{
        "Strings": {
            Name:   "Strings",
            Values: map[int]string{}, // 与 map[int]any 不匹配
        },
        "Uints": {
            Name:   "Uints",
            Values: map[int]uint64{}, // 同样,不是 map[int]any
        },
    }
}

这就是实际上发生的情况。再次强调,正如我之前在评论中提到的,这是有原因的。泛型的整个目的是允许你创建类型并编写可以处理受约束的所有类型的有意义的函数。对于泛型的Table类型,如果它接受不同类型的Column值,那么这个目的就不再适用了。

func (t Table[T any]) ProcessVals(callback func(v T) T) {
    for _, col := range t.Columns {
        for k, v := range col.Values {
            col.Values[k] = callback(v)
        }
    }
}

对于像Table[uint64]这样的类型,你可以做一些类似这样的事情:

tbl.ProcessVals(func(v uint64) uint64 {
    if v%2 == 1 {
        return v*3 + 1
    }
    return v/2
})

但是,如果表中的某些列是字符串,那么显然这个回调函数就没有意义了。你需要做类似这样的事情:

tbl.ProcessVals(func(v interface{}) interface{} {
    switch tv := t.(type) {
    case uint64:
    case int64:
    case int:
    case uint:
    default:
        return v
    }
})

使用类型切换/类型断言来理解每个值,然后相应地处理它。这并不是真正的泛型...基本上就是在引入Go泛型之前我们所做的事情,那么为什么要费这个劲呢?最好的情况下没有任何区别,最坏的情况下,你将在各个地方得到大量乏味的代码,因为回调函数将由调用者编写,他们的代码将变得混乱不堪。


那么该怎么办呢?

老实说,这个问题有点像X-Y问题。我不知道你确切想要做什么,所以很有可能你只是以次优的方式去做。过去我曾公开表示我不是最喜欢泛型的人。我使用它们,但很少使用。往往情况下,我发现(尤其是在C++模板中),人们之所以使用泛型并不是因为它有意义,而是因为他们可以使用。尽管如此,在这种特殊情况下,你可以利用Go语言中更不被重视的特性之一:鸭子类型接口(duck-type interfaces):

type Column[T any] struct {
    Name   string
    Values map[int]T
}

type Col interface {
    Get() Column[any]
}

type Table struct {
    Columns map[string]Col
}

func (c Column[T]) Get() Column[any] {
    r := Column[any]{
        Name:   c.Name,
        Values: make(map[int]any, len(c.Values)),
    }
    for k, v := range c.Values {
        r.Values[k] = v
    }
    return r
}

现在,Table不再期望一个特定类型的Column,而是期望任何实现了Col接口的值(一个只有一个方法返回Column[any]值的接口)。这在表面上消除了底层类型的影响,但至少你能够塞进任何你喜欢的Column

演示

约束

如果依赖于这样一个简单的接口不符合你的口味(在某些情况下是不可取的),你可以选择更受控制的方法,并与上述的Get方法结合使用类型约束:

type OrderedCols interface {
    Column[uint64] | Column[int64] | Column[string] // 等等
}

type AllCols interface {
    // OrderCols 约束,或任何类型
    Column[any] | OrderedCols
}

有了这些约束,我们可以使我们的Table类型在某种程度上更安全地使用,因为我们可以确保Table只包含实际的Column[T]值,而不是只是恰好与接口匹配的其他东西。请注意,我们仍然需要擦除一些类型:

type Table[T AllCols] struct {
    Columns map[string]T
}

tbl := Table[Column[any]]{
    Columns: map[string]Column[any]]{
        c1.Name: c1.Get(), // 转换为 Column[any]
        c2.Name: c2.Get(),
    },
}

在这里演示

英文:

So what you're trying to do do is essentially have a number of columns, with values in different types, and collect them all in a Table type. Using plain old generics, you can do something like Ado Ren suggested:

type Column[T any] struct {
    Name   string
    Values map[int]T
}

type Table[T any] struct {
    Columns map[string]Column[T]
}

However, as I suspected, you want your Table to be able to contain multiple columns, of different types, so something like this would not work:

sCol := Column[string]{
    Name: "strings",
}
uiCol := Column[uint64]{
    Name: "Uints",
}
tbl := Table[any]{
    Columns: map[string]any{
        sCol.Name:  sCol,
        uiCol.Name: uiCol,
    },
}

The reason, if you look at what this implies, is that the type for tbl doesn't make sense, compared to the values you're assigning. The any (or interface{} type means that what tbl is initialised to - and what it expects - is this (I replaced the Column type with an anonymous struct so the type mismatch is more obvious):

Table[any]{
    Columns: map[string]struct{
        Name   string
        Values map[int]any // or interface{}
    }{
        "Strings": {
            Name:   "Strings",
            Values: map[int]string{}, // does not match map[int]any
        },
        "Uints": {
            Name:   "Uints",
            Values: map[int]uint64{}, // again, not a map[int]any
        },
    }
}

This is essentially what is going on. Again, and as I mentioned in a comment earlier, there's a reason for this. The whole point of generics is that they allow you to create types and write meaningful functions that can handle all types governed by the constraint. With a generic Table type, should it accept differently typed Column values, that no longer applies:

func (t Table[T any]) ProcessVals(callback func(v T) T) {
    for _, col := range t.Columns {
        for k, v := range col.Values {
            col.Values[k] = callback(v)
        }
    }
}

With a type like Table[uint64], you could do stuff like:

tbl.ProcessVals(func(v uint64) uint64 {
    if v%2 == 1 {
        return v*3 + 1
    }
    return v/2
})

But if some of the columns in table are strings, then naturally, this callback makes no sense. You'd need to do stuff like:

tbl.ProcessVals(func (v interface{}) interface{} {
    switch tv := t.(type) {
    case uint64:
    case int64:
    case int:
    case uint:
    default:
        return v
    }
})

Use type switches/type assertions to make sense of each value, and then process it accordingly. That's not really generic... it's basically what we did prior to the introduction of go generics, so why bother? At best it makes no difference, at worst you'll end up with a lot of tedious code all over the place, as callbacks are going to be written by the caller, and their code will end up a right mess.


So what to do?

Honestly, this question has a bit of the X-Y problem feel about it. I don't know what you're trying to do exactly, so odds are you're just going about it in the sub-optimal way. I've been vocal in the past about not being the biggest fan of generics. I use them, but sparingly. More often than not, I've found (in C++ templates especially), people resort to generics not because it makes sense, but because they can. Be that as it may, in this particular case, you can incorporate columns of multiple types in your Table type using one of golangs more underappreciated features: duck-type interfaces:

type Column[T any] struct {
    Name   string
    Values map[int]T
}

type Col interface {
    Get() Column[any]
}

type Table struct {
    Columns map[string]Col
}

func (c Column[T]) Get() Column[any] {
    r := Column[any]{
        Name:   c.Name,
        Values: make(map[int]any, len(c.Values)),
    }
    for k, v := range c.Values {
        r.Values[k] = v
    }
    return r
}

Now Table doesn't expect a Column of any particular type, but rather any value that implements the Col interface (an interface with a single method returning a value of Column[any]). This effectively erases the underlying types as far as the Table is concerned, but at least you're able to cram in whatever Column you like.

Demo

Constraints

If relying on a simple interface like this isn't to your taste (which, in some cases, is not desirable), you could opt for a more controlled approach and use a type constraint, in conjunction with the above Get method:

type OrderedCols interface {
    Column[uint64] | Column[int64] | Column[string] // and so on
}

type AllCols interface {
    // the OrderCols constraint, or any type
    Column[any] | OrderedCols
}

With these constraints in place, we can make our Table type a bit safer to use in the sense that we can ensure the Table is guaranteed to only contain actual Column[T] values, not something else that just happens to match the interface. Mind you, we still have some types to erase:

type Table[T AllCols] struct {
    Columns map[string]T
}

tbl := Table[Column[any]]{
    Columns: map[string]Column[any]]{
        c1.Name: c1.Get(), // turn in to Column[any]
        c2.Name: c2.Get(),
    },
}

Demo here

huangapple
  • 本文由 发表于 2022年9月22日 17:24:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/73812248.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定