定数 #
Constants by Rob Pike
はじめに #
Goは、数値型を混合して操作することを許さない静的型付け言語です。 flaot64
を int
に
足せませんし、さらに言えば int32
を int
を足すこともできません。しかし、 1e6*time.Second
や
math.Exp(1)
あるいは 1<<('\t'+2.0)
と書くことは許されています。Goでは、定数は変数と違って、
通常の数字と同様に振る舞います。この記事では、なぜそうなっているのか、そしてそれが何を意味するのかを
説明します。
背景: C言語 #
Go言語を考え始めた初期の頃、C言語やその系譜の言語が数値型をまぜこぜに使うことを許していることで起きている多くの問題について話しました。 多くの奇妙なバグ、クラッシュ、移植可能性の問題は、サイズと「符号有無」が異なる整数を混ぜている式によって起きています。 経験の多いCプログラマにとっても、次の計算結果はわかるかもしれませんが、アプリオリには明らかではありません。
unsigned int u = 1e9;
long signed int i = -1;
... i + u ...
結果はどれくらいの大きさになるのでしょう。その値はなんでしょうか。符号は付いているのかいないのか。
見難いバグがここに潜んでいます。
C言語には「the usual arithmetic conversions」と呼ばれる規則があり、年月を経てその規則が変わってきたことがこの規則の儚さを示しています。 (長年にわたって多くのバグを生んできました)
Goを設計するときに、数値型を混ぜてはいけないということをしっかりと決めることでこの地雷原を避ける事にしました。
i
と u
を足したい場合には、結果をどうしたいかを明示しなければいけません。次のように変数があったとして、
var u uint
var i int
uint(i)+u
または i+int(u)
のどちらかに書けます。これはどちらの書き方でも足し算の意味と型がはっきりと表現されていますが、
C言語の場合とは異なって i+u
と書くことはできません。 int
が32ビット型の場合でさえ、 int
と int32
を混ぜることすらできません。
この厳格さによって、よくありがちなバグや他の問題が取り除かれます。これはGoにおいて非常に重要な性質です。 しかしこの厳格さにはコストが掛かります。意味を自明にするために、ときにこの厳格さがプログラマに不格好な 型変換のコードを追加することを求めます。
では定数ではどうでしょうか。これまでに述べてきたことからすると、どうすれば規則を守ったまま i = 0
または u = 0
と書けるでしょうか。
0
の型は何でしょうか。単純な状況、たとえば i = int(0)
ような場面で、型変換を書かなければいけないのは筋がよくありません。
私たちはすぐに数値の定数を扱う場合には、他のC言語の類似言語での動作とは異なる振る舞いをさせれば良いと気が付きました。
多くの考察と実験の後に、ほぼ常に正しいと信じられ、つねにプログラマを定数の型変換から解放し、それでいてコンパイラに
警告されることなく math.Sqrt(2)
のように書ける設計を思いつきました。
手短に言えば、Goの定数は、とにかくほとんど場合、うまく動きます。ではそれがどのようになっているか見てみましょう。
用語解説 #
まず、手短に定義します。Goでは const
は 2
、 3.14159
、 "scrumptious"
といったスカラー値に対する名前を決める
キーワードです。このような値は、名前が付いているかにかぎらず、Goでは 定数 と呼ばれます。定数は定数からなる式からも
生成することが出来ます。たとえば 2+3
、 2+3i
、 math.Pi/2
あるいは ("go"+"pher")
といったものがそれです。
定数がない言語もありますし、 const
という単語に対してより汎用的な定義がある言語やより汎用的な用途がある言語もあります。
たとえばC言語はC++では、 const
はより複雑な値のより複雑な性質を分類する型修飾子です。
しかしGoでは、定数はとても単純で、変化しない値のことを指します。以降はGoでの場合のみを話します。
文字列定数 #
数値の定数には多くの種類があります。たとえば整数、浮動小数点数、ルーン(訳注:Unicodeのコードポイントを表す整数値)、符号あり、符号なし、虚数、複素数などがあります。 まずはより単純な形式の定数である文字列定数から話を始めましょう。文字列定数は理解がしやすく、Goにおける定数の型の問題を調べるときにより小さな部分だけ考えればよくなります。
文字列定数はある文字列をダブルクォーテーションで囲ったものです。(Goにはraw文字列リテラルもあり、これはバッククォートで囲みます。ここの議論においては 両者ともに同様の性質があるため省きます。)ここに文字列定数があります。
"Hello, 世界"
(文字列の表現と解釈に関してより詳しい内容はこちらのブログポストを参照してください。)
この文字列定数はどんな型でしょうか。見たままで答えれば string
ですが、それは 間違い です。
これは 型付けされていない文字列定数 で、不変の文字列の値を持っているがまだ決まった型がないもの、といえるでしょう。
そうです、これは文字列なのですが、Goでの string
型の値ではないのです。型付けされていない文字列定数は、名前を与えた時にも
同様のままでいます。
const hello = "Hello, 世界"
この宣言のあと、 hello
もまた型付けされていない文字列定数となります。型付けされていない定数はただの値で、
異なる型の値と結合する厳格な規則に従わなければならなくなる決められた型をまだ持っていない値となります。
この 型付けされていない 定数という考え方によって、Goで定数を自由度高く使うことができるのです。
それでは 型付けされた 文字列定数とはなんなのでしょう。それは次のように型を与えられた定数のことです。
const typedHello string = "Hello, 世界"
typedHello
の宣言は、等号の前で string
型が明示されていることに注目してください。これは typedHello
が
Goの string
型であることを意味していて、Goでの異なる型の変数に代入できないことを意味しています。
これは次のコードは動きますが、
var s string
s = typedHello
fmt.Println(s)
このコードは動かないということです。
type MyString string
var m MyString
m = typedHello // 型エラー
fmt.Println(m)
変数 m
は MyString
型で、異なる型には値は代入できません。 MyString
型の値のみ代入できます。
たとえば次のコードを見てください。
const myStringHello MyString = "Hello, 世界"
m = myStringHello // OK
fmt.Println(m)
あるいは型変換をさせてもできるでしょう。
m = MyString(typedHello)
fmt.Println(m)
型付けされていない 文字列定数の話に戻ると、型がないことによって、型付けされた変数に代入しても 型エラーが発生させないというのは役に立つ性質です。つまり、このようなコードや
m = "Hello, 世界"
あるいはこのように書けるのです。
m = hello
なぜなら、型付けされた定数の typedHello
や myStringHello
とは違って、型付けされていない定数の "Hello, 世界"
や
hello
には 型がないからです 。 string
と互換性のあるどのような型の変数にもエラーなしで代入できます。
これらの型付けされていない文字列定数はもちろん文字列であるため、文字列を使って良い所でのみどこでも使えますが、
string
という 型 を持っていないのです。
デフォルト型 #
Goのプログラマであるあなたは、まちがいなく次のような宣言を数多く見てきたことでしょう。
str := "Hello, 世界"
ここまでの説明を読んで、「もし定数が型付けされていないのであれば、どうやって str
はこの変数宣言で型を得るのだろう」と疑問に思ったことでしょう。
その答えはというと、型付けされていない定数にはデフォルトの型、つまり型が与えられてなかった場合に必要なときに値に対して与える暗黙的な型を持っているということです。
型付けされていない文字列定数であれば、デフォルト型は明らかに string
なので、
str := "Hello, 世界"
や
var str = "Hello, 世界"
は次の宣言とまったく同じ意味になります。
var str string = "Hello, 世界"
型付けされていない定数について考える1つの方法としては、彼らは値の理想的な空間にいて、そこではGoの完全な型システムほど
制限がないと思えばいいでしょう。しかしその定数にたいして何かしたいと思ったら、それを変数に代入する必要があり、
変数に代入するときには(定数自身ではなく) 変数 には型が必要で、その定数は変数に対して自分がどのような型であるべきか
伝えることができます。この例では str
は string
型の値になります。なぜなら型付けされていない文字列定数は
そのデフォルト型である string
であると宣言するからです。
このように変数宣言をした場合、その変数は型と初期値を含めて宣言されています。しかしながら、ときに定数を、その値の代入先が 不明瞭なときに使うことがあります。たとえば次の文を考えてみましょう。
fmt.Printf("%s", "Hello, 世界")
fmt.Printf
のシグネチャは次のとおりです。
func Printf(format string, a ...interface{}) (n int, err error)
これは、この関数の(フォーマット文字列のあとの)引数がインターフェース値であることを意味しています。 fmt.Printf
が
型付けされていない定数を使って呼び出された場合、インターフェース値が作られて引数に渡されます。そしてその引数に保存される
具象型はその定数のデフォルト型になります。このプロセスは先に型付けされていない文字列定数で初期値を宣言した状況に似ています。
この結果は次の例で確認できます。ここでは fmt.Printf
で値を表示するのにフォーマット文字 %v
を使い、値の型を表示するのに %T
を使っています。
fmt.Printf("%T: %v\n", "Hello, 世界", "Hello, 世界")
fmt.Printf("%T: %v\n", hello, hello)
(訳注: 出力は次のとおりです。)
string: Hello, 世界
string: Hello, 世界
もし定数に型があれば、それがインターフェースに伝わり、次の例のようになります。
fmt.Printf("%T: %v\n", myStringHello, myStringHello)
(訳注: 出力は次のとおりです。)
main.MyString: Hello, 世界
(インターフェース値がどのように扱われるかをより詳しく知りたい場合は、次の ブログポスト の最初の節を参照してください。)
まとめとして、型付けされた定数はGoの型付けされた値のルールに従います。一方で、型付けされていない定数は 同じやり方でGoの型を決めるのではなく、より自由に型を混ぜたり一致させたりします。しかしながら、 あからさまなデフォルト型が、他の型情報が得られない場合、そしてその場合に限り適用されます。
構文から決定されるデフォルト型 #
型付けされていない定数のデフォルト型は構文により決定されます。文字列定数であれば、唯一取りうる
暗黙型は string
です。 数値定数 の場合は
暗黙型はより多くの種類を取り得ます。整数定数は int
がデフォルト型ですし、浮動小数点数定数は
float64
、 ルーン定数は rune
( int32
のエイリアス)、虚数定数は complex128
がデフォルト型です。
ここで、この権威あるprint式でデフォルト型を実際に動かして確認しましょう。
fmt.Printf("%T %v\n", 0, 0)
fmt.Printf("%T %v\n", 0.0, 0.0)
fmt.Printf("%T %v\n", 'x', 'x')
fmt.Printf("%T %v\n", 0i, 0i)
(演習: 'x'
の場合の結果を説明しなさい。)
真偽値 #
型付けされていない文字列定数について話したことはすべて型付けされていない真偽値定数についてもあてはまります。
true
と false
はどのような真偽値の変数にも代入できる型付けされていない真偽値定数で、一度型が与えられると
混ぜることは出来ません。
type MyBool bool
const True = true
const TypedTrue bool = true
var mb MyBool
mb = true // OK
mb = True // OK
mb = TypedTrue // ダメ
fmt.Println(mb)
上の例を実行して何が起きるか見たあと、 “ダメ” とコメントされている行をコメントアウトして 再び実行してみましょう。ここにあるパターンはすべて文字列定数の場合の規則に従っています。
浮動小数点数 #
浮動小数点数定数は多くの場合真偽値定数と同様です。これまで何度か出している例は浮動小数点数の場合にも 適用できます。
type MyFloat64 float64
const Zero = 0.0
const TypedZero float64 = 0.0
var mf MyFloat64
mf = 0.0 // OK
mf = Zero // OK
mf = TypedZero // ダメ
fmt.Println(mf)
ひとつこの例でうまくいかないことがあるとすればGoには 2つの 浮動小数点数があることです。
float32
と float64
です。型付けされていない浮動小数点数定数は float32
に問題なく代入できますが、
そのデフォルト型は float64
です。
var f32 float32
f32 = 0.0
f32 = Zero // OK: Zeroは型付けされていない
f32 = TypedZero // ダメ: TypedZeroはfloat64でありfloat32ではない
fmt.Println(f32)
浮動小数点数値はオーバーフロー、つまり値の範囲という概念を紹介するのに良い題材です。
数値定数は任意精度の数値空間に存在しています。つまり、それらは単なる有限小数なのです。 しかし、それらがある変数に代入された場合には、その値が代入先に合うようにならなければいけません。 定数で非常に大きな値を宣言することが出来ます。
const Huge = 1e1000
これは単なる数字で、それ以外の何者でもありません。しかし、それを代入できませんし、表示することさえできません。 この式はコンパイルすら通りません。
fmt.Println(Huge)
エラーは「constant 1.00000e+1000 overflows float64 (定数 1.00000e+1000 は float64 の範囲からオーバーフローしています)」
というもので、これは正しい内容です。しかし、 Huge
が使える時もあります。それは、他の定数との式の中で使って、
その式の結果が float64
の範囲内に収まる場合です。次の式を見てください。
fmt.Println(Huge / 1e999)
これは 10
を出力します。予想通りです。
同様に、浮動小数点数定数には非常に高い精度を持たせることが出来るので、それが関わる数値計算はより正確になります。
math パッケージで定義されている定数は float64
で扱える範囲よりも
多くの桁数を持っています。これは math.Pi
の定義です。
Pi = 3.14159265358979323846264338327950288419716939937510582097494459
この値が変数に代入されたときに、精度がある程度失われます。代入することで元の高精度の値に最も近い
float64
(あるいは float32
)の値ができます。次のスニペットでは
pi := math.Pi
fmt.Println(pi)
3.141592653589793
が出力されます。
桁数を多く持てることで、 Pi/2
のような計算やより複雑な評価をする際に結果が代入されるまで、
より高い精度を維持したままでいられます。これによって、定数が関わる計算を精度を失うことなく
書きやすくなっています。また、浮動小数点の無限、アンダーフロー、 NaN
といったコーナーケースが
定数の式では発生しません。(ゼロ除算はコンパイル時エラーとなり、またすべてが数字の場合には
「not a number」というようなエラーは発生しえません。)
複素数 #
複素数定数は多くの点で浮動小数点数定数と同様に振る舞います。いつもの例を複素数でやってみます。
type MyComplex128 complex128
const I = (0.0 + 1.0i)
const TypedI complex128 = (0.0 + 1.0i)
var mc MyComplex128
mc = (0.0 + 1.0i) // OK
mc = I // OK
mc = TypedI // ダメ
fmt.Println(mc)
複素数のデフォルト型は complex128
で、2つの float64
の値で構成された精度が大きい型です。
曖昧さ回避のためにいうと、例では (0.0+1.0i)
と完全表記をしていますが、この値は 0.0+1.0i
と短く出来ますし、
さらに言えば 1.0i
あるいは 1i
と書けます。
ちょっと遊んでみましょう。Goでは、数値定数はただの数字だと知っています。もし数字が虚数部がない複素数だった場合に、 つまりそれは実数なのでしょうか。一つ用意してみます。
const Two = 2.0 + 0i
これは型付けされていない複素数です。たとえ虚数部がなくても、式の 構文 がデフォルト型 complex128
を持つように
定義します。それゆえ、変数宣言にこの定数を使うと、デフォルト型は complet128
になります。このスニペットは
complex128: (2+0i)
と出力します。
s := Two
fmt.Printf("%T: %v\n", s, s)
しかし数値としては、定数 Two
は情報を失うことなくスカラーの浮動小数点数、つまり float64
あるいは float32
に
保存することが出来ます。したがって、初期化あるいは代入で何も問題なく Two
を flaot64
に代入できます。
var f float64
var g float64 = Two
f = Two
fmt.Println(f, "and", g)
出力は 2 and 2
です。たとえ Two
が複素数定数だとしても、スカラーの浮動小数点数の変数に代入できます。
このように定数が型を「渡り歩く」ことができる機能があることが役立つとわかるでしょう。
整数 #
ついに整数までやってきました。整数には サイズや符号の有無など
変化する部分がたくさんあります。しかしこれまでと同じ規則に従います。最後にまた、今回は int
だけを使って
いつもの例を見てみましょう。
type MyInt int
const Three = 3
const TypedThree int = 3
var mi MyInt
mi = 3 // OK
mi = Three // OK
mi = TypedThree // ダメ
fmt.Println(mi)
同じ例は整数型のどの型でも成立します。
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64
uintptr
(上記の型に加えて、 uint8
のエイリアスの byte
と int32
のエイリアスの rune
も同様です)
とても多くの型がありますが、これまで十分に見てきた定数の振る舞いがここでも当てはまります。
先に述べたように、整数はいくつかの形式で表されますし、それぞれの形式にはそれぞれのデフォルト型があります。
123
、 0xFF
、 -14
というような単純な定数は int
で、 '世'
や '\r'
というような引用された文字は
rune
です。
どの定数の形式も符号なし整数型をデフォルト型としては持っていません。しかしながら、型付けされていない定数の柔軟性によって
私たちが型を明確にする限り単純な定数で符号なし整数の変数を初期化できます。これは虚部がゼロの複素数で float64
の変数を
初期化できることに似ています。次にいくつかの方法で unit8
の変数を初期化する方法を並べます。
これらはすべて等価ですが、結果が符号なしになるように明示的に型を与えなければなりません。
var u uint = 17
var u = uint(17)
u := uint(17)
浮動小数点数型の節でふれた値域に関する問題と同様に、すべての整数値がすべての整数型に適用できるわけではありません。
2つの問題が発生しえます。値が大きすぎる、または符号なし整数に負の値が代入されるかの2つです。たとえば、 int8
は
-128 から 127 の値域があり、この範囲を外れた定数は int8
型の変数には決して代入できません。
var i8 int8 = 128 // エラー: 値が大きすぎます。
同様に、 byte
としても使われる uint8
は、値域が0から255で、これより大きい整数もしくは負の整数は uint8
型の整数には代入できません。
var u8 uint8 = -1 // エラー: 負の値。
型チェックによりこのような誤りを検出できます。
type Char byte
var c Char = '世' // エラー: '世' の値は 0x4e16 で、大きすぎます。
もしコンパイラがあなたの定数の使い方に対して文句を言ってきたら、おそらく今挙げたような間違いの現実でのバグでしょう。
演習: 最大の符号なし整数 #
ここで、ちょっとした有益な演習を行ってみましょう。 uint
の範囲内で最大値の定数を表現できるでしょうか。
もし uint
ではなく uint32
であれば、このように書くことが出来ます。
const MaxUint32 = 1<<32 - 1
しかし私たちがほしいのは uint
であり uint32
です。 int
型とuint
型は同様に、32ビットか64ビットかといったビット数の指定はありません。
どのビット数が使えるかはアーキテクチャに依存するので、単純に値を書くことは出来ません。
2の補数 、これはGoの整数が利用すると定められていますが、
そのファンであれば -1
はビットがすべて1となるので、 -1
のビットパターンは内部的には符号なし整数の最大値と同じになると知っています。
それゆえ、次のように考えてしまいますが、
const MaxUint uint = -1 // エラー: 負の値
これは不正となります。なぜなら-1は符号なしの変数では表現しえないからです。
-1
は符号なしの値の値域にはありません。
同様の理由で、型変換も意味をなしません。
const MaxUint uint = uint(-1) // エラー: 負の値
たとえ、実行時に-1の値が符号なし整数に変換出来たとしても、定数の 変換 のルールはコンパイル時にこの種の抜け穴を禁止します。 つまり、次のコードは実行はできますが
var u uint
var v = -1
u = uint(v)
これは v
が変数だからという理由だけです。もし v
を定数にすると、たとえ型付けされていない定数だとしても、禁止区域に戻ってきてしまいます。
var u uint
const v = -1
u = uint(v) // エラー: 負の値
また先ほどと同様のアプローチに戻ってみましょう。ただし今度は -1
の代わりに ^0
として、0のビットの否定(NOT)にしてみましょう。
この例も同様の理由で失敗します。数値の空間では ^0
は無限数を表すため、固定サイズの整数に代入する際に情報を失ってしまいます。
const MaxUint uint = ^0 // エラー: 桁あふれ
ではどのように符号なし整数の最大値を定数で表せばよいのでしょうか。
鍵となるのは、 uint
型の変数内のビットに対する操作に制約をかけて、 uint
で表現できない、たとえば負の数といった値を避ける事です。
最も単純な uint
型の値は、片付けされた定数の uint(0)
です。もし uint
が32ビットまたは64ビットだった場合、それに応じて
uint(0)
も32個または64個の0のビットを持つことになります。これらのビットをそれぞれ反転すれば、正しい数の1のビットを持つこととなり、
これが uint
型の値の最大値となります。
したがって定数 0
のビットを反転するのではなく、型付けされた定数 uint(0)
のビットを反転するのです。
これで欲しかった定数が得られます。
const MaxUint = ^uint(0)
fmt.Printf("%x\n", MaxUint)
現在の実行環境での uint
を表現するビット数がいくつでも(playground では32ビットです)、
この定数は正しく uint
型が持てる最大値を表現しています。
この結果に至るまでの分析を理解すれば、Goでの定数について重要なてを理解したといえるでしょう。
数字 #
Goにおける型付けされていない定数の概念は整数、浮動小数点数、複素数、さらには文字といった数値定数は すべてある種統一された空間に存在するということを意味しています。それらの定数を変数、代入、演算といった 計算の世界に持ち込んだときに実際の型が関係してきます。しかし、数値定数の世界に留まる限り、 値を混ぜたり合致させたりすることができます。これらの定数はすべて数値 1 を持っています。
1
1.000
1e3-99.0*10-9
'\x01'
'\u0001'
'b' - 'a'
1.0+3i-3.0i
したがって、これらは暗黙のデフォルト値を持っていますが、型付けされていない定数として書かれているので、 これらは任意の整数型の変数に代入できます。
var f float32 = 1
var i int = 1.000
var u uint32 = 1e3 - 99.0*10.0 - 9
var c float64 = '\x01'
var p uintptr = '\u0001'
var r complex64 = 'b' - 'a'
var b byte = 1.0 + 3i - 3.0i
fmt.Println(f, i, u, c, p, r, b)
この出力は次のようになります。 1 1 1 1 1 (1+0i) 1.
こんなおかしなことさえもできます。
var f = 'a' * 1.5
fmt.Println(f)
これは145.5を出力しますが、この例は先の話を証明する以外特に意味はありません。
しかし、これらのルールの本当に大事な点は柔軟性です。この柔軟性の意味するところは、
Goでは同じ式の中で浮動小数点数と整数の変数を混ぜること、さらには int
と int32
の
変数すら混ぜることは不正ですが、たとえば
sqrt2 := math.Sqrt(2)
や
const millisecond = time.Second/1e3
あるいは
bigBufferWithHeader := make([]byte, 512+1e6)
と書くことは許されていて、あなたの期待するとおりの結果となります。
なぜならGoでは、数値定数はあなたの期待したとおり、つまり数字として動作するからです。
By Rob Pike