はじめに
今月でGolangに触れてからちょうど一年になりますが、未だにGoについて初めて知ることが多々あります。その一つを紹介したいと思います。
以下のGoでなぜキャストが必要なのでしょうか? 当たり前だろ、GoはCなどとは違って明示的なキャストが必要な言語だろとツッコミが入りそうですが、
type INT int
var i1 int
var i2 INT
i1 = 1
i2 = INT(i1) // 明示的なキャストが必要
では、以下はキャスト不要で代入できますが、これはなぜでしょうか? といわれると案外答えられない人もいるのではないでしょうか?
そのあたりをGoの仕様をちゃんと読んで理解しましょうというのが今回のテーマです。
type INTSLICE []int
var l1 []int
var l2 INTSLICE
l1 = []int{1,2,3}
l2 = l1 // 明示的なキャストが不要
仕様をよんでみる
代入可能性(Assignability)について定義されているのは、https://golang.org/ref/spec#Assignability です。
Assignabilityがあると関数の引数や返り値に指定できたり、比較演算値でお互いに比較が可能になったりします。他に何ができるようになるか全て知りたければGoの仕様をassignableでgrepしましょう。
Assignabilityの記述を引用します(連番は筆者による)。
A value x is assignable to a variable of type T ("x is assignable to T") if one of the following conditions applies:
- x's type is identical to T.
- x's type V and T have identical underlying types and at least one of V or T is not a defined type.
- T is an interface type and x implements T.
- x is a bidirectional channel value, T is a channel type, x's type V and T have identical element types, and at least one of V or T is not a defined type.
- x is the predeclared identifier nil and T is a pointer, function, slice, map, channel, or interface type.
- x is an untyped constant representable by a value of type T.
こちらをちゃんと理解して、INTのキャストが必要な理由を把握しましょう。
No.5などは確かにそういえばと思いますね。ポインタを返す関数でnilを返却できたりしますもんね。
No.1をよんで「型の同一性とは?」となりますが、それはすぐ上の
https://golang.org/ref/spec#Type_identity にあります。
抜粋すると、
A defined type is always different from any other type. Otherwise, two types are identical if their underlying type literals are structurally equivalent; that is, they have the same literal structure and corresponding components have identical types. In detail:
・ Two array types are identical if they have identical element types and the same array length.
・ Two slice types are identical if they have identical element types.
・ Two struct types are identical if they have the same sequence of fields, and if corresponding fields have the same names, and identical types, and identical tags. Non-exported field names from different packages are always different.
...(省略)
defined typeとかunderlying typeという用語がでてきます。ここではごく一部のみ引用し、説明しますが、正確な定義は仕様をご確認ください。
ざっくり説明すると、新しい型を定義するときの type [defined type] [underlying type]に該当します(後述の例の型BやDのように異なる場合があります。後ほど正確な定義に触れます)。
type INT intでいうところのdefined typeがINT、underlying typeがintです。
defined typeの仕様では、
The new type is called a defined type. It is different from any other type, including the type it is created from.
とあり、INTとintは全く別物とみなされます。
少し脱線しますが、
A defined type may have methods associated with it. It does not inherit any methods bound to the given type, but the method set of an interface type or of elements of a composite type remains unchanged:
とあるように、typeではメソッドは引き継がれません。
underlying typeの仕様では、
Each type T has an underlying type: If T is one of the predeclared boolean, numeric, or string types, or a type literal, the corresponding underlying type is T itself. Otherwise, T's underlying type is the underlying type of the type to which T refers in its type declaration.
とあります。predeclaredとはGo言語側が事前に定義してあるintやstringなどを指します。type literalとは
TypeLit = ArrayType | StructType | PointerType | FunctionType | InterfaceType |
SliceType | MapType | ChannelType .
です。[]intなどはtype literalだとわかりますね!
一方、int、stringなどはtype literalではないことに注意してください。
type declaration(型宣言)はtypeによる型定義の他、エイリアスによる型宣言の二つ両方を含みます。
注意深く読むとunderlying typeは再起的定義になっていて、
type A int
type B A
type C []B
type D C
の場合、int、A、Bのunderlying typeはintであることや、[]B、C、Dのunderlying typeは[]Bであることが導かれます。
ここまで、「型の同一性」「defined type」「underlying type」といった概念が出てきました。これらを踏まえてやっとAssignabilityが理解できます。Assignabilityの説明に戻る前にここまででわかることを一度整理します。
-
INTとintは型が異なる -
INTSLICEと[]intは型が異なる -
INTはdefined type -
INTSLICEはdefined type -
intのunderlying typeはint自身 -
INTのunderlying typeはint -
[]intのunderlying typeは[]int自身 -
INTSLICEのunderlying typeは[]int
では、Assignabilityの定義にもどりましょう。
まず、No.1はINT、INTSLICEの両方のケースにあてはまらないことがわかります。
No.2ですが、
x's type V and T have identical underlying types and
この部分は両方のケースであてはまります。INTのunderlying typeはint、INTSLICEのunderlying typeは[]intだからです。
at least one of V or T is not a defined type.
ここの条件から逆算すると、
-
intはdefined type -
[]intはdefined typeでない
。。。え、?intがdefined typeだと、、、、
この部分が今回の肝です。10分くらい必死に仕様よんでカラクリに気づくまでかなり混乱しました。
が、わかってしまえば簡単。
都合上、Go言語側で事前にint、stringなどはtypeで型定義しているのです
https://golang.org/ref/spec#Numeric_types には以下にこう書いてます。
To avoid portability issues all numeric types are defined types and thus distinct except byte, which is an alias for uint8, and rune, which is an alias for int32.
なるほどですね。ちなみにbyteはuint8のエイリアス、runeはint32のエイリアスなので、それらは互いに代入可能です(キャストなしで代入できます)
試しに、Golandでおもむろにintに対して、「Go to Declartion」すると、
https://github.com/golang/go/blob/be08e10b3bc07f3a4e7b27f44d53d582e15fd6c7/src/builtin/builtin.go#L75
にて、type int intとの定義があります。
これですっきりしたのでした!!
おまけ
今日のテーマは代入可能性でしたが、キャストで型変換可能かはまた別の話で、 https://golang.org/ref/spec#Conversions にあります。
constant等はまた別に細かい定義があるようですが、constant以外では以下の記載があります。
A non-constant value x can be converted to type T in any of these cases:
・x is assignable to T.
・ignoring struct tags (see below), x's type and T have identical underlying types.
・ignoring struct tags (see below), x's type and T are pointer types that are not defined types, and their pointer base types have identical underlying types.
・x's type and T are both integer or floating point types.
・x's type and T are both complex types.
・x is an integer or a slice of bytes or runes and T is a string type.
・x is a string and T is a slice of bytes or runes.
x's type and T have identical underlying typesの部分の条件で、INTはintにキャスト可能になるんですね。
型の同一性判定では、structのtagも一致する必要があったのですが、型変換可能性においてはtagは無視するのが興味深いですね。
最後に今回の総復習として、コードを載せておきます。この挙動が理解できたらカンペキです!!
package main
import "fmt"
type point struct {
x int
y int
}
type point2 struct {
x int
y int
}
type point3 struct {
x int `json:"x"`
y int `json:"y"`
}
func main() {
var p1 = struct {
x int
y int
} {
200,
400,
}
var p2 = struct {
x int
y int
} {
200,
400,
}
fmt.Println(p1==p2) //true t1とt2は型同一かつ値が同じ
p1 = p2 //型同一なのでキャストなしで代入化
var p3 point = p2
var p4 point2 = p2
fmt.Println(p2==p3) // true p2とp3は型同一
// fmt.Println(p3==p4) p3とp4は共にdefined typeなので型が異なる。コメントアウト外すとコンパイルエラー
fmt.Println(p4==point2(p3)) // true 型変換は可能
// var p5 point3 = p2 tagが異なるのでp2とp5は型が異なる。コメントアウト外すとコンパイルエラー
var p5 point3 = point3(p2)
fmt.Println(p5 == point3(p2)) // tagが異なってもキャスト可能
}