はじめに
プログラミング言語はどれも「変数」というもので何かの値を格納して色々利用しますね。
コードを書いているうちによく考えていることがあります。「ある変数の値ってコードの中でどこからわかるの?」又は「予想外なところで変わったりしないよね?」って。
何かの変数に値を与える時に普段はこのように=
を使うことが多いですね。(他にも<-
を使う少数派の言語もありますが、私はまだ使ったことない)
a = 129.3;
この文が出たら確実に変数の値が決められるでしょう。たとえ既に定義された変数でもここで書き換えられますね。だから変数の値はこの部分を見てわかるのです。
しかし実際にそう簡単にはいかないですね。それ以外も変数が変わる場面はいくらでもあるから。
ということで今回はこれについて纏めてこの記事を書くことにしました。
概念としては色んな言語で共通のはずなので、どの言語を使っている人でも読めるような内容にしようとしていますが、ここでは例として主に私が最近関わっている5つの言語を使います。それはRuby、Python、JavaScript、Dart、MATLABです。
複合系データの変数の中身が変わったが?
今の話は単純系か複合系かによって話は大きく違うから、まずはその違いから始めます。
単純系はint、floatなど基本的に一つだけの値です。複合系は配列や構造体など複数の値から成されるもので、何かの方法で中身を参照することができるものです。
複合系は色んな種類ありますが、一番わかりやすいのは「配列」(PythonやDartでは「リスト」)を挙げます。
配列(リスト)は普段各要素を[ ]
で囲むことで作られるものです。その中身も[ ]
(MATLABなどでは( )
)で参照して中身を入れられます。
だから参照して=
などで代入する時に、直接ではないが、「その配列が変わる」ということになるでしょう。
ar = [2,2,2]
ar[0] = 3
print(ar) # [3, 2, 2]
var ar = [2,2,2]
ar[0] = 3
console.log(ar) // Array(3) [ 3, 2, 2 ]
void main() {
var ar = [2,2,2];
ar[0] = 3;
print(ar); // [3, 2, 2]
}
ar = [2,2,2];
ar(1) = 3;
disp(ar) % 3 2 2
文字列も普段は複合系の変数であり、各要素(各文字)へアクセスすることができます。Rubyでは配列と同じように書き換えられます。
s = "aaka"
s[0] = "b"
print(s) # baka
ただしPython、JavaScript、Dartでは同じような方法で文字列の中を参照できるが、書き換えることはできません。
MATLABの場合は文字列の概念はやや複雑で、2種類の文字列があって使い分けがあります。
-
' '
で囲むことで作られる文字列 -
" "
で囲むことで作られる文字列
' '
の場合は「文字の配列」ということであり、配列と同じように書き換えられます。
s = 'aaka'
s(1) = 'b'
disp(s) % baka
このように、複合系は書き換えられるものと書き換えられないものがあります。
ここで注意して欲しいのは、実は複合系の変数は普段その値を格納するのではなく、その値が置かれている場所を指す値を格納しているのです。C言語で言うと「ポインター」という概念ですね。
これについて深く語ると難しい話なので、ここではそこまで説明するつもりはありません。ただ簡単に言うと、複合系の変数はただの「容器」だと思ってもいいでしょう。容器はたとえ中に入っているものが変わっても同じ容器だと認識するでしょう。でも普段は容器のことを構う場合は少ないでしょう。例えば味噌ラーメンが入っていたボウルに豚骨ラーメンを入れたらこれは同じ容器でも別物扱いですね。逆に豚骨ラーメンが2つ並んでも違う容器なのに同じもの扱いですね。
あ、つい食べ物の話になってしまいました。この記事の真面目さは激減しますね。もっといい比喩があるかもしれませんが、最近よく豚骨ラーメンを食べているから。
《博多駅周辺の店の豚骨ラーメン。関係ないけど、丁度一昨日食べて写真を撮ったから》
例えばこんな配列があるとします。
ar = [a,b]
この変数に起きる変化は代入の仕方によって違います。
ar[0] = [z] # 容器をそのままで中身を入れ替えるだけ
ar = [x,y] # 容器も含め全部新しいものに入れ替わる
ar = [x,y] # 再び同じ中身を入れても容器は別物に変わった
「中身が入れ替えられたらその変数は変わる」ということだけ考えていいとは思いますが、変数そのものがただの容器であることを意識する場合もあるでしょう。そうでないとわけわからないバグが起きることもあったりします。
同じ場所を指す複合系が変わったが?
上記の例ではたとえその変数に直接代入しても、その中の要素に代入しても、変化が起きるとわかりやすいでしょう。
しかし次に説明する状況はもっと複雑でわかりにくいです。例えばこのような例はどうでしょう。
ar = [[1,2],[3,4]]
ax = ar[0]
ax[0] = 5
ay = ar[1]
ay = [6,7]
print(ar) # [[5, 2], [3, 4]]
void main() {
var ar = [[1,2],[3,4]];
var ax = ar[0];
ax[0] = 5;
var ay = ar[1];
ay = [6,7];
print(ar); // [[5, 2], [3, 4]]
}
ここでar
という「配列が入っている配列」の複合系の変数に直接何も代入していないのに、いつの間にか中身が変わってしまったが……。その原因はax
という変数が配列の中の配列を参照して、その中身が入れられられたからです。
前にも説明した通り、複合系の変数が実際に格納しているものは値が置かれる場所です。そして違う変数が同じ場所を指す場合もあって、それでそこの値が変わったらどの変数にも反映されます。
今回の例ではax
がar[0]
になって、こうなるとax[0]
でもar[0][0]
でも同じ場所を指すことになったからです。
ただしこの例の中で似ているようなことをay
にもしましたが、変化はなかったでしょう。それはay
の場合は中身ではなく、変数そのものに代入したからです。だからここで元々ar[1]
を参照するay
が別の配列を示す変数に変わっただけで、ar
とは全然影響ありません。
混同しやすいことかもしれませんが、この違いをちゃんと理解できればわけわからないバグを避けられるでしょう。
なお、逆のことも勿論起こり得ます。
az = [1,2]
ar = [az]
ar[0][0] = 0
print(az) # [0, 2]
var az = [1,2]
var ar = [az]
ar[0][0] = 0
console.log(az) // Array [ 0, 2 ]
void main() {
var az = [1,2];
var ar = [az];
ar[0][0] = 0;
print(az); // [0, 2];
}
C言語を始めとして主流のプログラミング言語は大体これが共通する常識ですが、全てがそうであるわけではありません。MATLABなどの場合は違います。別の変数が配列の中を参照したらその値をもらって、その後はもう関係なくなって、何の変化もお互いに影響ありません。
ar = [[1,2];[3,4]];
ax = ar(1);
ax(1) = 5;
ay = ar(2);
ay = [6,7];
disp(ar)
このようにMATLABの複合系の変数は他の言語のように勝手に変わるという心配はないでしょう。
コピーしますか?
複合系データを持つ変数は普段もしそのまま別の変数に代入されたらそれは同じ場所を指すということになって、中身に何かの変化を齎したら当然どの変数からもその変化が起きます。これはどの言語の入門書でも注意事項として書かれているはずです。
ar1 = [5,6,7]
ar2 = ar1
ar2[0] = 10
print(ar2) # [10, 6, 7]
print(ar1) # [10, 6, 7]
var ar1 = [5,6,7]
var ar2 = ar1
ar2[0] = 10
console.log(ar2) // Array(3) [ 10, 6, 7 ]
console.log(ar1) // Array(3) [ 10, 6, 7 ]
void main() {
var ar1 = [5,6,7];
var ar2 = ar1;
ar2[0] = 10;
print(ar2); // [10, 6, 7]
print(ar1); // [10, 6, 7]
}
勿論、この話もMATLABなどの場合は違いますけど。
ar1 = [5,6,7];
ar2 = ar1;
ar2(1) = 10;
disp(ar2) % 10 6 7
disp(ar1) % 5 6 7
Rubyの場合、文字列も配列と同じように同時に変更します。
s1 = "aba"
s2 = s1
s2[0] = "b"
print(s2) # baba
print(s1) # baba
こうやって何かしない限りこういう挙動になっていますが、勿論そうならないための方法があります。例えばRubyでは.dup
をPythonでは.copy()
をJavaScriptとDartでは...
を使います。こうすることで中身がコピーされて別の新しい配列になります。
ar1 = [5,6,7]
ar2 = ar1.dup
ar2[0] = 10
print(ar2) # [10, 6, 7]
print(ar1) # [5, 6, 7]
ar1 = [5,6,7]
ar2 = ar1.copy()
ar2[0] = 10
print(ar2) # [10, 6, 7]
print(ar1) # [5, 6, 7]
var ar1 = [5,6,7]
var ar2 = [...ar1]
ar2[0] = 10
console.log(ar2) // Array(3) [ 10, 6, 7 ]
console.log(ar1) // Array(3) [ 5, 6, 7 ]
void main() {
var ar1 = [5,6,7];
var ar2 = [...ar1];
ar2[0] = 10;
print(ar2); // [10, 6, 7]
print(ar1); // [5, 6, 7]
}
ただしこんなことをしたとしても、実は完全にコピーして孤立になるわけではありません。中身も複合系だったらそれはまだ同じ場所を指します。そしてその複合系の中身が変わったら同時に変わることになります。
ar1 = [[5,6,7]]
ar2 = ar1.dup
ar2[0][0] = 10
print(ar2) # [[10, 6, 7]]
print(ar1) # [[10, 6, 7]]
ar1 = [[5,6,7]]
ar2 = ar1.copy()
ar2[0][0] = 10
print(ar2) # [[10, 6, 7]]
print(ar1) # [[10, 6, 7]]
var ar1 = [[5,6,7]]
var ar2 = [...ar1]
ar2[0][0] = 10
console.log(ar2[0]) // Array(3) [ 10, 6, 7 ]
console.log(ar1[0]) // Array(3) [ 10, 6, 7 ]
void main() {
var ar1 = [[5,6,7]];
var ar2 = [...ar1];
ar2[0][0] = 10;
print(ar2); // [[10, 6, 7]]
print(ar1); // [[10, 6, 7]]
}
もし中身まで完全にコピーしたい場合、つまり「ディープコピー」という行動は、勿論それぞれの言語で方法がありますが、それは複雑になるのでことでは割愛します。
とりあえずこのように複合系データを持つ変数をコピーするかどうかについて注意する必要があります。
定数にしたぐらいで絶対に変わらないとでも思ったか?
これはたった一部のプログラミング言語だけの話ですが、一度定義したらもう一度代入することができないという「定数」の概念があります。
普段定数になるかは宣言の時に決めることなので、PythonやMATLABみたいな宣言する必要ない言語には無縁です。
JavaScriptでは宣言する時にconst
を使ったら定数になります。定数を使ったら途中で値が変わるという心配がない……とは言いたいけど、実際にそうとは言い切れないですね。
なぜならJavaScriptのconst
は直接代入することができないようにするだけで、実際に上述も書いた通り複合系の場合は間接の方法で中身が変わることもあるから。たとえそれがconst
で定義された変数でも。
const a = [1,2]
a[0] = 3
console.log(a) // Array [ 3, 2 ]
Dartでも同じようにconst
はあります。しかしJavaScriptと違ってDartのconst
は強力的です。中身でも変更不可能となります。
void main() {
const ar = [1,2];
ar[0] = 3; // エラー
}
勿論その中身がまた複合系であってもそうです。
更に他の変数に渡したらその変数の中身も変更できなくなります。
void main() {
const a = [1,2];
var b = a;
b[0] = 3; // エラー
}
ただしconst
の他にDartはfinal
があります。final
で宣言された変数も定数になりますが、JavaScriptのconst
と同じように中身が変更できます。
void main() {
final ar = [1,2];
ar[0] = 3;
print(ar); // [3, 2]
}
実際にconst
とfinal
は他にもありますが今回とは関係ない話になるので割愛します。
また、Rubyではアルファベット大文字で始まる変数が「定数」と呼ばれますが、実際に再代入しても警告が出るくらいでエラーにはならないので、本当の意味の定数ではないでしょう。
その他に、複合系の中身を変更して欲しくない場合は.freeze
を使うという方法があります。
ar = [9,8,7]
ar.freeze
ar[1] = 5 # エラー
でもその変数自体は定数になったわけではないので、直接代入することで変更できます。
ar = [9,5,7] # エラーが出ない
関数に渡された変数の末路は?
変数が関数に渡されることで、その中の何らかの動きで値が変わるというのはよくある話ですね。これは殆どの主流の言語で起こり得ることです。
関数が変数を受け取る理由は2つしかないでしょう。
- 変数の中の値を使うため
- その変数の中身を変えるため
普段関数の中の変数は渡された変数とは別のもので、関数の中で代入しても外での値は変わりません。ただし複合系の変数は実際に場所を示すものなので、中身は一緒だということになります。だから中身の変化実際にその変数の変化になります。
例えばこの例。
def fcn(x,ar)
x = 0
ar[0] = 0
end
x = 5
ar = [1,2,3]
fcn(x,ar)
print(x) # 5
print(ar) # [0, 2, 3]
def fcn(x,ar):
x = 0
ar[0] = 0
x = 5
ar = [1,2,3]
fcn(x,ar)
print(x) # 5
print(ar) # [0, 2, 3]
function fcn(x,ar) {
x = 0
ar[0] = 0
}
var x = 5
var ar = [1,2,3]
fcn(x,ar)
alert(x) // 5
console.log(ar) // Array(3) [ 0, 2, 3 ]
void fcn(x,ar) {
x = 0;
ar[0] = 0;
}
void main() {
var x = 5;
var ar = [1,2,3];
fcn(x,ar);
print(x); // 5
print(ar); // [0, 2, 3]
}
このように関数内の単純系データの変数x
の代入は外とは関係ないが、ar
の中身の変化は外と直接繋がります。
ただしMATLABの場合は話が違いますね。MATLABでたとえ複合系でも変数を関数に渡すことで変化することがありません。
function fcn(x,ar)
x = 0;
ar(1) = 0;
end
x = 5;
ar = [1,2,3];
fcn(x,ar);
disp(x) % 5
disp(ar) % 1 2 3
普段MATLABで関数を使って変数の値を変えたい場合は返り値にしてまた代入するのです。
function ar = fcn(ar)
ar(1) = 0;
end
ar = [1,2,3];
ar = fcn(ar);
disp(ar) % 0 2 3
ちょっと冗長に見えるかもしれませんが、この書き方で変数の値が変わることをわかりやすいです。外で明白に代入を書かない限り変数の値が絶対に変わらないと自信が持てますから。
Rubyでは、変数に変化を齎す関数は「破壊的メソッド」と呼びます。そんな関数の名前の最後に!
が付いていることが多いのでわかりやすいです。ただしこれは標準の関数の場合だけで、自分で定義する関数や外部ライブラリーはその制限がないので、!
の存在はただの目安だけで、実際に!
がなくても変数が変わらないという保証はないのです。
このように関数の中で変数の値が起きることがあるということですが、普段中身が変えられるのは複合系の変数だけ。単純系の変数は関数に渡されることで値が変わることはないと思ってもいいでしょう。
しかし、単純系の変数でも関数に渡されたことで値が変わる言語もあります。このようにことが起きるのは、私が知っている限りではIDLという言語だけ。これはあまりにもマイナー言語で、知っている人は殆どいないと思いますけど、ある意味面白いところもあります。以前の記事で紹介しました。詳しいことはこの記事で。
モジュールやコードが呼び出されたが?
普段変数は直接その場で宣言や代入を書くことで初めて定義され使えるようになりますが、その他にも例えば他のファイルに書いてあるコードを読み込んで実行することで、その中で変数の定義が書かれたら突然変数が現れるということになりますね。モジュールとして読み込む場合もそうです。
そしてもしその変数が既に定義された場合でも書き換えられることになる可能性があります。
ただし直接コードを読み込んで実行するという方法は可読性があまりよくなくて普段は推奨されないことなので、たとえできてもやらない方が無難でしょう。
モジュールの呼び出しも普段は最初にやることで、途中で呼び出すことができてもあまりよくないことなのでこうする場合は少ないでしょう。
だからこんなことで予期せず変数が変えられることは少ないとは思いますが、一応起こり得ることなので気をつけておいてもいいでしょう。
終わりに
以上変数の中身が変わる原因について纏めてみました。主に配列(リスト)の話になっていますが、オブジェクトや構造体など他の複合系のデータも同じようなことが起こります。
ここで話したのはただ私の経験から書いたことなので、その他にも変わる場合があるでしょう。特に私が勉強したことない言語でまだわからない意外な原理があるはずです。何かもっと注意することがあればコメントを書いたら嬉しいです。
参考
- Ruby 定数について
- [ruby]浅いコピーと深いコピー
- 【Ruby】オブジェクトをコピーするときの注意
- 【Ruby】メソッドの引数は値渡し?参照渡し?
- 【Ruby】requireとloadの違いについて
- Pythonで変数に変数代入したら思っていたのと違ったので値渡しと参照渡し等について整理した【ゼロからPython勉強してみる】
- copy.copyとcopy.deepcopy
- 「PythonよりJavaScriptの方が簡単」ということについての思案
- JavaScript における配列コピー
- javascript での配列のコピーの方法
- 【Dart】Listのディープコピー
- Dartの変数定義時の修飾static/final/const、そしてconst constructorについて
- [Dart] const vs final
- [Dart]定数のfinalとconstの違い
- MATLABで関数型プログラミングをできるだけ頑張ってみる
- かくも読みにくい mファイル