どうもです。
制作部プログラマの若尾です。
今回カテゴリーC++で綴りますが、緩めの内容でお送りします。
C++でC#にあるプロパティのようなものを使ってみたくなったのでその過程と結果をブログに書き留めました。初めてのブログなので、見づらいところもあると思いますが、お付き合いください。
アクセス修飾子について
はじめにプロパティとは何かの説明の前に
プロパティ機能に関わるアクセス修飾子についておさらいで簡単な説明をします。
アクセス修飾子はメソッドやメンバ変数などが外部からのアクセスを許可するかどうかを司ります。種類は下記の3つが代表的ですね。
- public
あらゆる位置からのアクセスを許可することを表します。 - protected
クラスの派生クラスからのアクセスをのみを許可します。 - private
クラスの外部からのアクセスを全て拒否します。
許可されてないものはコンパイルができません。例でみてみると
class Player
{
private:
float Hp = 100;
protected:
float Mp = 50;
public:
float Power = 10;
};
class Hero: public Player
{
public :
float GetMp()
{
return Mp; //OK コンパイルできる
};
};
//メイン実行
int main() {
Hero hero;
float hp = hero.Hp; //NG コンパイルできない
float mp_1 = hero.GetMp(); //OK コンパイルできる
float mp_2 = hero.Mp; //NG コンパイルできない
float power = hero.Power; //OK コンパイルできる
return 0;
}
NGという部分はアクセスができないのでコンパイルエラーとなります。
アクセス修飾子を使うことで他クラスから利用できるかどうか明確にすることができます。
アクセスの可否がなぜ必要か
アクセスの許可を制限できることが分かったと思います。
ただ、こんな風に考える人もいると思います。
全部publicにしたらよくね?
私もそう思ってました。全部publicなら、どこからでも使えるし楽ですよね。
しかし、個人ではなく多人数での開発を経験すると、このアクセスの可否を決めることが重要で便利なものだというのがわかりました。
上記のクラスでの例を少し書きます。
機能漏れを回避、不正な書き換えを回避
Playerはゲームに登場するプログラムだと考えてください。
Hpはゲーム中になにかしらのアクションによって減っていくものとして
Hpが0になったときなにかしらの処理が行われるようにしたいとします。
Hpが0まで減らされたときに、その処理を行えばよいですね。
下記のようなメソッドになります。
class Player
{
private:
float Hp = 100;
public:
void SetHpDamage(float damage)
{
Hp -= damage;
if(Hp <= 0)
{
//0になった時の処理
}
};
};
この状態であれば、SetHpDamage
を呼び出せばよいですね。
しかしながら、もしprivateにあるHpがpublicにあった場合
player.Hp = 0;
といった呼び出しが出来てしまい…
Hpが0になったのにも関わらず、Hpが0になった時の処理が呼び出されなくなってしまいます。
このような呼び出し方を防ぐために
アクセス修飾子を使い分けて、変数アクセスとメソッドを使い分けます。こういった作りをカプセル化と言います。
このことは、例のような必要な呼び出しを漏らさないようにする役割を果たしてくれます。
後からの変更に柔軟
アクセス修飾子を活かしたカプセル化のメリットでもうひとつ紹介します。
企画案からゲームルールの追加要素が出てきました。PlayerのHpにExtraHpと題し、いままでのHpとは別にHpの要素を持ちたいそうです。
某江戸時代ゲームの鎧とか
某スパイfpsゲームの防弾チョッキみたいなものです。
ExtraHpは、PlayerがHpが減るようなこと(ダメージを受けたり)があったときExtraHpが存在する場合そちらを先に減らすというものです。
実装すると下記のようなものです。
class Player
{
private:
float Hp = 100;
float ExtraHp = 50;
public:
void SetHpDamage(float damage)
{
//先にひく.
ExtraHp -= damage;
//Extraが足りない分は、Hpをひく
if (ExtraHp < 0)
{
//不足分である負の値を加えて,Hpを減らす.
Hp += ExtraHp;
}
//ExtraHpを下限に戻す.
ExtraHp = 0;
if (Hp <= 0)
{
//0になった時の処理
}
};
};
ここでの変更点のポイントは
ExtraHp変数を追加したことと
Player::SetHpDamage
の中身の処理のみを変更したということです。
これは他のクラスでPlayer::SetHpDamage
を呼び出していたとしても、その部分を加筆変更修正する必要がないということです。player.SetHpDamage(20)が
どこかで記載されていたとしても、その部分を修正する必要がないということです。
これが逆に、publicでHpがアクセスが許可されていて利用されていた場合では、player.Hp -= 20; と書かれていた部分にExtraHpがあるかどうかの式を書かないといけない…という事態になってしまいます。
このように変更点が最小限になる役割も担っているのがアクセス修飾子を使ってカプセル化を行うメリットになります。
プロパティとは
プロパティ(property:所有物、特性)とは、JavaやC++にはない(Visual Basicにはある)機能で、 クラス外部から見るとメンバー変数のように振る舞い、 クラス内部から見るとメソッドのように振舞うものです。
出典:https://ufcpp.net/study/csharp/oo_property.html
まさにこの通りです。
後からの変更に柔軟、ということを書きましたが
これはあくまでも最初にアクセス修飾子を分けてカプセル化を果たしていた場合です。後から既にpublicでそのまま利用されている場合はやはり、カプセル化の為に、変数とメソッドを分けたものを使うように変更が必要です。
C#のプロパティでは、既にpublicで変数のようにそのまま扱われていても
変数を持つクラス側でプロパティとして実装することで、クラス側のみの変更に抑えることができます。
PlayerをC#で記してみてみましょう。
変更前
public class Player
{
//publicでHpを利用していた.
public float Hp = 100;
}
変更後
public class Player
{
//元の名前から変更する
private float hp = 100;
public float Hp { get => hp;
set
{
hp = value;
//Hpが0になった時などの処理をここに追記できる。
}
}
}
これはあくまで一例であり、getやsetの式の書き方は他にも存在します。
上記の方法であれば、元の変数の名前と同様でかつ同じように = によるsetや
player.Hp といったgetをそのまま使うことができます。
また、setを記載しないことでreadonlyのような形式にすることができます。
これは既存のset( = 20 のような記述)をコンパイルエラーにして検知することもできます。どうしても単純なプロパティでは利用できない場合などに使われます。
プロパティを求めた経緯
長くなってしまいました。すみません!
今回私が、C++でプロパティやカプセル化をしたかった経緯は
後から生まれた追加要素を実装し、なるべく他の機能・クラスに影響がないようにしたかったためです。
まとめるとこんな感じです。
- 既に利用されている機能に追加機能を実装する
- 変数のまま使われているので関数化による変更を少なくしたい
C++でプロパティ的なのを目指す
調べてみたところ下記サイトにたどり着き、先駆者に感謝しながら試してみました。C++にはプロパティはないんですよね。
参考にしたサイト:https://ufcpp.net/study/miscprog/accessor.html
なるほど…
operatorを利用するんですね。
演算子のオーバーロードは、struct同士の加算や減算に対して使われているイメージでしたが、=をオーバーロードするという発想に感動しました。
というわけで、PlayerのHpをプロパティっぽくしてみます。
class Player
{
private:
float hp = 100;
public:
//operator用に内部にクラスを用意する
class HpProperty
{
Player& p;
public:
HpProperty(Player& p0) : p(p0) {}
//Setの式
HpProperty& operator= (float to_ho)
{
this->p.hp = to_ho;
if (this->p.hp <= 0)
{
//Hpが0になった時の処理
}
return *this;
}
//Getの式
operator float()
{
return this->p.hp;
}
} Hp;
friend class HpProperty;
Player() : Hp(*this) { this->Hp = 0; }
Player(float to_hp) : Hp(*this) { this->Hp = to_hp; }
};
//メイン実行
int main()
{
Player player;
player.Hp = 20; //内部で追加の処理も行っている
float temp_hp = player.Hp; //単純なGet
return 0;
}
そうしたなかで一応できたけど…
長い…見づらい…かえって混乱しそうだ…
実際作ってみての感想はそんな感じでした。
利用されているところの変更はないものの、Playerクラスそのものがかなりわかりづらくなってしまいました。既に存在するクラスをこのように編集するのは、かなり…よろしくないのでは…という判断になりました。
そうです。結局プロパティ化は採用しませんでした!
挫折からのGetSet作成
では、どうしたかというと
カプセル化でGetとSetをメソッドとして分けて作成しました。
参照(Get)はそのままでも良いのですが、変数がpublicのままだと
あとから変更( = 20 等)として使われてしまうと意図しない扱われ方になってしまうため、変数をprivateに変更し、後から追記する際にも漏れなく実装できるように変更しました。
PlayerのHpを書き直すと下記のようになります。
class Player
{
private:
float hp = 100;
public:
float GetHp() { return hp; };
void SetHp(float to_hp)
{
hp = to_hp;
//その他処理
};
}
以上です。
結局は利用箇所を検索してひとつずつ直していく形になりました。
大分長くなってしまいましたが、最後まで読んでいただきありがとうございました!!
【免責事項】
本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。