iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0

前言

在前幾天的學習中,已經熟悉了變數、流程控制、函式以及物件導向的基礎。今天要來認識屬性(Property)與封裝(Encapsulation) —— 這是 C# 非常重要的概念,因為它能幫助我們保護資料,避免被外部程式隨意修改,並且能加入驗證邏輯。
那什麼是封裝呢?封裝(Encapsulation)指的是將類別的資料隱藏起來,只允許透過定義好的方法或屬性存取,這麼做有幾個好處:資料安全性:防止不合理的值被設定。易於維護:當程式需要改動時,不會影響到外部呼叫程式碼。更符合物件導向的思維。


Properties(屬性)

屬性的概念:屬性(Property) 是一種特殊的類別成員,用來讀取、寫入或計算欄位的值,對外看起來像「公開的資料成員」,但實際上是透過 存取子(Accessors) 來實作。

👉 傳統欄位(Field)範例:

public class Person
{
    public string? FirstName; // 欄位,直接存取
}

自動實作屬性(Automatically Implemented Properties)

在 C# 中,可以用簡化語法直接宣告屬性:

public class Person
{
    public string? FirstName { get; set; }
}

在這段程式碼內,{ get; set; } 就是存取子,編譯器會自動產生一個隱藏的欄位(backing field),用來存放屬性值,get 負責讀取,set 負責寫入。

屬性初始化(Property Initialization)

屬性可以有初始值(避免使用預設值 null 或 0),範例:將 FirstName 預設為空字串,而不是 null:

public class Person
{
    public string FirstName { get; set; } = string.Empty;
}

這樣建立新物件時,如果沒有指定 FirstName,它會自動是 ""(空字串),而不是 null。

欄位支援屬性 (Field-backed Properties)

在 C# 13 可以用 field 關鍵字 在存取子中直接操作隱藏的 backing field,例如加入驗證:

public class Person
{
    public string? FirstName 
    { 
        get;
        set => field = value.Trim();
    }
}

這樣可以在設定值時自動進行處理,而不需要顯式定義私有欄位。

必填屬性 (Required Properties)

透過 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

運算式主體屬性 (Expression-bodied Properties)

當屬性邏輯只有一行,可以用 => 簡化:

public class Person
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }

    public string Name => $"{FirstName} {LastName}";
}

這裡的 Name 是計算屬性,沒有 backing field,每次呼叫時都會重新組合字串。

存取控制 (Access Control)

屬性不一定要完全公開讀寫,可以針對 get 與 set 設定不同存取範圍:

public class Person
{
    public string? FirstName { get; private set; }
}
  • get 是 public → 任何地方都能讀取。
  • set 是 private → 只能在 Person 類別內修改。
    accessor 的存取修飾子必須比屬性本身更嚴格,不能有 private 屬性卻擁有 public 的存取子。

init 存取子

init 比 private set 更嚴格,它只允許在建構函式或物件初始化器中設定:

public class Person
{
    public string? FirstName { get; init; }
}

若今天要輸入值,正確的做法如下:

var p1 = new Person { FirstName = "John" };

若只輸入以下內容會出錯,這就是所謂的比private set 更嚴格,他輸入的限制更嚴謹:

p1.FirstName = "Tom"; // 編譯錯誤

唯讀屬性 (Read-only Properties)

若屬性只有 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; }
}

Properties with Backing Fields

可以根據下列兩個範例來做比較,首先是範例一:

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}");
    }
}

結果:
https://ithelp.ithome.com.tw/upload/images/20250922/20178767HY4SlGvPQv.png


在 C# 裡,屬性 (Property) 就像是「智慧型欄位 (smart field)」,從物件外部看起來,它們和欄位 (field) 很像,但其實背後可以包裝邏輯,例如:驗證 (validation)、快取 (caching)、不同存取權限 (accessibility control) 等等。屬性看起來像欄位,但實際上是方法 (accessors) 的語法糖。包含兩個主要的存取子 (accessors):

  • get → 讀取屬性值
  • set → 指派新值給屬性
    而init → 只能在物件建構 (constructor 或 object initializer) 階段設定值,比 set 更嚴格。

屬性的好處

✔ 對外公開存取方式,但可以隱藏內部實作。
✔ 可以加入驗證邏輯,避免錯誤值被設定。
✔ 可以控制不同存取權限,例如 public get + private set。
✔ 可以支援延遲計算 (lazy evaluation)、快取機制。


上一篇
Day9-類別與物件 🏗️
下一篇
Day11-建構子與物件初始化
系列文
30 天從 Python 轉職場 C# 新手入門12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言