C#プロパティの基本:get/set・自動実装・init・セッターの検証

C#プロパティのget/setとvalue、自動実装プロパティ、init(C# 9)、=> と { get; } = の違い、セッターでの値検証まで。.NET 10で実行した出力とエラーコードで確認した基本編。

2026年11月改訂: この記事は2021年に韓国語で書いたプロパティ入門を、日本語向けに書き直したものです。.NET 10.0.14(C# 14)でコンパイルし直すと、元の例は動くものの warning CS8618(null 非許容フィールド)が新たに出て、セッターが不正な値を黙って無視する作りと、「3文字以上は不可」というメッセージ(実際の条件は3文字超)に問題がありました。修正版に加え、2021年版になかった自動実装プロパティの初期化子、init、式形式のメンバーを、導入バージョンをコンパイラで確かめたうえで足しています。

「フィールドを public にしていいのか」と迷っている人向けの、プロパティ基本編です。結論を先に書くと、外に見せる値はフィールドではなくプロパティにし、値にルールがあるならセッターで確認して例外を投げるのが基本です。C# 14 の field キーワード、required、INotifyPropertyChanged、計算プロパティとキャッシュ、フィールドとの速度比較は C#プロパティ応用編 で扱い、ここでは繰り返しません。

public フィールドの何が困るのか

クラスを書いていると、public int Age; のように public フィールドで済ませたくなります。手軽ですが、どこからでも Age = -1 が書けてしまい、あとで挙動がおかしくなっても誰が壊したのか追えません。C++ や Java でおなじみの対策は、private フィールドに Get/Set メソッドを付ける方法です。

class MyClass
{
    private int field;
    public int GetField() { return field; }
    public void SetField(int field) { this.field = field; }
}

動きはしますが、フィールドが1つ増えるたびにメソッドが2つ増え、呼び出し側も obj.SetField(obj.GetField() + 1) と冗長になります。C# のプロパティは、private フィールドと2つのメソッドという中身はそのままに、外からは obj.Field += 1 とフィールドのように書けるようにする仕組みです。

get・set と value:2021年の例を .NET 10 で

プロパティには値を返す get アクセサーと、新しい値を受け取る set アクセサーがあります。セッターの中では、代入された値を value という名前で受け取ります。value は自分で宣言する変数ではなく、コンパイラがセッターに付ける暗黙の引数です。どちらか片方だけでもよく、get だけなら読み取り専用、set だけなら書き込み専用になります(書き込み専用は実務ではまず使いません)。

2021年の例を、文字列を日本語にして不要な using を外したものです。

class Student
{
    private string name;
    private int age;

    public string Name
    {
        get { return name; }
        set
        {
            if (value.Length > 3)
                Console.WriteLine("名前は3文字を超えられません。");
            else
                name = value;
        }
    }
    public int Age
    {
        get { return age; }
        set { age = value; }
    }
}

class Program
{
    static void Main()
    {
        Student student = new Student();
        student.Age = 13;
        student.Name = "アレクサンダー";
        student.Name = "佐藤";
        Console.WriteLine("私の名前は" + student.Name + "、" + student.Age + "歳です。");
    }
}
Program.cs(3,20): warning CS8618: null 非許容の フィールド 'name' には、コンストラクターの終了時に null 以外の値が入っていなければなりません。'required' 修飾子を追加するか、フィールド を Null 許容として宣言することを検討してください。
名前は3文字を超えられません。
私の名前は佐藤、13歳です。

ポイントは3つです。

  1. CS8618 は .NET 6 以降の警告:プロジェクトテンプレートで null 許容参照型が有効になったため、初期値のない string フィールドに警告が出ます。private string name = ""; と初期値を入れれば消えます。
  2. メッセージと条件のずれ:2021年版のメッセージは「3文字以上は不可」でしたが、条件 > 3 はちょうど3文字を通します。文言を直しました。
  3. 黙って失敗する:呼び出し側は「アレクサンダー」が拒否されたことを知る手段がありません。後半で例外に置き換えます。

元の出力の最後にあった「続行するには何かキーを押してください」は Visual Studio のコンソールが付けていた行で、dotnet run では出ません。

プロパティの正体は2つのメソッド

2021年版は「プロパティは変数ではなくメソッド」と書いていましたが、これは正確です。リフレクションで Student の public メソッドを列挙すると、コンパイラが作ったアクセサーがそのまま見えます。

foreach (var m in typeof(Student).GetMethods(
    BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
    Console.WriteLine(m.Name);
get_Name
set_Name
get_Age
set_Age
図。上段:s.Name = "Kim" という代入は set_Name(value) を呼び、値が妥当かを確認する。妥当なら private フィールド name に保存し、そうでなければ ArgumentException を投げてフィールドは元の値のまま。下段:s.Name を読む式は get_Name() を呼び、private フィールド name の値を返す。
プロパティは2つのメソッドの組。代入すると新しい値を value として set_Name が呼ばれ、読み取ると get_Name が呼ばれる。private フィールドを書き換えるかどうかはセッターだけが決める。

メソッド呼び出しなので、フィールドなら通る次の2行がプロパティではエラーになります。

Bump(ref t.Count);    // Count がプロパティの場合
r.Size.Width = 5;     // Size が構造体を返すプロパティの場合
error CS0206: 参照を返さないプロパティまたはインデクサーを out 値または ref 値として使用することはできません
error CS1612: 変数ではないため、'Rect.Size' の戻り値を変更できません

CS0206 はプロパティには参照で渡せる「置き場所」がないという意味です。CS1612 はコンパイラが守ってくれている例で、get_Size() が返すのは構造体のコピーなので、それを書き換えても元は変わりません。ローカル変数に受けて直し、代入し直します(var s = r.Size; s.Width = 5; r.Size = s;)。

自動実装プロパティと初期化子

ここから下の例は、トップレベルステートメントを使う Program.cs 1つで実行しました。紙面ではクラスと実行コードを並べていますが、実際のファイルでは実行文をクラス宣言より上に置く必要があります。逆にすると error CS8803: トップレベルのステートメントは、名前空間および型の宣言の前にある必要があります。 になります。

セッターにルールがないなら、フィールドを自分で書く必要はありません。{ get; set; } だけで、コンパイラが隠れたバッキングフィールドを作ります。各構文の導入バージョンは、-p:LangVersion を1つ下げてコンパイルして確かめました。

class Tank
{
    public int Count { get; set; }                    // 自動実装プロパティ(C# 3)
    public int Level { get; } = 1;                    // 読み取り専用 + 初期値(C# 6)
    public string Label { get; private set; } = "";   // 外からは読むだけ
}
構文 導入 1つ下のバージョンでのエラー
{ get; set; } C# 3 CS8023(C# 2)
{ get; } = 1 C# 6 CS8026(C# 5)
int X => 1(式形式) C# 6 CS8026(C# 5)
get => x; set => x = value; C# 7.0 CS8059(C# 6)
{ get; init; } C# 9 CS8400(C# 8)

2021年の応用編では自動実装プロパティを「.NET Framework 3.0 から」と書いていましたが、これは C# 3 の機能で、一緒に出たのは .NET Framework 3.5 です。get だけの自動実装プロパティは、コンストラクターか初期化子でしか値を入れられません。

error CS0200: プロパティまたはインデクサー 'Tank.Level' は読み取り専用であるため、割り当てることはできません

init:生成するときだけ書ける

C# 9 の init は、オブジェクト初期化子では書けて、その後は読み取り専用になるアクセサーです。引数の多いコンストラクターを書かずに不変オブジェクトを作れます。

var p = new Point { X = 1, Y = 2 };
Console.WriteLine(p);   // (1, 2)
p.X = 10;               // エラー

class Point
{
    public int X { get; init; }
    public int Y { get; init; }
    public override string ToString() => $"({X}, {Y})";
}
error CS8852: init 専用プロパティまたはインデクサー 'Point.X' を割り当てることができるのは、オブジェクト初期化子の中か、インスタンス コンストラクターまたは 'init' アクセサーの 'this' か 'base' 上のみです。

init は「一度だけ書ける」ことを保証しますが、「必ず書かせる」ことまではしません。それは required の役目で、エラーコードと合わせて 応用編 で扱います。

=> と { get; } = は別物

式形式(=>)はゲッターを1行で書く構文ですが、見た目が似た次の2行はまったく違う動きをします。

var cc = new Counter();
_ = cc.A; _ = cc.A; _ = cc.B; _ = cc.B;
Console.WriteLine($"calls={Counter.Calls}");

class Counter
{
    public static int Calls;
    static int Next() => ++Calls;
    public int A { get; } = Next();   // 生成時に1回だけ計算して保存
    public int B => Next();           // 読むたびに実行
}
calls=3

A は生成時の1回、B は2回の読み取りのたびに Next() を呼びました。実務でいえば public DateTime Created { get; } = DateTime.Now; は生成時刻を記録し、public DateTime Created => DateTime.Now; は読むたびに現在時刻を返します。= 1文字の違いで、レビューでも見落とされがちです。計算結果をキャッシュすべきかどうかは、応用編 で計測しています。

セッターで値を検証する

2021年の例を3点直しました。名前の上限を10文字に広げ、条件に合わなければ例外を投げ、年齢には負数チェックを入れます。ArgumentNullException.ThrowIfNull(.NET 6)と ArgumentOutOfRangeException.ThrowIfNegative(.NET 8)を使えば、チェックは1行で済みます。大事なのは、コンストラクターでもフィールドではなくプロパティに代入して、同じ検証を通すことです。

var s = new Student("Kim", 13);
try { s.Name = "Maximilian Alexander"; } catch (ArgumentException e) { Console.WriteLine(e.Message); }
try { s.Age = -1; } catch (ArgumentOutOfRangeException e) { Console.WriteLine(e.Message); }
Console.WriteLine($"after: {s.Name}, {s.Age}");

class Student
{
    private string name = "";
    private int age;

    public Student(string name, int age) { Name = name; Age = age; }

    public string Name
    {
        get => name;
        set
        {
            ArgumentNullException.ThrowIfNull(value);
            if (value.Length > 10)
                throw new ArgumentException("Name must be 10 characters or fewer.", nameof(value));
            name = value;
        }
    }

    public int Age
    {
        get => age;
        set
        {
            ArgumentOutOfRangeException.ThrowIfNegative(value);
            age = value;
        }
    }
}
Name must be 10 characters or fewer. (Parameter 'value')
value ('-1') must be a non-negative value. (Parameter 'value')
Actual value was -1.
after: Kim, 13

不正な代入は例外として表に出て、オブジェクトは正しい状態(Kim, 13)を保ちます。もう1つの落とし穴は、string.Length が文字数ではなく UTF-16 のコード単位数だという点です。「佐藤」は2ですが、絵文字 "👍".Length は2になりました。ユーザーが見る「文字数」で制限したいなら StringInfo.LengthInTextElements を使います。

同じ検証を private フィールドなしで書く方法(C# 14 の field キーワード)は応用編にあります。

どれを使うか

状況 書き方
ルールのない値 public int Count { get; set; }
一度決めたら変えない値 { get; init; }、または { get; } + コンストラクター
外からは読むだけ、中では変更 { get; private set; }
他の値から毎回計算 public int Sum => A + B;
生成時に決まる値 public DateTime Created { get; } = DateTime.Now;
範囲や形式のルールがある値 セッターで確認して例外

プロパティの中身はメソッドなので、引数・ref・オーバーロードの話は C#のメソッド がそのまま当てはまります。セッターから投げた例外のコストは C#の例外処理 で計測しています。

確認環境:.NET SDK 10.0.403 / ランタイム 10.0.14、C# 14(net10.0 の既定値)、macOS(Apple Silicon Mac)。

よくある質問

自動実装プロパティと public フィールドは何が違う?

自動実装プロパティでは、コンパイラが private フィールドと get_X/set_X メソッドを生成します。あとからセッターに検証を足しても呼び出し側のコードやバイナリ互換性は崩れず、データバインディングやシリアライザーもプロパティを前提に動きます。速度は Release ビルドで差がありませんでした(2億回の読み取りでどちらも127.9ms、.NET 10)。

public int A { get; } = Next(); と public int B => Next(); は同じ?

違います。{ get; } = はオブジェクト生成時に一度だけ計算して保存し、=> は読むたびに式を実行します。A と B を2回ずつ読んだとき、Next() の呼び出し回数は3回(A が1回、B が2回)でした。

セッターに不正な値が来たら、無視する?例外を投げる?

例外を投げるほうが安全です。メッセージを出して無視するだけでは、呼び出し側は代入が失敗したことに気づけません。.NET 8 以降は ArgumentOutOfRangeException.ThrowIfNegative などのヘルパーで1行で書けます。

  • #C#
  • #プロパティ
  • #init
  • #自動実装プロパティ
  • #.NET

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑