📝

【C#】「プロパティ」再入門──ただのgetter/setterじゃない

に公開1

はじめに:C#の「プロパティ」って知ってる?

みなさん、C#の「プロパティ」って知ってますか?

「知ってるよ!
変わった書き方の getter/setter の事でしょ?」

…と答えた方。

これは間違いではないですが、間違いです!!!(?)

今のC#の「プロパティ」の役割は広くて重要

たしかに、C#の入門記事とかでは、

プロパティは外からみるとメンバー変数のように見えるgetter/setterだよ~
――どこかのC#入門

って説明してます。機能の説明、なら間違ってません。

でも、この説明、間違ってないけど説明が足りてません。

説明不足…!

今のC#のプロパティ、その説明で想像する範囲より

  • ずっと用途が広くて
  • ずっと重要度が高い

んです!!!

だから「再入門」 しましょう。今のC#のプロパティの世界に。

プロパティといえば、まず自動実装プロパティのこと

C#の「プロパティ」の宣言は、こういう記法です。

プロパティの定義方法
public int IntProp { get; set; }

めっちゃシンプルですね。
「フィールド」(他の言語でいうメンバー変数)とほとんど変わりません(後ろに{ get; set; }つくだけ)。

参考:フィールド(=メンバー変数)との比較
public int IntField;

また、使う方では、プロパティとフィールドの書き方はおんなじです。

アクセス方法に差がない
class A{
  public int IntProp { get; set; }  //ワイはプロパティ!
  public int IntField;              //おいどんはフィールドでごわす

  public void Clear()
  {
    //内部からのアクセス方法は同じ
    IntProp = default;
    IntField = default;
  }
}

//外部からのアクセス方法も同じ
var a = new A();
a.IntProp = 123;
a.IntField = 456;

そして、この状態でつかうんなら、getter/setter を意識することはありません

だって getter/setter 的な要素無いし!
ふつーに使う分には、クラスや構造体にくっついてる変数だと思ってて十分です!

後で詳しく理由書きますが、今のC#のプロパティは「このままで使う」ことが多いです。

公式推奨:”外向け”はフィールドよりプロパティ

プロパティとフィールドの使い分けで大事なポイントがあります。

【非推奨】↓これ、怒られます↓【CA1051】
public int IntField;

さっき例に出した↑コレ↑ (publicなフィールド)。
公式Analyzer(=Linter)が有効[1]だと、怒られます

公式Analyzer

CA1051: 参照可能なインスタンス フィールドを宣言しません (コード分析) - .NET | Microsoft Learn

公式の推奨では、外から見えるメンバー変数っぽいものはプロパティにしろ、となってます。

  • 理由:
    • プロパティを書く手間はフィールドと変わらない
    • フィールドは仕様変更に弱い

もう単純に、フィールドは不便で場合によっては危険、なんですよね。
実用上、プロパティは今のC#でめっちゃ大事です。

プロパティは仕様変更に強い

「フィールドは仕様変更に弱い」ということは、つまり、
プロパティはフィールドに比べて仕様変更に強いです!
中身の実装を変えれば、呼び出し先は変えなくていいですからね!

class A{
  int _intProp;
  public int IntProp
  {
    get => _intProp;
    set
    {
      // 仕様変更で2倍の値で記録することになった!
      _intProp = value * 2;
    }
  }
}

var a = new A();
//ここは変えなくてOK
a.IntProp = 123;
Console.WriteLine(a.IntProp);	//output: 246

ここでやっとgetter/setterらしさが…!

仕様変更は無いことに越したこと無いですけど、呼び出し先が変更できるとも限らないですからね…。
万一のときに強いのがプロパティです。

あとからなんとかできるクラスのメンバー変数」、って考えると心強い!
普段の使い勝手は他の言語で言う「クラスのメンバー変数」と変わらない実用性がありつつ、やっぱ仕様変更しなきゃ、って時にもそこそこ強いのがプロパティです。

安心安全のプロパティ

プロパティは安全にできる

プロパティはフィールドに比べてアクセス制御が色々できるので、(ちゃんと駆使すれば)コードが安全になります


//initアクセサ:immutableを実現
public int IntProp { get; init; }

//読み取り専用で初期値指定
public int IntProp2 { get; } = 10;

//外からは読み取り専用、中からは書き込み可能
public int IntProp3 { get; private set; }

//必須プロパティ:初期化時に定義しないといけない
public required string StringProp { get; set; }

//組み合わせ:初期化必須でimmutable
public required int IntProp4 { get; init; }

//set-onlyも自動実装プロパティ形式では作れないけどできる
int _setOnlyProp;
public int SetOnlyProp { set => _setOnlyProp = value; }

特にinitrequired は使うとコードが安全にできます。
しかも、最小限のコード量なので、getter/setter の意識は薄いままです。

https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/proposals/csharp-9.0/init

https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/proposals/csharp-11.0/required-members

record型のimmutableinitアクセサのプロパティ

ちなみに、record型のプライマリコンストラクターは、内部的には initアクセサのプロパティ が作られてます。

//recordのプライマリコンストラクター
record Person(string Name, DateTime Birthday);

//内部的にはこれが作られてる
class Person
{
    public string Name { get; init; }
    public DateTime Birthday { get; init; }

    public Person(string Name, DateTime Birthday)
    {
        this.Name = Name;
        this.Birthday = Birthday;
    }

    public void Deconstruct(out string Name, out DateTime Birthday)
    {
        Name = this.Name;
        Birthday = this.Birthday;
    }
}

https://ufcpp.net/study/csharp/datatype/record/#primary-constructor

今のC#で 安全でimmutableなデータ型と言えばrecord なので、
recordの安全性も プロパティの貢献が大きい です。

プロパティの使い所:MVVMパターンのデータバインディング

.NETの歴代標準のUIフレームワーク(MAUIやWPFなど)では MVVMパターン という考え方が広まっています[2]

歴代のUIフレームワークはViewにどれもXAMLというXML形式のフォーマットを使い、ViewModelクラスの プロパティとデータバインディングをする ことで、プロパティの変更がすぐに見た目に反映される、という仕組みです。

https://ja.wikipedia.org/wiki/データバインディング

昔のMVVMライブラリではごちゃごちゃとボイラープレートコードをプロパティの中で書かなくちゃいけなかったんですが、最近の新し目のMVVMライブラリでは、プロパティは自動実装プロパティを定義するだけでほぼそのままです。

XAML
<TextBlock Text="{Binding Name}" />
ViewModelのデータバインディング先はプロパティ
//CommunityToolkit.Mvvm v8.4+の場合
[INotifyPropertyChanged]
public partial class MyViewModel{
  [ObservableProperty]
  public partial string Name { get; set; }
}

//Epoxyの場合
[ViewModel]
public sealed class MyViewModel{
  public string? Name { get; set; }
}

つまり、C#のUIプログラミングのMVVMパターンではプロパティはめっちゃ使うし、今はもうgetter/setterとして意識することも無いです。

これをみれば、少なくともC#のUIプログラミングでは
「プロパティはgetter/setter」っていう説明がズレてる、説明不足だってことが具体的にわかる と思います。

プロパティの使い所:Source Generatorのフックポイント

今のC#ではコンパイル時にコードを自動生成してなるべく無駄コードを減らそうという仕組み、「Source Generator」(SG)があります。

https://learn.microsoft.com/ja-jp/shows/on-dotnet/c-source-generators

SGはネイティブ向けバイナリ書き出しの「NativeAOT」対応でとても重要なので、新しい.NETライブラリはどれもSGを使ってNativeAOT対応をアピールしてます。

https://zenn.dev/inuinu/articles/csharp-native-aot

で、この SGで肝になってくるのが「プロパティ」 です。
プロパティを足掛かりに、実装をSGで自動生成する、というアプローチが最近よく見るようになりました。

C#13.0で partialプロパティ(部分プロパティ) が定義できるようになりました。
これは プロパティをSGで自動実装する足掛かり(フットポイント)として作っておく、ということができるようになるものです…!

https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/proposals/csharp-13.0/partial-properties

partial class C
{
  // Defining declaration
  public partial string Prop { get; set; }
}

実はひとつ前の CommunityToolkit.Mvvm の例もプロパティに対するSGでした。
GeneratedRegexみたいに、バリバリ使われるSGの仕組みで、ちょうどいい対象がプロパティ、っていうのは これからどんどん増えてくる でしょう…。

プロパティ、これからもめっちゃ大事じゃん!

これからのプロパティ: fieldキーワード

これまでの例の中で、実は次期C#14.0でもっと簡単に書けるようになる例があります。

仕様変更につよいプロパティの例
class A{
  int _intProp;
  public int IntProp
  {
    get => _intProp;
    set
    {
      // 仕様変更で2倍の値で記録することになった!
      _intProp = value * 2;
    }
  }
}
set-onlyなプロパティの定義の例
//set-onlyも自動実装プロパティ形式では作れないけどできる
int _setOnlyProp;
public int SetOnlyProp { set => _setOnlyProp = value; }

この2つは、↓ こう書けるようになります。

fieldキーワードを使った新記法
public int IntProp
{
  get;
  set => field = value * 2;
}

//set-only
public int SetOnlyProp { set => field = value; }

うおおおお!便利!!
フィールドをわざわざ宣言しなくて済むようになります。

https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/field

プロパティは遅い?

「プロパティは中身は関数なので関数呼び出しが挟まる。だから遅い。パフォーマンスが重要なところでは使わない方が良い」

という話がたまにあります。

たしかに、C#のプロパティはどんなに単純でも、中間コードに変換すると関数呼び出しになってます。これを見ると呼び出しの分、遅そうです。

自動実装プロパティなら気にする必要ない

ですが!
自動実装プロパティなら、気にする必要ありません!

実行時に最適化が掛かってフィールドと差がなくなります。

public int Prop { get; set; }  // 自動実装プロパティ
public int Field;              // フィールド

public int GetProp() => Prop;  // プロパティアクセス
public int GetField() => Field; // フィールドアクセス
同じJITアセンブリコードに
C+Test.GetProp()
    L0000: mov eax, [ecx+4]
    L0003: ret

C+Test.GetField()
    L0000: mov eax, [ecx+8]
    L0003: ret

sharplab

これは、古くてパフォーマンス的には微妙な「.NET Framework」のランタイムですら、最適化されて、自動実装プロパティとフィールドでほとんど差がでません。

https://qiita.com/nabee_/items/cd12c3b53ca09ad3559f

厳密に見ればちょっとだけ差がありますが、これを気にするぐらいならアルゴリズムなりなんなり、もっと気にするべきことがあります。

気にするべき場合:中身が複雑なプロパティ・非同期処理

注意が必要になるのはgetter/setterとして複雑な処理を実装したときで、パッと見フィールドと区別ないので気軽にアクセスしちゃう、みたいなのは注意が必要です。

//え? 私、重くないっす。フィールドっす。
public int IamNotHeavy => DoHeavySomething();

//うわーーーー(毎回重い処理が実行される!)
var p1 = this.IamNotHeavy;  // 1回目:重い処理実行
var p2 = this.IamNotHeavy;  // 2回目:また重い処理実行

また、プロパティは async / await できないので、
非同期処理を挟むならメソッド化した方がいいです。

public int HeavyProp
{
  get {
    //プロパティの中で非同期処理はawaitできない
    //return await DoHeavySomethingAsync()

    //こうすれば呼べるけどデッドロック!!!
    return DoHeavySomethingAsync().Result;
  }
}

プロパティはあくまでも変数的に扱うようにして、
そうじゃない場合は素直にメソッドにしましょう。

まとめ

プロパティの現在地

  • プロパティ ≠ 単なるgetter/setter - 今のC#では用途が広く、重要度が非常に高い
  • 自動実装プロパティが主流 - { get; set; } の形で、getter/setterを意識せずに使える
  • 公式推奨事項 - 外向けのメンバーはフィールドではなくプロパティを使う(CA1051)

プロパティのメリット

  • 仕様変更に強い - 呼び出し元を変更せずに内部実装を変更可能
  • 安全性の向上 - initrequired、アクセス制御で堅牢なコードを記述
  • パフォーマンス - 自動実装プロパティならフィールドとほぼ同等の性能

プロパティのつかいどころ

  • MVVMパターン - UIフレームワークでデータバインディングの中心的役割
  • Source Generator - partialプロパティでコード自動生成のフックポイント
  • record - immutableなデータ型の基盤技術

注意すべきポイント

  • 重い処理は避ける - プロパティに複雑な処理を隠すと予期しない遅延
  • 非同期処理は不可 - async/awaitが使えないため、非同期処理が必要ならメソッド化

これからのプロパティ

  • fieldキーワード(C#14.0予定) - バッキングフィールドを自動生成してより簡潔な記述が可能

結論:プロパティは今のC#において、単なるgetter/setterを超えた、言語の中核的機能として位置づけられています。

従来の「プロパティ = getter/setter」という理解から一歩進んで、「プロパティ = 柔軟で安全なクラスメンバー」 として捉え直すことで、より効果的なC#コードが書けるようになる… といいなぁ!

脚注
  1. Recommended以上 ↩︎

  2. MVVMパターン自体、WPFが発祥らしいです ↩︎

  3. 昔ながらの書き方のRegexよりNativeAOTで20倍速いらしいです ↩︎

Discussion

junerjuner

ref 戻り値を使えば プロパティだって フィールドの様に 参照を返すことができるわかる(フィル―ドをプロパティにしてもそんな困ることはあんまないわかる)

using System;
{
    var v = new Class1();
    v.Value = 1;
    ref int value = ref v.Value;
    Console.WriteLine($"{v}");
    value = 2;
    Console.WriteLine($"{v}");
}
{
    var v = new Class2();
    v.Value = 1;
    ref int value = ref v.Value;
    Console.WriteLine($"{v}");
    value = 2;
    Console.WriteLine($"{v}");
}

public class Class1 {
    int _value;
    public ref int Value {get => ref _value;}
    public override string ToString() => $"{nameof(Class1)}{{{nameof(Value)}:{Value}}}";
}
public class Class2 {
    public int Value;
    public override string ToString() => $"{nameof(Class2)}{{{nameof(Value)}:{Value}}}";
}

https://sharplab.io/#v2:C4LgTgrgdgPgAgJgAwFgBQiCM6De6AEh+AbgIZgn4C8+UApgO74DCANqQM4eYAUAlAG4CRMHQBm+AJZRgJUqwh1q+UROIA6AGrzFQtEXxxMATh4ASAEQ5iAXwuDhhMgqU0Eeg0dOXrdh2htcRzkKYmV6JjZODgR+DxFxKRk5F2VVEi0dOnjCL3MrW3sclMVld2C8n0L/QLR0OABmQwQWdi5MfDx9ImlZAH1nXQqm9N78bVScAHM6WSoAPhVEgayBWs8mgHtiOjAwSQATJSMkfAAVTYBlYH2oKf5qRZ8oUgBbOk2xHij2vhscAEvd6fHgTRR/EA4MF0Gywix6WqNZqtaItLobJKyaHFJHbXb7I6GTCnC7XW73PiPfDPN4fL4/GJ/AE4IF00FZCFQrKwuwIoA=