在Go语言(1.18版本)中,实现泛型的最佳方式是什么?

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

What is the best way to have polymorphic implementations on a generic in go (1.18)?

问题

我想创建一个向量类型,它在内部数据上是通用的,但在方法的实现上可能因输入类型而异。

type SupportedType interface {
    ~int64 | ~uint64 | ~float64 | string | bool | time.Time
}

type Vec[T SupportedType] struct {
    data []T
}

我想在函数上添加不同的实现,具体取决于类型。例如:

func (vec Vec[T]) Sort() {
    ...
}

在大多数通用类型中,< 运算符都可以正常工作。然而,如果 T -> time.Time,我想使用 Before 方法;如果 T -> bool,则希望所有的 false 值都排在 true 值之前。

我有一些关于如何实现这一点的想法,但在新的泛型世界中,什么被认为是“惯用的”方式呢?我的应用对性能要求较高。


使用具有相同函数的类型联合并不起作用(https://play.golang.com/p/QWE-XteWpjL)。

在特定类型的结构体中嵌入一个容器是可行的(https://play.golang.com/p/j0AR48Mto-a),但需要使用接口,这意味着示例函数中的 LessVal 无法内联。如果类型联合中没有清晰的分界线,这种方法可能也不太适用。

英文:

I want to create a Vector type that is generic over its internal data but may have differ in how the methods are implemented given the input type.

type SupportedType interface {
    	 ~int64 | ~uint64 |  ~float64 | string | bool | time.Time
}

type Vec[T SupportedType] struct {
    data []T
}

and I want to add a varying implementations on a function depending on the type. For example:

func (vec Vec[T]) Sort() {
    ...
}

In most of the generic types &lt; will work just fine. However, if T -&gt; time.Time I want to use the Before method and if T --&gt; bool then I want all the false values to go before the true.

I have some ideas on how to accomplish this but what would be considered "idiomatic" in the new generics world? My application is performance sensitive.


Using a type union with types that all have the same function doesn't work (https://play.golang.com/p/QWE-XteWpjL).

Embedding a container inside type specific structs does work ( https://play.golang.com/p/j0AR48Mto-a ) but requires the use of an interface which means that the Less and Val in the example functions can't be inlined. It also might not work so nicely if there isn't a clean delineation between the subsets in the type union.

答案1

得分: 2

顺便提一下,已经有一个用于排序的库了。

https://pkg.go.dev/golang.org/x/exp/slices#Sort

1. 你可以创建一个带有泛型的接口,然后进行类型断言。

示例:

type Lesser[T SupportedType] interface {
	Less(T) bool
}

type Vec[T SupportedType] []T

func (vec Vec[T]) Less(a, b int) bool {
	return any(vec[a]).(Lesser[T]).Less(vec[b])
}

func main() {
	vs := Vec[String]([]String{"a", "b", "c", "d", "e"})
	vb := Vec[Bool]([]Bool{false, true})
	fmt.Println(vs.Less(3, 1))
	fmt.Println(vb.Less(0, 1))
}

playground 1

2. 你可以在 Vec 上保存类型。

示例:

type Lesser[T SupportedType] interface {
	Less(T) bool
}

type Vec[T SupportedType, L Lesser[T]] []T

func (vec Vec[T, L]) Less(a, b int) bool {
	return any(vec[a]).(L).Less(vec[b])
}

func main() {
	vs := Vec[String, String]([]String{"a", "b", "c", "d", "e"})
	fmt.Println(vs.Less(3, 1))
}

playground 2

3. 嵌套类型约束

感谢 @blackgreen

示例:

type SupportedType interface {
	Int8 | Time | Bool | String
}

type Lesser[T SupportedType] interface {
	Less(T) bool
}

type Vec[T interface {
	SupportedType
	Lesser[T]
}] []T

func (vec Vec[T]) Less(a, b int) bool {
	return vec[a].Less(vec[b])
}

func main() {
	vs := Vec[String]([]String{"a", "b", "c", "d", "e"})
	fmt.Println(vs.Less(3, 1))
}

playground 3

基准测试:

benchmark 1 : 28093368	        36.52 ns/op	      16 B/op	       1 allocs/op

benchmark 2 : 164784321	         7.231 ns/op	       0 B/op	       0 allocs/op

benchmark 3 : 212480662	         5.733 ns/op	       0 B/op	       0 allocs/op

将容器嵌入到特定类型的结构体中
benchmark 4 : 211429621	         5.720 ns/op	       0 B/op	       0 allocs/op

哪种方法对你来说最好,完全取决于你。但在我看来,第三种方法是最好的。

英文:

BTW there is already a library for sorting

https://pkg.go.dev/golang.org/x/exp/slices#Sort

1. You can create interface with generic, then type assert to that.

example:

type Lesser[T SupportedType] interface {
	Less(T) bool
}

type Vec[T SupportedType] []T

func (vec Vec[T]) Less(a, b int) bool {
	return any(vec[a]).(Lesser[T]).Less(vec[b])
}

func main() {
	vs := Vec[String]([]String{&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;, &quot;e&quot;})
	vb := Vec[Bool]([]Bool{false, true})
	fmt.Println(vs.Less(3, 1))
	fmt.Println(vb.Less(0, 1))
}

playground 1

2. You can save the type on Vec.

example:

type Lesser[T SupportedType] interface {
	Less(T) bool
}

type Vec[T SupportedType, L Lesser[T]] []T

func (vec Vec[T, L]) Less(a, b int) bool {
	return any(vec[a]).(L).Less(vec[b])
}

func main() {
	vs := Vec[String, String]([]String{&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;, &quot;e&quot;})
	fmt.Println(vs.Less(3, 1))
}

playground 2

3. Nesting type constraint

thanks @blackgreen

example :

type SupportedType interface {
	Int8 | Time | Bool | String
}

type Lesser[T SupportedType] interface {
	Less(T) bool
}

type Vec[T interface {
	SupportedType
	Lesser[T]
}] []T

func (vec Vec[T]) Less(a, b int) bool {
	return vec[a].Less(vec[b])
}

func main() {
	vs := Vec[String]([]String{&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;, &quot;e&quot;})
	fmt.Println(vs.Less(3, 1))
}

playgrond 3

Benchmark:

benchmark 1 : 28093368	        36.52 ns/op	      16 B/op	       1 allocs/op

benchmark 2 : 164784321	         7.231 ns/op	       0 B/op	       0 allocs/op

benchmark 3 : 212480662	         5.733 ns/op	       0 B/op	       0 allocs/op

Embedding a container inside type specific structs:
benchmark 4 : 211429621	         5.720 ns/op	       0 B/op	       0 allocs/op

It's up to you which one is best for you. But IMO number 3 is best.

答案2

得分: 1

个人而言,我认为最好不要在联合类型中包含许多不相关的类型,因为它们不会共享许多常见操作,最终你会编写特定于类型的代码。那么使用泛型的意义何在呢...?

无论如何,可能的策略取决于SupportedType约束的类型集中包含了什么,以及你想对这些类型做什么:

仅限确切类型且没有方法

T上使用类型开关,并运行对具体类型有意义的任何操作。当方法实现仅使用T类型的一个值时,这种方法效果最好,因为你可以直接在开关保护语句中使用变量(v := any(vec[a]).(type))。当开关保护语句中的T值之外还有更多T值时,情况就不那么简单了,因为你必须单独转换和断言所有这些值:

func (vec Vec[T]) Less(a, b int) bool {
	switch v := any(vec[a]).(type) {
	case int64:
		return v < any(vec[b]).(int64)

    case time.Time:
		return v.Before(any(vec[b]).(time.Time))

    // 更多的情况...
	}
	return false
}

带有方法

将包含方法的接口参数化,并将其T约束为支持的类型。然后将Vector的类型参数同时约束为这两者。这种方法的优点是确保Vector不能使用你忘记实现Less(T) bool的类型进行实例化,并消除了类型断言,否则在运行时可能会引发恐慌。

type Lesser[T SupportedType] interface {
	Less(T) bool
}

type Vec[T interface { SupportedType; Lesser[T] }] []T

func (vec Vec[T]) Less(a, b int) bool {
	return vec[a].Less(vec[b])
}

带有方法和预声明类型

不可能。考虑以下情况:

type SupportedTypes interface {
    // 确切的预声明类型
    int | string
}

type Lesser[T SupportedTypes] interface {
    Less(T) bool
}

约束Lesser具有一个空类型集,因为intstring都不能有方法。因此,在这种情况下,你回到了“仅限确切类型且没有方法”的情况。

带有近似类型(~T

将上述约束更改为近似类型:

type SupportedTypes interface {
    // 近似类型
    ~int | ~string
}

type Lesser[T SupportedTypes] interface {
    Less(T) bool
}

类型开关不是一个选项,因为case ~int:是不合法的。而且约束上的方法的存在会阻止你使用预声明类型进行实例化:

Vector[MyInt8]{} // 当MyInt8实现了Lesser时可以正常工作
Vector[int8]     // 无法编译,int8无法实现Lesser

因此,我看到的选项是:

  • 强制客户端代码使用定义的类型,在许多情况下可能是可以接受的
  • 将约束的范围缩小到支持相同操作的类型
  • 反射(进行基准测试以查看性能损失是否过大),但反射实际上无法找到底层类型,所以你只能使用一些使用reflect.KindCanConvert的技巧。

如果这个提案通过,这可能会改善并可能超越其他选项。

英文:

Personally, I think it's best to not include in a union a lot of types that are unrelated to each other, as they wouldn't share many common operations, and you end up writing type-specific code. So then what's the point of using generics...?

Anyway, possible strategies depend on what is included in the type set of SupportedType constraint, and what you want to do with those:

Only exact types and no methods

Use a type switch on T and run whatever operation makes sense for the concrete type. This works best when the method implementation works with only one value of type T, as you can work directly with the variable in the switch guard (v := any(vec[a]).(type)). It stops being pretty when you have more T values beside the one in the switch guard, as you have to convert & assert all of them individually:

func (vec Vec[T]) Less(a, b int) bool {
	switch v := any(vec[a]).(type) {
	case int64:
		return v &lt; any(vec[b]).(int64)

    case time.Time:
		return v.Before(any(vec[b]).(time.Time))

    // more cases...
	}
	return false
}

With methods

Parametrize the interface that contains the methods and constrain its T to the supported types. Then constrain Vector's type parameter to both.
The advantage of this one is to make sure that Vector can't be instantiated with types on which you forgot to implement Less(T) bool and get rid of the type assertion, which otherwise, could panic at runtime.

type Lesser[T SupportedType] interface {
	Less(T) bool
}

type Vec[T interface { SupportedType; Lesser[T] }] []T

func (vec Vec[T]) Less(a, b int) bool {
	return vec[a].Less(vec[b])
}

With methods and predeclared types

Impossible. Consider the following:

type SupportedTypes interface {
    // exact predeclared types
    int | string
}

type Lesser[T SupportedTypes] interface {
    Less(T) bool
}

The constraint Lesser has an empty type set, because neither int nor string can have methods. So here you fall back to the "exact types and no methods" case.

With approximate types (~T)

Changing the constraints above into approximate types:

type SupportedTypes interface {
    // approximate types
    ~int | ~string
}

type Lesser[T SupportedTypes] interface {
    Less(T) bool
}

The type switch is not an option, since case ~int: isn't legal. And the presence of a method on the constraint prevents you from instantiating with the predeclared types:

Vector[MyInt8]{} // ok when MyInt8 implements Lesser
Vector[int8]     // doesn&#39;t compile, int8 can&#39;t implement Lesser

So the options I see then are:

  • force client code to use defined types, which in many cases might be just fine
  • reduce the scope of the constraint to types that support the same operations
  • reflection (benchmark to see if the performance penalty is too much for you), but reflection can't actually find the underlying type, so you're left with some hacks using reflect.Kind or CanConvert.

This might improve and possibly trump other options when/if this proposal comes through.

huangapple
  • 本文由 发表于 2022年3月20日 05:52:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/71542373.html
匿名

发表评论

匿名网友

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

确定