在前幾天的學習中,已經熟悉了變數、流程控制、函式以及物件導向的基礎。今天要來認識屬性(Property)與封裝(Encapsulation) —— 這是 C# 非常重要的概念,因為它能幫助我們保護資料,避免被外部程式隨意修改,並且能加入驗證邏輯。
那什麼是封裝呢?封裝(Encapsulation)指的是將類別的資料隱藏起來,只允許透過定義好的方法或屬性存取,這麼做有幾個好處:資料安全性:防止不合理的值被設定。易於維護:當程式需要改動時,不會影響到外部呼叫程式碼。更符合物件導向的思維。
屬性的概念:屬性(Property) 是一種特殊的類別成員,用來讀取、寫入或計算欄位的值,對外看起來像「公開的資料成員」,但實際上是透過 存取子(Accessors) 來實作。
👉 傳統欄位(Field)範例:
public class Person
{
public string? FirstName; // 欄位,直接存取
}
在 C# 中,可以用簡化語法直接宣告屬性:
public class Person
{
public string? FirstName { get; set; }
}
在這段程式碼內,{ get; set; } 就是存取子,編譯器會自動產生一個隱藏的欄位(backing field),用來存放屬性值,get 負責讀取,set 負責寫入。
屬性可以有初始值(避免使用預設值 null 或 0),範例:將 FirstName 預設為空字串,而不是 null:
public class Person
{
public string FirstName { get; set; } = string.Empty;
}
這樣建立新物件時,如果沒有指定 FirstName,它會自動是 ""(空字串),而不是 null。
在 C# 13 可以用 field 關鍵字 在存取子中直接操作隱藏的 backing field,例如加入驗證:
public class Person
{
public string? FirstName
{
get;
set => field = value.Trim();
}
}
這樣可以在設定值時自動進行處理,而不需要顯式定義私有欄位。
透過 required 關鍵字強制呼叫者必須設定:
public class Person
{
public Person() { }
[SetsRequiredMembers]
public Person(string firstName) => FirstName = firstName;
public required string FirstName { get; init; }
}
所以如果要能夠正確執行的話要讓 FirstName 內一定要包含值,如下:
var p1 = new Person("John");
var p2 = new Person { FirstName = "John" };
所以說如果輸入為空值的話就會出現錯誤:
var p3 = new Person();
// Error: 必須設定 FirstName
當屬性邏輯只有一行,可以用 => 簡化:
public class Person
{
public required string FirstName { get; init; }
public required string LastName { get; init; }
public string Name => $"{FirstName} {LastName}";
}
這裡的 Name 是計算屬性,沒有 backing field,每次呼叫時都會重新組合字串。
屬性不一定要完全公開讀寫,可以針對 get 與 set 設定不同存取範圍:
public class Person
{
public string? FirstName { get; private set; }
}
init 比 private set 更嚴格,它只允許在建構函式或物件初始化器中設定:
public class Person
{
public string? FirstName { get; init; }
}
若今天要輸入值,正確的做法如下:
var p1 = new Person { FirstName = "John" };
若只輸入以下內容會出錯,這就是所謂的比private set 更嚴格,他輸入的限制更嚴謹:
p1.FirstName = "Tom"; // 編譯錯誤
若屬性只有 get,則只能在建構函式裡賦值,此時外部只能讀,不能改,且必須透過建構函式初始化:
public class Person
{
public Person(string firstName) => FirstName = firstName;
public string FirstName { get; }
}
常見組合是 required + init,強制呼叫者初始化,這樣可以保證物件建立後一定有 FirstName:
public class Person
{
public required string FirstName { get; init; }
}
可以根據下列兩個範例來做比較,首先是範例一:
public class Person
{
public Person() { }
[SetsRequiredMembers]
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public required string FirstName { get; init; }
public required string LastName { get; init; }
private string? _fullName;
public string FullName
{
get
{
if (_fullName is null) // 第一次取值才計算
_fullName = $"{FirstName} {LastName}";
return _fullName;
}
}
}
FirstName 與 LastName 是唯讀 (init),建構後無法修改,FullName 屬性第一次存取時才會計算,並快取在 _fullName,之後每次呼叫 FullName 都直接回傳快取結果。
範例二,如果允許修改 FirstName 或 LastName,那就必須在 setter 裡清空 _fullName,以便下次重新計算:
public class Person
{
private string? _firstName;
public string? FirstName
{
get => _firstName;
set
{
_firstName = value;
_fullName = null; // 讓 FullName 下次重新計算
}
}
private string? _lastName;
public string? LastName
{
get => _lastName;
set
{
_lastName = value;
_fullName = null;
}
}
private string? _fullName;
public string FullName
{
get
{
if (_fullName is null)
_fullName = $"{FirstName} {LastName}";
return _fullName;
}
}
}
當 FirstName 或 LastName 被修改時,fullName 會設為 null,下一次呼叫 FullName 時,才會重新拼接新的字串並快取。
在後方加上 Main 的區塊來執行查看結果,可以看到不管是FirstName還是LastName做了更動,FullName就會被重新計算:
class Program
{
static void Main()
{
// 創建一個 Person 實例
var person = new Person
{
FirstName = "Jessica",
LastName = "Lee"
};
// 顯示全名
Console.WriteLine($"Full Name: {person.FullName}");
// 修改姓氏
person.FirstName = "Cindy";
Console.WriteLine($"Updated Full Name: {person.FullName}");
// 只顯示名字
Console.WriteLine($"First Name: {person.FirstName}");
// 只顯示姓氏
Console.WriteLine($"Last Name: {person.LastName}");
}
}
結果:
在 C# 裡,屬性 (Property) 就像是「智慧型欄位 (smart field)」,從物件外部看起來,它們和欄位 (field) 很像,但其實背後可以包裝邏輯,例如:驗證 (validation)、快取 (caching)、不同存取權限 (accessibility control) 等等。屬性看起來像欄位,但實際上是方法 (accessors) 的語法糖。包含兩個主要的存取子 (accessors):
屬性的好處
✔ 對外公開存取方式,但可以隱藏內部實作。
✔ 可以加入驗證邏輯,避免錯誤值被設定。
✔ 可以控制不同存取權限,例如 public get + private set。
✔ 可以支援延遲計算 (lazy evaluation)、快取機制。