为什么 Golang 禁止将一个本地类型赋值给相同底层类型的变量?

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

Why does golang prohibit assignment to same underlying type when one is a native type?

问题

考虑以下代码:

package main
import "fmt"

type specialString string

func printString(s string) {
    fmt.Println(s)
}

// 不像C++,这是不合法的Go代码,因为它重新声明了printString函数
//func printString(s specialString) {    
//    fmt.Println("Special: " + s)
//}

func main() {
    ss := specialString("cheese")
    // ... 那么为什么这个不被允许呢?
    printString(ss)
}

我的问题是:为什么语言定义中不允许在main()函数中调用printString(ss)?(我不是在寻找关于Golang赋值规则的答案;我已经阅读过它们,并且我看到specialStringstring都有相同的“底层类型”,并且两种类型都是“命名的”——如果你认为通用类型string是命名的话,Golang显然是这样认为的——所以根据规则,它们是不可赋值的。)

但是为什么规则是这样的呢?通过将内置类型视为“命名”类型,并阻止将命名类型传递给所有接受相同底层内置类型的标准库函数,解决了什么问题?有人知道语言设计者在这里考虑了什么吗?

从我的角度来看,它似乎在代码中创建了很多无意义的类型转换,并且在实际上有意义的地方阻止了强类型的使用。

英文:

Consider this code:

package main
import "fmt"

type specialString string

func printString(s string) {
	fmt.Println(s)
}

// unlike, say, C++, this is not legal GO, because it redeclares printString
//func printString(s specialString) {    
//	fmt.Println("Special: " + s)
//}

func main() {
	ss := specialString("cheese")
	// ... so then why shouldn't this be allowed?
	printString(ss)
}

My question is: why is the language defined so that the call to printString(ss) in main() is not allowed? (I'm not looking for answers that point to the Golang rules on assignment; I have already read them, and I see that both specialString and string have the same 'underlying type' and both types are 'named' -- if you consider the generic type 'string' to be named, which Golang apparently does -- and so they are not assignable under the rules.)

But why are the rules like that? What problem is solved by treating the built-in types as 'named' types, and preventing you from passing named types to all the standard library functions that accepting the same underlying built-in type? Does anybody know what the language designers had in mind here?

From my point of view, it seems to create a lot of pointless type conversion in the code, and discourages the use of strong typing where it actually would make sense..

答案1

得分: 6

我相信初始作者在这里的逻辑是,命名类型是有原因的 - 它代表着不同的东西,而不仅仅是底层类型。

我想我在 golang-nuts 的某个地方读到过这个,但是记不清确切的讨论了。

考虑以下示例:

type Email string

你将它命名为 Email,因为你需要表示电子邮件实体,而 'string' 只是它的简化表示,足够开始使用。但是后来,你可能想将 Email 更改为更复杂的东西,比如:

type Email struct {
    Address string
    Name    string
    Surname string
}

这将导致所有隐式假定 Email 是一个字符串的代码都会出错。

英文:

I believe the initial authors' logic here is that named type is named for a reason - it represents something different, not just underlying type.

I guess I've read it somewhere in golang-nuts, but can't remember exact discussion.

Consider the following example:

type Email string

You named it Email, because you need to represent e-mail entity, and 'string' is just simplified representation of it, sufficient for the very start. But later, you may want to change Email to something more complex, like:

type Email struct {
    Address string
    Name    string
    Surname string
}

And that will break all your code that work with Email implicitly assuming it's a string.

答案2

得分: 3

这是因为Go语言没有类继承,而是使用结构体组合。命名类型不会从其底层类型继承属性(这就是为什么它不被称为“基础类型”)。

因此,当你声明一个名为specialString的命名类型,其底层类型为预定义类型string时,你的新类型与底层类型完全不同。这是因为Go语言假设你希望为新类型分配不同的行为,并且在运行时不会检查其底层类型。这就是为什么Go语言既是静态语言又是动态语言的原因。

当你打印:

fmt.Println(reflect.TypeOf(ss))        // specialString

你得到的是specialString,而不是string。如果你看一下Println()的定义如下:

func Println(a ...interface{}) (n int, err error) {
        return Fprintln(os.Stdout, a...)
}

这意味着你可以打印任何预声明的类型(int、float64、string),因为它们都实现了至少零个方法,这使它们已经符合空接口并作为“可打印”的,但是你的命名类型specialString不行,在编译时Go语言对其一无所知。我们可以通过打印interface{}的类型来检查它与specialString的关系。

type specialString string 
type anything interface{}

s := string("cheese")
ss := specialString("special cheese")
at := anything("any cheese")

fmt.Println(reflect.TypeOf(ss))     // specialString
fmt.Println(reflect.TypeOf(s))      // string
fmt.Println(reflect.TypeOf(at))     // 哇,这也是string!

你可以看到specialString一直保持着自己的身份。现在,看看它在运行时传递给函数时的表现。

func printAnything(i interface{}) {
        fmt.Println(i)
}

fmt.Println(ss.(interface{}))       // 编译错误!ss不是interface{}类型,但是
printAnything(ss)                   // 正确地打印出"special cheese"

ss已经可以作为interface{}传递给函数了。到那时,Go语言已经将ss转换为了interface{}类型。

如果你真的想深入了解底层实现,这篇关于接口的文章非常有价值:Interfaces in Go

英文:

This is because Go does not have class inheritance. It uses struct composition instead. Named types do not inherit properties from their underlying type (that's why it's not called "base type").

So when you declare a named type specialString with an underlying type of a predefined type string, your new type is a completely different type from the underlying one. This is because Go assumes you will want to assign different behaviors to your new type, and will not check its underlying type until run-time. This is why Go is both a static and dynamic language.

When you print

fmt.Println(reflect.TypeOf(ss))        // specialString

You get specialString, not string. If you take a look at Println() the definition is as follows:

func Println(a ...interface{}) (n int, err error) {
        return Fprintln(os.Stdout, a...)
}

This means you can print any predeclared types (int, float64, string) because all of them implements at least zero methods, which makes them already conform to the empty interface and pass as "printable", but not your named type specialString which remains unknown to Go during compile time. We can check by printing the type of our interface{} against specialString.

type specialString string 
type anything interface{}

s := string("cheese")
ss := specialString("special cheese")
at := anything("any cheese")

fmt.Println(reflect.TypeOf(ss))     // specialString
fmt.Println(reflect.TypeOf(s))      // string
fmt.Println(reflect.TypeOf(at))     // Wow, this is also string!

You can see that specialString keeps being naughty to its identity. Now, see how it does when passed into a function at run-time

func printAnything(i interface{}) {
        fmt.Println(i)
}

fmt.Println(ss.(interface{}))       // Compile error! ss isn't interface{} but
printAnything(ss)                   // prints "special cheese" alright

ss has become passable as interface{} to the function. By that time Go has already made ss an interface{}.

If you really want to understand deep down the hood this article on interfaces is really priceless.

答案3

得分: 0

这被称为名义类型。它简单地意味着类型由其名称标识,并且必须明确指定才能发挥作用。

从便利性的角度来看,它很容易受到批评,但它非常有用。

例如,假设你有一个函数的参数是一个字符串,但它不能是任意字符串,你需要检查一些规则。如果你将类型从字符串更改为暗示你已经检查了字符串潜在问题的某种类型,那么你做出了一个很好的设计决策,因为通过查看代码,很明显字符串需要通过某个函数进行验证输入(并在此过程中丰富其类型)。

type Validated string
func Validate(input string): (Validated, err) {
  return Validated(input), nil // 假设你实际上验证了字符串
}

Go语言做出这些权衡是因为它提高了可读性(即使对于不熟悉你的代码的人也能快速理解其工作原理),这是Go语言设计者最看重的东西。

英文:

It's called nominal typing. It simply means that the type is identified by it's name and it has to be made explicit to be useful.

From a convenience point of view it is easy to critique but it super useful.

For example, let's say you have a parameter to a function that is a string but it cannot be just any string, there are rules you need to check. If you change the type from string to something that implies that you checked the string for potential problems you made a good design decision because it's now clear from just looking at the code that the string needs to go via some function to validate the input first (and enrich it's type in the process).

type Validated string
func Validate(input string): (Validated, err) {
  return Validated(input), nil // assuming you actually did validate the string
}

Go makes these tradeoffs because it does improve readability (i.e. the ability of someone unfamiliar with your code to quickly understand how things work) and that's something they (the Go language designers) value above all else.

huangapple
  • 本文由 发表于 2015年2月21日 01:20:32
  • 转载请务必保留本文链接:https://go.coder-hub.com/28634648.html
匿名

发表评论

匿名网友

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

确定