【ゲーム開発のためのC#入門講座・応用拡張編】staticでインスタンスに依存しない処理を作ろう【#6】

5.0_C#応用拡張編

オブジェクト指向ってとても便利

クラス、インスタンス、メンバー変数などなど、オブジェクト指向によってもたらされた技術がいかに「理解しやすく、再利用しやすいプログラムを作る」に貢献するものだったか、これまでずっと勉強してきました。

継承やポリモーフィズムなどの難しい話もありましたが、総じて「確かにとても便利っぽい!」というのは理解してもらえたんじゃないかと思っています。

一方で、オブジェクト指向には弱点もある

一方で、オブジェクト指向には弱点もあります。

理解しにくいというのは今までずっと言ってきているので置いておくとして……。

例えば、型変換を考えてみましょう。

もし皆さんがstring型をint型に変換するクラス、仮に「IntConverter」と名付けたクラスを作るとしたら、どのように実装しますか?

まずはクラスを作り、メソッドを追加し、処理を実装するという流れになりますよね。

// ↓クラスを作り、
public class IntConverter
{
    // ↓メソッドを追加し、
    public int Convert(string number)
    {
        // ↓処理を実装する
        return int.Parse(number);
    }
}

それでは実際に使ってみましょう。

IntConverter intConverter = new IntConverter();
Console.WriteLine(intConverter.Convert("1"));
コンソール画面

1

きちんと変換処理ができましたね。

これはこれで問題ないのですが、もしこの型変換をあちこちのメソッドで行う必要が出てきたら、どうでしょうか?

public class InputTester
{
    public int Test1(string number)
    {
        IntConverter intConverter = new IntConverter();
        return intConverter.Convert(number);
    }
    
    public int Test2(string number)
    {
        IntConverter intConverter = new IntConverter();
        return intConverter.Convert(number) * 2;
    }
    
    public int Test3(string number)
    {
        IntConverter intConverter = new IntConverter();
        return intConverter.Convert(number) * 3;
    }
}

なんか、いちいちインスタンスを作らなきゃいけないのって意外と面倒ですよね。

例えばメソッド内でメンバー変数を活用しているならインスタンス毎に処理内容が変わるので、インスタンス化することには意味があります。

でも今回の実装はメンバー変数も一切使っていないので、どのインスタンスでも必ず結果は一緒です。だとすると、インスタンス化する意味も正直ないんですよね。

public class IntConverter
{
    // ↓メンバー変数を使った処理ではないので、
    //  どのインスタンスでも常に結果は同じ!
    public int Convert(string number)
    {
        return int.Parse(number);
    }
}

だったらオブジェクト指向が登場する前の関数のように、インスタンスなんて作らずとも、直接呼ぶだけで実行できるようにしたいものです。

// ↓もしも関数だったら、
public static int ConvertToInt(string number)
{
    return int.Parse(number);
}
​
// ↓わざわざインスタンスを作らなくても処理を実行できるのに……
Console.WriteLine(ConvertToInt("1"));

あれ、関数とメソッドの違いって……

あれ、そういえば……?

関数の定義にずっと「static」ってついていたのに、クラスになった途端、「static」のおまじないって消えましたよね。

public class BattleCharacter
{
    public string Name = "";
    public int Level = 0;
    public int HP = 0;
    public int ATK = 0;
​
    // ↓クラスになったら消えた「static」のおまじない
    public int Attack()
    {
        return ATK + Level * 2 / 5;
    }
}

「static」がついていた関数はインスタンスを作らなくても実行できて、「static」がついていないメソッドはインスタンスを作らないと実行できなくて……?

んん?

これって、もしや?

待たせたな!(メタルギア風

いよいよ「static」の謎を解き明かします。

以前にもお話しましたが、C#はオブジェクト指向言語です。そのため、構造体などの一部を除き、ほとんどすべてのものがクラスとして扱われています。

例えば、基礎編でずっと使ってきたWebサービスの初期設定コードを見てみましょう。

// ↓実はこれもクラスだった!
public class Hello{
    public static void Main(){
        // Your code here!
        
        System.Console.WriteLine("Hello C#");
    }
}

コメントにも記述している通り、実はこれもクラスだったのです。

ですが、この頃はクラスのことなんて全く知りませんでしたし、インスタンスなんて全く作成していませんでしたよね。

それでもプログラムを実行できていたのは、実はこのstaticというキーワードをつけていたからなのです。

staticって?

staticとは、インスタンスに紐づかないデータやメソッドを実装することができる機能です。

例えばこのstaticをつけたメソッドは、インスタンスを作成しなくても実行できます。

public class IntConverter
{
    // ↓staticのキーワードをつけると、
    public static int Convert(string number)
    {
        return int.Parse(number);
    }
}
// ↓インスタンスを作らなくても、直接、
//  「クラス名.メソッド」の形式で実行できるようになる!
Console.WriteLine(IntConverter.Convert("1"));
コンソール画面

1

エラーにならず、きちんと結果が算出されましたね!

このインスタンスを作らずにメソッドを実行するという方法、何だか今までもずっと行ってきませんでしたか?

そう、型変換もそうですし、我らが「System.Console.WriteLine」も、今までずっとインスタンスを作らずに実行してきましたよね。

// ↓インスタンスを作らずに、
int number = int.Parse("1");
// ↓メソッドを実行している!
Console.WriteLine(number);

実は、これらはすべてstaticキーワードがつけられたメソッドだったのです。

だからインスタンスなしでも処理を実行することができたんですね。

もしこれが先程説明したように、必ずインスタンスを生成しないと実行できないルールになっていたら、どうでしょうか?

// ↓こんな風に何をするにも必ず、
IntConverter intConverter = new IntConverter();
int number = intConverter.Convert("1");
// ↓インスタンス生成が必要だとしたら……
Console console = new Console();
Console.WriteLine(number);

ぱっと見でもかなり面倒なのがわかりますよね。

このように、staticキーワードをつけると、インスタンスに依存しない処理を実装することができます。逆にいえば、staticキーワードをつけたメソッドは、通常のメンバー変数を参照することはできません。

メンバー変数はあくまでインスタンス毎に保持されるものですから、当然といえば当然ですね。

public class BattleCharacter
{
    public string Name = "";
    public int Level = 0;
    public int HP = 0;
    public int ATK = 0;
​
    // ↓staticなメソッドの中で、
    public static int Attack()
    {
        // ↓メンバー変数を扱うことはできない!
        return ATK + Level * 2 / 5;
    }
}

これがstaticの弱点になります。

逆にいえば、メンバー変数を必要としない処理であれば、インスタンス生成のコードを書かずに実行できるので非常に手軽になるんですね。

  • その処理がメンバー変数の参照・更新に関わるような、インスタンス毎に振る舞いを変えたいものであるならば、通常のメソッドとして実装する
  • メンバー変数は一切関わらない、そのメソッド内で扱うデータだけで処理が完結するようなものはstaticメソッドとして実装する

このように使い分けると、これまた再利用しやすいプログラムに繋げられるのです。

staticは常に実行可能、または常に存在するもの

クラスのメソッドは、本来インスタンスを作らなければ実行できません。

ですが、staticなメソッドは常に実行することができます。

また、staticはメソッドに限らず、メンバー変数やクラスにまで適用することができます

// staticなクラス
public static class Test
{
    // staticなメンバー変数
    public static string Number = "";
}

この場合、メンバー変数は「常に存在するデータ(なので、クラスにひとつしか存在しない)」になりますし、クラスは「常に存在するクラス(なので、インスタンス不要かつプログラム実行中にひとつしか存在しない)」になります。

一見すると非常に便利そうに見えるかもしれませんが、逆にいえばプログラム実行中は常にメモリを占有し続けるということでもあります。また、アクセス修飾子がpublicであった場合、文字通りどこからでもアクセスできてしまうので、通常のメンバー変数以上に「どこでどんな風に更新されたり参照されたりするのかわからない」という状況に陥ってしまうリスクが高いです。

安易に使うと後々地獄を見ることになるので、ご利用は計画的に!

Unityでの活躍ポイント

Unityではシーンという単位で処理が区切られています。そのため、シーンをまたぐと、それまでのシーンで活用していたゲームデータなどはすべて消えてしまいます。

でも、例えばミニゲームの実行シーンと結果発表シーンを分けた場合、ミニゲームで実行した内容(レースゲームならゴールした順とか、得点制ならキャラクター毎の得点だとか)を結果発表シーンでも引き継がないと、正しく順位をつけることができなくなってしまいますよね。

そういった時、対応方法としていくつかのやり方があるのですが、一番簡単なのはそれらのデータをstaticにしてしまうことです。そうすればすべてのシーンでデータを共有することができます。

ただ、先程もいったようにどこからでもアクセスできてしまうというのは、実は非常に高いリスクを孕んでいます。なので、他の方法を検討したり、使うとしても本当に必要なものだけに限定するなど、最小限の利用を心掛けるとよいでしょう。

実践演習

それでは実際にstaticを使ってみましょう。

演習①

仕様

クラス「BattleCharacter」に、下記のstaticメソッドを追加してください。
実装メソッド :Guard
戻り値  :int
引数   :int damage
処理   :引数「damage」を半減した値を返す

テンプレート
// Program.cs

// ↓ここはプロジェクト名の名前空間なので任意
using Csharp5_7;
​
Console.WriteLine(BattleCharacter.Guard(20));
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
​
// ↓ここはプロジェクト名の名前空間なので任意
namespace Csharp5_7
{
    internal class BattleCharacter
    {
        public string Name = "";
        public int Level = 0;
        public int HP = 0;
        public int ATK = 0;
​
        public int Attack()
        {
            return ATK + Level * 2 / 5;
        }
​
        // ↓ここにstaticメソッドを実装しよう!
        
        // ここまで
    }
}

演習②

仕様

システムで共通のデータを管理するstaticなクラス「SystemData」を作りました。
staticなデータとして下記を実装しています。
・string BattleLevel
・float TextInterval
Program.csから上記データを下記の通り更新してみてください。
・BattleLevel <= “Hard"
・TextInterval <= 0.5f

テンプレート
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
​
// ↓ここはプロジェクト名の名前空間なので任意
namespace Csharp5_7
{
    internal static class SystemData
    {
        public static string BattleLevel = "Normal";
        public static float TextInterval = 2.0f;
    }
}
// Prgoram.cs

// ↓ここはプロジェクト名の名前空間なので任意
using Csharp5_7;
​
// ↓ここでstaticなメンバー変数を更新してみよう!
​
// ここまで
​
Console.WriteLine(SystemData.BattleLevel);
Console.WriteLine(SystemData.TextInterval);

答え合わせ

演習①の答え

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
​
namespace Csharp5_7
{
    internal class BattleCharacter
    {
        public string Name = "";
        public int Level = 0;
        public int HP = 0;
        public int ATK = 0;
​
        public int Attack()
        {
            return ATK + Level * 2 / 5;
        }
​
        // ↓ここにstaticメソッドを実装しよう!
        public static int Guard(int damage)
        {
            return damage / 2;
        }
        // ここまで
    }
}
コンソール画面

10

演習②の答え

// ↓ここはプロジェクト名の名前空間なので任意
using Csharp5_7;
​
// ↓ここでstaticなメンバー変数を更新してみよう!
SystemData.BattleLevel = "Hard";
SystemData.TextInterval = 0.5f;
// ここまで
​
Console.WriteLine(SystemData.BattleLevel);
Console.WriteLine(SystemData.TextInterval);

きちんとインスタンスなしで、常に存在するデータを更新できましたね。

使いどころを間違えなければ、こちらも便利です。

まとめ

  • staticとは、インスタンスに紐づかないデータやメソッドを実装することができる機能のこと
  • 通常のメンバー変数などは扱えないが、インスタンスを生成しなくても実行することができるというメリットがある
  • メソッドに限らず、メンバー変数やクラスもstaticにすることができる。その場合は常駐データとなるため、プログラムでたったひとつあればいいデータを扱う分には非常に便利だが、使い方を間違えるとバグの元なのでご利用は計画的に!

次回は配列以外で複数データを扱う方法を学習します。お楽しみに!

それでは、今回もお疲れ様でした!

また次の記事でお会いしましょう!

Posted by 夕目紅