英文:
What's the purpose of a blank identifier in a type parameter?
问题
这两个函数有什么区别?
func f[_ string, p string](s ...p) {
fmt.Println(s)
}
func f[p string](s ...p) {
fmt.Println(s)
}
为什么在第一个函数的类型参数中放置一个空标识符?
英文:
What's the difference between these two functions?
func f[_ string, p string](s ...p) {
fmt.Println(s)
}
func f[p string](s ...p) {
fmt.Println(s)
}
Why even put a blank identifier in a type parameter in the first place?
答案1
得分: 1
在函数范围内,使用下划线 _
代替类型参数名称只是表示该类型参数在函数范围内未被使用。
在你的示例代码中,这并没有什么区别;两个版本的 f
都可以被调用为 f("blah")
而无需提供显式的类型参数。但这仅仅是因为约束条件 string
限制为一个确切的类型。它只能是 string
,所以类型推断仍然有效。
如果将其更改为近似类型,就必须显式实例化:
// 无法从任何地方推断出第一个类型参数
func f[_ ~string, P ~string](s ...P) {
fmt.Println(s)
}
func main() {
// 必须提供第一个类型参数,可以省略第二个类型参数
f[string]("blah")
}
在实际场景中,如果约束条件不是确切类型,下划线可能会强制调用者指定类型参数:这实际上会破坏向后兼容性,因为客户端代码必须确实进行更新以使其工作。
需要明确的是,这适用于彼此独立的类型参数。如果可以从另一个类型参数推断出带有下划线的类型参数,它仍然可以在没有显式实例化的情况下编译:
// 调用为 foo("blah")
func foo[T ~string, P *T](t T) {
var p P = &t
fmt.Println(t, p)
}
// 仍然调用为 foo("blah")
func foo[T ~string, _ *T](t T) {
fmt.Println(t)
}
然而,值得注意的是,如果一个函数有两个彼此独立的可推断类型参数:
func f[T any, U any](t T, u U) {
fmt.Println(t, u)
}
...使 U
不可推断也意味着需要删除 u U
参数,因此你已经进行了一个不向后兼容的更改。
// 参数减少了一个,无论如何都需要进行主要版本升级
func foo[T any, _ any](t T) {
fmt.Println(t, u)
}
对于方法而言,情况就不同了。由于方法无法指定在接收器类型上未声明的类型参数,因此使用下划线更为频繁。可能只是某些方法不需要所有类型参数。那么下划线恰当地反映了这一点:
type Foo[T,U any] struct {
ID T
Val U
}
// 我们不需要在这里引用 U
func (f *Foo[T,_]) SetID(t T) {
f.ID = t
}
// 我们不需要在这里引用 T
func (f *Foo[_,U]) SetVal(v U) {
f.Val = v
}
英文:
Using underscore _
in place of a type parameter name simply signals that the type parameter is not used within the function scope.
In your example code, it doesn't really make a difference; both versions of f
can be called as f("blah")
without supplying explicit type parameters. But this is possible only because the constraint string
restricts to an exact type. It can only ever be string
, so type inference still works.
If you change it to an approximate type instead it has to be instantiated explicitly:
// can't infer first type param from anywhere
func f[_ ~string, P ~string](s ...P) {
fmt.Println(s)
}
func main() {
// must supply first type param, may omit second one
f[string]("blah")
}
In real world scenarios where constraints are not exact types, the underscore would likely force callers to specify the type parameter: this effectively would break backward compatibility, because client code must indeed be updated to work.
Just to be clear, this applies to type parameters that are independent of each other. If the underscored type parameter can be inferred from another one, it can still compile without explicit instantiation:
// called as foo("blah")
func foo[T ~string, P *T](t T) {
var p P = &t
fmt.Println(t, p)
}
// still called as foo("blah")
func foo[T ~string, _ *T](t T) {
fmt.Println(t)
}
However it's worth noting that if a function has two independent type parameters that are inferrable from arguments:
func f[T any, U any](t T, u U) {
fmt.Println(t, u)
}
...making U
not inferrable implies also removing the u U
argument too, so you already have a backward-incompatible change.
// one less argument, requires major version upgrade anyway
func foo[T any, _ any](t T) {
fmt.Println(t, u)
}
With methods instead, it's a different story. Since methods can't specify type parameters that weren't declared on the receiver type, using underscore is a much more frequent occurrence. It may just happen that some methods do not need all type parameters. Then the underscore aptly reflects that:
type Foo[T,U any] struct {
ID T
Val U
}
// we do not need to reference U here
func (f *Foo[T,_]) SetID(t T) {
f.ID = t
}
// we do not need to reference T here
func (f *Foo[_,U]) SetVal(v U) {
f.Val = v
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论