将接口参数替换为类型参数的好处是什么?

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

What are the benefits of replacing an interface argument with a type parameter?

问题

定义一个接口类型来作为类型参数,像这样:

func CallByteWriterGen[W io.ByteWriter](w W, bytes []byte) {
    _ = w.WriteByte(bytes[0])
}

...会导致通过字典进行额外的指针解引用(通过AX传递):

MOVQ 0x10(AX), DX // < -- 额外的指针解引用
MOVQ 0x18(DX), DX
MOVZX 0(CX), CX
MOVQ BX, AX
MOVL CX, BX
CALL DX

相比之下,使用接口参数,像这样:

func CallByteWriter(w io.ByteWriter, bytes []byte) {
    _ = w.WriteByte(bytes[0])
}

可能会有以下无法通过简单使用接口参数实现的好处:

英文:

Defining an interface type to type parameters like this:

func CallByteWriterGen[W io.ByteWriter](w W, bytes []byte) {
  _ = w.WriteByte(bytes[0])
}

...causes extra pointer dereference through dictionary (passed using AX):

MOVQ 0x10(AX), DX // &lt;-- extra pointer dereference 
MOVQ 0x18(DX), DX
MOVZX 0(CX), CX
MOVQ BX, AX
MOVL CX, BX
CALL DX

What might be the benefits that cannot be achieved by simply using an interface argument, like this:

func CallByteWriter(w io.ByteWriter, bytes []byte) {
  _ = w.WriteByte(bytes[0])
}

答案1

得分: 6

接口版本是惯用语,而不是类型参数版本-在需要接口的地方使用接口。

有关更多信息和详细信息,请参阅《何时使用泛型》博文,特别是不要用类型参数替换接口类型一节:

> 例如,可能会诱惑将第一个函数的签名更改为只使用接口类型的第二个版本,如下所示:

> func ReadSome(r io.Reader) ([]byte, error)

> func ReadSome[T io.Reader](r T) ([]byte, error)

> 不要进行这种更改。省略类型参数使函数更容易编写、更容易阅读,并且执行时间可能相同。

英文:

The interface version is idiomatic, not the type parameter one - use an interface where an interface is called for.

See the When to use Generics blog post for additional information and details, specifically the section Don’t replace interface types with type parameters:

> For example, it might be tempting to change the first function
> signature here, which uses just an interface type, into the second
> version, which uses a type parameter.
>
> func ReadSome(r io.Reader) ([]byte, error)
>
> func ReadSome[T io.Reader](r T) ([]byte, error)
>
> Don’t make that kind of change. Omitting the type parameter makes the
> function easier to write, easier to read, and the execution time will
> likely be the same.

答案2

得分: 1

通常情况下,当接口用于抽象行为且函数体内的动态类型实际上是无关紧要的时候,不要用类型参数替换接口。

至于用法,在调用处可能不会有太大变化。无论哪种方式,你都可以将实现了 io.ByteWriter 接口的内容传递给函数参数,就像你在没有类型参数的情况下所做的那样

当接口的动态类型变得有趣时,差异就变得相关了。io.ByteWriter 参数的静态类型只是 io.ByteWriter,要获取动态类型,你必须使用断言 w.(*bytes.Buffer)(可能会引发 panic)或类型切换;而使用类型参数时,函数直接处理具体类型 W

这至少有两种用例。在函数体内:

  • 你可能会声明这些具体类型的新值
  • 你可能会进行比较

io.ByteWriter 在这两种情况下都不是一个好的例子,因为你将使用它来抽象某些行为,而不是为了其动态类型,所以它不适合作为类型参数化的候选对象。

可能还有其他情况,比如基于 interface{} 的 Go1.18 之前的“泛型”代码,或者可能是基于 protobuffers 的代码,其中 proto.Message 是一个接口,工厂模式、单元测试助手等,其中动态类型是有趣的。

然后,可以用 reflect.Newreflect.Zero 来创建新值的笨拙出现可以被 new(T)var x T 替换。示例:

func newFrom(v Setter) Setter {
	return reflect.Zero(reflect.TypeOf(v)).Interface().(Setter)
}

vs.

func newFrom[T Setter](v T) T {
	return *new(T)
}

关于比较,接口本身支持相等运算符 ==!=,但如果动态值不可比较,比较可能会引发 panic。而使用类型参数,只有在显式添加 comparable 约束时,才会编译方法限定的接口之间的比较,从而提高代码的安全性。

// 编译通过,可能会引发 panic
func equal(v, w Setter) bool {
	return v == w
}

vs.

// 无法编译,必须显式添加 comparable
func equal[T Setter](v, w T) bool {
	return v == w
}

这并不意味着类型参数总是适用于这些用例,但你可以将它们视为候选对象。

英文:

In general don’t replace interfaces with type parameters when the interface is used to abstract behavior and the dynamic types are actually irrelevant within the function body.

As for usage, it may not change much at call site. Either way you can pass into the function argument only something that implements io.ByteWriter, just like you do without type parameters.

The differences become relevant when the dynamic types of the interface are interesting. The static type of an io.ByteWriter argument is just io.ByteWriter, and to retrieve the dynamic type you have to use an assertion w.(*bytes.Buffer) which may panic, or a type switch; whereas with type parameters, the function deals directly with the concrete type W.

There’s at least two use cases for this. Within the function body:

  • you happen to declare new values of those concrete types
  • you happen to do comparisons

io.ByteWriter is a bad example in either case, because you are going to use that one precisely for abstracting some behavior rather than for its dynamic type, so that’s not a good candidate for type parametrization.

There may be other situations like pre-Go1.18 "generic" code based on interface{}, or perhaps protobuffers, where proto.Message is an interface, factory patterns, unit test helpers, etc. where the dynamic types are interesting.

Then, clumsy occurrences of reflect.New or reflect.Zero to create new values can be replaced by new(T) or var x T. Demo:

func newFrom(v Setter) Setter {
	return reflect.Zero(reflect.TypeOf(v)).Interface().(Setter)
}

vs.

func newFrom[T Setter](v T) T {
	return *new(T)
}

About comparisons, interfaces do support equality operators == and != natively, but the comparison may just panic if the dynamic values are not comparable. With type parameters instead the comparison between method-only interfaces simply won't compile unless you explicitly add comparable to the constraint, thus improving code safety.

// compiles, might panic
func equal(v, w Setter) bool {
	return v == w
}

vs.

// doesn&#39;t compile, must add comparable explicitly
func equal[T Setter](v, w T) bool {
	return v == w
}

<hr>

This doesn't mean type parameters are always appropriate for these use cases either, but you may consider them as candidates.

huangapple
  • 本文由 发表于 2022年4月16日 01:28:11
  • 转载请务必保留本文链接:https://go.coder-hub.com/71887083.html
匿名

发表评论

匿名网友

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

确定