🧐

【オブジェクト指向/C#/Unity】結局インスタンスってなんなのよ

に公開

「オブジェクト」「クラス」「インスタンス」「コンストラクタ」「メソッド」...

プログラミングはただでさえコードでおまじないが多いのに、知識もカタカナ満載のおまじないが多くてやんなっちゃう。

「インスタンスって何?」に答えられる?

2年間ぐらいゲームを作ってきて最近やっとオブジェクト指向プログラミングの概念が納得できるようになってきました。でも先日ゲーム制作配信をしてたら「インスタンスって何?」と言われて、うまく分かりやすく説明できませんでした。よくある「クラスは設計図、インスタンスは設計図から作った製品」という説明は、思えば理解できた人からしたら分かりやすいかもしれないけど、新しく学ぶ人には分かりにくいのかもしれないと思いました。

その時うまく説明できなかったのをリベンジすべく(笑)自分の理解を書き留めていきます。

設計図の例の方がわかりやすい、とかそっちとあんまり変わらん、とかあるかもしれないですが...

対象読者

この文章は、オブジェクト指向について学び始めたばかりの方を対象にしています。C#を例に挙げていますが、他の多くのプログラミング言語でも基本的な概念はほぼ同じです。最後に、Unity特有の話題にも触れる予定ですので、Unityを使う方にも役立つ内容となっています。

自動販売機で考えるオブジェクト指向

飲み物とその機能

まず、自動販売機に入っている飲み物を考えます。この飲み物には次の2つの特徴があります。

  • データ(状態): 飲み物の中身(例: コーラ、オレンジジュース、緑茶など)
  • 機能: 喉の渇きを癒すこと

クラスとインスタンスの対応

  • クラス: 自動販売機に表示されている商品のパネル
    • パネルには、飲み物がどんな中身を持っているのか(データ)や、どんな効果があるのか(機能)が記されています。
    • しかし、パネルそのものを飲むことはできません。あくまで「どんな飲み物か」を定義しているだけです。
  • インスタンス: 自動販売機から出てくる実際の飲み物
    • パネルのボタンを押すと、飲み物が手元に出てきます。この「出てきた飲み物」がインスタンスです。
    • 実際に喉の渇きを癒すことができるのは、このインスタンスだけです。

プログラムに置き換えると?

この例をプログラムに置き換えると、次のようになります。

// クラス定義
public class Drink
{
    public string name = "コーラ";  // データ
    public void QuenchThirst()
    {     // 機能
        Console.WriteLine($"{Name}が喉の渇きを癒した!");
    }
}

// インスタンスの生成
Drink cola = new Drink(); // 実際の飲み物(インスタンス)
cola.QuenchThirst(); // 出力:コーラが喉の渇きを癒した!

ここで、Drinkというクラスが自動販売機のある飲み物のパネルに該当し、colateaというインスタンスが実際に出てきた飲み物に該当します。それぞれのインスタンスは独立しており、異なるデータを持ちながら同じ機能を果たします。

Drink cola = new Drink(); // インスタンスを生成する

さてここ、newって何でしょうか?なぜDrink()がついているのでしょうか?

newはC#でクラスからインスタンスを生成するためのキーワードです。Drink()と書くことで、このクラスに対応するインスタンスが生成され、同時に初期化処理が行われます。クラス内で定義された特別なメソッド、コンストラクタがこの初期化を担当します。

以下に、コンストラクタを具体的にどのように記述するのかを見ていきましょう。

コンストラクタとは?

C#でコンストラクタの書き方

コンストラクタはクラス内で定義され、インスタンス生成時に自動的に呼び出されます。C#ではコンストラクタはコードで書かなくてもデフォルトのものが存在するので、わざわざ書く必要はありません。デフォルトのコンストラクタを使えば、作られるインスタンスはクラスで定義された変数や関数を自動で持つことになります。

コンストラクタを自分で書くのはクラスの持つ変数をインスタンスを外部から初期化(最初に決めておくこと)したい時です。

C#ではコンストラクタは初期化する変数を引数に持ち、クラスのインスタンスを返り値に持つメソッドです。普通のメソッドと違って名前はつけず、コンストラクタの名前はクラスの名前と同じになります。

// クラス定義
public class Drink {
    public string Name { get; private set; }

    // コンストラクタ
    public Drink(string name) {
        Name = name; // 名前を初期化
    }

    public void QuenchThirst() {
        Console.WriteLine($"{Name}が喉の渇きを癒した!");
    }
}

// インスタンスの生成
Drink cola = new Drink("コーラ"); // コンストラクタに"コーラ"を渡す
cola.QuenchThirst(); // コーラが喉の渇きを癒した!

Drink tea = new Drink("緑茶"); // 別の飲み物を生成
tea.QuenchThirst(); // 緑茶が喉の渇きを癒した!

このコードでは、Drinkクラスのコンストラクタが、インスタンスを生成する際に名前を受け取り、その名前をNameプロパティに設定しています。

普通の自販機: 引数を持たないコンストラクタ

普通の自販機では、ボタンを押すだけでデフォルトの飲み物(例: コーラ)が出てくるとします。この場合、飲み物を初期化する際に特別な情報を指定する必要はありません。以下は、その例です。

// クラス定義
public class VendingMachineDrink {
    public string Name { get; private set; }

    // 引数を持たないコンストラクタ
    public VendingMachineDrink() {
        Name = "コーラ"; // デフォルト値を設定
    }

    public void QuenchThirst() {
        Console.WriteLine($"{Name}が喉の渇きを癒した!");
    }
}

// インスタンスの生成
VendingMachineDrink drink = new VendingMachineDrink();
drink.QuenchThirst(); // コーラが喉の渇きを癒した!

コーヒーメーカー: 引数を持つコンストラクタ

一方で、コーヒーメーカーでは「コーヒーの種類(例: ラテ、エスプレッソ)」「砂糖の有無」「サイズ」など、さまざまな選択肢を指定する必要があります。この場合、引数を持つコンストラクタを使って初期化を行います。

// クラス定義
public class CoffeeMachineDrink {
    public string Type { get; private set; }
    public bool HasSugar { get; private set; }
    public string Size { get; private set; }

    // 引数を持つコンストラクタ
    public CoffeeMachineDrink(string type, bool hasSugar, string size) {
        Type = type;
        HasSugar = hasSugar;
        Size = size;
    }

    public void Serve() {
        string sugarText = HasSugar ? "砂糖入り" : "砂糖なし";
        Console.WriteLine($"{Type}{sugarText}{Size})をお楽しみください!");
    }
}

// インスタンスの生成
CoffeeMachineDrink latte = new CoffeeMachineDrink("ラテ", true, "トール");
latte.Serve(); // ラテ(砂糖入り、トール)をお楽しみください!

CoffeeMachineDrink espresso = new CoffeeMachineDrink("エスプレッソ", false, "ショート");
espresso.Serve(); // エスプレッソ(砂糖なし、ショート)をお楽しみください!

オーバーロードとは?

オーバーロード(Overload)は、同じ名前のメソッドやコンストラクタを、異なる引数リストで複数定義することです。これにより、状況に応じた使い分けができる柔軟な設計が可能になります。コンストラクタのオーバーロードを活用することで、同じクラスに対してさまざまな初期化方法を提供できます。

例えば、自販機が複数の飲み物を提供する場合、引数を持たないコンストラクタではデフォルトの飲み物を設定し、引数を持つコンストラクタでは特定の飲み物を指定できるようにします。

// クラス定義
public class MultiDrinkVendingMachine {
    public string Name { get; private set; }

    // 引数を持たないコンストラクタ
    public MultiDrinkVendingMachine() {
        Name = "コーラ"; // デフォルト値
    }

    // 引数を持つコンストラクタ(オーバーロード)
    public MultiDrinkVendingMachine(string name) {
        Name = name; // 引数で指定された飲み物を設定
    }

    public void QuenchThirst() {
        Console.WriteLine($"{Name}が喉の渇きを癒した!");
    }
}

// インスタンスの生成
MultiDrinkVendingMachine defaultDrink = new MultiDrinkVendingMachine();
defaultDrink.QuenchThirst(); // コーラが喉の渇きを癒した!

MultiDrinkVendingMachine customDrink = new MultiDrinkVendingMachine("オレンジジュース");
customDrink.QuenchThirst(); // オレンジジュースが喉の渇きを癒した!

Unityのお話

MonoBehaviourクラス

UnityではMonoBehaviourというクラスを継承したクラスを使います。これらのクラスはGameObjectにコンポーネントとしてアタッチされることを前提としています。そのため、gameObjecttransformといったプロパティが直接使える便利さがある反面、通常のC#のクラスと同じようには扱えない側面があります。

コンストラクタでインスタンスを生成できないMonoBehaviour

MonoBehaviourクラスのインスタンスは、Unityのシーンオブジェクトにアタッチされて初めて有効になります。newキーワードを使って直接インスタンスを生成することは技術的には可能ですが、非推奨です。その理由は、多くのUnityのコールバックメソッド(例: StartUpdate)が動作しなくなるからです。

推奨されるインスタンス生成方法

MonoBehaviourのインスタンスは、以下のようにGameObjectにアタッチすることで生成されます。

// 新しいGameObjectにMonoBehaviourをアタッチ
GameObject obj = new GameObject("MyObject");
MyComponent component = obj.AddComponent<MyComponent>();

この方法で生成されたMyComponentは、Unityのライフサイクルに従ってStartUpdateが正しく呼び出されます。

禁止事項: newでの生成

以下のようなコードでMonoBehaviourをインスタンス化すると、ランタイムエラーや予期しない動作を引き起こす可能性があるらしい...試したことないので分からんけど。

// 非推奨: newで直接生成
MyComponent component = new MyComponent();

ScriptableObjectクラス

ScriptableObjectクラスもMonoBehaviourと同様にnewを使ってインスタンスを生成することができません。しかし、ScriptableObjectは以下のように別の方法でインスタンスを作成します。

// ScriptableObjectのインスタンス生成
MyScriptableObject myObject = ScriptableObject.CreateInstance<MyScriptableObject>();

Discussion