プログラミングの世界で重要な概念の一つに、データ型があります。 データ型は単に「型」とも呼ばれます。
この記事では値型と参照型の違いを理解するための手助けとなるよう、説明を行っていきます。
データ型の種類
C# プログラミング言語には、値型と参照型の 2 種類のデータ型があります。
参考サイト learn.microsoft.com
プログラム内でいつも利用しているデータ型も、この2つのいずれかに属しています。 これらは変数がどのようにデータを格納し、どのように扱われるかを決定する重要な要素です。
値型(Value Types)
「あたいがた」読みます。
値型はその名の通り、値そのものを保持します。 値型の変数を別の変数に代入すると、その値がコピーされます。 このため、一つの変数に対する変更が他の変数に影響を及ぼすことはありません。
C#の値型には、int、float、boolなどの基本的な型の他に、struct(構造体)も含まれます。 ゲーム制作において頻繁に利用する構造体としては、Vector2 型、Vector3 型、enum 型があります。
値型のサンプルコード
以下に、値型のサンプルコードを提示します。
using UnityEngine; public class ValueTypesExample : MonoBehaviour { void Start() { int var1 = 10; // int型は値型 int var2 = var1; // var2にvar1の値をコピー。この時点ではvar2も10という値を保持 Debug.Log(var2); // Console ビューに 10 と出力 var2 = 20; // var2の値を20に変更 Debug.Log(var1); // Console ビューに 10 と出力 Debug.Log(var2); // Console ビューに 20 と出力 } }
このサンプルコードでは、var1という値型の変数を作成し、それをvar2にコピーしています。 その後でvar2の値を変更しても、var1の値は変更されません。
これは、var1からvar2への代入時に、var1の値がコピーされてvar2に渡されるためです。 したがって、その後にvar2を変更しても、それはvar2のコピーに影響を与えるだけで、元のvar1の値は影響を受けません。
これが値型の主な特性で、このように値型の変数はそれぞれが独立して値を保持していると考えることができます。
特に座標管理を行う際に、Vector3 型の情報を利用して、前の座標を保持しておいて比較する際なども利用されます。 これは、Vector3 型も値型であるため、一度、値を保持しておけば、その元になった値に変更があったとしても、 前の座標の値のまま保持されるためです。
参照型(Reference Types)
「さんしょうがた」と読みます。
参照型は値そのものではなく、メモリ上のオブジェクトへの参照(つまりアドレス)を保持します。 参照型の変数を別の変数に代入すると、その参照(アドレス)がコピーされます。値ではありません。 このため、一つの変数に対する変更が他の変数にも影響を及ぼすことがあります。
C#の参照型には、string、配列、class(クラス)が含まれます。 Unity で数多く用意されている各コンポーネントもクラスです。
参照型のサンプルコード
以下に、参照型のサンプルコードを提示します。
using UnityEngine; public class ReferenceTypesExample : MonoBehaviour { Rigidbody rb1; // クラスは参照型なので、初期値は null void Start() { rb1 = GetComponent<Rigidbody>(); // Rigidbody クラスのインスタンスを参照 rb1.isKinematic = true; // rb1 が参照するインスタンスの isKinematic の値に true を設定 Rigidbody rb2 = rb1; // rb2に rb1 の参照をコピー。この時点で rb1 と rb2 は同じインスタンスを参照 Debug.Log(rb2.isKinematic); // Console ビューに true と出力 rb2.isKinematic = false; // rb2が参照するインスタンスの isKinematic の値を false に変更 Debug.Log(rb1.isKinematic); // Console ビューに false と出力 Debug.Log(rb2.isKinematic); // Console ビューに false と出力 } }
このサンプルコードでは、rb1という参照型の変数を作成し、その参照をrb2にコピーしています。 その後でrb2が参照するインスタンスの値を変更すると、rb1が参照するインスタンスの値も変更されます。 これは、rb1からrb2への代入時に、rb1の参照がコピーされてrb2に渡されるためです。
したがって、その後にrb2を介してインスタンスを変更すると、それはrb1も参照する同じインスタンスに影響を与えます。
これが参照型の主な特性で、このように参照型の変数は同じオブジェクトへの参照を共有することができると考えることができます。
初期値
C#では、変数を宣言すると、それぞれの型に応じたデフォルトの値が自動的に設定されます。 これを初期値(しょきち)と呼びます。
初期値を正しく理解しておくと、変数を宣言した時点でどのような状態になるかを予測しやすくなります。
値型の初期値は、その型に応じて定まっています。 例えば、intやfloatの初期値は0、boolの初期値はfalseとなります。
一方、参照型の初期値はnull(ヌル)です。 これは、何も参照していない状態を表します。
初期値が設定されているとはいえ、それがそのままプログラムに適しているかは状況によります。 具体的な値を必要とする場合、適切な値で初期化することが重要です。
例えば、プレイヤーの HP の値を int 型で表す場合、int 型の初期値の 0 では負けたことになってしまいますね。 初期値ではなく、100 とか 500 といった、適切な値を代入して初期化しておくことが大切です。
null について
参照型の変数がnullの初期値を持つことにより、その変数がまだ有効なオブジェクトを参照していないことがわかります。 そのため、nullである参照型変数に対する操作を行う前には、適切なオブジェクトで初期化(あるいは取得)すること、 必ずnullでないことを確認する(その変数が有効なオブジェクトを指していることを確認する)ことが重要です。
この確認方法を「null チェック」と呼びます。
サンプルコードで null チェックの使い方を2パターン紹介します。
null チェックのサンプルコード①
一般的な null チェックの方法です。 GetComponent メソッドを利用して取得した参照が null ではないかを確認しています。
using UnityEngine; public class NullCheckExample_1 : MonoBehaviour { Rigidbody rb = null; void Start() { // 適切なオブジェクトを取得する rb = GetComponent<Rigidbody>(); // rbがnullでないことを確認してから操作を行う(null チェック) if (rb != null) { rb.mass = 10; } } }
null チェックのサンプルコード②
こちらの方法では TryGetComponent メソッドを利用して、rb 変数へのインスタンスの取得と、null チェックの両方を一緒に行っています。
using UnityEngine; public class NullCheckExample_2 : MonoBehaviour { Rigidbody rb = null; void Start() { // 適切なオブジェクトを取得し、rbがnullでないことを確認してから操作を行う(null チェック) if (TryGetComponent(out rb)) { rb.mass = 10; } } }
以上のことからもわかるように、Unity でも頻繁に目にする機会のある NullReferenceExceptionエラーは、 nullの状態を持つことが可能な参照型に特有のエラーです。 値型の変数はnullを保持することができません(例外もありますが、基本的には保持できません)ので、このエラーを引き起こすことはありません。
この違いを理解することで、エラーメッセージが示す問題を特定するために役立ちます。
なぜなら、NullReferenceExceptionエラーが発生した場合、問題となっているのは参照型の変数またはオブジェクトであり、 それがnullであることが原因である可能性が高いと推測することができます。 よって値型の変数については確認しなくても問題がないことも合わせて分かります。
Null に起因するエラーの場合、エラーの本質を理解しておくことで原因の切り分けを行うことが出来、問題の解決に向けてのデバッグ作業が効率的に進めることが出来ます。
まとめ
値型と参照型はそれぞれ異なる振る舞いをしますので、使い分けが重要です。 プログラミングを進める上で、これらの違いを意識して変数を扱うことが求められます。
値型と参照型の違いを理解し、それぞれが最も適している状況で使用することが、良いコードを書くための基本となりますので、 しっかりと復習して、覚えていくことが大切ですね。