T.TAO
Back to Blog
/16 min read/Programming

Unity Engine #0 C# 基礎 1

#programming
Unity Engine #0 C# 基礎 1

Unity Engine #0 C# 基礎 1

このノートは主に C# 言語についてです。Unity の機能を完全に理解するには、C# 言語から始める必要があります。この部分は型にはまっていますが、プログラムのパフォーマンス向上と動作の理解に役立ちます。

基本概念

C# には2種類の型があります。

値型(Value type): int, bool, float, char, struct, enum

参照型(Reference types): string, object, delegate, interface, class, array

これらには以下の違いがあります:

String 型

特に、string は参照型として一定の特殊性があります。string のあらゆる変更は、実際には新しい string を作成します。new を使って新しい参照型を作成する限り、ヒープに新しい領域が割り当てられます。このとき、スタック内のコピーもヒープ内の新しいオブジェクトを指すため、string は変更され新しいオブジェクトとなり、元の string とは関係がなくなります。

解決策は、文字列を頻繁に変更する場合は、String の代わりに StringBuilder クラスを使用することです。

StringBuilder の基本的な考え方

おそらく拡張をサポートする char 配列です。スペースが不足すると、元の配列サイズの容量を確保します。新しく作成された配列は使い切った前の配列を指し、GC 自体は呼び出されません。

Garbage Collector / GC

Garbage Collector(GC と略称)は、C# に付属するガベージコレクタです。これらは、もう使用されないメモリの内容を管理する責任があります。詳細は Unity Engine #1 メモリのセクションを参照してください。ここではいくつかの概念を簡潔に説明します。

  1. メモリ管理プール:上述の通り、Unity にはメモリヒープ(heap)とメモリスタック(stack)の2つの内部部分があります。Stack は小さく一時的なデータの格納に使用し、Heap は大きく永続的なデータの格納に使用します。
  2. 変数がアクティブである限り、対応するメモリ使用量は Allocated されます。
  3. 変数がアクティブでなくなり、占有するメモリが不要になると、回収されます。Stack 上の回収は高速ですが、Heap 上のガベージは即座に回収されません。そのメモリは使用中としてマークされます。使用されなくなったメモリは GC フェーズでのみ回収されます。

GC 操作はパフォーマンスに大きく影響するため、GC を避けるように努めるべきです。

ボクシング / アンボクシング

ボクシング(Boxing) とは、値型を object 型、またはこの値型が実装する任意のインターフェース型に変換するプロセスを指します。

実際の操作:

  • Heap に行き、Object クラスのオブジェクトを new する。
  • 値型データを Object クラスオブジェクトに格納する。
  • Heap 上に作成されたオブジェクトのアドレスを参照型変数に返す。

逆のステップがアンボクシング(Unboxing)です。アンボクシング は object 型から値型(またはインターフェース型)への明示的な変換です。

  • ボクシングされたオブジェクトのアドレスを取得し、オブジェクトインスタンスをチェックして、指定された値型のボクシングされた値であることを確認する。
  • 値をインスタンスから値変数型にコピーする。

ボクシングとアンボクシングは実際に多くの GC を生成します。Unity のボクシング操作では、Heap 上に System.Object 型の参照が割り当てられてカプセル化され、対応するキャッシュがメモリガベージを生成します。コード内で変数に対する直接的なボクシング操作がなくても、関数やサードパーティプラグインにこのような現象が存在する可能性が高いです。ボクシングとアンボクシング操作をできるだけ減らすように努めるべきです。

方法:

  1. ジェネリクスを使用する。
  2. yield return 0 の代わりに yield return null を使用する。
  3. 不要な Log を減らす。

オブジェクト指向プログラミング

周知の通り、C# は C++ と同様に Object Oriented Programming(オブジェクト指向プログラミング)です。オブジェクト指向言語には多くの重要な特徴があります。

カプセル化

カプセル化(Encapsulation) とは、コードがデータを変更できる範囲を制限することでデータの安全性を高めることを指します。オブジェクトの内部状態と機能実装の詳細を隠蔽し、必要な操作インターフェースプロセスのみを公開します。

C# では、アクセス修飾子を使用してメンバーのアクセスレベルを制御することでカプセル化を実現します。例えば:

  • public: 任意のクラスとメンバーに公開、アクセス制限なし。
  • private: 現在のクラスにのみ公開。
  • protected: 現在のクラスとその派生クラスに公開。
  • internal: このクラスを含むアセンブリ内でのみアクセス可能。

protected 修飾子の動作に注意してください。

Plain Textpublic class BaseClass
{
    protected int protectedField;

    public void BaseMethod()
    {
        protectedField = 10; // accessible from the current class
    }
}

public class DerivedClass : BaseClass
{
    public void DerivedMethod()
    {
        protectedField = 20; // accessible from the derived class
    }
}

public class AnotherClass
{
    public void AnotherMethod()
    {
        BaseClass baseClass = new BaseClass();
        // baseClass.protectedField = 30; // will result in a compiler error
    }
}

ここで、AnotherClass は BaseClass の派生クラスではないため、protected で修飾された int 値型変数 protectedField は BaseClass と DerivedClass によってのみアクセスでき、AnotherClass ではアクセスできません。

継承

継承(Inheritance) はもう一つの重要な特徴です。継承により、新しく作成されたクラス(派生クラス)が既存のクラス(基底クラス)のプロパティとメソッドを継承できます。

C# では単一継承のみサポートされており、各クラスは1つの基底クラスからのみ継承できますが、複数のインターフェースを実装できます。

最も簡単な例は、Unity で新しいスクリプトを作成すると、MonoBehavior クラスを継承することです。したがって、新しいスクリプトでは MonoBehavior に既に定義されている関数を使用できます。

Plain Textpublic class Cube : MonoBehavior { 
    void Start(){}
    void Update(){}
}

ここで、Cube は MonoBehavior の派生クラスです。MonoBehavior は Cube の基底クラスです。

注意:新しいスクリプトで書いたこれらの MonoBehavior 関数の具体的な実装は、一見オーバーライドのように見えますが、実際には MonoBehavior クラスで void メソッドや abstract メソッドとして明確に定義されていません。これらは実際には Unity エンジンが特定のメカニズムを通じて特別に呼び出します。Unity エンジンは実行時に MonoBehavior の派生クラスに Start() や Update() のような特定の関数が存在するかチェックし、存在する場合は規定の時間にこれらの新しい関数を自動的に呼び出します。これはオブジェクト指向の継承ではなく特殊なリフレクション機構であるため、以下の多態性で言及する override キーワードは不要です。

C# では、sealed キーワードを使用して他のクラスがそのクラスを継承するのを防ぐこともできます。メソッドで sealed キーワードを使用すると、他のクラスがそのメソッドをオーバーライドするのを防げます。この機構を**シーリング(Sealing)**と呼びます。

多態性

多態性(Polymorphism) とは、同じ操作が異なるクラスインスタンスで異なる実装を持つことができることを意味します。

C# では、多態性はメソッドオーバーライド修飾子 override とインターフェース実装によって実現されます。派生クラスオブジェクトをその基底クラス型のオブジェクトとして扱いながら、固有の動作を保持することを可能にします。

この例で多態性を説明できます。

Plain Text// Base Class
public class Animal
{
    public virtual void MakeSound()
    {
        Console.WriteLine("Some sound");
    }
}

// Derived Class Dog
public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Bark");
    }
}

// Derived Class Cat
public class Cat : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Meow");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Animal myDog = new Dog();
        Animal myCat = new Cat();

        myDog.MakeSound(); // Output "Bark"
        myCat.MakeSound(); // Output "Meow"

        Animal[] animals = new Animal[2];
        animals[0] = new Dog();
        animals[1] = new Cat();

        foreach (var animal in animals)
        {
            animal.MakeSound(); // Output "Bark" and "Meow" respectively
        }
    }
}

基底クラスは Animal で、Dog と Cat はその派生クラスです。基底クラスは関数 MakeSound を定義し、異なる派生クラスで override を通じてこの関数をオーバーライドし、それぞれ独自の定義を持たせます。基底クラスを通じて直接呼び出すと、派生クラスの MakeSound メソッドが自動的に呼び出されます。

抽象化

C# では、抽象クラス(インスタンス化できないクラス)とインターフェース(実装のないメソッドのセットを定義する)を通じて抽象化を実現できます。

抽象クラスは抽象メソッド(実装のないメソッド)と具体メソッド(実装のあるメソッド)を含むことができます。コンストラクタ、フィールド、メソッド、プロパティなどのメンバーを含むことができます。抽象メソッドは抽象クラスでのみ宣言でき、非抽象の派生クラスでオーバーライドする必要があります。

以下の例は抽象クラスのコード例です。上記の多態性のコードと比較できます。実際の重点は関数の抽象化にあります(抽象クラスには非抽象メソッドも含めることができます)。

Plain Textpublic abstract class Animal
{
    public abstract void MakeSound();

    // Abstract Class may have non-abstract functions
    public void Eat()
    {
        Console.WriteLine("Animal is eating.");
    }
}

public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Bark");
    }
}

public class Cat : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Meow");
    }
}

この例では、MakeSound が唯一の抽象メソッド(abstract 修飾が必要)です。Eat は具体メソッドです。抽象メソッドは派生クラス Dog と Cat で具体的に実装する必要があります。

インターフェース(Interface) は完全に抽象的な構造で、メンバーのシグネチャのみを定義し、実装は含みません。ただし、インターフェースはフィールド、コンストラクタ、デストラクタを含むことができません。すべてのメンバーはデフォルトで public であり、アクセス修飾子を含むことができません。

以下の例はインターフェースのコード例です。

Plain Textpublic interface IVehicle
{
    void StartEngine(); 
    void StopEngine();  
    void Drive();       
}

public class Car : IVehicle
{
    public void StartEngine()
    {
        Console.WriteLine("Car engine started.");
    }

    public void StopEngine()
    {
        Console.WriteLine("Car engine stopped.");
    }

    public void Drive()
    {
        Console.WriteLine("Car is driving.");
    }
}

public class Boat : IVehicle
{
    public void StartEngine()
    {
        Console.WriteLine("Boat engine started.");
    }

    public void StopEngine()
    {
        Console.WriteLine("Boat engine stopped.");
    }

    public void Drive()
    {
        Console.WriteLine("Boat is sailing.");
    }
}

具体クラスがインターフェース内の具体関数を実装します。ただし、インターフェース自体はフィールドを持つことができず、抽象メソッドのみ持つことができます。

対比すると、インターフェースはクラスによって実装され、抽象クラスはクラスによって継承されます。インターフェースはメソッドを宣言するだけで実装を持てませんが、抽象クラスは具体実装を持つことができます。1つのクラスは複数のインターフェースを実装できますが、1つのクラスからのみ継承できます。抽象クラスは異なるアクセスレベルを持つことができますが、インターフェースのすべてのメンバーはデフォルトで public です。

構造体(Struct)

Struct はクラスの構造と非常に似ていますが、多くの本質的な違いがあります。

クラスとの違い

  1. Struct は値型でスタックに格納され、class は参照型でヒープに格納されます。したがって、Struct はアクセス速度が速く容量が小さく、点座標、矩形、円、色などの軽量オブジェクトにのみ適しています。
  2. Struct はパラメータなしコンストラクタを宣言できませんが、クラスはできます。
  3. Struct 定義時、メンバーは初期化できません。構造体を定義する際、すべてのメンバーを自分で代入して初期化する必要があります。
  4. 構造体を宣言した後、new で構築オブジェクトを作成することも、new なしで作成することもできます。new なしの場合、すべてのフィールドを初期化するまで、フィールドは未代入のままでオブジェクトは使用できません。
  5. Struct は継承できません。
  6. Struct は static で修飾できませんが、クラスはできます。

参考文献

  1. Unity Game Development Client Interview - C# (Elementary): https://blog.csdn.net/Sea3752/article/details/127354146?spm=1001.2014.3001.5501