屬性提供「欄位般的語法,方法般的封裝」,是 C# 物件導向封裝的核心機制。優於 Java 的 getter/setter 慣例,優於 C++ 的公開欄位。
public class Person
{
// 私有備份欄位
private string _name = "N/A";
private int _age = 0;
// 完整屬性宣告
public string Name
{
get => _name; // 讀取
set => _name = value ?? "N/A"; // 寫入 (value 為隱含參數)
}
public int Age
{
get => _age;
set
{
if (value < 0 || value > 150)
throw new ArgumentOutOfRangeException(nameof(value), "年齡必須 0-150");
_age = value;
}
}
// 唯讀屬性 (僅 get)
public string Id => Guid.NewGuid().ToString("N")[..8];
// 唯寫屬性 (僅 set) - 極少用
public string WriteOnlySecret { set { /* 儲存至安全存儲 */ } }
public override string ToString() => $"Name={Name}, Age={Age}";
}
// 使用
var p = new Person();
p.Name = "Joe"; // 呼叫 set
p.Age = 99;
Console.WriteLine(p.Name); // 呼叫 get
p.Age += 1; // get + set
Console.WriteLine(p); // Name=Joe, Age=100
編譯器自動產生隱私備份欄位,簡化程式碼:
public class Product
{
// 僅需指定型別與名稱
public string SKU { get; set; }
public decimal Price { get; set; }
// 唯讀自動屬性 (C# 6+),建構函式初始化
public DateTime CreatedAt { get; } = DateTime.UtcNow;
// init 存取子 (C# 9+):物件初始化器可設定,之後不可變
public string Category { get; init; }
}
// 使用
var product = new Product
{
SKU = "ABC-123",
Price = 99.9m,
Category = "Electronics"
};
// product.CreatedAt = DateTime.Now; // 編譯錯誤:唯讀
// product.Category = "Books"; // 編譯錯誤:init 僅限初始化
get/set 可有不同存取層級:
public class BankAccount
{
public decimal Balance { get; private set; } // 外部可讀,內部可寫
public string Owner { get; } = "John"; // 唯讀
// 內部邏輯控制
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("金額需為正");
Balance += amount;
}
}
定義「契約」而非實作,強制衍生類別提供具體行為:
// 抽象基底類別
public abstract class Shape
{
protected string _id;
public string Id
{
get => _id;
protected set => _id = value;
}
// 抽象屬性:僅宣告 get,不提供實作
public abstract double Area { get; }
// 抽象屬性:同時需要 get/set
public abstract double Perimeter { get; set; }
public Shape(string id) => Id = id;
public override string ToString() => $"{Id}: Area={Area:F2}";
}
// 衍生類別必須實作所有抽象屬性
public class Circle : Shape
{
private double _radius;
public Circle(string id, double radius) : base(id) => _radius = radius;
// 實作抽象唯讀屬性
public override double Area => Math.PI * _radius * _radius;
// 實作抽象讀寫屬性 (需自行管理備份欄位)
private double _perimeter;
public override double Perimeter
{
get => _perimeter;
set => _perimeter = value > 0 ? value : throw new ArgumentException("周長需為正");
}
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public Rectangle(string id, double w, double h) : base(id)
{
Width = w;
Height = h;
}
public override double Area => Width * Height;
public override double Perimeter
{
get => 2 * (Width + Height);
set => throw new NotSupportedException("矩形周長由寬高決定,不可直接設定");
}
}
// 多型使用
Shape[] shapes =
{
new Circle("C1", 5),
new Rectangle("R1", 4, 5)
};
foreach (var s in shapes)
{
Console.WriteLine(s); // 呼叫 ToString() → 使用 Area 屬性
}
// 輸出:
// C1: Area=78.54
// R1: Area=20.00
public interface IEntity
{
int Id { get; set; }
DateTime CreatedAt { get; }
bool IsActive { get; set; }
}
public class User : IEntity
{
public int Id { get; set; }
public DateTime CreatedAt { get; } = DateTime.UtcNow;
public bool IsActive { get; set; } = true;
public string Username { get; set; }
}
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
public class ViewModel : ObservableObject
{
private string _title;
public string Title
{
get => _title;
set => SetProperty(ref _title, value);
}
}
public class GradeBook
{
private readonly Dictionary _grades = new();
public int this[string studentName]
{
get => _grades.TryGetValue(studentName, out var g) ? g : 0;
set => _grades[studentName] = value;
}
public IEnumerable Students => _grades.Keys;
}
// 使用
var book = new GradeBook();
book["Alice"] = 95;
book["Bob"] = 87;
Console.WriteLine(book["Alice"]); // 95
public class MathUtils
{
// 唯讀屬性
public double Pi => Math.PI;
// 含邏輯的屬性
public double CircleArea(double r) => Math.PI * r * r;
// 建構函式
public MathUtils() => Console.WriteLine("Initialized");
// 方法
public double Add(double a, double b) => a + b;
}
| 陷阱 | 後果 | 解決方案 |
|---|---|---|
| 屬性執行耗時操作 (IO、計算、查詢) | 呼叫者誤以為輕量,導致效能問題 | 改用方法 (如 GetDataAsync()),或快取結果 |
| set 存取子拋出例外但未驗證 | 物件進入無效狀態 | 驗證優先,失敗拋 ArgumentException,不修改欄位 |
| 自動屬性隱藏備份欄位導致無法序列化控制 | 無法自訂 JSON/XML 序列化行為 | 改用完整屬性 + [JsonProperty] 等屬性 |
| 抽象屬性在建構函式中被呼叫 | 衍生類別尚未初始化,存取未就緒欄位 | 避免在基底建構函式呼叫虛擬/抽象成員 |
| 大量唯讀自動屬性未初始化 | 預設值 (null/0/false) 可能非預期 | 使用物件初始化器、建構函式或 ?? 提供預設值 |
C# 屬性提供:語法糖 (自動屬性)、封裝控制 (存取權限非對稱)、契約定義 (抽象/介面屬性)、擴充點 (INotifyPropertyChanged、索引器)。掌握 get/set/init、自動屬性、抽象屬性多型、運算式主體等現代語法,並避開「屬性做太多事」的反模式,即能撰寫清晰、安全、可維護的物件導向代碼。