渋谷ほととぎす通信

エンジニア社長によるUnityとAIのブログ & エンジニアの生存戦略

【Unity】構造体って何?クラスとの違い・よくあるトラブル解説

【Unity】構造体って何?クラスとの違い・よくあるトラブル解説

こんにちは、Unityエンジニアのオオバです。

お悩みさん
お悩みさん
  • クラスと構造体の違いが分からない
  • 構造体の使い所ってどこ?
  • 構造体のメリデメを知りたい
  • オオバ
    オオバ
    本記事ではこれらの悩みを解決します。

    Unityでプログラミングしていると登場する構造体。クラスと似ているけど、な〜んか挙動が違いますよね。

    実は構造体とクラスは全く別物で、それぞれの性質を知らないと大きなトラブルに発展する可能性があります。そこで本記事ではUnityプログラミングで使うC#における構造体とクラスの違いを解説していきます。

    今まで全てクラスで実装していた人も構造体の特性を知ることでよりゲームのパフォーマンスが高くなる可能性もあります。また構造体特有のトラブルもあるのでぜひ最後まで読んでみてください。

    👉DOTweenの教科書を読んでUnityアニメーションをプログラミングしてみよう!

    構造体とは?

    そもそも構造体とは何かというと、一言で表すと「値」です。

    お悩みさん
    お悩みさん
    値ってint型やfloat型のことじゃないの?

    その通り!構造体はint型やfloat型と同じ「値」なのです。わかりやすくするためにコードで確認していきましょう。

    public struct HogeStruct  
    {
        public int foo;  
    }
    

    構造体は「struct」キーワードを使って宣言します。

    上記の場合 「int型の変数fooを宣言したHogeStruct構造体」 ということになります。

    構造体の生成方法

    構造体はクラスと同じくnewキーワードで生成します。

    // newキーワードで生成  
    HogeStruct hoge = new HogeStruct();  
    // 変数に値を代入  
    hoge.foo = 123;  
    Debug.Log(hoge.foo); // 出力:123  
    

    クラスと同様、インスタンスを生成し、変数に値を代入することができます。

    お悩みさん
    お悩みさん
    構造体もクラスも同じじゃない?

    ここまで見る限り、構造体もクラスも同じもって思いますよね。しかしここからが構造体とクラスに大きな違いが生まれてきます。

    構造体はコピーされてしまう

    次のコードを見ていきましょう。さきほど作成した変数hogeを別の変数barに代入してみました。

    HogeStruct hoge = new HogeStruct();  
    hoge.foo = 123;  
    
    // hogeを別変数に代入  
    HogeStruct bar = hoge;  
    // 変数fooを書き換えた  
    bar.foo = 456;  
    
    Debug.Log($"{hoge.foo} / {bar.foo}");  
    

    hogeをbarに代入し変数fooの値を書き換えています。Debug.Logで何が出力されるのか。

    【Unity】構造体って何?クラスとの違い・よくあるトラブル解説_0

    上図のように 123/456 と出力されるのです。つまり 変数hogeと変数barは別物 になってしまったということです。

    クラスの場合は参照渡し

    同じ処理をクラスで実装してみましょう。

    先と同じように 「int型の変数fooを定義したHogeClassクラス」 を宣言しました。

    public class HogeClass  
    {
        public int foo;  
    }
    

    先と同様に構造体をクラスに置き換えたサンプルコードです。

    HogeClass hoge = new HogeClass();  
    hoge.foo = 123;  
    
    // hogeを別変数に代入  
    HogeClass bar = hoge;  
    // 変数fooを書き換えた  
    bar.foo = 456;  
    
    Debug.Log($"{hoge.foo} / {bar.foo}");  
    

    結果はどうなるでしょうか。

    【Unity】構造体って何?クラスとの違い・よくあるトラブル解説_1

    上図のようにどちらも「456」と出力されます。これは hogeとbarが同じもの だからです。

    ReferenceEqualsでクラスのインスタンスを比較

    本当にhogeとbarが同じものなのか確認してみます。

    // ReferenceEqualsで両者を比較  
    bool isSameObject = ReferenceEquals(hoge, bar);  
    Debug.Log($"isSameObject : {isSameObject}");  
    

    ReferenceEquals 関数に両者を代入すると、同じものかどうかをチェックしてくれます。

    【Unity】構造体って何?クラスとの違い・よくあるトラブル解説_2

    上図のようにhogeとbarは同じものだということが分かりました。

    つまり、 構造体は「値渡し」クラスは「参照渡し」ということ なのです。この特徴をしっかりと理解しておきましょう。

    Unityでよく使う構造体とは?

    Unityで開発しているときによく登場する構造体はVector系です。

    • Vector2, Vector2Int
    • Vector3, Vector3Int
    • Vector4

    上記のようなVectorは構造体なので、前章で紹介した値を渡した際の特性をしっかりと理解しておかないといけません。

    var position1 = new Vector3(1f, 2f, 3f);  
    
    var position2 = position1;  
    position2.x = 10f;  
    
    Debug.Log($"{position1} / {position2}");  
    

    position1変数をposition2変数に代入し「x」の値だけを更新してみました。

    【Unity】構造体って何?クラスとの違い・よくあるトラブル解説_3
    するとposition1とposition2は別物なので、別々の値が出力されるのです。

    構造体は値を渡した後は別物になってしまう(コピーされてしまう)ため、実装する際は注意しましょう。

    Vector以外でよく使われるUnityの構造体

    Vector系以外だと以下の構造体がよく使われます。

    • Quaternion
    • Matrix4x4
    • Color, Color32
    • Rect, RectInt
    • Ray, RayCastHit
    • Plane
    • LayerMask
    • ParticleSystemの各モジュール

    など。

    クラスに比べると構造体は少ないですが、要所要所で登場します。クラスと同じように扱っているとトラブルの元なので注意して使っていきましょう。

    構造体とクラスはメモリの使い方違う

    構造体の特徴としてクラスと比較してメモリの使い方が違う場合がある点です。条件付きではありますが構造体はスタックメモリ、クラスはヒープメモリを使用します。

    スタックメモリとは

    スタックメモリは一時的なメモリの領域で高速に動作するのが特徴の1つです。例えば関数内で使用する値型のローカル変数はスタックメモリを確保します。また確保されたスタックメモリは開発者が解放する必要がなく、使い終わると自動で解放されるのも大きな特徴です。

    例えば関数内でローカル変数を使用していた場合は、関数実行時にスタックメモリを確保し、関数の実行が終われば自動的にメモリは解放されるという感じです。

    ただし、一時的なメモリ領域であるため、あまり大きなサイズのデータは扱えません。

    ヒープメモリとは

    一方ヒープメモリはデータの出し入れが可能なメモリ領域です。スタックメモリと違い長期間保持することが可能。newでクラスのインスタンスを生成した場合や配列はヒープメモリ上に確保されます。ただし自由に使える反面、開発者が明示的に片付ける必要があります。

    具体的には使用済みの変数にnullを代入して使用済みにします。ヒープメモリは使い終わった途端に解放するわけではありません。GC(ガベージコレクション)と呼ばれるメモリ管理の仕組みで解放されます。

    余談になりますが、このGCが曲者で負荷が高くなりがちです。GCが発生するたびにゲーム中のカクつきにつながるため、できるだけGCが発生しないように実装します。

    こんな感じで構造体とクラスではメモリ確保先が異なる場合があるということも覚えておきましょう。

    構造体だからといって必ずスタックメモリを確保するわけではない

    構造体だから必ずスタックメモリを使うというわけではありません。以下のケースではヒープメモリに確保されます。

    • クラス内フィールドの構造体の場合
    • 配列の要素にした場合

    クラス内フィールドの構造体の場合

    以下のコードを見てみましょう。クラスHogeの中に構造体Fooの変数を宣言した場合です。

    public struct Foo  
    {
        public int id;  
    }
    
    public class Hoge  
    {
        public Foo foo;  
    }
    

    この場合、構造体Fooの変数fooはスタックメモリではなくヒープメモリ上に確保されます。構造体だからといってスタックメモリを使わない一例です。

    配列の要素にした場合

    次に構造体を配列の要素にした場合です。

    public struct Foo  
    {
        public int id;  
    }
    
    Foo[] foos = new Foo[10];  
    

    構造体の配列はヒープメモリ上に確保されるということを覚えておきましょう。

    構造体のよくあるトラブル

    ここからは構造体でよくあるトラブルについて解説していきます。

    error CS1612: Cannot modify a value type return value of `....'.  
    Consider storing the value in a temporary variable  
    

    上記のエラーに出会ったことはありませんか?

    下記のようにSampleという構造体を作成しました。

    // 構造体Sample  
    public struct Sample  
    {
        public int y;  
    }
    

    構造体Sampleはint型の変数「y」を1つだけ宣言したシンプルな構造です。ではこの構造体を生成し、アクセスしていこうと思います。

    構造体をプロパティDataとして宣言。

    class Hoge  
    {
        public Sample Data { get; private set; }  
    
        void Foo ()  
        {
            // 個々でエラー  
            Data.y = 1;  
        }
    }
    

    関数Foo内でDataの変数yを書き換えようとするとコンパイルエラーが起きます。

    これはどういうエラーかというと、プロパティで宣言した構造体の中身を直接変更できないためです。

    正しくは以下のように記述します。

    void Foo ()  
    {
        var temp = Data;  
        temp.y = 1;  
        Data = temp;  
    }
    

    一度コピーを変数に代入し、変数の中身を書き換えてプロパティに代入するとうまくいきます。

    Unityでよくある構造体トラブル

    実際Unityで開発中には以下のようなコードでエラーが起きます。

    void Bar ()  
    {
        transform.localPosition.x = 10f;  
    }
    

    Vector3型のlocalPositionはTransformのプロパティであるため同様のエラーが出力されます。正しくは以下です。

    void Bar ()  
    {
        var pos = transform.localPosition;  
        pos.x = 10f;  
        transform.localPosition = pos;  
    }
    
    

    上記のようにVector3型を直接localPositionに代入すると正常に動作します。

    構造体にすべきか、クラスにすべきか

    構造体にすべきか、クラスにすべきか悩むケースがありますが、Microsoft公式にはこのようにあります。

    プリミティブ型のような単一の値を論理的に表す (int, 、double, などです。)。
    ・16 バイト未満のサイズのインスタンスがあります。
    ・変更可能なことはできません。
    ・頻繁にボックス化することはありません。

    クラスまたは構造体の選択 - Framework Design Guidelines | Microsoft Docsより

    構造体は容量の少ないスタックメモリに積まれ、値型であるためアクセスする度にメモリ内にコピーされます。構造体内の変数が多ければ多いほど メモリコピーの負荷が上がる ということです。

    要するに構造体の容量を小さくする必要があるのです。

    まとめ:結局はケースバイケース

    構造体を使うかクラスを使うか迷うことは多いと思います。結論構造体を使わなくてもゲームは作れますが、構造体を活用することでよりパフォーマンスを発揮できるケースがあるのも事実です。

    • スタックメモリの特性
    • ヒープメモリの特性
    • 構造体のメモリコピーの負荷

    これらを天秤にかけ、実際に数値を計測して決めて行くことになるかなと思います。実際に試してもあまり効果はなかったっていうこともよくあります。何度も試してベストな実装を模索してもらえたらなと思います。

    この記事があなたのゲーム開発に役立ったら嬉しいです。

    Unityオブジェクトの描画順の制御って難しいですよね。
    この度、Unityの描画順を体系的に学べる「Unity描画順の教科書」を執筆しました。

    Unityの描画順を基礎から学びたい方はぜひ確認してみてください!
    Unity描画順の教科書

    最後まで読んでいただきありがとうございました!
    すばらしいUnityライフをお過ごしください。

    オススメ記事
    検証環境
    • Unity6000.0.32f1