nil は nil
【2020-10-15 追記】 最近 Zenn で以下の記事を書いたので,こちらも併せてどうぞ。
Interface 型の nil
値は「依存の注入(dependency injection)」と絡めて考えると分かりやすいかも知れない。
宣伝でした(笑)
Qiita を覗いてたら
という記事を見かけた。
おそらくは nil
の理解のための方便として意図的に書かれているのだろう。
それはそれで悪くないのだが,微妙に危険な香りがするので私なりの解説を記しておく。
nil は nil
たとえば fmt.Printf()
関数などで nil
の型と値を取ろうとすると
fmt.Printf("Type: %T, Value: %v", nil, nil)
// Output:
// Type: <nil>, Value: <nil>
などと表示されるので,いかにも nil
型のようなものがあるように見えるが,実際にはこれは「型がない」ことを示している。
同様に値についても,厳密には nil
という値ではなく「値がない」ことを示しているのだ。
「nil
とは何か」をきちんと定義した文章は見かけないが, Go 言語の仕様書には,型 T
の変数 x
に対して
であると記されている。
Go 言語では,ある型の値が「宣言されていない」状態のことを「ゼロ値(zero value)」と呼んでいる。
たとえば int などの数値型では「ゼロ値」として数値の 0
を, bool では false を,文字列では空文字列をとる。
同じようにポインタ型や interface 型などでは nil
を「ゼロ値」としましょう,ということなのである。
このように仕様として定義することで曖昧な状態を排除でき,私達ユーザは安心してその変数を使用することができるわけだ。
したがって nil
は状態を表す「識別子」あるいは「表現」に過ぎず1,それ自身は型も値も持たない2。
強いて言うなら(プログラミング言語で最も悪名高いとされる3)「null 参照」の一種だとは言えるだろう。
だから本当は
if err != nil {
...
}
なんかも
if !(err is nil) {
...
}
みたいな感じに書ければ分かりやすかったのかもしれないが,シンプルを旨とする Go 言語でそんな迂遠な表現がとられるわけもなく,敢えて「nil
との同値性(equality)」という表現をとっているわけだ(偏見)。
nil と比較可能な型
nil
と ==
または !=
で比較可能(comparable)な型は以下の通り(ポインタ型を除く)。
- slice 型
- map 型
- 関数型
- channel 型
- interface 型
このうち slice, map, および関数の各型は nil
との同値性のみ検証できる(nil
でないオブジェクト同士は単純比較できない4)。
また interface 型はクセが強い(笑)型なので,後述の通り,取り扱いには若干の注意が必要である。
Interface 型は,型と値への参照を属性に持つ
まずは,以下の簡単なコードを考えてみる。
package main
import (
"fmt"
"strconv"
)
type Binary uint64
func (i Binary) Get() uint64 {
return uint64(i)
}
func (i Binary) String() string {
return strconv.FormatUint(i.Get(), 2)
}
func main() {
b := Binary(200)
fmt.Println(b.String())
// Output:
// 11001000
}
変数 b
を図式化してみよう5。
こんな感じ。
これを覚えておいてね。
ここで main()
関数の中身を以下のように変えてみる。
func main() {
b := Binary(200)
s := fmt.Stringer(b)
fmt.Println(s.String())
}
ちなみに fmt
.Stringer
型は以下に定義される interface 型である。
type Stringer interface {
String() string
}
ゆえに変数 s
は以下のように図式化できる。
このように interface 型は,型と値への参照を属性に持つオブジェクトとして実装されている。
ただし要素が空の interface{}
型では
のように最適化されているらしい。 まぁ,ユーザレベルで両者を区別する必要はないけれど。
interface 型では nil
を「ゼロ値」とすると書いたが,そのためには「型と値」の2つの属性が共に nil
でなければならない。
値(への参照)だけが nil
でも型全体としては nil
にならないのである。
これでハマりやすいのがエラーハンドリングである。
エラーハンドリングのハマりどころ
Go 言語の組み込み型である error
は以下のように表すことができる。
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
これを踏まえて,こんなコードを考えてみよう。
package main
import "fmt"
type ErrorObject struct{}
func (m *ErrorObject) Error() string {
return "I'm error object."
}
func foo() *ErrorObject {
return nil
}
func bar() error {
return foo()
}
func main() {
if err := bar(); err != nil {
fmt.Printf("%#v is not nil\n", err)
} else {
fmt.Printf("%#v is nil\n", err)
}
}
このコードはコンパイルエラーにならないし完全に動くが,実行結果は
(*main.ErrorObject)(nil) is not nil
となる。
前節まで読んだなら既にお分かりだろうが bar()
関数の返り値の error
は *ErrorObject
という型を持つため nil
にならないのである。
したがって err != nil
は真(true
)となる。
bar()
関数の返り値を正しく評価するには Conversion 構文で型を括りだすか,いっそ foo()
関数を
func foo() error {
return nil
}
と書き換えるかだろう。 まぁ,後者だよね。 そうすれば実行結果は
<nil> is nil
となる。
ブックマーク
参考図書
- プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
- 丸善出版 2016-06-20
- 単行本(ソフトカバー)
- 4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN), 9784621300251 (ISBN)
- 評価
著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。
-
もちろんこれは言語仕様上の話で実装上は何らかの値をとる。「Go言語における式の評価文脈を理解する」によると本当にゼロで埋めるらしい。 ↩︎
-
「型も値も持たない」という意味では最初に紹介した記事の “Goのnilは(nil, nil)” は間違いではなと思う。 ↩︎
-
拙文「「null 安全」について」を参照のこと。 ↩︎
-
slice 型と map 型については
reflect
.DeepEqual()
関数でnil
以外のオブジェクトと比較可能である。 ↩︎ -
引用元の記事(“Go Data Structures: Interfaces”)では 1 word = 32 bits のシステムとして解説されているのでご注意を。 ↩︎