プロパティは、フィールドのように値へ簡単にアクセスできる文法と、メソッドのように処理や検証を挟める柔軟さを両立したC#の機能です。
本記事では、get
と set
の基本、自動実装プロパティ、バッキングフィールドを用いた検証や計算まで、動くサンプルを交えて順番に解説します。
C# プロパティの基本
プロパティは、クラスの外側からはフィールドのように見えますが、内部では get
アクセサと set
アクセサという2つのメソッドを通して読み書きを制御します。
get
は値の取得時に呼ばれ、set
は代入時に呼ばれます。
これにより、外部からの見た目はシンプルなまま、検証やキャッシュ、値の変換といったロジックを挟めます。
フィールドとの違いとメリット
フィールドは純粋なデータの入れ物で、読み書き時に処理を挟めません。
一方プロパティは読み書きのタイミングに処理を挟めるため、安全性と拡張性が高くなります。
フィールドとは
フィールドはクラス内に直接保持される変数です。
public
なフィールドは外部から直接値を入れ替えられるため、思わぬ不整合を招きやすいという欠点があります。
プロパティのメリット
プロパティはフィールドへのアクセスをラップし、検証や変換などを集中管理できます。
将来の仕様変更にも強く、APIの互換性を保ちながら内部の実装だけを差し替えられます。
フィールドとプロパティの比較
観点 | フィールド | プロパティ |
---|---|---|
アクセス | 直接アクセス | get と set を通じてアクセス |
検証・変換 | 原則不可 | 可能。代入時のバリデーションや取得時の加工ができる |
実装の隠蔽 | 弱い | 強い。実装を後から変更しやすい |
インターフェースでの定義 | 不可 | 可能 |
継承・ポリモーフィズム | 不可 | virtual と override で可能 |
データバインディング | 弱い | 仕組みによっては相性が良い |
使い分けの目安
同一クラス内でのみ完結し、外部公開の必要がない内部状態にはフィールドを使い、外部へ公開する状態や将来の拡張性が必要な箇所にはプロパティを使うのが基本です。
get と set の役割
get
はプロパティの値を返すアクセサで、set
は代入された値を受け取り内部の状態を更新します。
set
内では自動的に value
というキーワードで代入値を受け取れます。
get アクセサ
get
は戻り値としてプロパティの型の値を返します。
計算や加工、キャッシュの返却などに使えます。
public class Temperature
{
private double celsius;
// 取得時に摂氏をそのまま返す
public double Celsius
{
get { return celsius; }
set { celsius = value; }
}
// 取得時に華氏へ変換して返す(計算を挟む)
public double Fahrenheit
{
get { return celsius * 9 / 5 + 32; }
}
}
set アクセサ
set
は代入値 value
を受け取り、状態を更新します。
値の検証や範囲チェックの場として使います。
public class Player
{
private int level;
public int Level
{
get { return level; }
set
{
if (value < 1) throw new ArgumentOutOfRangeException(nameof(value), "Level は 1 以上である必要があります。");
level = value;
}
}
}
よくある注意点
set
の中で誤ってプロパティ自身に代入すると無限再帰になります。
必ずバッキングフィールドへ代入してください。
また、get
内で副作用の大きい処理を行うと予想外の回数呼ばれた際に性能問題が出ます。
自動実装プロパティの使い方
自動実装プロパティは、バッキングフィールドをコンパイラに自動生成させる書き方です。
簡単なラップで十分な場合は { get; set; }
の最小構文で書けます。
最小コード例と書き方
最小の書式はプロパティの型と名前、そして { get; set; }
です。
書式
public 型名 プロパティ名 { get; set; }
サンプル
public class Person
{
// 自動実装プロパティ。バックのフィールドはコンパイラが生成
public string Name { get; set; }
public int Age { get; set; }
}
既定値の設定(= 初期値)
自動実装プロパティにも初期値を与えられます。
プロパティ初期化子を使う方法と、コンストラクタで設定する方法があります。
プロパティ初期化子で設定する
宣言時に = 初期値;
を書きます。
参照型の既定値は null
、値型の既定値は型ごとのデフォルト値ですが、初期化子で明示しておくと null
参照を避けやすくなります。
public class Settings
{
public int RetryCount { get; set; } = 3; // 値型は 0 が既定値だが 3 に変更
public string Endpoint { get; set; } = "https://api.example.com";
public string ApiKey { get; set; } = string.Empty; // 参照型の既定値は null。空文字で初期化
public bool Enabled { get; set; } // 既定値は false のまま
}
コンストラクタで初期化する
初期値の計算や引数に基づいた設定が必要な場合はコンストラクタで行います。
コンストラクタ経由なら、バリデーションを set
に任せつつ安全に初期化できます。
public class Person
{
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
// 引数つきコンストラクタで初期化
public Person(string name, int age)
{
Name = name; // set を通る
Age = age; // set を通る
}
}
値型と参照型では既定値が異なるため、必要に応じて明示的に初期化しておくとバグを防げます。
プロパティにロジックを入れる
検証や変換などのロジックを入れたい場合は、バッキングフィールドを自分で用意してプロパティを記述します。
これにより、set
で検証し、get
で計算結果を返すといった柔軟な振る舞いが実装できます。
バッキングフィールドの作り方
まず private
なフィールドを用意し、get
と set
でそのフィールドを参照します。
定義手順
- クラス内に
private
フィールドを宣言する。 - 同じ型の
public
プロパティを宣言し、get
でフィールドを返し、set
でフィールドに代入する。
典型パターン
public class Product
{
private decimal price; // バッキングフィールド
public decimal Price
{
get { return price; }
set { price = value; } // 注意: プロパティ名ではなくフィールドへ代入する
}
}
set で検証・get で計算する
検証ロジックや計算ロジックをアクセサに分担させます。
set での検証
set
では代入値 value
をチェックし、不正な場合は例外を投げたり、丸め込みを行ったりします。
public class User
{
private int age;
public int Age
{
get => age;
set
{
if (value < 0 || value > 150)
{
throw new ArgumentOutOfRangeException(nameof(value), "Age は 0〜150 の範囲で指定してください。");
}
age = value;
}
}
}
get で計算する
get
では他のフィールドやプロパティを使って値をその場で組み立てられます。
内部状態を重複して持たず、一貫性を保てる利点があります。
public class Person
{
private string firstName = string.Empty;
private string lastName = string.Empty;
public string FirstName
{
get => firstName;
set => firstName = value?.Trim() ?? string.Empty; // set で軽く整形
}
public string LastName
{
get => lastName;
set => lastName = value?.Trim() ?? string.Empty;
}
// 取得時に計算して返す
public string FullName
{
get
{
var space = (firstName.Length > 0 && lastName.Length > 0) ? " " : string.Empty;
return $"{lastName}{space}{firstName}";
}
}
}
プロパティ内部では同名プロパティではなくフィールドを直接参照することで、不要な再帰呼び出しを避けられます。
手順で確認:C# プロパティの使い方
ここまでのポイントをひとつのプログラムで確認します。
クラスにプロパティを定義し、インスタンス化して get
と set
の動きを観察します。
クラスにプロパティを定義する
ここでは、生徒情報を表す Student
クラスを定義します。
Name
と Score
は自動実装プロパティ、Age
は検証つきプロパティ、Profile
は取得時に計算するプロパティです。
コンストラクタで初期化も行います。
コード
using System;
public class Student
{
// 自動実装プロパティ
public string Name { get; set; } = string.Empty;
// set で検証するプロパティ(バッキングフィールドあり)
private int age;
public int Age
{
get => age;
set
{
if (value < 6 || value > 120)
{
throw new ArgumentOutOfRangeException(nameof(value), "Age は 6〜120 の範囲で指定してください。");
}
age = value;
}
}
// 自動実装プロパティに既定値
public int Score { get; set; } = 50;
// 取得時に計算するプロパティ
public string Profile
{
get => $"{Name} さんは {Age} 歳で、スコアは {Score} 点です。";
}
// コンストラクタで初期化
public Student(string name, int age, int score = 50)
{
Name = name; // set を通る
Age = age; // set を通る(検証される)
Score = score;
}
}
インスタンス化して get/set を使う
作成したクラスを使ってインスタンスを生成し、プロパティを読み書きして挙動を確認します。
例外が投げられるケースも試します。
実行用コード
using System;
public class Program
{
public static void Main()
{
// 正常な初期化
var s1 = new Student("Taro", 12, 88);
Console.WriteLine(s1.Profile); // get で計算された文字列を表示
// set を通じて値を更新
s1.Score = 92; // 自動実装プロパティの書き込み
Console.WriteLine($"更新後スコア: {s1.Score}");
Console.WriteLine(s1.Profile); // 計算結果も更新される
// 既定値が効くケース
var s2 = new Student("Hanako", 10); // score を省略し既定値 50
Console.WriteLine(s2.Profile);
// 不正値の代入で検証が働くケース
try
{
s2.Age = 4; // 例外: 範囲外
}
catch (ArgumentOutOfRangeException ex)
{
Console.WriteLine($"検証エラー: {ex.Message}");
}
// 整形入りの set の例: 空白を含む名前を整える
s2.Name = " Hanako ";
Console.WriteLine($"整形後の名前: '{s2.Name}'");
}
}
// 先ほど定義した Student クラスを同一ファイル内に置くか、別ファイルで参照してください。
public class Student
{
public string Name { get; set; } = string.Empty;
private int age;
public int Age
{
get => age;
set
{
if (value < 6 || value > 120)
{
throw new ArgumentOutOfRangeException(nameof(value), "Age は 6〜120 の範囲で指定してください。");
}
age = value;
}
}
public int Score { get; set; } = 50;
public string Profile => $"{Name} さんは {Age} 歳で、スコアは {Score} 点です。";
public Student(string name, int age, int score = 50)
{
Name = name;
Age = age;
Score = score;
}
}
Taro さんは 12 歳で、スコアは 88 点です。
更新後スコア: 92
Taro さんは 12 歳で、スコアは 92 点です。
Hanako さんは 10 歳で、スコアは 50 点です。
検証エラー: Age は 6〜120 の範囲で指定してください。 (Parameter 'value')
整形後の名前: ' Hanako '
上の例では、Profile
は get
のたびに最新状態から文字列を組み立てるため、Score
を更新するとすぐに反映されます。
Age
は set
内で検証され、範囲外なら例外が発生します。
まとめ
プロパティは、外部からはフィールドのように扱えつつ、内部で get
と set
によって読み書きの挙動を細かく制御できる強力な仕組みです。
まずは自動実装プロパティでシンプルに始め、必要になった時点でバッキングフィールドを導入し、set
での検証や get
での計算を加える流れが実践的です。
初期値はプロパティ初期化子かコンストラクタで明示し、プロパティ内部ではフィールドを直接操作して無限再帰を避けることを心がけてください。
プロパティを上手に使い分けることで、読みやすく安全で拡張しやすいコードに近づけます。