これは何?
変数ってなに? という話。
値なのか、参照なのか、代入したらどうなるのか、とか。そのあたり。
いくつかの言語の事例
いくつかの言語の事例を書いてみる。
あと。等値比較のことをこのあと何度か書くけど、いずれも 浮動小数点数の非数や -0.0
の話は無視している。
C言語の場合
C 言語の変数は(概念としては)型が付与された一連のメモリである。この文脈ではオブジェクトと言ってもいい。
変数の型が決まれば、バイト数が決まる。
オブジェクトの正体は、構造体かもしれないし、ポインタかもしれない。
「概念としては」と書いたのは、コンパイルの結果その変数自体が消滅したり、いろいろあり得るから。
C言語では、同じ「一連のメモリ」を別の変数が直接指すということはできない。そうしたければポインタを使う。
あるいは union
を使えば異なるメンバが同じ「一連のメモリ」を指すことが出来る。
a = b;
という代入文は、暗黙の型変換が入らない場合「b が指す一連のメモリを a が指す一連のメモリにコピーする」という意味になる。
つまり、代入が行われても依然として a は b と別のもの(アドレスが異なる)であり続けるが、値としては同じものになる。
関数呼び出しも同様で、暗黙の変換が入らなければ
void f(T a){ /*略*/ }
f(b);
は、関数を呼ぶ側の b という変数が代表している「一連のメモリ」から、関数の中で使われる「一連のメモリ」へのコピーが発生すると思って良い。
もちろん本当はメモリじゃなくてレジスタだったり、最適化でそれもなくなったりするかもしれないんだけど、概念としてはそういうこと。
if (a==b){略}
のような等値比較は、a と b が同じ値かどうかを判断している。整数なら整数として同じかどうか、ポインタならポインタとして同じかどうかを比較している。
なお、==
の両辺に文字列(のように見えるもの)をおいても==
に供されるのは文字列ではなくポインタなので、ポインタとしての比較にしかならない。構造体は ==
で比較できない。ということで複雑さを回避している。
C++ の場合
C++ は「参照」とよばれる種類の変数を作ったりすることが出来る。
「参照」は別の変数が指しているのと同じオブジェクト(この文脈では、型が決まっている一連のメモリ)を指しているかもしれない。
それでも、変数の型が決まればその変数が指しているオブジェクトのバイト数は確定するし、「参照」と呼ばれる種類の変数だからといって指している先が参照という種類のオブジェクトだったりはしない。
a = b;
という代入文は演算子のオーバーロードがあるので「b が指す一連のメモリを a が指す一連のメモリにコピーする」という意味になるとは限らない。たとえば std::string
はそうなっていない。
C++ の代入文はユーザー定義でどうとでもなるのでどうなるかわからないといえばわからないのだけれど「代入が行われても依然として a は b と別のもの(アドレスが異なる)であり続けるが、値としては同じものになる」ということが期待されている。std::string
なんかもそうなっている。
if (a==b){略}
のような等値比較は、C言語と同様、a と b が同じ値かどうかを判断している。整数なら整数として同じかどうか、ポインタならポインタとして同じかどうかを比較している。
「同じ値」とはどういう意味なのかはクラスの設計者が決める。
たとえば。std::vector<char>
と std::shared_ptr<char[]>
は似たようなものだと思える局面もあるが、vector
は値の並びが等しいかどうかを見て、shared_ptr
はポインタとして等しいかどうかを見る。
ruby・Python の場合
ruby・Python の変数はすべてオブジェクトにつけた名前である。アクセス手段と言ってもいい。
一つのオブジェクトに複数の名前(つまり、変数)を付与することが普通にある。
C言語でいうと「ポインタしかない」という状態と似ている。
C言語の世界観に引き寄せたい場合「変数は「参照」という趣旨の値が入っている一連のメモリを代表している」と思っておけば普通は困らないわけだけれど、これは実装の詳細に立ち入りすぎているし、場合によっては不正確である。
ruby の場合、小さい整数などは、私の理解が正しければ(自信ない)参照先のオブジェクトは存在しないという意味でそれは参照ではない。
数値の大きさによって変数の意味が変わるというヤバそうなことをしているわけだけど、実は全然困らない。これは、「immutable な値を保持している」と「immutable な値への参照を保持している」はユーザーから見たら実用上区別できないから。
Python だとたぶんすべて参照なんだけど、将来のアップデートで「xx の場合、参照先のオブジェクトは必ずしも存在しないことになった」という変更が入る可能性だってある。そうなっても ruby 同様普通のユーザーは気づかない。
すべての変数はオブジェクトにつけた名前で、代入文は「名前をつける」という行為を示している。
なので
a = b
の結果、 a と b は同じオブジェクトへの名前となる。小さな整数でも巨大なクラスのインスタンスでも、この点に差はない。
代入文同様、関数呼び出しでも
def f(a); 略; end
f(b)
などとすると、 a と b は同じオブジェクトを指すことになる。C 言語で
void f(T a){ /*略*/ }
f(b);
としても a と b が同じ「一連のメモリ」を指すことがないのとは対照的である。
なお、代入文やメソッド呼び出しによって同一のオブジェクトを指す変数が多数同時に存在することになるが、そういうことはとてもよくある。
C言語でポインタを一切使わずに実用的なソフトウェアが書くことができないのと似ているかもしれない。
たとえば
sorted = [foo, bar, baz].sort_by{ |e| e.name }
などとすると、 e
は foo
bar
baz
のいずれかの変数と同じオブジェクトを指すことになる。
ruby・python は、いずれも「同じ値」と「同じオブジェクト」という趣旨の二種類の比較がある。
意味\言語 | ruby | python |
---|---|---|
同じ値 | == |
== |
同じオブジェクト | equal? |
is |
私の経験の範囲では、ruby や python で equal?
や is
を書く必要がある局面はほとんどない。論理的には ==
で良いはずの計算を is
などを使うことで高速化できる事はありそうだとおもうけれど、実際必要になった場面はぱっと思いつかない。
Python の複合代入演算子
Python は a
が mutable なオブジェクトを指している場合、 a = a + b
と a += b
の動作が全く異なる(となるように、標準ライブラリのクラスは設計されている)気持ち悪い仕様(個人の感想です)となっている。
つまり、先ほど代入は名付けであるという趣旨のことを書いたが、Python の +=
などは名付けではない(ことがある)。
詳しくはこちらに記事を書いている
標準ライブラリにあるクラスを見る限り、in-place になるか否かはプリミティブ型のオブジェクト(そんなものは Python にはないと思う)か否かではなく、mutable か否かによる。mutable の場合、新たなオブジェクトの生成をサボって直接上書きするが、immutable の場合は上書きしようがないので in-place にはできない。
ユーザー定義クラスの場合は コメントいただいている 通り、__iadd__
などが定義されているかどうかとなる。
振る舞いは定義した人次第ではあるけれど、標準ライブラリのクラスと同じように振る舞うのが良いとされているんだと思う。
JavaScript の場合
JavaScript も代入のあたりの考え方は Ruby 同じだと思うけど自信がない。
===
による比較については
- Object 型以外は同値(Python などの
==
と似てる) - Object 型では同一(Pytyhon の
is
と似ている)
ということらしい。難しい。
Java の場合
Java のことはよく知らないので自信がないけど一応書いておくと。
Java の変数は、プリミティブ型の場合は C言語の変数と同じような感じになっている。そこには値がある。
参照型(っていうのかな)の変数の場合は、変数が指す先には「参照」という趣旨の値が入っている。この点は ruby や Python の変数と似ている。C++ の参照型変数はオブジェクト(C++ の用語としてのオブジェクト)への参照だが、Java の参照型を指す変数は、参照という趣旨の値が入っている。ここは決定的に異なる。
C/C++ と違って、Java プリミティブ型の値のアドレスを取ることができないのでユーザーの体験としては「参照ではない値」と「immutable な値への参照」を区別する必要がない。
なので、ユーザーの感覚としては、プリミティブ型は immutable な値への参照と見なしてもほとんど困らない。困るとすれば、Java 以外の言語(C言語とか)で書かれたライブラリとのやり取りを実装するときぐらいだと思う。どうだろう。
……と書いていたのだけど、 等値比較のことがすっぽり抜けていて、コメントいただいて、ああそうだったと思った。
ということで、ほぼ等値比較の場合のみ、Java では参照型とプリミティブ型を区別する必要がある。(ほぼ、と書いたのは頂いた コメント にある、マルチスレッド処理の話があるらしい(私は未調査)という件や、他にもあるかも(私は知らない)ということ)
Java は、参照の値(C言語で言うところのポインタの値)に興味がある局面はわりと少ない(特に String
なんかは immutable なのでほぼ無い)にも関わらず、 ==
は参照の値が等しいかどうかを比較してしまい、多くの初心者を苦しめている。
C# の場合
当初
C#のことはもっと知らないんだけど、この観点では Java とほぼ同じじゃないかと思う。
と書いていたのだけれど、コメントいただいている通り、C# は Java とは全然違うのであった。
変数が値となるか参照となるかは変数の型で決まるという点は Java とおなじなんだけれど、参照代入 があるので、そこは Java とは大きく異なる。
関数に引数を渡す際、
- 値の値渡し(C言語と同じ)
- 参照の値渡し(ruby・Python と同じ)
- 値の参照渡し(C++ の
T &
で受けるのと同じ) - 参照の参照渡し(C++ で
T * &
で受けるのと似ている) - 他にもある
と、多様な方法がある。存在するだけでなく、実際適宜使いわけられているし、使い分けられることが要求されているとも思う。
参照代入があるので、int
のような型の変数を「immutable なオブジェクトへの参照を保持している変数」とみなすことはできない。その変数が値を保持しているのか参照を保持しているのかについて意識的にならないとバグを生む。
私に関して言えば、あんまり使いこなせていないのだけれど。
C# の場合 ==
はオーバーロードできる。なので、C++ と同様、「同じ値」とはどういう意味なのかはクラスの設計者が決める。ということで、文字列が文字の並びとして等しいかどうかの評価も普通に ==
が使える。
Haskell の場合
Haskell については本当にちっとも詳しくないけど、オモシロイと思ったので加筆した。
Haskell は基本的に全ての変数の値が変更不可能なので、値なのか参照なのかオブジェクトにつけた名前なのかとかいう戦いがそもそも発生しない。
変数は値につけた名前である。ということだと思う。簡単。
また、
a = b
という形の文はありえるが、これは代入ではなく束縛。Haskell に これまで挙げられた言語の「代入」に相当する機能はない。「束縛」によって値に名前をつけることができる。
という理解をしているが、書いたとおりよくわかってはいない。
==
の比較は型定義の一部だと思う。どうだろう。
まとめ
雑に表にする
言語など | a=b |
a==b |
---|---|---|
C | 値のコピー | 値が等しいかどうか |
C++ | 値(※3)のコピー | 値(※1)が等しいかどうか |
ruby | オブジェクトへの名付け | 値(※1)が等しいかどうか |
Python | オブジェクトへの名付け | 値(※1)が等しいかどうか |
Java | 値(※2)のコピー | 値(※2)が等しいかどうか |
C# | 値(※2)のコピー | 値(※1)が等しいかどうか |
Haskell | 値を変数に束縛する | たぶん、値(※1)が等しいかどうか |
※1 「等しい」の意味は型を設計した人の気持ち次第
※2 「参照」という趣旨の値のこともある。
※3 「コピー」の意味は型を設計した人の気持ち次第
文章でもまとめ
C言語は素朴。変数は一連のメモリ。束縛という言い方はできない。代入はコピーで ==
は値の比較。
C++ は、C言語の世界観のまま、より複雑なオブジェクトを扱おうとした努力の結果こうなったんだと思っている。型の設計がちゃんとしてれば、そして中身を気にしなければ C言語の世界観のまま生きていける。
Ruby / Python も(Pytyon の +=
なんかの気持ち悪さを気にしなければ)シンプル。変数はオブジェクトにつけた名前。束縛という言い方もする。代入は名付けで、 ==
は値の比較。比較のときに「等しいって何?」という問いが発生し、その問にはクラス設計者が答える。
Java と C# は複雑な印象。変数は値かもしれないし参照かもしれない。代入は、値のコピーかもしれないし、参照という値のコピーかもしれない。C# については「参照」という言葉の意味が文脈によって二種類あるので気を付けたほうが良い。一方は正式な用語ではないかもしれないけれど(未調査)。
Java の ==
は、参照という趣旨の値または普通の値の比較。C言語っぽい世界観。
C# の ==
はユーザー定義ができるので、C++ っぽい世界観。でも、C++とちがって代入演算子はオーバーロードできない。
Haskell は C言語とは違う意味でシンプル。そこにはメモリもポインタも参照もない。ただただ値がある。
という具合に。
言語によって、同じ言語でも変数の型などによって「変数とは何か」は変わる。
a = b
が何を引き起こすのかも、a==b
の意味も変わる。
Python の感覚で C言語に触ると思いがけないことが起こるし、逆も然り。