【问题标题】:Stringer method requires valueStringer 方法需要值
【发布时间】:2018-01-17 08:40:19
【问题描述】:

The Go FAQ answers a question 关于方法中按值与按指针接收器定义的选择。该答案中的陈述之一是:

如果该类型的某些方法必须有指针接收器,其余的也应该有,因此无论该类型如何使用,方法集都是一致的。

这意味着如果我有一些数据类型的方法可以改变数据,因此需要按指针接收器,我应该对为该数据类型定义的所有方法使用按指针接收器。

另一方面,"fmt" 包调用String() 方法,如Stringer 接口按值 中定义的那样。如果使用接收器按指针定义String() 方法,则当关联数据类型作为fmt.Println(或其他fmt 格式化方法)的参数时,它不会被调用。这使得人们别无选择,只能通过值接收器来实现String() 方法。

在满足Stringer 接口的fmt 要求的同时,如何才能与常见问题解答所建议的按值与按指针的选择保持一致?

编辑:

为了强调我提到的问题的本质,考虑这样一种情况,其中一个数据类型具有一组由接收者按值定义的方法(包括 String())。然后,一个人希望添加一种改变该数据类型的附加方法 - 所以他用接收器按指针定义它,并且(为了保持一致,根据常见问题解答)他还更新了该数据类型的所有其他方法以供使用-指针接收器。此更改对使用此数据类型方法的任何代码的影响为零 - 但用于调用 fmt 格式化函数(现在需要传递指向变量的指针而不是其值,与更改之前一样)。所以一致性要求只有在fmt 的上下文中才有问题。需要根据接收者类型调整向fmt.Println(或类似函数)提供变量的方式,这破坏了轻松重构包的能力。

【问题讨论】:

  • 请注意,这是常见问题解答而不是规范,规范允许单一类型的混合接收器(值/指针),如果方法 A 在指针上而 B 在上,您的程序仍将编译一个值。另外shouldmust是不一样的,我个人认为FAQ中的陈述是一般性的建议,而不是盲目遵循的东西。你有一个没有意义的案例吗?那就不要坚持了!
  • 哇!您发现以后的代码更改可能会产生意想不到的结果!欢迎来到俱乐部。测试。
  • @mkopriva ,我的问题是关于核心包“fmt”的常见问题解答一致性,它需要按值接收器(假设希望以“自然”方式使用“fmt”方法来提供变量本身,而不是它们的引用/指针)。正如您所建议的,在这种情况下我需要忽略常见问题解答,但我想问,如果在核心包的上下文中不能遵守这种方法,为什么常见问题解答会说明这种方法?
  • @ElazarR 首先,fmt 包不需要按值 afaik;第二,“自然”? “自然”是谁的意见?如果一个变量已经是一个指针,那么这个*v 是否比这个v 更“自然”?无论如何,FAQ 陈述了一个一般性建议,这取决于 Go 语言的用户,核心包与否,他们是否遵守建议。就使用fmt 而言,客户端库可以很容易地遵守它,您的情况可能会有所不同,但这并不意味着“它不能被遵守”。
  • @ElazarR 添加该方法不会破坏任何内容。更换接收器即可。最好的建议是:停止担心现实生活中的非问题。

标签: go methods interface


【解决方案1】:

如果你用指针接收器定义你的方法,那么你应该使用和传递指针值而不是非指针值。这样做传递的值确实实现了Stringer,并且fmt 包将没有问题“检测”并调用您的String() 方法。

例子:

type Person struct {
    Name string
}

func (p *Person) String() string {
    return fmt.Sprintf("Person[%s]", p.Name)
}

func main() {
    p := &Person{Name: "Bob"}
    fmt.Println(p)
}

输出(在Go Playground 上试试):

Person[Bob]

如果您将Person 类型的值传递给fmt.Println(),而不是*Person 类型的指针,是的,确实不会调用Person.String()。但是,如果Person 的所有方法都有指针接收器,则强烈指示您应该将类​​型及其值用作指针(除非您不打算使用它的方法)。 p>

是的,您必须知道是否必须使用Person*Person。处理它。如果你想编写正确高效的程序,你必须知道的不仅仅是使用指针还是非指针值,我不知道为什么这对你来说很重要。不知道的就去查,懒的可以用指针作为指针值(类型)的方法集,包含指针和非指针接收器的方法。

Person 的作者也可能会为您提供一个 NewPerson() 工厂函数,您可以依靠它来返回正确类型的值(例如 Person 如果方法具有值接收器,*Person 如果方法有指针接收器),所以你不必知道使用哪个。

回答稍后将带有指针接收器的方法添加到以前只有带有值接收器的方法的类型:

是的,正如您在问题中所描述的那样,这可能不会破坏现有代码,但继续使用非指针值可能不会从稍后添加的带有指针接收器的方法中受益。

我们可能会问:这是一个问题吗?使用该类型时,您刚刚添加的新方法不存在。所以原始代码没有假设它的存在。所以应该没问题。

第二个考虑:该类型只有带有值接收器的方法,因此可以很容易地假设通过它们的使用,值是不可变的,因为带有值接收器的方法不能改变值。使用该类型的代码可能是在此基础上构建的,假设它没有被其方法更改,因此在多个 goroutine 中使用它可能会正确地省略某些同步。

所以我确实认为将带有指针接收器的新方法添加到以前只有带有值接收器的方法的类型不应该是“不透明的”,添加这个新方法的人有责任修改这种类型的使用“切换”到指针并确保代码保持安全和正确,或者处理非指针值不会有这个新方法的事实。

提示:

如果将来某个类型有可能具有 mutator 方法,您应该开始使用带有指针接收器的方法来创建它。这样做可以避免以后必须执行上述过程。

另一个技巧是完全隐藏类型,只发布接口。这样做,这种类型的用户不必知道接口是否包装了指针,这无关紧要。它们接收接口值,并调用接口的方法。包作者负责处理适当的方法接收器,并返回实现接口的适当类型。客户看不到这一点,他们不依赖于此。他们看到和使用的只是界面。

【讨论】:

  • 但在 Go 中,实际上并不需要取消对变量的引用来调用由指针定义的方法。因此,即使对于具有指针方法的数据类型,该数据类型的用户也永远不会费心取消引用该值(在您的示例中为 Person)。只有在使用“fmt”时,才需要记住对于该数据类型,方法是按指针的并取消引用它。这需要知道每个数据类型的方法是如何实现的——这对于普通的方法调用是不需要的(考虑数据类型在另一个包中定义的情况)。
  • 这不是可能与否的问题,我知道这种情况有一些解决方法,人们总是可以检查他使用的包的定义,但是这些解决方案在大范围内并不是真正可扩展的涉及许多独立程序员的项目。请在问题中查看我的澄清编辑。
  • “修改这种类型的使用以“切换”到指针......”是什么意思?在 GO 中,如果方法的接收者从按值更改为按指针,则无需更改使用代码中的任何内容。所有按值调用仍然可以正确编译和运行(感谢编译器)。问题仍然存在——在这种情况下,只有与“fmt”相关的调用需要修改。
  • @ElazarR 我的意思是,如果有人通过创建它的非指针值来使用该类型,则必须将其更改为指针,例如p := Person{}p := &Person{}.
  • 并非如此。我可以继续使用p.SomeMethod(),无论该方法是否接收指针或值,p 作为指针(引用)以及值变量。 GO 编译器不在乎。由于对象(值)或其指针只有一种方法,因此编译器会调用正确的方法,因此无需更改使用(调用者)代码。只有“fmt”函数在为 Println 和其他函数调用 String() 方法时区分按值和按指针的情况。
【解决方案2】:

为了强调我提到的问题的本质,考虑这样一种情况,其中一个数据类型具有一组由接收者按值定义的方法(包括 String())。然后,一个人希望添加一种改变该数据类型的附加方法 - 所以他用接收器按指针定义它,并且(为了保持一致,根据常见问题解答)他还更新了该数据类型的所有其他方法以供使用-指针接收器。此更改对使用此数据类型的方法的任何代码的影响为零 - 但用于调用 fmt 格式化函数(现在需要传递指向变量的指针而不是其值,如更改之前一样)。

这不是真的。它的所有interface 和一些类型断言也会受到影响——这就是 fmt 受到影响的原因。例如:

package main

import (
    "fmt"
)

type I interface {
    String() string
}

func (t t) String() string { return "" }

func (p *p) String() string { return "" }

type t struct{}
type p struct{}

func S(i I) {}

func main() {
    fmt.Println("Hello, playground")
    T := t{}
    P := p{}
    _ = P
    S(T)
    //S(P) //fail
}

要从根本上理解这一点,您应该知道指针方法和值方法与基础不同。但是,为了方便起见,就像省略 ; 一样,golang 编译器会查找使用没有指针的指针方法的情况并将其改回。

这里解释:https://tour.golang.org/methods/6

回到最初的问题:指针方法的一致性。如果您更仔细地阅读常见问题解答,您会发现它是考虑使用值或指针方法的最后一部分。您可以在container/heap 的标准库示例中找到反例:

// A PriorityQueue implements heap.Interface and holds Items.
type PriorityQueue []*Item

func (pq PriorityQueue) Len() int { return len(pq) }

func (pq PriorityQueue) Less(i, j int) bool {
    // We want Pop to give us the highest, not lowest, priority so we use greater than here.
    return pq[i].priority > pq[j].priority
}

func (pq PriorityQueue) Swap(i, j int) {
    pq[i], pq[j] = pq[j], pq[i]
    pq[i].index = i
    pq[j].index = j
}

func (pq *PriorityQueue) Push(x interface{}) {
    n := len(*pq)
    item := x.(*Item)
    item.index = n
    *pq = append(*pq, item)
}

func (pq *PriorityQueue) Pop() interface{} {
    old := *pq
    n := len(old)
    item := old[n-1]
    item.index = -1 // for safety
    *pq = old[0 : n-1]
    return item
}

// update modifies the priority and value of an Item in the queue.
func (pq *PriorityQueue) update(item *Item, value string, priority int) {
    item.value = value
    item.priority = priority
    heap.Fix(pq, item.index)
}

确实,正如FAQ所说,要确定是否使用指针方法,请按顺序考虑以下因素:

  1. 方法需要修改receiver吗?如果是,请使用指针。如果不是,应该有充分的理由,否则会造成混淆。
  2. 效率。如果接收器很大,例如一个大结构,使用指针接收器会便宜得多。然而,效率并不容易讨论。如果您认为这是一个问题,请在这样做之前对其进行分析和/或进行基准测试。
  3. 一致性。如果该类型的某些方法必须具有指针接收器,则其余的也应该具有,因此无论该类型如何使用,方法集都是一致的。对我来说,这意味着如果将类型用作指针(例如,频繁修改),它应该使用设置的方法来标记。这并不意味着一种类型只能有指针方法或相反。

【讨论】:

  • 我无法理解您的示例如何证明我的主张。 S() 不是一种方法(即,按照 GO 的行话,一个带有接收器的函数) - 它是一个普通函数,因此参数的类型是明确的。另一方面,对于 String() 方法,我可以调用 t.String()(&t).String() - 两者都可以。但是,fmt.Println(t) 可以按预期工作,而 fmt.Println(&t) 则不能(反之亦然)。
  • 无论如何,我接受容器/堆的例子,因为它表明第三条规则(一致性)很弱。尤其是考虑到 GO 中缺少 const 方法修饰符(const 方法,如在 C++ 中)时,我宁愿避免将指针传递给这么多不会改变数据的方法,只是为了“一致”。这也将使我摆脱String() 的接收器类型的问题 - 即使其他方法是按指针的,我也会按值保留它。
  • @ElazarR 你应该注意到fmt.Println 是一个普通的函数,而不是一个方法。关键是,interface 受到影响(而S() 不采用固定类型的参数,而是采用接口,在这种情况下非常简单)。
  • 我同意fmt.Println 是一个普通函数,但是它的实现会调用为其参数类型定义的String() 方法。但是,由于给定类型(按值或按指针)只能有一个版本的 String() 方法,因此当 Println 被赋予参数的另一个“模式”时,它将调用默认的 @987654338 @ 该数据类型(例如,结构)的方法,而不是自定义的方法。这不是普通方法调用的情况,可以确定会为指针或值接收器调用相应的方法。
  • @ElazarR 不,这不是 fmt 的工作方式。 Println 接受 interface{} 并进行类型断言。它首先尝试将其转换为Stringer 接口,并且像示例一样,它可能会失败。失败后,它使用反射做一些复杂的事情来使用“默认”方式。它和例子一样,没有魔法。
【解决方案3】:

这里以前的答案没有解决根本问题,尽管叶子 bebop 的答案是可靠的建议。

给定一个值,实际上您可以在其上调用指针或值接收器方法,编译器会为您执行此操作。但是,这不适用于通过接口实现调用时。

这归结为this dicussion 关于接口实现。

在那个讨论中,讨论是关于使用 nil 指针实现接口。但是底层的讨论围绕着同一个问题:实现接口时必须选择指针或值类型,编译器不会尝试,golang代码也不会尝试确定到底是什么类型是的,并相应地调整接口调用。

例如,当调用时

fmt.Println(object)

您正在使用 X 类型的对象实现 interface{} 类型的 arg。其中的 fmt 代码对知道对象的类型是否为指针类型没有兴趣。如果不使用反射,它甚至无法分辨。它只会在任何类型上调用 String()。

所以如果你提供了一个 X 类型的值,并且恰好有一个 (*X) String() string 方法,那没关系,该方法不会被调用,它只会类型断言该 X 类型是否实现 Stringer ,如果类型 *X 断言 Stringer 则没有兴趣。由于没有(X) String() string 方法,它将继续。它不会尝试检查 X 可能是什么,它是否是指针类型,如果不是,关联的指针类型是否实现了 Stringer,而是调用该 String() 方法。

所以这实际上不是指针与值方法的问题,这是在调用 fmt 方法时实现 interface{} 时的接口实现问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-16
    • 2021-12-02
    • 2020-01-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多