【ゲーム開発のためのC#入門講座・応用拡張編】プロパティで保守性を強化しよう【#2】

2022-01-275.0_C#応用拡張編

おさらい

前回はオブジェクト指向の三大要素のひとつ「カプセル化」について学びました。

メンバー変数はインスタンス毎に共有しておきたい情報を保持することができるし、メソッドはそのメンバー変数を活用して処理を実行できるので非常に便利です。

ただ、何でもかんでもアクセス可にしていると、バグの原因になったりいらぬ誤解を受けたりしてしまいます。だからアクセス修飾子を使って、クラスの中でしか使わないメンバー変数やメソッドはクラスというカプセルの中に閉じ込めて隠してしまおう、という内容でしたね。

全部隠せない場合もある

ただ、前回の最後に提示していたように、ほんの少しでもクラスの外側で利用する機会がある場合、アクセス修飾子を「private」にしてクラスの中に隠してしまうことはできません。

Healer healer = new Healer();
// ↓本当はMPも勝手に設定して欲しくないから隠したいんだけど、
healer.MP = 20;
int playerHP = 1;
​
// ↓クラスの外で参照してるから、
while (healer.MP > 4)
{
    playerHP += healer.Heal();
}
​
Console.WriteLine(playerHP);
​
public class Healer
{
    // ↓privateにする訳にはいかない……
    public int MP;
    private int HealPower;
    
    public Healer()
    {
        Random random = new Random();
        MP = random.Next(20) + 1;
        HealPower = random.Next(10) + 1;
    }
    
    public int Heal()
    {
        MP -= 4;
        return HealPower * 2;
    }
}

それなら「public」にするしかないね、と割り切るのもひとつの手ではあります。

が、今回のプログラムをよくよく見てみましょう。

隠したいところと、隠せないところに実は決定的な違いがあります。

Healer healer = new Healer();
// ↓隠したいところ(設定して欲しくない)
healer.MP = 20;
int playerHP = 1;
​
// ↓隠せないところ(参照はしたい)
while (healer.MP > 4)
{
    playerHP += healer.Heal();
}

そう、今回のケースでは、

  • メンバー変数の設定は許可したくないけど、
  • メンバー変数の参照だけは許可したい

ということなんですね。

つまり理想は「設定は許可せず、参照だけ許可する」なのです。

これはメソッドとの組み合わせによって実現することができます。

privateなメンバー変数の値を返すメソッドを作成しよう

早速実例を見ていきましょう。

public class Healer
{
    // ↓アクセス修飾子をprivateにしたよ!
    private int MP;
    private int HealPower;
    
    public Healer()
    {
        Random random = new Random();
        MP = random.Next(20) + 1;
        HealPower = random.Next(10) + 1;
    }
    
    public int Heal()
    {
        MP -= 4;
        return HealPower * 2;
    }
    
    // ↓privateなメンバー変数の値を返すよ!
    public int GetMP()
    {
        return MP;
    }
}

メンバー変数「MP」のアクセス修飾子をprivateに変更しました。その上で、そのメンバー変数の値を返してくれるpublicなメソッドを新しく作成しました。

これなら、メンバー変数「MP」の値を参照したい時だけメソッド「GetMP」を利用することで、メンバー変数の参照だけを許可することができますね。

クラスの外からみた時、あたかも読み取り専用のように扱うことができます。

Healer healer = new Healer();
// ↓privateなメンバー変数はアクセス不可なので、コメントアウト
// healer.MP = 20;
int playerHP = 1;
​
// ↓読み取り専用メソッドで、参照のみ可能
while (healer.GetMP() > 4)
{
    playerHP += healer.Heal();
}
​
Console.WriteLine(playerHP);

privateなメンバー変数に値を設定するメソッドを作成しよう

今度は反対に、

  • メンバー変数の設定だけは許可したいけど、
  • メンバー変数の参照は許可したくない

という場合を考えてみましょう。

これもメソッドと組み合わせることで実現できそうですね。

早速やってみましょう。

public class Healer
{
    // ↓アクセス修飾子をprivateにしたよ!
    private int MP;
    private int HealPower;
    
    public Healer()
    {
        Random random = new Random();
        MP = random.Next(20) + 1;
        HealPower = random.Next(10) + 1;
    }
    
    public int Heal()
    {
        MP -= 4;
        return HealPower * 2;
    }
    
    // ↓privateなメンバー変数に引数の値を設定するよ!
    public void SetMP(int mp)
    {
        MP = mp;
    }
}

メンバー変数「MP」のアクセス修飾子をprivateに変更しました。その上で、そのメンバー変数に値を設定してくれるpublicなメソッドを新しく作成しました。

これなら、メンバー変数「MP」の値を設定したい時だけメソッド「SetMP」を利用することで、メンバー変数の設定だけを許可することができますね。

クラスの外からみた時、あたかも書き込み専用のように扱うことができます。

Healer healer = new Healer();
// ↓privateなメンバー変数はアクセス不可なので、コメントアウト
// healer.MP = 20;
​
//  書き込み専用メソッドで、設定のみ可能
healer.SetMP(20);

C#にはもっと手軽に実装する方法がある

このように、アクセス修飾子とメソッドを組み合わせることで、メンバー変数を「読み取り専用」にしたり、「書き込み専用」にしたりすることができます。

例えばJavaという言語では実際にこのような方法でメンバー変数への部分的なアクセスを実装します。俗に「ゲッターメソッド」「セッターメソッド」と呼ばれるものです。

ただ、C#ではこれを実現するための専用の機能が用意されています。

それがプロパティです。

プロパティを実装しよう

「MP」というメンバー変数へのアクセスを提供するプロパティを作ってみましょう。

public class Healer
{
    // ↓アクセス修飾子をprivateにしたよ!
    private int MP;
    private int HealPower;
    
    // ↓これがプロパティだよ!
    public int MPProperty
    {
        get 
        {
            return MP;
        }
        set
        {
            MP = value;
        }
    }
    // ここまで
    
    public Healer()
    {
        Random random = new Random();
        MP = random.Next(20) + 1;
        HealPower = random.Next(10) + 1;
    }
    
    public int Heal()
    {
        MP -= 4;
        return HealPower * 2;
    }
}

「get」「set」という記述から察することができるように、「get」がデータを返すための処理、「set」がデータを設定するための処理を定義する箇所になっています。

ポイントとしては、プロパティはクラスに紐づくデータにアクセスするためだけの専用機能なので、引数という概念がないということですね。

public int MPProperty
{
    // ↓値を返すだけの処理なので、引数なし
    get 
    {
        return MP;
    }
    // ↓値を設定するだけの処理なので通常の引数はなく、
    //  設定値が格納された特別な変数「value」だけが渡される
    set
    {
        MP = value;
    }
}

そのため、プロパティはメソッドと違って「()」をつける必要がなく、メンバー変数と同じ感覚でアクセスすることができます。

// ↓ここで代入している値がsetの「value」に設定される
healer.MPProperty = 20;
// ↓()をつける必要がないので、メンバー変数と同じ感覚でアクセスできる
while (healer.MPProperty > 4)
{
}
​
// ↓メソッドで実装する方法だと()が必要になるし、
//  参照と設定でそれぞれ違う名前のメソッドを作らなきゃいけないから面倒
healer.SetMP(20);
while (healer.GetMP() > 4)
{
}

また、「get」と「set」のどちらかだけを実装することが可能であるため、そのプロパティを「読み取り専用」にしたり「書き込み専用」にしたりすることができます。

// ↓「get」だけにすれば読み取り専用にできる
public int MPProperty
{
    get 
    {
        return MP;
    }
}
// ↓「set」だけにすれば書き込み専用にできる
public int MPProperty
{
    set
    {
        MP = value;
    }
}

さらに、「get」はクラスの外に公開する(public)けど、「set」はクラスの中からしかアクセスできないようにする(private)、なんてこともできます。

public int MPProperty
{
    get 
    {
        return MP;
    }
    private set // ←「get」と「set」は個別に「private」にすることが可能
    {
        MP = value;
    }
}

プロパティを使うと、より細かくアクセス制御ができるようになるんですね。

プロパティの定義って結構だるくない?

プロパティの構文を見てみましょう。

// getとsetのいずれかは省略可(読み取り専用や書き込み専用にできる)
アクセス修飾子 型 プロパティ名
{
    [private] get // 個別にprivateにしたい場合のみprivate設定
    {
        ……データ参照処理……
    }
    [private] set // 個別にprivateにしたい場合のみprivate設定
    {
        ……データ設定処理……
    }
}

うーん、確かに便利ではあるものの、結構縦長ですよね。

「もっとこう、しゅっと書けないものでしょうか?」

きっとそんな要望が世界中からたくさん寄せられていたんでしょうね。

C#のバージョンアップに合わせて、色んな簡略記法が追加されました。

一通り見てみましょう。

プロパティの書き方いろいろ

2022/01/26現在、下記のような書き方ができます。

// 構文(getとsetのいずれかは省略可(読み取り専用や書き込み専用にできる)
アクセス修飾子 型 プロパティ名
{
    [private] get => データ参照処理;
    [private] set => データ設定処理;
}

// 例
public int MPProperty
{
    get => MP; //この時はreturnの記述も不要
    set => MP = value;
}
// 構文(読み取り専用時のさらに簡略的な記述方法)
アクセス修飾子 型 プロパティ名 => データ参照処理;

// 例
public int MPProperty => MP; //この時はreturnの記述も不要

だいぶしゅっと書くことができるようになっていますね。

そして最も便利なのが、これから紹介する自動実装プロパティです。

この機能はプロパティをより簡略的に記述できるようにしつつ、アクセス制御したいメンバー変数も同時に自動実装してくれる優れものです。

// ↓通常はこのように、メンバー変数とプロパティをそれぞれ実装する必要があるが、
private int _MP;
public int MP
{
    get => _MP;
    set => _MP = value;
}

// ↓自動実装プロパティでは下記のように記述することで、
//  プロパティの定義とメンバー変数の実装を同時にできる
public int MP { get; set; }

とても同じ内容を実装しているとは思えないぐらい、記述量が減っていますね。

// 構文(getとsetのいずれかは省略可(読み取り専用や書き込み専用にできる)
アクセス修飾子 型 プロパティ名 { [private] get; [private] set; } = 初期値(省略可);

// 例
public int MP { get; set; } = 20;

どうでしょうか?

色々書き方があるせいで覚えるのはちょっと大変ですが、かなり便利だというのはご理解いただけたんじゃないかと思います。

ヒーラークラスを最適化しよう

改めて、ヒーラークラスを最適な実装にしてみましょう。

Healer healer = new Healer();
int playerHP = 1;

while (healer.MP > 4)
{
    playerHP += healer.Heal();
}

Console.WriteLine(playerHP);

public class Healer
{
    private int HealPower;
    
    // ↓MPを読み取り専用プロパティに変更したよ!
    //  (クラス内なら書き込みも可)
    public int MP { get; private set; }
    
    public Healer()
    {
        Random random = new Random();
        MP = random.Next(20) + 1;
        HealPower = random.Next(10) + 1;
    }
    
    public int Heal()
    {
        MP -= 4;
        return HealPower * 2;
    }
}

これなら「HealPower」はクラスの外側からアクセスできませんし、「MP」もクラスの外側から設定することはできなくなります(参照のみ可)。Y氏も誤解することなく、想定通りにヒーラークラスを使ってくれることでしょう。

このように、アクセス修飾子とプロパティを組み合わせることで、より安全で誤解のない(=使いやすい)プログラムを作ることができます。

クラスカプセルの中に閉じ込めたデータやメソッドに対するアクセス制御、すなわち「カプセル化」がいかに「理解しやすく、再利用しやすいプログラムを作る」ことに貢献しているか、ご理解いただけたでしょうか?

少しでも実感を持って伝わっていれば幸いです。

Unityでの活躍ポイント

Unity Technologiesが作ってくれたクラスも細かくアクセス制御が行われています。

例えばUnity上に設置したゲームオブジェクトがアクティブかどうかを判断できる「ActiveSelf」というbool型のプロパティがありますが、これは読み取り専用になっています。

// ↓参照はできるけど、
if (gameObject.activeSelf)
{
	// ↓設定はできない!
	gameObject.activeSelf = false;   
}

時々不便に思うこともあるかもしれません。が、クラスを作った側にもそのアクセスを制御したい理由がきちんとあるということは、今回学んだことで想像してもらえるんじゃないかと思います。

実践演習

それでは実際にプロパティを使ってみましょう。

演習①

仕様

下記クラス「BattleCharacter」のメンバー変数のうち、アクセス修飾子が「public」になっているものをすべて読み取り専用の自動実装プロパティに変更してください。

テンプレート
public class BattleCharacter
{
    // ↓このメンバー変数を自動実装プロパティにしよう!
    public string Name;
    private int Level;
    // ↓このメンバー変数を自動実装プロパティにしよう!
    public int HP;
    private int ATK;
    private Random Random = new Random();

    public BattleCharacter(int level, int hp, int atk)
    {
        Level = level;
        HP = hp;
        ATK = atk;
    }

    public int Attack()
    {
        return ATK + Level * 2 / 5 + CalcRandomDamage();
    }

    private int CalcRandomDamage()
    {
        return Random.Next(5) + 1;
    }
}

演習②

プロパティはクラスに紐づくデータにアクセスする機能です。

必ずしもメンバー変数の読み書きをしなければいけない訳ではないです。

プロパティでは処理を実装できることを活かし、下記のようなプロパティを作ってみましょう。

仕様

敵を倒すと経験値をもらえます。経験値はHPの2倍です。
経験値を参照できる読み取り専用のプロパティ「EXP」を下記のクラスに実装してください。
なお、実装方法は「get」の中でメンバー変数「HP」×2を返すものとします。

テンプレート
Enemy enemyA = new Enemy();
Console.WriteLine(enemyA.EXP);

public class Enemy
{
    public string Name;
    public int HP = 10;
    public int ATK = 5;
    // ↓ここに読み取り専用プロパティを定義しよう!

    // ここまで
}

答え合わせ

演習①の答え

public class BattleCharacter
{
    // ↓このメンバー変数を自動実装プロパティにしよう!
    public string Name { get; private set; }
    private int Level;
    // ↓このメンバー変数を自動実装プロパティにしよう!
    public int HP { get; private set; }
    private int ATK;
    private Random Random = new Random();

    public BattleCharacter(int level, int hp, int atk)
    {
        Level = level;
        HP = hp;
        ATK = atk;
    }

    public int Attack()
    {
        return ATK + Level * 2 / 5 + CalcRandomDamage();
    }

    private int CalcRandomDamage()
    {
        return Random.Next(5) + 1;
    }
}

演習②の答え

Enemy enemyA = new Enemy();
Console.WriteLine(enemyA.EXP);

public class Enemy
{
    public string Name;
    public int HP = 10;
    public int ATK = 5;
    // ↓ここに読み取り専用プロパティを定義しよう!
    public int EXP => HP * 2;
    // ここまで
}

このように、プロパティは非常に自由度が高く柔軟な機能です。

メンバー変数のアクセス制御だけでなく、色んな使い方を考えてみるのも面白いかもしれないですね。

まとめ

  • プロパティはクラスに紐づくデータへの細かなアクセス制御を可能とする専用機能
  • 参照処理である「get」と設定処理である「set」で構成されており、それぞれアクセス修飾子を設定することができる
  • C#のバージョンアップに伴い、簡易的な記述方法や自動実装プロパティなどが導入されたため、メソッドで実現するよりも遥かに簡単で便利

次回はオブジェクト指向の三大要素ふたつ目「継承」について学びます。お楽しみに!

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

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

Posted by yuumekou