関数(※1)を学んだことで、ついに値型と参照型の違いを知ることができます。
ここではC#で最重要とも言える値型と参照型の違いを理解しましょう。
※1現在の知識を踏まえて、ここではメソッドではなく関数と呼びます。
ここではC#の関数について学びます。関数 (function)関数とは、同一の処理を1機能としてまとめ、いつでも呼び出しできるようにする機能です。例えばコードに同じような記述が10回ある場合、同じコードを10回書くのは非効率です。こういった場合に該当するコードを関数化して、いつでも呼び出せるようにします。ちなみに、厳密にはC#に関数は存在せず、それよりも強化された仕組みとしてメソッドが存在します。メソッドは必ずクラスに属する必要があるのですが、今はそこまでの知識は...
値型と参照型の復習
まずは値型と参照型について復習しましょう。
この2つは値を保存する場所が大きなポイントになります。
値型
値型とは用意した領域に直接的に値を保存します。
参照型
参照型は値を別の領域に格納し、それを示す参照値(アドレス)を使って操作します。
値渡し
プログラムには、変数から変数、または関数に値を渡す場面がありますよね。
この時に値を渡すことを値渡しと呼びます(たぶん)。
具体的には次のようなパターンです。
- 変数から変数に代入
- 関数の引数に値を指定
- 関数の戻り値を利用
何気なく書いてると思いますが、この時に値はコピーされてます。
そのコピーにおいて値型と参照型は全く異なる動作をするので、これを理解する必要があります。
1番分かりやすいのが関数で値渡しする場合の例でしょう。
値型を関数に渡す場合、引数に指定した値と同じだけのサイズが新たに確保され、そこに現在の値が全てコピーされます。
つまり引数に渡すサイズが大きいほどコピーに時間を要し、その分だけ処理に遅延が発生します。
これは戻り値が値型の場合も同じです。とは言え、昨今のコンピュータでは数十byte程度は気にするレベルではないです。
加えてもう1つの重要なポイントがあります。それは値渡しで渡した変数と関数内の変数は別物ということです。
これはコピーしてるので当然のことなのですが、例え同じ名前で作った変数でも、それは同じ変数を示しません。
要は関数内で同名の変数の値を変更しても、元の値渡しで利用した変数には一切影響がないのです。
これを確認するサンプルコードがあるので実行してみましょう。
namespace Sample
{
internal class Program
{
static void Main(string[] args)
{
int x = 10;
Console.WriteLine(x);
x100(x); // xの値は変わらない
Console.WriteLine(x);
}
static void x100(int x)
{
x *= 100;
}
}
}
このように値型は関数を経由する時にコピーが行われるため、元の変数には影響が発生しないのです。
では、次は参照型の値渡しを理解しましょう。参照型の場合、コピーされるのは参照値です。
つまり参照値をコピーした場合、関数内の参照値も同じ変数を示します。
これは引数で与える名前が違う場合でも、参照値が示す変数は同一です。
では、その動作を次のサンプルコードで確認してみましょう。
namespace Sample
{
internal class Program
{
static void Main(string[] args)
{
int[] array = new int[] { 10, 20, 30 };
foreach (int i in array)
Console.WriteLine(i);
x100(array); // 元の変数に影響する
foreach (int i in array)
Console.WriteLine(i);
}
static void x100(int[] array)
{
for (int i = 0; i < array.Length; i++)
array[i] *= 100;
}
}
}
参照型である配列の要素が関数を経由することで変化しましたね。
このように値型と参照型では動作に大きなが違いがあります。
メリット・デメリット
当然にお互いにメリットとデメリットがあります。ただ、これを理解するにはスタックやヒープの概念を必要とするため、今回は大雑把に説明します。
まず、スタックやヒープとはメモリを確保する領域の種類です。そしてスタックでメモリを確保するほうが早いです。
じゃあ、全部スタックでいいじゃん。と思っても、そうはいかないのでヒープが存在します。
そしてヒープはメモリの動的確保ができます。動的確保とはプログラムの実行中に必要なサイズのメモリを確保することを言います。
C#に話を戻すと、参照型は動的確保を必要とするためヒープにしか確保できません。対して値型はスタックに確保されます。
ちなみにスタックと名前が付いてる通りでスタック構造をしてます。
プログラムで利用するスタックとキュー、これをコンピュータサイエンスの視点から理解しましょう。汎用的な知識を得ることで、効率のいいコードが書けたり、別のプログラミング言語を学ぶ時の役に立ちます。スタック (stack)データ構造の1つで、リストと同じく要素数が可変するコレクションです。そしてスタックの特徴は要素の格納と取り出しにあります。具体的には、要素を順番に格納するけど、取り出す時は最後に格納した要素から取り出します。この仕組みをLIFO(Last-In First-O...
では、メモリの確保に手間の掛かる参照型のメリットはなんでしょうか。それはデータのコピーを必要としないことです。
先程のサンプルコードを例に説明すると、配列の要素はint型3つで構成されてました。仮にこれが100個あったとしましょう。
この場合は4byte x 100 = 400byteのコピーが発生します。しかし、実際にコピーされたのは参照値です。
そして参照値は64bit環境なら8byteになります。このように無駄にコピーが発生しないので効率がいいです。
このようにお互いにメリットとデメリットがあります。ちなみに変数へのアクセス速度って意味でもスタックのほうが早いです。
string型って参照型?
初心者を混乱に陥れる諸悪の根源です。C#のstring型は参照型になります。
では、ここで次のサンプルコードを実行してみましょう。
namespace Sample
{
internal class Program
{
static void Main(string[] args)
{
string str = "りさ";
Convert(str);
Console.WriteLine(str);
}
static void Convert(string s)
{
s = "LISA";
}
}
}
本当に参照型? 関数内で変更したのに値が変わってないよ。
お気付きの通り参照型なのに値が変わりません。まるで値型です。
なんで?
そういう仕組みだから... 僕も設計者じゃないので細かいことは分かりません。
でも仕様としてそうなってます。気に入らない人は米Microsoftにでも電凸してください。
危険な行為を推奨するのやめてください。
冗談はさておき、これに関してはこういう仕様と理解してもらうしかないです。
文字列に関する処理で疑問を感じたら、これを思い出してください。
参照型の中でも特殊な動作をするstring型ですが、利用頻度はとても多いです。例えば画面上に文字を表示する、動作記録をファイルに残すとか、これらは全て文字列を経由します。ここでは、そんな文字列についての知識を深め、より色々な場面で活用できるようになりましょう。参照型と値型のどっち?結論としてstring型は参照型です。ただし、以前にも伝えた通り動作的には値型です。これはいくら悩んでも意味がなく、C#の言語仕様で決まってるからです。それを確認するサンプルコー...
あとがき
今回の話は非常に重要です。これを知らないまま先に進むと、どこかで必ず詰みます。
全てを覚えろとは言いませんが、値型と参照型の値渡しの挙動の理解は必須です。
少し立ち止まってもいいので、必ず理解して先に進みましょう。
◆ C#に関する学習コンテンツ
この記事は参考になりましたか?
コメント