読者です 読者をやめる 読者になる 読者になる

百日半狂乱

Shut the fuck up and write some code!!

CとRubyの代入文を比較してみた

「コーディングを支える技術」を読んだ.

この本がいかに素晴らしいかはいつかブログで書くとして、同書に以下のようなRubyのコードがある*1

#Ruby
x = "Ruby"
p x #-> "Ruby"
z = x
x[0] = "P"
p x #-> "Puby"
p z #-> "Puby"

x[0] = "P"とすることでxもzも"Puby"になる.

C言語脳な僕*2はこれを見て「え?」ってなった.

なんと言ってもいきなりz = xである.普通の代入文が「値のコピー」じゃないってどういうこと?*3などとよくわからない迷路に迷い込んだ.

しかし、冷静に考えると扱っているのは「文字列」だった.Cで言ったらこんな感じ?

//C
#include <stdio.h>

int main(){
    char x[2] = "C";
    char *z = NULL;

    z = x;
    printf("x=%s z=%s\n", x, z); //-> x=C z=C

    x[0] = 'D';
    printf("x=%s z=%s\n", x, z); //-> x=D z=D
    
    return 0;
}

ここでのz = x;ははっきりzにxの参照(=ポインタ=配列の先頭アドレス)を代入しているというのが「zの宣言」から見て取れる.

しかし,Rubyには変数の宣言がない*4

自分はどうも無意識に宣言と関連付けて代入分を見ているようで上記のコードだと混乱してしまった.

RubyもCで言うところのポインタを渡していると思えばわかる.結局Rubyもxの参照の値をzにコピーしているので,Rubyの代入文はCと同様に「値のコピー」を行なっていた.

しかし,文法的には宣言が一切ないただの代入文だ.データが数値だったらどうするんだって感じだ.

調べてみると,Rubyは数値も文字列も含めて全て"オブジェクト"らしい.

んでもって変数はそのオブジェクトの参照を持つらしい.つまり数値だったとしても変数が持つのはオブジェクトの参照...

だったらやっぱりx=1; y=x; x+=10;でyが11となってしまうではないか!なーんて考えてしまった.つまり,やっぱりRubyだと,

//C
#include <stdio.h>

int main(){
    int x = 1;
    int *y;
    printf("x=%d p=%p\n", x, &x); //-> x=1 p=0xbf95e6c8

    y = &x; //xの参照をyに渡す
    x += 10;
    printf("x=%d p=%p\n", x, &x); //-> x=11 p=0xbf95e6c8
    printf("y=%d p=%p\n", *y, y); //-> y=11 p=0xbf95e6c8
    
    return 0;
}

となってしまうのでは?なーんて思ってしまった.

でもそうはならない.

Rubyではx=1; y=x; x+=10;でxが持つ参照を渡されたyがなぜxに10を足してもなお値が1であり続けるのか調べてみる.

手始めに以下のように文字列自体を再定義した.するとオブジェクトIDが変化した*5が変化した.

#Ruby
x = "Ruby"
p x #-> "Ruby"
p x.object_id #-> -610474798
#(余談:自分の環境ではマイナスの大きな数値になったけど何を基準に決めているんだろう??)

x[0] = "P"
p x #-> "Puby"
#一部だけ変更してもオブジェクトIDは変わらない
p x.object_id #-> -610474798

x = "C" #xを再定義
p x #-> "C"
#オブジェクトIDが変化した
p x.object_id #-> -610474858

次にこれを数値でやってみた.例えば足し算してみるとなんと文字列と同様にxのオブジェクトIDが変化する!!

#Ruby
x = 1
p x #-> 1
p x.object_id #-> 3
#余談:数値の場合,オブジェクトIDが3や23などの小さな値になった!!一体何が基準なんだ??

x += 10
p x #-> 11
#オブジェクトIDが3から23に変化した!なんじゃこりゃー!
p x.object_id #-> 23

Cではこうはならない.

//C
#include <stdio.h>

int main(){
    int x = 1;
    int y;
    y = x;
    printf("x=%d p=%p\n", x, &x); //-> x=1 p=0xbfc62758
    printf("y=%d p=%p\n", y, &y);//-> y=1 p=0xbfc6275c

    x += 10;
    //(xに格納されている値が変化してもxのアドレスは変化しない)
    printf("x=%d p=%p\n", x, &x); //-> x=11 p=0xbfc62758
    printf("y=%d p=%p\n", y, &y);//-> y=1 p=0xbfc6275c
    
    return 0;
}

int型の変数xの値のアドレスは変化していない.ここでのy = x;はやはり値のコピーだけどその値というのはポインタではなくてここでは整数値1だ.んでもって変数xの持つ値(整数値の1)に10を足しているのだから物理的に別の領域に存在する変数yには当然何の影響もない.

さっきRubyの変数はオブジェクトへの参照を持つと言ったけど,数値の値を変更すると参照が変わる.もはや言うまでもないかもしれないけど,この代入時に新たなオブジェクトを割り当てるという行為によって上記のCと同じ挙動を実現している.

#Ruby
x=1
p x #-> 1
p x.object_id #-> 3

y = x
x += 10
p x #-> 11
p x.object_id #-> 23 
p y #-> 1
p y.object_id #-> 3

yはxの参照をそのまま受け継いでいるのが面白い.誰にも受け継がれずに宙ぶらりんになったオブジェクトはRubyガベージコレクションが動的に解放する(らしい).

ちなみに、x = 1の時、x[0] = "2"としてみるとundefined method[]=' for 1:Fixnum (NoMethodError)`とちゃんと怒られた.数値の間はちゃんと数値扱いしてくれる.これが動的型付けの威力なの...か?こんなんでも遊べるんだから無知って悲しい.

*1:#->は実行結果.以下同様に,ソースコード中のコメントで->で指しているのは全て実行結果

*2:ずっとCしか触ってなくて最近C++でクラスとかSTLとか覚えた程度の脳

*3:じゃあ,Rubyではx=1; z=x; x+=1;とするとzは2になるんか?とか思いました.ならないです,はい.

*4:もっと言えばRubyだとx="Ruby"だからxに"配列感"がない

*5:Rubyリファレンスには「オブジェクトIDは,オブジェクトごとの固有の整数値です.」とあったのでそのオブジェクトに対応する物理アドレスのエイリアスと考えて良いと思う.