[Unity] structをちょっと分かるようになりたい #2
はじめに
この記事は前回の記事の続きです。
普段Unityのゲーム開発でメモリ管理を意識している人からしたら、ビックリするくらい当たり前のような内容しか書いていません。
前回の記事よりも分からない部分が多くなると思います。
この記事を読む方はナイフを研がないで、やさしい気持ちで呼んでください。研いでしまう方はブラウザバックを推奨します。
参考にした記事
前回のおさらい
structはclassと違った部分かなりあります。
・継承ができないけどinterfaceを実装できる
・フィールド変数を初期化できない verによる
・コンストラクタに引き数やフィールド変数の初期化が必要 verによる
C#のバージョンが10.0以降だと一部制限が緩和されます。
具体的にできる/できないの一覧は下記の記事に掲載されています。一応引用?で表を出します。
そして、前回は下のソースコードを用意してstructとclassの結果の違いを確認しました!
using UnityEngine;
public class Test : MonoBehaviour
{
void Start()
{
TestStruct testStruct = new();
// 数値を増やす
Increment(testStruct);
Debug.Log(testStruct.X);
// 数値を引き数に代入
Equal(testStruct, 5);
Debug.Log(testStruct.X);
}
private void Increment(TestStruct testStruct)
{
testStruct.X++;
}
private void Equal(TestStruct testStruct, int num)
{
testStruct.X = num;
}
}
public struct TestStruct
{
public int X;
}
結果としては、structでは数値は変わらず0という結果でしたが、classでは加算された5が出てきたという所までやりました!
おさらいが大体終わったので、前回の参照渡しや値渡し~がと書いていた部分から再開です!
参照渡しと値渡しってなに…?
タイトルの内容を説明する前に、もう一度ソースコードを実行します!分かりやすくするためにソースコードを少し修正しました!
最初にclassで数値を加算!
using UnityEngine;
public class Test : MonoBehaviour
{
private TestInt testInt = new();
void Start()
{
// classが持っている数値に加算する
testInt.X += 5;
// 一度数値を確認する
Debug.Log(testInt.X);
// TestIntを変数aにコピーして、aの数値を加算する
var a = testInt;
a.X += 95;
// aの値を確認する
Debug.Log(a.X);
// classの値を確認する
Debug.Log(testInt.X);
}
}
public class TestInt
{
public int X;
}
上の実行結果が下の画像になります。
結果としては最初にTestIntに入れた5、そして変数aにTestIntを代入した後にaに95の数値を加算します。そしてaとTestIntの数値をログで出しています!
結果としては、aもTestIntも数値が100になっていることが分かりました。
次にstructで数値を加算!
TestIntのclassをstructに変えただけなのでソースコードは省略します!
そして実行結果はこちら…?
さっきと全然違いますね…!やっていることは変わっていません。
変数aにTestIntを代入してaの数値に95を加算して、aとTestIntの数値をログに出してます。結果としてはa = 100、TestInt = 5となり、数値が違います。
ここで、参照渡しと値渡しの違いが出てきていますので違いを図を元に解説します。
参照渡しの場合、コピーを作成してコピーの値を変更しても、コピー元の変数に変更が反映されます。
言葉にすると少しややこしいですが、図にすると分かりやすいです!
下記記事のイラスト説明を参考にしてます。真似…?になっちゃうけど大丈夫かな… https://wa3.i-3-i.info/diff345programming.html
Aさんが管理している果物の場所が分かるメモをBさんが手に入れます。
クラスで置き換えると、class A(public string Apple,Orengeを管理している)を変数Bに代入した感じです。
遠回りせずに答えを書くと、class Aはpublic変数でAppleとOrengeを公開しています。つまり、Aの情報知っている変数Bから値の変更ができます。
図で行くと、string Orengeを"none"とかにしたんでしょうね。
このようにコピーの値を変更しても、コピー元に反映されるのが参照型です。
ここで、最初に紹介したソースコードを確認します。変数aにtestIntを代入してコピーを行ったという部分ですね。
// TestIntを変数aにコピーして、aの数値を加算する
var a = testInt;
a.X += 95;
// classなので参照型
public class TestInt
{
public int X;
}
正確にはaにtestIntの内容をコピーしたというより、testIntの場所を教えただけです。なので、aの値を変更するというのは、aが知っているtestIntの値を変更するという形になります。
もしaに何かしらの情報を入れていない場合、UnityからNullReferenceExceptionのエラーで怒られます。
そして、Unityでよく見かけるNullReferenceExceptionエラーは参照型の変数にアクセスして空だった時に出てくるエラーです。
普段ゲーム開発で使っている変数やコンポーネント達は実は参照型だった…(最近知った)
使った記憶が無いと思うかも知れませんが、Unityでゲーム開発をしている人は知らない間に使ってます。特に座標移動でTransformとか使ってい
せんか…これも先ほど紹介した図と同じ状態になってるんですよね。
Transform myTransform;
void Start()
{
// Transformを入れる
myTransform = transform;
// myTransformの座標を変更する
myTransform.position = new Vector3(10, 0, 0);
}
Transformにカーソルを合わせると、中身がclassだったということが分かります。知らない間に参照型を利用していたんだ。というかTransformってclassだったんだね。
じゃあ値渡しはどうなるのか…
値渡しは、コピー元のその時点の情報をコピーして別の存在になります。
これも図にしちゃいましょう!
正直、図のままです。コピーが終われば別の存在として扱われるので、コピーの値をどれだけ変更しても、コピー元の値が変わることはありません。
PlayerのHPで想像すると分かりやすいです。
hpにmaxHPを代入して、その後にダメージを受けてhpが10減ったみたいな感じです。
public class Player : MonoBehaviour
{
private int hp;
private int maxHP = 100;
void Start()
{
hp = maxHP;
hp -= 10;
}
}
ソースコードではhpの数値が減りましたが、maxHPの数値は減りません。これは、hpとmaxHPが別の存在として扱われているからです。
ここまでの流れから、コピーが終わったら別の存在になるのが値渡しという覚え方でも良いのではないでしょうか。
そして、intの中身を確認すると正体がstructということが分かります。
実は値型だったという分けですね…
そしてVector系統もstruct…
知らないって正直怖いですね。
まとめ
・参照渡し
コピー元の場所をコピー先に教える
コピーの値を変更すると元の値も変更される(元の値を変更しているだけ)
・値を渡し
コピー時点の情報を渡したら別物として扱われる
コピーの値を変更しても、コピー元の値は変わらない
おわりに
本当に長文になってしまい申し訳ない。認識が間違っている部分もあるかも知れないですが、おおよそは正しいと思います(多分)
正直まだ内容が物凄くあって…ヒープやスタックとかboxingとか…まだまだ
ただ、この記事が物凄くなりそうなので一度切ろうと思います。
正直、調べれば調べるほど深そうな情報が出てくるので内心面白いと思ってます!スローペースな記事になりそうですが、お付き合いいただけると幸いです。