LoginSignup
4
0

More than 3 years have passed since last update.

Goの代入可能性についてちゃんと理解してみる

Posted at

はじめに

今月で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:
1. x's type is identical to T.
2. x's type V and T have identical underlying types and at least one of V or T is not a defined type.
3. T is an interface type and x implements T.
4. 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.
5. x is the predeclared identifier nil and T is a pointer, function, slice, map, channel, or interface type.
6. 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]に該当します(後述の例の型BDのように異なる場合があります。後ほど正確な定義に触れます)。
type INT intでいうところのdefined typeINTunderlying typeintです。

defined typeの仕様では、

The new type is called a defined type. It is different from any other type, including the type it is created from.

とあり、INTintは全く別物とみなされます。
少し脱線しますが、

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言語側が事前に定義してあるintstringなどを指します。type literalとは

TypeLit = ArrayType | StructType | PointerType | FunctionType | InterfaceType |
SliceType | MapType | ChannelType .

です。[]intなどはtype literalだとわかりますね!
一方、intstringなどはtype literalではないことに注意してください。

type declaration(型宣言)はtypeによる型定義の他、エイリアスによる型宣言の二つ両方を含みます。

注意深く読むとunderlying typeは再起的定義になっていて、

type A int
type B A

type C []B
type D C

の場合、intABunderlying typeintであることや、[]BCDunderlying type[]Bであることが導かれます。

ここまで、「型の同一性」「defined type」「underlying type」といった概念が出てきました。これらを踏まえてやっとAssignabilityが理解できます。Assignabilityの説明に戻る前にここまででわかることを一度整理します。

  • INTintは型が異なる
  • INTSLICE[]intは型が異なる
  • INTdefined type
  • INTSLICEdefined type
  • intunderlying typeint自身
  • INTunderlying typeint
  • []intunderlying type[]int自身
  • INTSLICEunderlying type[]int

では、Assignabilityの定義にもどりましょう。
まず、No.1はINTINTSLICEの両方のケースにあてはまらないことがわかります。
No.2ですが、

x's type V and T have identical underlying types and

この部分は両方のケースであてはまります。INTunderlying typeintINTSLICEunderlying type[]intだからです。

at least one of V or T is not a defined type.

ここの条件から逆算すると、

  • intdefined type
  • []intdefined typeでない

。。。え、?intdefined typeだと、、、、
この部分が今回の肝です。10分くらい必死に仕様よんでカラクリに気づくまでかなり混乱しました。
が、わかってしまえば簡単。

都合上、Go言語側で事前にintstringなどは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.

なるほどですね。ちなみにbyteuint8のエイリアス、runeint32のエイリアスなので、それらは互いに代入可能です(キャストなしで代入できます)

試しに、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の部分の条件で、INTintにキャスト可能になるんですね。
型の同一性判定では、structtagも一致する必要があったのですが、型変換可能性においては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が異なってもキャスト可能
}
4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0