C#:アセった時に読む【値型と参照型】

C#

今回はC#における『値型』と『参照型』について取り上げます。

この辺りは人によるのかもしれませんが、C#で実装を行っていると、
値型がどうとか、参照型がどうとかそんなに考えなくても仕事ができてしまいます。

しかしそれは、何となくの経験則から体が覚えているだけで、
意外と説明できない人が多いのではないのでしょうか。

『int は何となく値型』、『class って定義したら参照型』

こんな風に覚えていませんか?
というわけで、今回は値型と参照型について頭で理解するのではなく、体で体感して理解しましょう。

『そんなこと、常識じゃん!』という方は、いちゃもんを付けるつもりで見てください。

急いでいる方向けに言うと、

  • 値型は宝箱
  • 参照型は宝の地図

これで覚えることができます。

スポンサードサーチ

種類から見ていく

そもそも、値型と参照型は、
型でいうと、どれに対応するのでしょうか。身近なものだけ見ていきましょう。

  • 値型 : int, float, double, struct, bool型になります。
  • 参照型 : 皆さんが呼吸するように定義しまくっている【class】や【string】型です

※stringは本来参照型ですが、ふるまいとしては値型になるニクイやつです。今回は詳しくは触れません。

ここでは特に考えず、ふむふむと頭で理解しておきましょう。

どんな風に考えるか

まず、値型と参照型のの違いを抑えましょう。
ここではなるべく、メモリや番地という概念を出さずに、例え話で説明してみます。
ここでは、『宝箱』を例にとって、解説しましょう。

値型は、宝箱で覚えましょう

『まず、ただただ広い空間を想像してください。そしてあなた自身は冒険ハンターです。
そして目の前には、今までの冒険で手に入れた宝箱が辺り一面に広がっています。
この宝箱を、ひとつずつ、ボスであるレジスター卿に届けなくてはなりません。』

このような状況です。

さて、『目の前に置かれている宝箱』とは、何を指すのでしょうか。

そうですね。
プログラムをやっている人ならわかりますね。
宝箱 は『変数』のことを指しています。
そして、宝箱の中身が変数の値ということです。。


int x = 3 ; // xはxという名前の宝箱。宝箱xの中身は3

ただし、値型で扱われる宝箱は指輪のケースぐらいの大きさしかありません。
やたらと小ぶりなのです。
それがたくさん目の前に置かれているのです。
目の前に置かれているという事は、手に取ってすぐに確認できますし、
ボスであるレジスター卿にもすぐに届けることができます。

これが値型の特徴になります。
まとめると、

宝箱で考える

手に取りやすい大きさ

となります。
この、同じ大きさだが、素早く手に取って渡せる。
この辺りが値型のキーポイントです。

参照型は宝の地図で考える

では次に、参照型について考えてみましょう。
参照型は皆さん大好きなクラスとかですね。

シチュエーションは値型の時と同じです。
宝箱をレジスター卿に届けなくてはなりません。
しかし、今回はちょっと違います。

宝箱は目の前にありません。
今いる部屋から少し離れた倉庫に宝箱が置いてあります。
皆さんの感覚で言うなら、家から出て徒歩五分のコンビニと言ったところでしょうか。
そのくらい離れた倉庫に宝箱がたくさん置いてあります。

また、値型と比べて、その宝箱の大きさは大小さまざまです。
小包程度のものから、家具をいれるサイズのものまであります。

この宝箱をレジスター卿に運ぶのですが、ルールがありました。
ひとつずつしか運べないというルールです。

まず宝箱を取ってくるのに5分くらいかけて倉庫に行きます。
倉庫から宝箱を取ってきて、また5分かけてレジスター卿に届けます。
届けるのに10分はかかっていますね。

このように参照型というのは値型に比べ、アクセスに手間がかかります。
値型では、目の前にデータがあったため探しに行く手間はほぼありませんでした。

では、参照型の利点とは何でしょうか。
それは、大小さまざまな大きさのデータを確保できるという事です。

どのようにして大小さまざまなデータを管理しているのでしょうか。

たとえば、目の前に乱雑にの大きな宝箱を置いてしまっては管理ができません。
どの荷物が今どこに行ったのか、あの荷物はどこにしまったのか。
まったくもって管理ができません。

つまり、目の前に何個か置いてある指輪のケースサイズのものなら管理できるが、
大小さまざまなサイズの宝箱は難しいという事です。
何となく分かりますよね。把握がしにくくなるのです。

では、参照型はどうやって管理しているのでしょうか。
それは、宝の地図です。

目の前に宝箱自体をたくさん並べるのではなく、それぞれの宝の地図を作り、
それを管理しているのです。
たとえば、『Aという宝箱は、倉庫の3456番地』、『Bという宝箱は倉庫の678番地にある』

こんな風に管理しています。

まとめると、参照型は宝の地図で管理される。
データにアクセスするときは、どこか別の場所に本体を入れているので、アクセスに手間がかかる。

スポンサードサーチ

代入を考えてみる

二つの型の違いが分かったところで、もう少し突っ込んで考えてみましょう。
二つの型で、代入とは、いったいどのような事が起きているのでしょうか。
また、その違いは何でしょうか。

ではまず、値型から見てみましょう。


// 準備
int item1 = 11 ; 

// item2にitem1を代入する
int item2 = item1 ;  

ただ代入するだけです。
ここで起きていることは、item2にはitem1のコピーが入ります。
item1を複製して、複製したデータがitem2に入ります。その後、item2から1を引いたとします。

そして、item1とitem2を比べてみましょう。



item2 = item2 - 1 ;

// item1は11
// item2は10

コピーをしているため、item1とitem2はそれぞれ違う値を示します。
これは一見、当たり前のように感じます。

同じ事を、参照型にも行いましょう。


// hitujiクラスのインスタンスを作る
var yagi = new Hituji();
yagi.Name = "やぎさん";
yagi.age = 10; 

// 変数hitujiの名前を変更する
var hituji = yagi; // 代入
hituji.Name = "ひつじさん";

値型の時と同じように、Hitujiクラスのインスタンスであるhitujiに、同じクラスのインスタンスのyagiを代入します。
そして、hituji のName を変更しました。
では、ふたつのインスタンス の中身にはどんな値が入っているでしょうか?

正解は以下です。


// 変数yagiの中身
// yagi.Name = "ひつじさん"
// yagi.age = 10

// 変数hitujiの中身
// hituji.Name = "ひつじさん"
// hituji.age = 10

そう、同じになるのです。
理由は、hituji = yagi の代入をしたとき、
変数 yagiが持つ、宝の地図がコピーされ、変数 hitujiに渡されるからです。

宝の地図はコピーしても、同じ宝の地図です。

そして、同じ宝の地図を渡したのですから、当然ながら、宝箱のありかも同じ場所を指しています。
同じところを指している宝箱の中身を変えているため、同じ値になるのです。

値型では、片方を変更したとしても影響はありませんでしたが、
このように、参照型では変化が伝搬します。

参照型・値型の利点と欠点

このようにして、参照型が使われている理由は何でしょうか。
値型のように、宝箱自体を保持するようにし、必要な時は宝箱をコピーして、値を渡していくというスタイルの方が自然な気がします。

実は、データが大きくなるにつれ、コピー作業が遅くなってしまうのです。
クラスの実体である、ばかでかい宝箱を直接持つというのは、あちらこちらでコピーが生じ、動作が遅くなってしまいます。

そのため、『宝の地図』というワンクッションを挟むことで、コピーデータの受け渡しを小さく済ましているのです。
と、ここまでいうと、『じゃあ、値型いらないじゃん!』となってしまいますね。

ですが、参照型の場合は、データの読み書きをする時は、
遠いところにある宝箱にアクセスしなければなりません。
それは直接宝箱を抱えている値型より、圧倒的にアクセス時間が遅いのです。

つまりまとめると、

  • 軽いデータは値型で(int, float, double など)扱う
  • 重いデータは参照型で扱う

として扱うことで、両方の欠点を解決しているのです。

スポンサードサーチ

メソッド内での変化

次に、メソッドの引数に値を渡したときを想定しましょう。
渡した引数が、メソッド内で書き換わった時に、呼び出し元にどう影響するかを考えてみます。

一般には、

  • 値渡し:メソッド内で引数の値を変えても、呼び出し元には影響がない
  • 参照渡し:メソッド内での値の書き換えの影響が、呼び出し元に伝番する

となるらしいですが、どうでしょうか。
変数の代入と同じように考えてみます。

値型では、メソッドの引数に変数を渡すとき、『宝箱』をコピーして渡します。
代入の時も同じことをしていましたね。

という事は、メソッドで使われるデータは、コピーされた別物になります。
よって、メソッド内でいくら中身を変更しても、呼び出し元には何ら影響はありません。


int xValue = 12;
int yValue = 13; 

int result = sum(xValue, yValue); // sum(12, 13) 値がコピーされる

では、次に参照型ではどうでしょうか。参照型の代入の時と同じように考えます。

参照型では、メソッドの引数に変数を渡すとき、『宝の地図』をコピーして渡します。

そのため、呼び出し元データとメソッドで使われるデータは、同じ宝箱を指すため、同じデータを指します。

よって、メソッド内で引数の値を変更したとき、呼び出し元も同様に変化します。

となりますね。参照型は『宝の地図』理論を使うことによって、メソッド内での特徴も説明することができます。

まとめると、

  • 値渡し:メソッド内で引数の値を変えても、呼び出し元には影響がない
  • 参照渡し:メソッド内での値の書き換えのが、呼び出し元に伝番する

になります。
今なら意味が分かりますね。

まとめ

  • 値型は『宝箱』を直接見る。
  • 参照型は『宝の地図』を見る。データを取り出すときは、遠くに置かれている宝箱を探しに行くため、アクセスに時間がかかる。

以上、今回は、値型と参照型についてでした。
結構納得できる説明だったのではないでしょうか。

コーディングに慣れてくると、慣れてきた分、
なぜか値型と参照型の特徴を忘れてしまっている方は、かなり多いと思います。

しかしながら、値型・参照型のそれぞれの特徴を忘れてしまったとしても、
体は経験で覚えているため、実はそれなりの実装はできてしまいます。
しかし、初歩的なことですので、きちんと理解しておきたいですね。