C#のプロパティについて調べてみた

  • 18
    Like
  • 4
    Comment
More than 1 year has passed since last update.

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にする必要はない。(関連付けた方が開発しやすいだろうけど)

これでもOK
    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 を書いた後の行に処理を書いてもコンパイルエラーは起きない。

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はどちらか片方だけであれば省略することができる

setを省略してみた。
    private int hoge;

    public int Hoge {
        get {
            return hoge;
        }
    }

省略した方は外部からアクセスできなくなる。
オブジェクト利用者からすれば、setが省略されているのか、private or protectedなのかがわからないので誤解させない必要があるかもしれない。
スクリーンショット 2016-08-04 14.31.44.png

プロパティの構文(アクセスレベルを分ける)

アクセス修飾子の基礎構文はこんな感じ

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は省略できない。
省略してもあんまり良いことないけど

setを省略(エラーは発生しない。警告はでるけど)
    public int Hoge {
        get { 
            return fuga;
        }
        set {

        }
    }
getを省略するとエラー
    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;
    }
}

プロパティオーバーライド時のアクセスレベル

結論を言うと、プロパティのアクセスレベルはオーバーライド時に合わせる必要がある。

エラーが起きない例(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;
    }
}
エラーが起きる例(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以外にしたいなら以下のようになる。

インターフェースにsetを書かない
public interface IPropatyable
{
    int Piyo {
        get;

    }
}

これを実装するクラスでは、setはprivateやprotectedのアクセス修飾子を使うことができるようになる。

setがprivateなプロパティを実装
public class PropertyTest : IPropatyable
{
    public int Piyo {
        get;
        private set;
    }
}

この実装で問題なのは、実装先でアクセスレベルが変わってしまう、という点である。
例えば先ほどのPropertyTestのPiyoプロパティのsetはprivateであった。
しかし次のコードをみると、

setがprotectedなプロパティ
public class PropertyTest2 : IPropatyable
{
    public int Piyo {
        get;
        protected set;
    }
}

このように継承先でアクセスレベルが変わってしまう問題がある。

また、setが実装されないケースもありえる。

setがないがエラーはおきない
public class PropertyTest3 : IPropatyable
{
    public int Piyo {
        get {
            return 0;
        }
    }
}

これらの実装オブジェクトからプロパティにアクセスする時も問題がある。

getもsetもあるよ(setはインターフェースにない)
public class PropertyTest4 : IPropatyable
{
    private int piyo;

    public int Piyo {
        get {
            return 0;
        }
        set {
            piyo = value;
        }
    }
}

このようなクラスがあったとして、

PropertyTest4オブジェクトにアクセスする
    void Start ()
    {
        PropertyTest4 p4 = new PropertyTest4 ();

    }

PropertyTest4型としてp4変数をインスタンス化すると、Piyoプロパティのget/set両方にアクセスできる。
スクリーンショット 2016-08-04 14.51.10.png

対して以下のようにIPropatyableオブジェクトにアクセスする時は、getにしかアクセスできない。

IPropatyableオブジェクトにアクセスする
    void Start ()
    {
        IPropatyable p4 = new PropertyTest4 ();
    }

スクリーンショット 2016-08-04 14.51.32.png

インターフェース 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;
        }
    }


}
独自get/setのコード
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かかるのか
スクリーンショット 2016-08-04 13.19.41.png

測定コード

こんな感じで

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

スクリーンショット 2016-08-04 13.32.33.png

set系はpropertyの方が時間がかかる。
getはオリジナルメソッドもpropertyも変わらない(若干originalの方が時間がかかる時があるくらい)

うーん。propertyの方が早いと思ったんだけど違うみたいだ。。。
1万回回しても0.1msも差がでないならもう誤差の範囲といってもいいレベルか。
速度的な指標がプロパティを使う理由にはならないということがわかった。

252contribution

プロパティ自体のアクセスレベルはpublicのみ使用可能

C#ではprotectedprivateなプロパティも作れますよ。

https://ideone.com/R5Nvpt

class Class1
{
    public    int Prop1 { get; set; }
    protected int Prop2 { get; set; }
    private   int Prop3 { get; set; }
    internal  int Prop4 { get; set; }
    protected internal int Prop5 { get; set; }
}

getsetに対してはプロパティ自体のアクセスレベルよりも低いものをどちらか片方にしか設定できないというルールになります。

hoge.piyo is inaccessible due to its protection levelが表示されるのは、protectedなプロパティに外からアクセスしようとした場合などではないでしょうか?

@muro さん
ご指摘ありがとうございます!

hoge.piyo is inaccessible due to its protection levelが表示されるのは、protectedなプロパティに外からアクセスしようとした場合などではないでしょうか?

まさにその通りでした:sweat:

229contribution

以下のコードのプロパティにアクセスするとクラッシュするというのは、再起呼び出しのためのスタックオーバーフローが原因でインターフェースは無関係です。
ゲッターの中にあるPiyo += 1Piyo = Piyo + 1と同じなので、ゲッターの中からゲッターを呼び出している為、無限再起になりスタックオーバーフローで死ぬというわけです。

継承先
public class PropertyTest5 : IPropatyable
{
    private int piyo;

    public int Piyo {
        get {
            Piyo += 1;//プロパティのsetを使って+1する
            return piyo;//フィールドを直接返す
        }
        set {
            piyo = value;
        }

    }
}

ちなみに自動実装プロパティを使用したときでも値を保存する為のフィールド(バッキングフィールド)は存在します。

自動実装するプロパティ (C# プログラミング ガイド)
https://msdn.microsoft.com/ja-jp/library/bb384054.aspx

コンパイラによって、プロパティの get および set アクセサーを介してのみアクセスできる、プライベートの匿名バッキング フィールドが作成されます。

@rryu さん
ご指摘ありがとうございます :)
確かにこのgetコードですとスタックオーバーフローによってクラッシュしますね。。。
うーん、なんで勘違いしたんだろう。。。
(試行錯誤していたのでちょい前のコードをコピペして掲載している気がする・・・)