C#: アクセス修飾子とプロパティ
1. アクセス修飾子
アクセス修飾子は、クラスメンバーへのアクセス権を制御し、カプセル化の中核となる仕組みです。C# では、6 つのアクセスレベルが用意されています:
| 修飾子 | 同じクラス | サブクラス(同じアセンブリ) | サブクラス(別のアセンブリ) | 同じアセンブリ | 別のアセンブリ |
|---|---|---|---|---|---|
public |
✅ | ✅ | ✅ | ✅ | ✅ |
private |
✅ | ❌ | ❌ | ❌ | ❌ |
protected |
✅ | ✅ | ✅ | ❌ | ❌ |
internal |
✅ | ❌ | ❌ | ✅ | ❌ |
protected internal |
✅ | ✅ | ❌ | ✅ | ✅ |
private protected |
✅ | ✅ | ❌ | ❌ | ❌ |
💡
protected internalは「OR」の関係です:サブクラスであるか、または同じアセンブリ内にあるものがアクセスできます。private protectedは「AND」の関係です:サブクラスであるかつ同じアセンブリ内にある両方の条件を満たす必要があります。
▶ サンプル
CSHARP
public class Animal
{
public string Species;
private int health = 100;
protected int energy = 90;
internal string category = "Mammal";
public void ShowInfo()
{
Console.WriteLine($"Species: {Species}, Health: {health}, Energy: {energy}, Category: {category}");
}
public void SetHealth(int h) { health = h; }
}
public class Dog : Animal
{
public void Run()
{
energy -= 10;
category = "Mammal";
}
}
class Program
{
static void Main()
{
Animal a = new Dog { Species = "Dog" };
a.ShowInfo();
((Dog)a).Run();
a.ShowInfo();
}
}
TEXT
📖 参照専用
Species: Dog, Health: 100, Energy: 90, Category: Mammal
Species: Dog, Health: 100, Energy: 80, Category: Mammal
2. カプセル化の原則
カプセル化を行うには、フィールドをprivateに設定し、publicプロパティを通じて読み取り/書き込みアクセスを制御することで、外部コードによる内部データの直接的な変更を防ぐ必要があります。
▶ サンプル
CSHARP
public class BankAccount
{
private decimal balance;
public decimal Balance
{
get { return balance; }
set
{
if (value < 0)
throw new ArgumentException("Balance cannot be negative");
balance = value;
}
}
}
CSHARP
var account = new BankAccount();
account.Balance = 1000m;
Console.WriteLine($"Balance: {account.Balance}");
TEXT
📖 参照専用
Balance: 1000
⚠️
publicフィールドを直接作成すると、カプセル化が破られ、後で検証ロジックを追加できなくなります。必ずprivateフィールドとpublicプロパティを併用してください。
3. プロパティ
プロパティはフィールドの安全なラッパーであり、getおよびsetアクセサを通じて読み書きのロジックを制御します。valueはsetアクセサの暗黙のパラメータであり、呼び出し元によって割り当てられた値を表します。
▶ サンプル
CSHARP
public class Student
{
private string name;
public string Name
{
get { return name; }
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Name cannot be empty");
name = value;
}
}
}
CSHARP
var s = new Student();
s.Name = "Alice";
Console.WriteLine(s.Name);
TEXT
📖 参照専用
Alice
4. 自動プロパティ
追加のロジックが必要ない場合は、自動プロパティ{ get; set; }を使用してください。これにより、コンパイラが自動的に非表示のバッキングフィールドを生成します。
▶ サンプル
CSHARP
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
CSHARP
var p = new Product { Name = "Pen", Price = 2.5m };
Console.WriteLine($"{p.Name}: ${p.Price}");
TEXT
📖 参照専用
Pen: $2.5
📌 自動プロパティは、定型コードを削減します。検証が必要な場合は、呼び出し元に影響を与えることなく、いつでも完全なプロパティに展開することができます。
5. init アクセサ (C# 9)
initアクセサを使用すると、オブジェクトの初期化時にプロパティを割り当てることができ、その後そのプロパティは読み取り専用となるため、不変オブジェクトを実現できます。
▶ サンプル
CSHARP
public class Config
{
public string Host { get; }
public int Port { get; }
public Config(string host, int port)
{
Host = host;
Port = port;
}
}
class Program
{
static void Main()
{
var cfg = new Config("localhost", 8080);
System.Console.WriteLine($"{cfg.Host}:{cfg.Port}");
}
}
TEXT
📖 参照専用
localhost:8080
⚠️ 初期化後に値を代入すると、コンパイルエラーが発生します:
cfg.Port = 9090;は不正です。
6. 読み取り専用プロパティ
getのみが指定されているプロパティは、読み取り専用プロパティです。その値は、コンストラクタ内でバッキングフィールドを介してのみ代入できます。
▶ サンプル
CSHARP
public class Circle
{
public double Radius { get; }
public Circle(double radius)
{
Radius = radius;
}
}
CSHARP
var c = new Circle(5.0);
Console.WriteLine($"Radius: {c.Radius}");
TEXT
📖 参照専用
Radius: 5
7. 計算プロパティ
計算プロパティにはバッキングフィールドがありません。データを格納することなく、式本体のメンバ=>を使用して動的に値を計算し、返します。
▶ サンプル
CSHARP
public class Rectangle
{
public double Width { get; set; }
public double Height { get; set; }
public double Area => Width * Height;
public double Perimeter => 2 * (Width + Height);
}
CSHARP
var r = new Rectangle { Width = 3, Height = 4 };
Console.WriteLine($"Area: {r.Area}, Perimeter: {r.Perimeter}");
TEXT
📖 参照専用
Area: 12, Perimeter: 14
8. 必須の修飾子 (C# 11)
required修飾子を使用すると、呼び出し側は初期化時にプロパティに値を割り当てる必要があり、そうしないとコンパイルエラーが発生します。
▶ サンプル
CSHARP
public class User
{
public string Name { get; }
public string Email { get; }
public int Age { get; }
public User(string name, string email, int age)
{
Name = name;
Email = email;
Age = age;
}
}
class Program
{
static void Main()
{
var u = new User("Bob", "bob@test.com", 25);
System.Console.WriteLine($"{u.Name} ({u.Email})");
}
}
TEXT
📖 参照専用
Bob (bob@test.com)
💡
requiredは通常、initと組み合わせて使用され、不変オブジェクトの必須フィールドが見落とされないようにします。
9. レコードの概要
recordは、C# 9で導入された参照型であり、不変性と値による等価性をネイティブにサポートしているため、純粋なデータオブジェクトに最適です。
▶ サンプル
CSHARP
public class Person : IEquatable<Person>
{
public string Name { get; }
public int Age { get; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
public bool Equals(Person other)
{
return other != null && Name == other.Name && Age == other.Age;
}
public override bool Equals(object obj) => Equals(obj as Person);
public override int GetHashCode() => System.HashCode.Combine(Name, Age);
public override string ToString() => $"Person {{ Name = {Name}, Age = {Age} }}";
}
class Program
{
static void Main()
{
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);
System.Console.WriteLine($"Equal: {p1 == p2}");
System.Console.WriteLine(p1);
}
}
CSHARP
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);
Console.WriteLine($"Equal: {p1 == p2}");
Console.WriteLine(p1);
TEXT
📖 参照専用
Equal: True
Person { Name = Alice, Age = 30 }
🔥 このレコードのパラメータリストは、自動的に
initという読み取り専用プロパティを生成し、==は参照ではなく値で比較を行います。これについては、後のレッスンで詳しく解説します。
❓ よくある質問
Q
protected internalとprivate protectedの違いは何ですか?A
protected internalは「OR」の関係(サブクラスまたは同じアセンブリに属している場合)、private protectedは「AND」の関係(サブクラスであり、かつ同じアセンブリに属している必要がある)です。Q オートプロパティとパブリックフィールドの違いは何ですか?
A オートプロパティにはバッキングフィールドがあり、いつでもロジックを追加するために拡張でき、インターフェースの実装やデータバインディングをサポートしていますが、パブリックフィールドにはこれらの機能がありません。
Q
initとsetの違いは何ですか?A
setはいつでも代入できますが、initはオブジェクトの初期化時(コンストラクタまたはオブジェクト初期化子)にのみ代入でき、その後は読み取り専用となります。Q 読み取り専用プロパティ
{ get; }は、コンストラクタの外で代入できますか?A いいえ。コンストラクタ内でバッキングフィールドを介してのみ代入でき、その後変更することはできません。
Q
recordとclassの主な違いは何ですか?A
recordはデフォルトで値による等価比較を行い、withというコピーを作成する式をサポートしており、不変データに適しています。 classは参照による比較を行います。📖 まとめ
- メンバーの可視性を制御するアクセス修飾子は6つあります:
public、private、protected、internal、protected internal、private protected - カプセル化の原則:フィールド
private、プロパティpublic、アクセサを通じて読み書きおよび検証を制御する - プロパティは、
get/setアクセサを通じてフィールドをラップしており、valueはsetの暗黙のパラメータです - 自動プロパティ
{ get; set; }にはコンパイラによって生成されたバッキングフィールドがあり、定型コードを削減します initアクセサ(C# 9)は、プロパティへの書き込みを初期化時のみに制限し、不変性を確保します- 読み取り専用プロパティ
{ get; }は、コンストラクタ内でのみ代入できます - 計算プロパティは、動的評価のために
=>式を使用し、バッキングフィールドは存在しません required修飾子(C# 11)は、初期化時に代入を強制しますrecord型は、不変性と値による等価性をネイティブにサポートしています
📝 練習問題
Temperatureフィールドがprivateに設定されたTemperatureクラスを作成し、Celsiusプロパティのsetアクセサを使用して、温度が-273.15以上になるように制限し、 さらに、変換後の値を返すFahrenheit計算プロパティを用意する- 自動プロパティを使用して
Bookクラスを作成し(Title、Author、Price)を使用してBookクラスを作成し、Priceのsetに非負の検証を追加してください(ヒント:完全なプロパティに展開してください) ServerConfigクラスを定義し、その中でHostとPortがinitのアクセサを使用し、requiredとしてマークされるようにします。初期化後に変更を試みて、コンパイルエラーが発生するかどうかを確認してください。recordを用いてPoint(double X, double Y)を定義し、同じ座標を持つ2つのインスタンスを作成し、==との比較結果がTrueとなることを確認するEmployeeクラスを作成し、Idを読み取り専用プロパティ(コンストラクタで代入)として、Nameをrequired initを使用して定義し、Salaryは検証機能付きのsetプロパティを使用し、AnnualSalaryを計算プロパティとして定義する