Effective-C#を読んでプロパティについて興味を持ったので調べてみた
今更だけど、色々と知らないこともあったから整理できてよかった。
##C#プロパティの基礎
- プロパティはフィールドにアクセスする窓口になってくれる。
- プロパティを使うのにusingは必要ない。
- getとsetでそれぞれ違うアクセス修飾子をつけれる。
- virtualをつけることでオーバーライドさせることも可能。
- フィールドがなくても動作する(getもsetもできる)。
- ~~ プロパティ自体のアクセスレベルはpublicのみ使用可能 ~~ そんなことはなかった
###プロパティの構文(スタンダードなプロパティ)
using UnityEngine;
public class PropertyTest
{
private int hoge;
public int Hoge {
get{ return hoge; }
set{ hoge = value; }
}
}
public class Test :MonoBehaviour
{
void Start ()
{
var property = new PropertyTest ();
property.Hoge = 10;//setを使って代入している
Debug.Log (property.Hoge);//getを使って取得している
}
}
プロパティ名の頭文字は大文字なのが一般的。フィールドがhogeだからといってプロパティをHogeにする必要はない。(関連付けた方が開発しやすいだろうけど)
private int fuga;
public int Hoge {
get{ return fuga; }
set{ fuga = value; }
}
プロパティの構文をみると、
public 戻り値 プロパティ名 {
get {return フィールド名;}
set {フィールド名 = value;}
}
となっている。ほとんど関数みたいだ。valueはこのプロパティに代入された値を指す。getとsetの順番は逆でも良い。
`アクセス修飾子 戻り値 プロパティ名`ではなく`public 戻り値 プロパティ名`となっているのは、プロパティ自体のアクセスレベルがpublicじゃないと`プロパティ is inaccessible due to its protection level`というエラーがでるからだ。
そんなことはなかった。
普通にプロパティ自体のアクセス修飾子にpublic以外も使える。X(
property.Hoge = 10;
このように代入された場合は、valueの中に10が入る。
getやsetは複数行の処理を書くことができる
public int Hoge {
get {
int a = 1;
int b = 2;
return fuga + a + b;
}
set {
int a = 1;
int b = 2;
fuga = value + a + b;
}
}
return や valueを使うタイミングはいつでも良い(別に最後の行じゃなくても良い)
return を書いた後の行に処理を書いてもコンパイルエラーは起きない。
public int Hoge {
get {
int a = 1;
int b = 1;
return fuga + a + b;
Debug.Log ("fuga:" + fuga);//実行されない
}
set {
int a = 1;
int b = 1;
fuga = value + a + b;
Debug.Log ("fuga:" + fuga);//実行される
}
}
注意点は、get時にreturn後の処理(Debug.Log ("fuga:" + fugue);
)が行われないことだ。(setは全処理行われる)
return ???
で処理カーソルが呼び出し元に戻ってしまうからその後の処理は行われない。
プロパティはgetで値を返して、setで値を受け取れば良い。
そのため、次のようにフィールドを使用しないプロパティを定義してもエラーは発生しません。
public int Hoge {
get {
return 10;
}
set {
Debug.Log ("set:" + value);
}
}
###プロパティの構文(get/setの省略)
プロパティのget/setはどちらか片方だけであれば省略することができる
private int hoge;
public int Hoge {
get {
return hoge;
}
}
省略した方は外部からアクセスできなくなる。
オブジェクト利用者からすれば、setが省略されているのか、private or protectedなのかがわからないので誤解させない必要があるかもしれない。
###プロパティの構文(アクセスレベルを分ける)
アクセス修飾子の基礎構文はこんな感じ
public 戻り値 プロパティ名 {
アクセス修飾子 get {return フィールド名;}
アクセス修飾子 set {フィールド名 = value;}
}
使い方
public int Hoge {
get {
return fuga;
}
protected set {
fuga = value;
}
}
基本的にはsetの方をprivateやprotectedにしてアクセス制限する。(もちろんgetにアクセス修飾子をつけることもできる)
注意点は、getとsetどちらかにしかアクセス修飾子をつけることができないという点だ
たとえば
public int Hoge {
protected get {
return fuga;
}
protected set {
fuga = value;
}
}
と書くと、 Cannot specify accessibility modifiers for both accessors of the property or indexer
エラー(プロパティやインでクサーはget/set両方にアクセス修飾子をつけることはできまぬ)が発生する。
それもそのはずで、プロパティは外部へ公開するための窓口なのでgetもsetもprivateやprotectedならアクセスできない。(getもsetもpublicをつけておけば外部へ公開することになるが、アクセス修飾子を省略している時とアクセス制限が変わらないのでエラーが発生する)
get/setのアクセス修飾子を省略すると必ずpublic扱いとなる。
そのため、get/setのアクセス修飾子にpublicをつけることはできない(そもそもつけなくてもpublicになる)
public int Hoge {
get {
return fuga;
}
public set {//パブリックはつける意味なし
fuga = value;
}
}
get/setのどちらかを省略している場合は、アクセス修飾子によるアクセス制限をかけることはできない
private int hoge;
public int Hoge {
private get {
return hoge;
}
}
get/setのアクセス修飾子早見表
修飾子 | レベル |
---|---|
public | コンパイルエラー |
省略 | public扱い |
private | private |
protected | protected |
###プロパティの構文(処理の省略)
setは省略できるがgetは省略できない。
省略してもあんまり良いことないけど
public int Hoge {
get {
return fuga;
}
set {
}
}
public int Hoge {
get {
}
set {
fuga = value;
}
}
###プロパティの自動実装機能(自動実装プロパティ)
プロパティに値をget/setする以外の処理がない場合、もっと記述を省略して書くことができる。それがC#3.0から実装された自動実装機能である。
public int Hoge {
get;
set;
}
これは、冒頭で書いた
private int hoge;
public int Hoge {
get{ return hoge; }
set{ hoge = value; }
}
とほぼ同じ実装が行われる。(ただし自動実装機能の場合は、hogeフィールドは存在しない。フィールド数が一つ少なくて済む)
Hogeプロパティ自体に値が記録されるのがポイントである。
自動実装機能のプロパティに対してもアクセス制限をつけることができる。
public int Hoge {
get;
private set;
}
例ではsetをprivateにしたが、getの方のアクセス制限を変更することも可能だ。
自動実装機能の仕組みについてはこちらを読んでみるのが良い。
http://csharp.keicode.com/basic/auto-impl-properties-internal.php
###自動実装機能プロパティのメリット
自動実装機能のメリットは、プロパティの実装内容が決まっていないが、とりあえずプロパティを書いておく時に簡単に書けることだと思う。
あとあとフィールドをプロパティに書き換えるのは大変だし、思わぬバグの原因になる。そのため、とりあえずプロパティだけ実装することは多々ある、、、はず。そういう時に自動実装機能を使う。
##プロパティをオーバーライドする
プロパティにvirtual修飾子をつけることで継承先でオーバーライドすることができる。(複雑になるので使用するかどうかはよく考えることが必要だ)
public class PropertyTest
{
private int fuga;
public virtual int Hoge {
get;
set;
}
}
public class PropertyChild : PropertyTest
{
public override int Hoge {
get {
return base.Hoge;
}
set {
base.Hoge = value;
}
}
}
これでもエラーは起きない
public class PropertyChild : PropertyTest
{
public override int Hoge {
get;
set;
}
}
##プロパティオーバーライド時のアクセスレベル
結論を言うと、プロパティのアクセスレベルはオーバーライド時に合わせる必要がある。
public class PropertyTest
{
private int fuga;
public virtual int Hoge {
get;
private set;
}
}
public class PropertyChild : PropertyTest
{
public override int Hoge {
get;
private set;
}
}
public class PropertyTest
{
private int fuga;
public virtual int Hoge {
get;
private set;
}
}
public class PropertyChild : PropertyTest
{
public override int Hoge {
get;
set;
}
}
##インターフェースとしてのプロパティ
インターフェースにプロパティを定義することもできる。
public interface IPropatyable
{
int Piyo {
get;
set;
}
}
public class PropertyTest : IPropatyable
{
public int Piyo {
get {
return 10;
}
set {
// todo
}
}
}
public class Test :MonoBehaviour
{
void Start ()
{
IPropatyable property = new PropertyTest ();
property.Piyo = 10;
Debug.Log (property.Piyo);
}
}
インターフェースでプロパティを定義する時にget/setの両方を省略することはできない。(省略すると関数を定義していると思われてしまうのだろう)
public interface IPropatyable
{
int Piyo {
}
}
また、以下のように書いてもエラーがでる
public interface IPropatyable
{
int Piyo {
get {
}
set {
}
}
}
インターフェースでプロパティを定義する時は、自動実装機能のようにget;set;と書かなくてはいけないので注意。
###インターフェースのプロパティのアクセスレベル
インターフェースでプロパティを定義する場合は、実装時のアクセスレベルが制限されることに注意が必要だ。例をあげながら解説していく。
まず、インターフェースではアクセス修飾子を指定できない。すべてがpublicになるからだ。
以下のようにたとえプロパティのget/setのアクセス修飾子であってもエラーが発生する
public interface IPropatyable
{
int Piyo {
get;
private set;
}
}
そのため通常はインターフェースのプロパティを実装すると、そのプロパティはpublicなgeとsetになってしまう。
public class PropertyTest : IPropatyable
{
public int Piyo {
get; //publicレベル
set; //publicレベル
}
}
これを回避する方法は、インターフェースでプロパティを明示的に宣言しない、である。
具体的にコードを書くと、setのアクセスレベルをpublic以外にしたいなら以下のようになる。
public interface IPropatyable
{
int Piyo {
get;
}
}
これを実装するクラスでは、setはprivateやprotectedのアクセス修飾子を使うことができるようになる。
public class PropertyTest : IPropatyable
{
public int Piyo {
get;
private set;
}
}
この実装で問題なのは、実装先でアクセスレベルが変わってしまう、という点である。
例えば先ほどのPropertyTestのPiyoプロパティのsetはprivateであった。
しかし次のコードをみると、
public class PropertyTest2 : IPropatyable
{
public int Piyo {
get;
protected set;
}
}
このように継承先でアクセスレベルが変わってしまう問題がある。
また、setが実装されないケースもありえる。
public class PropertyTest3 : IPropatyable
{
public int Piyo {
get {
return 0;
}
}
}
これらの実装オブジェクトからプロパティにアクセスする時も問題がある。
public class PropertyTest4 : IPropatyable
{
private int piyo;
public int Piyo {
get {
return 0;
}
set {
piyo = value;
}
}
}
このようなクラスがあったとして、
void Start ()
{
PropertyTest4 p4 = new PropertyTest4 ();
}
PropertyTest4型としてp4変数をインスタンス化すると、Piyoプロパティのget/set両方にアクセスできる。
対して以下のようにIPropatyableオブジェクトにアクセスする時は、getにしかアクセスできない。
void Start ()
{
IPropatyable p4 = new PropertyTest4 ();
}
###インターフェース x プロパティまとめ。
インターフェースでプロパティを定義すると実装が不安定になる問題がある。
混乱を防ぐため、基本的にアクセスレベルは統一されるべきなのでインターフェースでプロパティを使う時は注意が必要だ。
##プロパティ vs 独自get/set (速度計算してみた)
自由に関数を定義できる以上、getter/setterを作れる。そのため「プロパティなくてもいいよね勢」は多々存在する。
確かに彼らの主張は正しい。しかし、プロパティという言語機能が実装されていることは、独自get/set関数より何かすぐれている点があるのではないだろうか。(例えばアクセス速度的な利点があるとか)
というわけで速度計算してみた。
環境は、
mac 10.11.4
Unity 5.3.2f1
Unityのプロファイラーを使って計測
ublic class PropertyTest
{
private int piyo;
public int Piyo {
get {
return piyo;
}
set {
piyo = value;
}
}
}
public class PropertyTest2
{
private int piyo;
public int GetPiyo ()
{
return piyo;
}
public void SetPiyo (int value)
{
piyo = value;
}
}
上記のどちらも利用者側に提供する機能は同じである。
計測用コードはこれ。
public class Test :MonoBehaviour
{
PropertyTest property = new PropertyTest ();
PropertyTest2 orgMethod = new PropertyTest2 ();
void Start ()
{
A ();
}
void A ()
{
Profiler.BeginSample ("AAA");
for (int i = 0; i < 100000; i++) {
//ここで何か処理
}
Profiler.EndSample ();
}
}
空for文100,000回で計測すると0.29msかかるので実際に計測する時は、0.29ms引いた値を使用する。
(空for文100,000回は0.27~0.30msの間でブレていたがなんとなく0.29msの時が多いと思ったので0.29msを使用する。)
for文一回あたり0.0000029msかかるのか
###測定コード
こんな感じで
void A ()
{
Profiler.BeginSample ("property set AAA");
for (int i = 0; i < 100000; i++) {
property.Piyo = 10;
}
Profiler.EndSample ();
}
void B ()
{
Profiler.BeginSample ("original set AAA");
for (int i = 0; i < 100000; i++) {
orgMethod.SetPiyo (10);
}
Profiler.EndSample ();
}
void C ()
{
Profiler.BeginSample ("property get AAA");
for (int i = 0; i < 100000; i++) {
int a = property.Piyo;
}
Profiler.EndSample ();
}
void D ()
{
Profiler.BeginSample ("original get AAA");
for (int i = 0; i < 100000; i++) {
int a = orgMethod.GetPiyo ();
}
Profiler.EndSample ();
}
###測定結果
タイプ | 10万回あたり(ms) | 1回あたりms |
---|---|---|
プロパティset | 0.87 | 0.0000087 |
オリジナルset | 0.78 | 0.0000078 |
プロパティget | 0.73 | 0.0000073 |
オリジナルget | 0.73 | 0.0000073 |
set系はpropertyの方が時間がかかる。
getはオリジナルメソッドもpropertyも変わらない(若干originalの方が時間がかかる時があるくらい)
うーん。propertyの方が早いと思ったんだけど違うみたいだ。。。
1万回回しても0.1msも差がでないならもう誤差の範囲といってもいいレベルか。
速度的な指標がプロパティを使う理由にはならないということがわかった。