- C#
C#クラスとプロパティの書き方|get・set・initとrecordまで
C#のプロパティとは、クラスの外からはフィールド(変数)のように読み書きでき、中ではメソッドのように処理を書けるメンバーです。 public string Name { get; set; } の1行が基本形で、get が読み取り、set が書き込み、init が「生成時だけの書き込み」を表します。
この記事では、publicフィールドから始めて、プロパティ→自動実装→get専用→init→required→fieldキーワード(C# 14)と 1段ずつ書き換えながら 違いを確かめます。各構文が使えるC#/.NETのバージョン対応表、「外から変更不可にしたい」「値を検証したい」といった目的別の早見表、コンストラクタ・プライマリコンストラクタ・オブジェクト初期化子・recordの使い分け、CS8618やinitへの再代入エラーの再現まで、.NET SDK 8.0.131(C# 12)で実行した結果 とあわせて紹介します。fieldキーワードだけは .NET 10 が必要なため未実行で、公式ドキュメントの記載に沿って説明します。
プロパティの書き方 早見表(目的別)
| やりたいこと | 書き方 | 外からの読み取り | 外からの書き込み |
|---|---|---|---|
| 自由に読み書きしたい | public string Name { get; set; } = ""; |
できる | いつでもできる |
| 外から変更不可にしたい(生成時に引数で渡す) | public string Name { get; } |
できる | できない(コンストラクタの中だけ) |
| 生成時だけ設定したい | public string Name { get; init; } = ""; |
できる | new X { Name = ... } のときだけ |
| 生成時の設定を必須にしたい | public required string Name { get; init; } |
できる | 生成時に必ず書く |
| クラスの中でだけ変更したい | public int Count { get; private set; } |
できる | できない(クラス内のメソッドから) |
| 値を検証したい | set { 検証; _stock = value; }(フィールドを自分で持つ) |
できる | 検証を通った値だけ |
| 値を検証したい(C# 14 以降) | set => field = 検証済みの値; |
できる | 検証を通った値だけ |
| 計算結果を返したい | public int Total => Price * Count; |
できる | できない |
| nullを許したい | public string? Note { get; set; } |
できる | いつでもできる |
| 一覧を読み取り専用で公開したい | public IReadOnlyList<string> Tags => _tags; |
できる | 追加はメソッド経由 |
| クラス全体で1つの値を持ちたい | public static int Created { get; private set; } |
クラス名.Created |
クラス内から |
| データをまとめるだけの型がほしい | record Point(int X, int Y); |
できる | できない(with でコピー) |
構文とバージョンの対応は次のとおりです。
| 構文 | 例 | 使えるC# | 対応する.NET |
|---|---|---|---|
| プロパティ(get/set を自分で書く) | get { return _name; } set { _name = value; } |
C# 1.0 | すべて |
| 自動実装プロパティ | { get; set; } |
C# 3.0 | .NET Framework 3.5 以降 |
| オブジェクト初期化子 | new Member { Name = "Sato" } |
C# 3.0 | .NET Framework 3.5 以降 |
| 自動実装プロパティの初期値 | { get; set; } = ""; |
C# 6.0 | .NET Framework 4.6 以降 |
| get専用の自動実装プロパティ | { get; } |
C# 6.0 | .NET Framework 4.6 以降 |
| 式形式のプロパティ | public int Total => A + B; |
C# 6.0 | .NET Framework 4.6 以降 |
| null許容参照型(CS8618の警告) | string? |
C# 8.0 | .NET Core 3.0 以降 |
| initアクセサ | { get; init; } |
C# 9.0 | .NET 5 以降 |
| record | record Point(int X, int Y); |
C# 9.0 | .NET 5 以降 |
| record struct | readonly record struct Size(int W, int H); |
C# 10 | .NET 6 以降 |
| required修飾子 | public required string Name { get; init; } |
C# 11 | .NET 7 以降 |
| クラスのプライマリコンストラクタ | class Shelf(string name) { } |
C# 12 | .NET 8 以降 |
| fieldキーワード | set => field = value.Trim(); |
C# 14 | .NET 10 以降 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「C#クラスとプロパティの書き方|get・set・initとrecordまで」
対応表は、Microsoft Learn の「The history of C#」と「What's new in C# 14」を今回開いて確認した内容です(record struct の行は一般に知られた仕様として記載しています)。使えるC#のバージョンは、プロジェクトファイルの <TargetFramework> で決まります。net8.0 なら C# 12、net10.0 なら C# 14 が既定です。
クラスの基本を30秒でおさらい
クラスは「データ(フィールド・プロパティ)」と「処理(メソッド)」をひとまとめにした型の設計図です。new で実体(インスタンス)を作って使います。
| 用語 | 意味 | 例 |
|---|---|---|
| クラス | 型の設計図 | class Member { ... } |
| インスタンス | クラスから作った実体 | var m = new Member(); |
| フィールド | クラスが持つ変数 | private int _age; |
| プロパティ | フィールドへの窓口 | public int Age { get; set; } |
| メソッド | クラスが持つ処理 | public void Rename(string name) { ... } |
| コンストラクタ | new のときに呼ばれる初期化処理 |
public Member(string name) { ... } |
| アクセス修飾子 | どこから見えるかの指定 | public(どこからでも)、private(クラスの中だけ) |
フィールドは private にして、外に見せるものはプロパティにする のがC#の基本的な書き方です。理由は次の章で、実際に書き換えながら確かめます。
フィールドからプロパティへ段階的に書き換える
dotnet new console -o prop で作ったプロジェクトの Program.cs を次の内容に置き換え、dotnet run で実行します。6つの段階を1つのファイルにまとめています。
// Program.cs : フィールドからプロパティへ、段階的に書き換える(.NET 8 / C# 12)
using System;
// ---- 段階1:public フィールド(外から何でも代入できる)
class Member1
{
public string Name = "";
public int Age;
}
// ---- 段階2:private フィールド + プロパティ(代入時に検証できる)
class Member2
{
private string _name = "";
private int _age;
public string Name
{
get { return _name; }
set { _name = value; }
}
public int Age
{
get => _age;
set
{
if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), "年齢は0以上にしてください");
_age = value;
}
}
}
// ---- 段階3:自動実装プロパティ(フィールドはコンパイラが用意する)
class Member3
{
public string Name { get; set; } = "";
public int Age { get; set; }
}
// ---- 段階4:get 専用(コンストラクタでだけ設定できる)
class Member4
{
public string Name { get; }
public int Age { get; }
public Member4(string name, int age) { Name = name; Age = age; }
}
// ---- 段階5:init(生成時のオブジェクト初期化子でも設定できる)
class Member5
{
public string Name { get; init; } = "";
public int Age { get; init; }
}
// ---- 段階6:required(生成時の設定を必須にする)
class Member6
{
public required string Name { get; init; }
public int Age { get; init; }
}
class Program
{
static void Main()
{
var m1 = new Member1 { Name = "Sato", Age = -5 };
Console.WriteLine($"1) {m1.Name} {m1.Age} ← 負の値もそのまま入る");
var m2 = new Member2 { Name = "Sato" };
try { m2.Age = -5; }
catch (ArgumentOutOfRangeException ex) { Console.WriteLine($"2) {ex.GetType().Name}: {ex.Message.Split('\n')[0].Trim()}"); }
var m3 = new Member3 { Name = "Sato", Age = 30 };
m3.Age = 31;
Console.WriteLine($"3) {m3.Name} {m3.Age}");
var m4 = new Member4("Sato", 30);
Console.WriteLine($"4) {m4.Name} {m4.Age}");
var m5 = new Member5 { Name = "Sato", Age = 30 };
Console.WriteLine($"5) {m5.Name} {m5.Age}");
var m6 = new Member6 { Name = "Sato" };
Console.WriteLine($"6) {m6.Name} {m6.Age}");
Console.WriteLine($"Runtime: {Environment.Version}");
}
}
実行結果(.NET SDK 8.0.131):
1) Sato -5 ← 負の値もそのまま入る
2) ArgumentOutOfRangeException: 年齢は0以上にしてください (Parameter 'value')
3) Sato 31
4) Sato 30
5) Sato 30
6) Sato 0
Runtime: 8.0.31
段階1:publicフィールド
public int Age; は書くのが簡単ですが、-5 のような値もそのまま入ります。あとから検証を足そうとすると、フィールドをプロパティに変える必要があり、そのクラスを使っている側の再コンパイルが必要になります。
段階2:privateフィールド+プロパティ
フィールドを private にして、get と set で出入り口を作ります。set の中の value は、代入された値を表す特別な名前 です。ここに条件を書けば、範囲外の値を受け取ったときに例外で知らせることができます。
段階3:自動実装プロパティ
検証が不要なら、{ get; set; } と書くだけでコンパイラがフィールドを用意してくれます。使う側の書き方は段階2と同じなので、あとから検証を足しても呼び出し側のコードは変わりません。 これが「最初からプロパティにしておく」理由です。
段階4:get専用プロパティ
{ get; } だけにすると、値を設定できるのはコンストラクタと宣言時の初期値だけになります。生成後に変わらないオブジェクト(イミュータブルなオブジェクト)を作る基本形です。
段階5:initアクセサ
{ get; init; } は、コンストラクタに加えて、new Member5 { Name = "Sato" } のオブジェクト初期化子でも設定できる 読み取り専用プロパティです。プロパティが多いクラスで、コンストラクタの引数が長くなるのを避けられます。
段階6:required修飾子
段階5のままだと、new Member5() と書いて Name を設定し忘れてもコンパイルが通ります。required を付けると、オブジェクト初期化子での設定が必須 になり、書き忘れはコンパイルエラー(CS9035)で分かります。
段階7:fieldキーワード(C# 14 / .NET 10)
段階2では、検証のために _age というフィールドを自分で宣言しました。C# 14 では、field と書くだけでコンパイラが用意したフィールドを参照 できます。次のコードは Microsoft Learn「What's new in C# 14」に載っている書き換え例です。
// 書き換え前:フィールドを自分で宣言する
private string _msg;
public string Message
{
get => _msg;
set => _msg = value ?? throw new ArgumentNullException(nameof(value));
}
// 書き換え後(C# 14):field キーワードを使う
public string Message
{
get;
set => field = value ?? throw new ArgumentNullException(nameof(value));
}
この構文は 今回の環境(.NET SDK 8.0.131)では未実行 です。同じ書き方を .NET 8 で <LangVersion>preview</LangVersion> を指定してビルドしたところ、次のエラーになり、.NET 8 のコンパイラでは使えないことを確かめました。
Program.cs(5,19): error CS8050: Only auto-implemented properties can have initializers.
Program.cs(5,26): error CS0501: 'Member.Name.get' must declare a body because it is not marked abstract, extern, or partial
.NET 10 以降を使っている場合は field、.NET 8 や 9 の場合は段階2の「privateフィールド+プロパティ」で書く、と使い分けます。
目的別のプロパティをまとめて書いたコード
早見表の書き方を1つのクラスにまとめた例です。コピーして、必要な行だけ使えます。
// Program.cs : 目的別のプロパティの書き方(.NET 8 / C# 12)
using System;
using System.Collections.Generic;
class Product
{
private readonly List<string> _tags = new();
private int _stock;
public required string Name { get; init; } // 生成時に必須・以後は変更不可
public int Price { get; private set; } // クラスの中でだけ変更
public string? Note { get; set; } // null を許す
public DateTime CreatedAt { get; } = DateTime.UtcNow; // 読み取り専用
public int TaxIncluded => (int)(Price * 1.1m); // 計算で求める(値を持たない)
public bool InStock => _stock > 0;
public IReadOnlyList<string> Tags => _tags; // 一覧は読み取り専用で公開
public static int Created { get; private set; } // クラス全体で1つ
public int Stock
{
get => _stock;
set
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
_stock = value;
}
}
public Product(int price) { SetPrice(price); Created++; }
public void SetPrice(int price)
{
ArgumentOutOfRangeException.ThrowIfNegative(price);
Price = price;
}
public void AddTag(string tag) => _tags.Add(tag);
}
class Program
{
static void Main()
{
var p = new Product(1000) { Name = "ノート", Stock = 3 };
p.AddTag("文具");
p.SetPrice(1200);
Console.WriteLine($"{p.Name} {p.Price}円 税込{p.TaxIncluded}円 在庫あり={p.InStock} タグ={string.Join(",", p.Tags)} メモ={p.Note ?? "(なし)"}");
Console.WriteLine($"作成数={Product.Created}");
try { p.Stock = -1; }
catch (ArgumentOutOfRangeException ex) { Console.WriteLine($"{ex.GetType().Name}: {ex.Message.ReplaceLineEndings(" ")}"); }
// null 許容参照型と警告
string? note = p.Note;
Console.WriteLine(note?.Length ?? 0);
}
}
実行結果(.NET SDK 8.0.131):
ノート 1200円 税込1320円 在庫あり=True タグ=文具 メモ=(なし)
作成数=1
ArgumentOutOfRangeException: value ('-1') must be a non-negative value. (Parameter 'value') Actual value was -1.
0
ポイントを表にまとめます。
| 書き方 | 覚えておきたい点 |
|---|---|
=> 式(式形式) |
値を保存せず、読むたびに計算します。{ get; } = 式 は生成時に1回だけ計算して保存します |
{ get; private set; } |
外からは読み取り専用、変更は SetPrice のようなメソッド経由にできます |
IReadOnlyList<T> で公開 |
List<T> をそのまま公開すると、外から Add や Clear ができます。読み取り専用のインターフェースで公開すると、変更の入口をメソッドに絞れます |
ArgumentOutOfRangeException.ThrowIfNegative |
.NET 8 で追加された検証用メソッドです。if と throw を1行で書けます |
string? |
「nullが入ることがある」とコンパイラに伝える書き方です。読むときは ?. や ?? でnullの場合を書きます |
DateTime.UtcNow を => で書くか { get; } = で書くかで意味が変わる点は、特に間違えやすいところです。
| 書き方 | 動き |
|---|---|
public DateTime CreatedAt { get; } = DateTime.UtcNow; |
生成時の時刻を保存し、何度読んでも同じ値 |
public DateTime Now => DateTime.UtcNow; |
読むたびに現在時刻を計算する |
コンストラクタ・プライマリコンストラクタ・オブジェクト初期化子・recordの使い分け
インスタンスに最初の値を渡す方法は4つあります。
| 方法 | 書き方 | 向いている場面 |
|---|---|---|
| コンストラクタ | new Book("本", 2800) |
必須の値が少なく、生成時に検証したい |
| プライマリコンストラクタ(C# 12) | class Shelf(string name, int capacity) |
受け取った値をそのまま使うだけの短いクラス |
| オブジェクト初期化子 | new Reader { Name = "Sato" } |
設定する項目が多く、名前付きで読みやすく書きたい |
| record | record Point(int X, int Y); |
データを運ぶだけの型。値での比較やコピーがほしい |
4つを1つのファイルで確かめます。
// Program.cs : コンストラクタ・プライマリコンストラクタ・オブジェクト初期化子・record(.NET 8 / C# 12)
using System;
// 1) コンストラクタ:必須の値を引数で受け取り、検証する
class Book
{
public string Title { get; }
public int Price { get; private set; }
public Book(string title, int price)
{
ArgumentException.ThrowIfNullOrWhiteSpace(title);
ArgumentOutOfRangeException.ThrowIfNegative(price);
Title = title;
Price = price;
}
public Book(string title) : this(title, 0) { } // 別のコンストラクタを呼ぶ
public void ChangePrice(int price)
{
ArgumentOutOfRangeException.ThrowIfNegative(price);
Price = price;
}
public override string ToString() => $"{Title} ({Price}円)";
}
// 2) プライマリコンストラクタ(C# 12):クラス名の後ろに引数を書く
class Shelf(string name, int capacity)
{
public string Name { get; } = name; // プロパティとして公開する
public int Capacity => capacity; // 引数をそのまま読む
private int _count;
public bool TryAdd() { if (_count >= capacity) return false; _count++; return true; }
public int Count => _count;
}
// 3) オブジェクト初期化子:required + init
class Reader
{
public required string Name { get; init; }
public string? Email { get; init; }
public int Level { get; init; } = 1;
}
// 4) record:値で比較でき、with でコピーできる
record Point(int X, int Y);
record Tag(string Name)
{
public string Color { get; init; } = "gray";
}
readonly record struct Size(int Width, int Height);
class Program
{
static void Main()
{
var b = new Book("C#の教科書", 2800);
b.ChangePrice(3000);
Console.WriteLine($"1) {b} / {new Book("無料冊子")}");
try { _ = new Book(" ", 100); }
catch (ArgumentException ex) { Console.WriteLine($" {ex.GetType().Name}: {ex.Message}"); }
try { _ = new Book("本", -1); }
catch (ArgumentException ex) { Console.WriteLine($" {ex.GetType().Name}: {ex.Message.ReplaceLineEndings(" ")}"); }
var s = new Shelf("A棚", 2);
Console.WriteLine($"2) {s.Name} {s.TryAdd()} {s.TryAdd()} {s.TryAdd()} {s.Count}/{s.Capacity}");
var r = new Reader { Name = "Sato", Email = "sato@example.com" };
Console.WriteLine($"3) {r.Name} {r.Email} Lv{r.Level}");
var p1 = new Point(1, 2);
var p2 = new Point(1, 2);
var p3 = p1 with { Y = 9 };
Console.WriteLine($"4) {p1} == {p2} : {p1 == p2}");
Console.WriteLine($" with: {p3} 元: {p1}");
var (x, y) = p3;
Console.WriteLine($" 分解: x={x}, y={y}");
Console.WriteLine($" {new Tag("csharp") { Color = "blue" }}");
Console.WriteLine($" {new Size(3, 4)} {new Size(3, 4) == new Size(3, 4)}");
var k1 = new Book("同じ本", 100);
var k2 = new Book("同じ本", 100);
Console.WriteLine($" class 同士の == : {k1 == k2} Equals: {k1.Equals(k2)}");
}
}
実行結果(.NET SDK 8.0.131):
1) C#の教科書 (3000円) / 無料冊子 (0円)
ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'title')
ArgumentOutOfRangeException: price ('-1') must be a non-negative value. (Parameter 'price') Actual value was -1.
2) A棚 True True False 2/2
3) Sato sato@example.com Lv1
4) Point { X = 1, Y = 2 } == Point { X = 1, Y = 2 } : True
with: Point { X = 1, Y = 9 } 元: Point { X = 1, Y = 2 }
分解: x=1, y=9
Tag { Name = csharp, Color = blue }
Size { Width = 3, Height = 4 } True
class 同士の == : False Equals: False
コンストラクタ:必須の値と検証をまとめる
コンストラクタは、クラス名と同じ名前で、戻り値の型を書かないメソッドです。「この値がないとオブジェクトとして成り立たない」ものを引数にする と、中途半端な状態のインスタンスが作られなくなります。: this(title, 0) と書くと、別のコンストラクタに処理を任せられ、検証を1か所にまとめられます。
プライマリコンストラクタ:引数をクラス全体で使う
C# 12 から、クラス名のすぐ後ろに引数を書けます。引数 name や capacity はクラスの中のどこからでも使えます。覚えておきたい点は2つです。
- 引数はプロパティにはなりません。 外に公開したいときは
public string Name { get; } = name;のように自分で書きます(recordの場合は自動でプロパティになります)。 - 引数は読み取り専用ではありません。 クラスの中で
capacity = 10;と代入できます。変えたくない値は、get専用プロパティかreadonlyフィールドに移しておくと意図が伝わります。
オブジェクト初期化子:名前付きで設定する
new Reader { Name = "Sato", Email = "..." } は、インスタンスを作った直後にプロパティへ代入する書き方です。init と required を組み合わせると、「生成時に必ず設定し、あとからは変えられない」 という性質を、コンストラクタを書かずに表せます。
record:値で比べられるデータの型
record Point(int X, int Y); の1行で、次のものがまとめて用意されます。
| 自動で用意されるもの | 実行結果での確認 |
|---|---|
X・Y の { get; init; } プロパティ |
p1.X で読める |
値での比較(==・Equals) |
p1 == p2 が True |
中身が分かる ToString |
Point { X = 1, Y = 2 } |
with 式でのコピー |
p1 with { Y = 9 } で新しいインスタンス。元の p1 は変わらない |
| 分解(Deconstruct) | var (x, y) = p3; |
通常の class は、中身が同じでも別のインスタンスなら == は False です(実行結果の最後の行)。「中身が同じなら同じもの」として扱いたいデータは record、状態を持って振る舞うものは class と分けると選びやすくなります。
| 型 | 比較 | 置き場所 | 向いている用途 |
|---|---|---|---|
class |
参照(同じインスタンスか) | ヒープ | 状態を変えながら使うオブジェクト |
record(record class) |
値(中身が同じか) | ヒープ | 画面やAPIの間で受け渡すデータ、設定値 |
readonly record struct |
値 | 値型として保持 | 座標・サイズなど小さな値 |
struct |
値(== は自分で定義) |
値型として保持 | 小さく、頻繁に作る値 |
よく出るエラーと警告を再現する
次のコードは、プロパティまわりで出やすいエラーを1つのファイルにまとめたものです。
// Program.cs : よく出るコンパイルエラー・警告を再現する(.NET 8 / C# 12)
var a = new Article("C#入門") { Category = "tech" };
a.Title = "変更"; // get 専用への代入
a.Category = "news"; // init への再代入
var t = new Tag(); // required の設定漏れ
var c = new Counter();
c.Count = 5; // private set への外からの代入
System.Console.WriteLine(a.Title + t.Name + c.Count);
class Article
{
public string Title { get; }
public string Category { get; init; } = "";
public string Body { get; set; } // 初期値なしの string
public Article(string title) { Title = title; }
}
class Tag
{
public required string Name { get; init; }
}
class Counter
{
public int Count { get; private set; }
public void Increment() => Count++;
}
dotnet build の結果(.NET SDK 8.0.131。パスは省略):
Program.cs(15,12): warning CS8618: Non-nullable property 'Body' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
Program.cs(3,1): error CS0200: Property or indexer 'Article.Title' cannot be assigned to -- it is read only
Program.cs(4,1): error CS8852: Init-only property or indexer 'Article.Category' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.
Program.cs(5,13): error CS9035: Required member 'Tag.Name' must be set in the object initializer or attribute constructor.
Program.cs(7,1): error CS0272: The property or indexer 'Counter.Count' cannot be used in this context because the set accessor is inaccessible
| コード | 意味 | 直し方 |
|---|---|---|
| CS8618(警告) | null非許容のプロパティが、コンストラクタを抜けるときに未設定 | 下の「CS8618の直し方」の4つから選ぶ |
| CS0200 | { get; } のプロパティに外から代入した |
生成時に渡す。変更が必要なら set か変更用メソッドを用意する |
| CS8852 | init のプロパティに、生成後に代入した |
with 式でコピーを作る(record)か、set に変える |
| CS9035 | required のメンバーを初期化子で設定していない |
new Tag { Name = "..." } と書く |
| CS0272 | private set のプロパティに外から代入した |
クラスが用意したメソッド(Increment など)を使う |
CS8618の直し方
CS8618は警告なのでビルドは通りますが、そのまま実行すると次のようになります。
var u = new User();
System.Console.WriteLine(u.Name.Length);
class User
{
public string Name { get; set; }
}
実行結果(.NET SDK 8.0.131):
Program.cs(5,19): warning CS8618: Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
at Program.<Main>$(String[] args) in Program.cs:line 2
警告は「このままだとNullReferenceExceptionになる可能性があります」という案内 です。直し方は4つあり、プロパティの性質で選びます。
| 直し方 | 書き方 | 選ぶ基準 |
|---|---|---|
| 初期値を入れる | public string Name { get; set; } = ""; |
空文字が自然な既定値のとき |
| 生成時の設定を必須にする | public required string Name { get; set; } |
必ず値が必要なとき(C# 11 以降) |
| コンストラクタで受け取る | public User(string name) { Name = name; } |
検証もあわせて行いたいとき |
| nullを許す | public string? Name { get; set; } |
「未設定」に意味があるとき |
= null!; と書いて警告を消す方法もありますが、これは「あとで必ず設定します」とコンパイラに約束する書き方です。フレームワークが値を入れてくれることが分かっている場合に限って使うと、警告の役割を保てます。
initのプロパティを変えたいとき
init のプロパティは生成後に代入できません。値を変えたものがほしいときは、recordの with 式で コピーを作ります。
var p1 = new Point(1, 2);
var p3 = p1 with { Y = 9 }; // p1 は変わらず、p3 が新しく作られる
元のオブジェクトが変わらないので、「どこで値が変わったのか」を追いかける手間が減ります。
プロパティにするかメソッドにするかの判断
| 判断のポイント | プロパティが向いている | メソッドが向いている |
|---|---|---|
| 処理の重さ | すぐ返せる(フィールドを返す、簡単な計算) | 時間がかかる(ファイル・通信・大きな計算) |
| 呼ぶたびの結果 | 状態が同じなら同じ値 | 呼ぶたびに変わることがある |
| 副作用 | ない | ある(保存する、送信する) |
| 引数 | 不要 | 必要 |
| 非同期 | 使えない(async にできない) |
async Task<T> にできる |
GetName()・SetName() のようなメソッドをそろえる書き方は、C#ではプロパティ1つにまとめるのが一般的です。逆に、時間のかかる処理や非同期の処理はメソッドにする と、呼び出す側が重さに気づけます。
書くときのチェックリスト
| 確認すること | 目安 |
|---|---|
フィールドは private になっているか |
外に見せるものはプロパティにする |
set が本当に必要か |
迷ったら { get; } か { get; init; } から始め、必要になったら広げる |
| 必須の値は生成時にそろうか | コンストラクタの引数、または required |
| null非許容の参照型に初期値があるか | CS8618 の警告が0件になっているか |
| 検証は1か所にまとまっているか | set またはコンストラクタ・変更用メソッドの中 |
| コレクションをそのまま公開していないか | IReadOnlyList<T> などで公開する |
| データを運ぶだけの型か | record を検討する |
| 命名は PascalCase か | プロパティは Name、privateフィールドは _name |
よくある質問
Q. フィールドとプロパティの違いは何ですか?
A. フィールドは値をそのまま保存する変数、プロパティは読み書きの入口です。プロパティは get と set に処理を書けるので、検証を入れたり、読み取り専用にしたりできます。外に公開するものはプロパティにしておくと、あとから中の実装を変えても呼び出し側を変えずに済みます。
Q. { get; } と { get; init; } と { get; private set; } はどう違いますか?
A. { get; } はコンストラクタと初期値でだけ設定できます。{ get; init; } はそれに加えてオブジェクト初期化子でも設定できます。{ get; private set; } は生成後もクラスの中のメソッドから変更できます。「生成後に変わるかどうか」と「どこから設定するか」で選びます。
Q. initとrequiredは一緒に使うものですか?
A. 別々の機能です。init は「生成時だけ書き込める」、required は「生成時に必ず書く」を表します。public required string Name { get; init; } と組み合わせると、「必ず設定し、以後は変えられない」プロパティになります。required は { get; set; } にも付けられます。
Q. recordとclassはどちらを使えばよいですか? A. データを受け渡すだけで、中身が同じなら同じものとして扱いたい場合は record が向いています。状態を持ち、メソッドで振る舞いを表すオブジェクトは class が向いています。
Q. fieldキーワードは .NET 8 で使えますか? A. 使えません。C# 14(.NET 10)からの機能で、今回 .NET SDK 8.0.131 で試したところコンパイルエラーになりました。.NET 8 や 9 では、privateフィールドを自分で宣言する書き方(段階2)を使います。
まとめ
- プロパティは、外からはフィールドのように使え、中に処理を書けるメンバー です。基本形は
public string Name { get; set; } = "";です。 { get; }はコンストラクタだけ、{ get; init; }は生成時だけ、{ get; private set; }はクラスの中だけ で設定できます。required(C# 11)を付けると、生成時の設定漏れをコンパイルエラーで気づけます。- 値の検証は
setの中に書きます。 C# 14(.NET 10)からはfieldキーワードで、フィールドの宣言を省けます。 - CS8618は「初期値を入れる・required・コンストラクタ・
string?」の4つから選んで直します。 - initのプロパティに再代入するとCS8852になります。 値を変えたいときは
with式でコピーを作ります。 - データを運ぶ型は record、状態と振る舞いを持つ型は class と分けると選びやすくなります。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「C#クラスとプロパティの書き方|get・set・initとrecordまで」
株式会社RJCでは、C#のクラス設計やプロパティの書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。