[Go] 言語仕様書の「型と値の特性」を理解する
この記事はCyberAgent 22 新卒 Advent Calendar 2021 8日目の記事です。
Goの言語仕様書を読む中で
- Type Identity、Assignability、Representabilityの性質の関係
- representableだと何が嬉しいのか
などの点が言語仕様書からはわかりづらいと感じたので、整理しようと試みたのがこの記事です。
忙しい人のためのまとめ
Goの型と値の特性(Properties of types and values)は3つある
- Type Identity 型が同じ(identical)かどうか
- Assignability 型に代入可能(assinable)かどうか
- Representability 型に表現可能(representable)かどうか
各性質を満たすと行える操作をまとめると次のようになる。
二項演算が可能 | 代入が可能 | |
---|---|---|
identical | ○ | ○ |
assignable | × | ○ |
representable | × | ○ |
Representabilityは定数についてのAssignabilityを定義したものである。
以下ではx
を型T
の変数、y
は型V
の変数とします。
var x T
var y V
また、以下で登場する用語が分からない場合、別記事にまとめたのでご覧ください。
Type identity
Goの2つの型T
とV
が同じ(identical)なら、==
等の二項演算が可能になります。
以下の条件を満たす必要があります。
-
T
かV
がdefined typeではない。 -
T
とV
はunderlying typeが同じである。
例えば次のようにtype definitionされていた場合、
type A int
type B = int
次の表のようになります。
1. TかVがdefined typeではない | 2. TとVはunderlying typeが同じ | TとVはidenticalか | |
---|---|---|---|
Aとint | X (Aもintもdefined type) | O | X |
Bとint | O (Bはdefined typeではない) | O | O |
intと[]int | O ([]intはdefined typeではない) | X | X |
複合型で「T
とV
はunderlying typeが同じである。」の判定を行うためには、次にあげる点が一致しないといけません。
配列型
- 要素の型
- 配列長
ポインタ型、スライス型
- 要素の型
チャネル型
- 要素の型
- 送受信の方向
構造体型
- フィールドの並び、型、名前、タグ
インターフェース型
- method set
関数型
- 引数の型と個数[1]
- 返り値の型と個数
- 可変長かどうか
次はその例です。
[5]int, [5]int // 同じ
chan<- int, chan<- int // 同じ
chan int, chan<- int // 違う
*string, *int // 違う
[]string, []int // 違う
func(x int,y int) int, func(z int,w int) int // 同じ
func(x int,y int) int, func(z int,w int) string // 違う
次の点には注意です。
- 関数の引数や返り値の名前は一致しなくてもいいが、構造体のフィールドは名前が一致しなければならない。
- チャネルの長さは一致しなくてもいいが、配列の長さは一致しなければならない。
構造体とインターフェースがidenticalかどうかの挙動は、以下のようになります
package main
func main() {
a := struct {
x int
y int
}{}
b := struct {
x int
y int
}{}
c := struct {
y int
x int
}{}
d := struct {
x int
z int
}{}
e := struct {
x int
y int `json:"b"`
}{}
_ = a == b // OK
_ = a == c // NG
_ = a == d // NG
_ = a == z // NG
}
package main
var a interface {
hoge(x int, y string)
}
var b interface {
hoge(y int, x string)
}
var c interface {
foo(x int)
}
var d interface {
hoge(x string, y int)
}
func main() {
_ = a == b // OK
_ = a == c // NG
_ = a == d // NG
}
部分的にエクスポートされていない構造体とインターフェース
ここまでの条件以外にもidenticalの条件があります。別のパッケージで宣言されていてエクスポートされていないフィールドのある構造体とインターフェースを利用する場合、その挙動の違いに注意する必要があります。
- 構造体の場合、どの構造体ともidenticalにはならない。
-
インターフェースの場合、エクスポートされているmethod setが一致しているインターフェースと空のインターフェース(
interface{}
)とidenticalになる。
次はその例です。例は以下のリポジトリに置いておきました。
まず別のパッケージnon_export
においてエクスポートされていないフィールドのある構造体とインターフェース型の変数とを定義します。
package non_export
var NonExportStruct struct {
Foo int
hoge string
}
package non_export
var NonExportInterface interface {
Foo()
hoge()
}
次にpackage main
から2つの変数をインポートして比較します。
package main
import "non_export/non_export"
func main() {
var a struct {
Foo int
}
var b struct {
Foo int
hoge int
}
var c interface {
Foo()
hoge()
}
var d interface {
Foo()
}
var e interface{}
_ = a == non_export.NonExportStruct // NG
_ = b == non_export.NonExportStruct // NG
_ = c == non_export.NonExportInterface // NG
_ = d == non_export.NonExportInterface // OK
_ = e == non_export.NonExportInterface // OK
}
このように構造体とインターフェースにおいて異なる挙動をすることがわかります。
Assignability
x
がy
に代入可能(assignable)なら、次のコンパイルが可能です。
y = x
x
の型T
とy
の型V
がidenticalなら代入可能です。assignableはidenticalより制約が弱く、次の場合でもassignableです。
-
x
がnil
でV
が複合型の場合
package main
func main() {
var (
a []string
b int
)
a = nil // OK
b = nil // NG
}
-
T
とV
のunderlying typeが同じかつ、T
とV
のうち1つ以上がdefinedでない
package main
func main() {
type (
A []string
B []string
)
var (
a []string
b A
c B
)
a = []string{"hoge"} // OK
b = a // OK
a = 1 // NG
c = b // NG
}
-
T
がV
を実装している
package main
import "io"
func main() {
var (
a interface{}
b io.Reader
)
a = 1 // OK
b = 1 // NG
}
- xが双方向のチャネルとすると、
V
がチャネルで、V
とT
の要素型が同じで、TとVの1つ以上がdefined typeでない
package main
func main() {
type A chan<- int
var (
a chan int
b chan<- int
c A
d chan string
)
a = make(chan int)
b = a // OK
c = a // OK
a = b // NG
d = a // NG
}
Representability
Assignabilityの条件は上記だけでなく
-
x
がuntyped constantである時、x
がT
としてrepresentableである
という条件があります。representableとは定数x
が
- 型
T
の表現できる範囲に含まれる。 -
x
が浮動小数点定数なら、x
はオーバフローなく型T
に丸められる。 -
x
が複素数定数なら、real(x)
とimag(x)
はT
の成分型で表現できる。
整数としてrepresentableならAssignableであることに加えて、シフト演算や配列のインデックスやmake
などに指定できます[2]。
このようにrepresentableは定数がassignableかどうかを定義した概念だと言えます。次は定数の型変換を用いたrepresentableかどうかの例です。
func main() {
var (
a uint8
b float32
c string
)
a = 255 // OK
a = 1.0 // OK
a = 256 // NG
a = 1.000000001 // NG
b = 12.0 // OK
b = 12 // OK
b = 99 + 0i // OK
b = 3.4e39 // NG
b = 0 + 99i // NG
b = 1e100000 // NG
c = 'a' // NG
}
representableと似たような概念としてconvertable[3]があります。例えば、ここ
を確認すると、Identicalやassinableはありますが、representableはなく、代わりにconvertable*となっています。
記事内容に誤りがあった場合、コメントで教えていただけると嬉しいです。
参考にさせていただいた記事
-
引数や返り値が2個以上あり、型の順番が異なる場合、違う型として見なされます。 ↩︎
-
型変換に関する詳細はをここを参考にしてください。https://go.dev/ref/spec#Conversions ↩︎
Discussion