2025年8月9日 星期六

25.VB.NET 屬性 (Property) 筆記 (進階篇)

VB.NET 屬性 (Property) 筆記 (進階篇)

VB.NET 屬性 (Property) 筆記 (完整篇)

在 VB.NET 中,Property (屬性) 可以想成一台精密的「智慧型自動販賣機販賣機對外提供簡單的投幣窗口,讓外部程式碼可以設定或取得物件的狀態,內部卻能執行驗證、加工等複雜邏輯。」。它對外提供簡單的存取窗口,讓外部程式碼可以設定或取得物件的狀態,內部卻能執行驗證、加工等複雜邏輯。其中,Get 程序就像是販賣機的「取貨口Get 程序專門負責取出商品,回傳物件的目前狀態。」,專門負責取出商品,回傳物件的目前狀態;而 Set 程序則是「投幣口Set 程序在接收外部傳入的值的同時,可以自動驗證、判斷,完成一系列內部檢查後才更新狀態。」,在接收外部傳入的值的同時,可以自動驗證、判斷,完成一系列內部檢查後才更新狀態。這個過程體現了物件導向中封裝 (Encapsulation)一種將物件的內部狀態(資料)和操作這些狀態的方法(程式碼)捆綁在一起的技術。它對外界隱藏了物件的內部實作細節,只提供一個公開的介面進行互動,從而提高程式碼的安全性、模組性和可維護性。原則。

認識屬性 (Property)

屬性 (Property): 屬性的核心目的,是為類別內部私有的資料欄位 (Field)在類別或結構中直接宣告的變數,通常宣告為 Private,用於儲存物件的內部狀態。屬性 (Property) 就是為了控制對這些欄位的存取而設計的。,提供一個安全且受控的公開管道。它透過 GetSet 存取子 (Accessor),將「資料的儲存」與「資料的存取邏輯」分離開來,讓資料的操作更加安全與靈活。

  • Get 程序 (取貨口): 當程式碼需要「讀取」屬性的值時觸發。此程序必須回傳與屬性相同資料類型的值。
  • Set 程序 (投幣口): 當程式碼需要「寫入」新值給屬性時觸發。此程序會接收一個名為 value 的隱含參數,其資料類型與屬性相同。

屬性的核心優勢

  1. 資料驗證在設定屬性值前,可以檢查資料是否符合特定條件,例如數值範圍、字串格式等。:可以在存入資料前進行格式檢查、範圍限制等驗證。
  2. 資料加工可以對輸入的資料進行處理,例如去除空白、轉換大小寫、格式化等。:可以對輸入的資料進行處理,例如去除空白、轉換大小寫等。
  3. 動態計算屬性的值可以根據其他屬性即時計算,而不需要預先儲存。:屬性值可以根據其他資料即時計算,不一定要儲存在記憶體中。
  4. 存取控制可以設定某些屬性為唯讀或限制存取權限,保護重要資料不被意外修改。:可以設定唯讀屬性或限制存取權限,保護重要資料。

屬性的三種核心類型

根據需求的不同,屬性的實作方式可以從極簡到複雜,以下是三種核心的應用場景整理。

類型一:自動實作屬性 (Auto-Implemented Property)

當屬性僅作為一個單純的資料容器,不需任何額外的處理邏輯時,可以使用最簡潔的「自動實作屬性」。編譯器會自動在背後建立一個對應的私有欄位來儲存資料,就像一台只有投幣和取貨口,沒有任何內部檢查機制的簡易販賣機。

基本語法
Public Property PropertyName As DataType
使用的控制項:
  • TextBox1:用於輸入商品名稱。
  • Button1:用於設定商品名稱。
  • Label1:用於顯示當前商品名稱。
範例程式碼
Imports System ' 導入 System 命名空間

Public Class Form1
    ' 宣告一個自動實作屬性,編譯器會自動在背後建立一個私有欄位
    Public Property ProductName As String
    Public Property ProductPrice As Decimal
    
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ' 透過 Set 程序將使用者輸入的值寫入屬性
        Me.ProductName = TextBox1.Text
        
        ' 嘗試解析價格輸入
        Dim price As Decimal
        If Decimal.TryParse(TextBox2.Text, price) Then
            Me.ProductPrice = price
        Else
            Me.ProductPrice = 0D
        End If
        
        ' 透過 Get 程序讀取屬性的值,並顯示在 Label1
        Label1.Text = "商品:" & Me.ProductName & ",價格:" & Me.ProductPrice.ToString("C")
    End Sub
End Class
使用的控制項:
  • TextBox1:用於輸入商品名稱。
  • TextBox2:用於輸入商品價格。
  • Button1:用於設定商品資料。
  • Label1:用於顯示商品資訊。
限制與迴避方法

限制:自動實作屬性最大的限制是無法加入任何驗證或加工邏輯。所有寫入的值都會被直接接受。
迴避方法:如果需要對存入的資料進行檢查 (例如,檢查商品名稱是否為空) 或加工 (例如,將商品名稱轉為大寫),則必須改用下面介紹的「完整屬性」。

類型二:完整屬性 (Full Property) - 資料驗證與加工

這是屬性最常見的進階功能,在儲存資料前,先對其進行處理。這就像智慧型販賣機的「投幣口」,收到錢後會先檢查是否為假幣、面額是否足夠,確認無誤後才會真正接受這筆交易。

基本語法
Private _backingField As DataType ' 宣告後端私有欄位

Public Property MyProperty As DataType
    Get
        Return _backingField ' 從後端欄位取值
    End Get
    Set(ByVal value As DataType) ' 接收傳入的值
        ' 在此處加入驗證或加工邏輯
        _backingField = value ' 將處理過的值存入後端欄位
    End Set
End Property
範例程式碼
Imports System ' 導入 System 命名空間

Public Class Form1
    ' 手動宣告一個後端私有欄位 (Backing Field),用來實際儲存資料
    Private _accountName As String
    Private _accountBalance As Decimal
    
    Public Property AccountName As String
        Get
            ' Get 程序:直接回傳後端欄位的值
            Return _accountName
        End Get
        Set(ByVal value As String)
            ' Set 程序:在儲存前加入驗證與加工邏輯
            ' 檢查傳入的 value 是否為 Null 或空白字串
            If String.IsNullOrWhiteSpace(value) Then
                ' 如果是,就將後端欄位設定為預設值
                _accountName = "(未設定)"
            Else
                ' 如果不是,就先移除前後空白 (Trim) 再轉為小寫 (ToLower)
                _accountName = value.Trim().ToLower()
            End If
        End Set
    End Property
    
    Public Property AccountBalance As Decimal
        Get
            Return _accountBalance
        End Get
        Set(ByVal value As Decimal)
            ' 限制帳戶餘額不能為負數
            If value < 0 Then
                _accountBalance = 0
            Else
                _accountBalance = value
            End If
        End Set
    End Property
    
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ' 設定一個包含前後空白且大小寫混合的字串
        Me.AccountName = TextBox1.Text
        
        ' 嘗試設定餘額
        Dim balance As Decimal
        If Decimal.TryParse(TextBox2.Text, balance) Then
            Me.AccountBalance = balance
        End If
        
        ' 顯示結果,此時 Get 程序會回傳已經被 Set 程序加工過的值
        Label1.Text = "帳號名稱:" & Me.AccountName
        Label2.Text = "帳戶餘額:" & Me.AccountBalance.ToString("C")
    End Sub
    
    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        ' 測試負數輸入
        Me.AccountBalance = -100
        Label2.Text = "測試負數後餘額:" & Me.AccountBalance.ToString("C")
    End Sub
End Class
使用的控制項:
  • TextBox1:用於輸入帳號名稱。
  • TextBox2:用於輸入帳戶餘額。
  • Button1:用於設定帳戶資料。
  • Button2:用於測試負數驗證。
  • Label1:用於顯示處理後的帳號名稱。
  • Label2:用於顯示帳戶餘額。
限制與迴避方法

限制:使用完整屬性需要手動宣告一個後端欄位 (Backing Field)一個通常被宣告為 Private 的變數,用於在屬性程序 (Get/Set) 內部實際儲存資料。,程式碼會比自動實作屬性稍長。
迴避方法:為了獲得控制權,額外的宣告是必要的。重點在於區分何時需要控制,何時不需要,以選擇最適合的屬性類型。

類型三:唯讀屬性 (ReadOnly Property) - 動態計算

屬性的值不一定需要被儲存,它可以是根據其他資料即時計算出來的。這就像販賣機螢幕上顯示的總金額,這個金額本身沒有被儲存在任何地方,而是根據選擇的商品單價和數量即時計算出來的。這種屬性通常是唯讀的 (ReadOnly),因為它的值是由其他屬性決定的,不應該被外部直接修改。

基本語法
Public ReadOnly Property MyProperty As DataType
    Get
        ' 執行計算並回傳結果
        Return [計算結果]
    End Get
End Property
範例程式碼
Imports System ' 導入 System 命名空間

Public Class Form1
    ' 商品單價屬性
    Public Property Price As Decimal
    ' 商品數量屬性  
    Public Property Quantity As Integer
    ' 稅率屬性
    Public Property TaxRate As Decimal
    
    ' 唯讀屬性,只有 Get 程序,沒有 Set 程序
    Public ReadOnly Property SubTotal As Decimal
        Get
            ' Get 程序:每次讀取時,都即時回傳「單價 * 數量」的計算結果
            Return Price * Quantity
        End Get
    End Property
    
    ' 含稅總額的唯讀屬性
    Public ReadOnly Property TotalWithTax As Decimal
        Get
            ' 基於其他唯讀屬性進行計算
            Return SubTotal * (1 + TaxRate)
        End Get
    End Property
    
    ' 顯示商品詳細資訊的唯讀屬性
    Public ReadOnly Property ProductSummary As String
        Get
            Return $"數量: {Quantity}, 單價: {Price:C}, 小計: {SubTotal:C}, 含稅總額: {TotalWithTax:C}"
        End Get
    End Property
    
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ' 設定單價與數量
        Dim price As Decimal, quantity As Integer
        
        If Decimal.TryParse(TextBox1.Text, price) Then
            Me.Price = price
        End If
        
        If Integer.TryParse(TextBox2.Text, quantity) Then
            Me.Quantity = quantity
        End If
        
        ' 設定稅率 (例如 5%)
        Me.TaxRate = 0.05D
        
        ' 讀取 SubTotal 屬性時,會自動觸發其內部的 Get 運算
        Label1.Text = "小計金額:" & Me.SubTotal.ToString("C")
        Label2.Text = "含稅總額:" & Me.TotalWithTax.ToString("C")
        Label3.Text = Me.ProductSummary
    End Sub
    
    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        ' 更新數量並重新顯示
        Me.Quantity += 1
        Label1.Text = "小計金額:" & Me.SubTotal.ToString("C")
        Label2.Text = "含稅總額:" & Me.TotalWithTax.ToString("C")
        Label3.Text = Me.ProductSummary
    End Sub
End Class
使用的控制項:
  • TextBox1:用於輸入商品單價。
  • TextBox2:用於輸入商品數量。
  • Button1:用於計算並顯示金額。
  • Button2:用於增加數量並重新計算。
  • Label1:用於顯示小計金額。
  • Label2:用於顯示含稅總額。
  • Label3:用於顯示商品摘要資訊。
限制與迴避方法

限制:唯讀屬性無法從外部賦值。任何嘗試 `Me.SubTotal = 100` 的程式碼都會導致編譯錯誤。
迴避方法:這是其設計目的,用來確保資料的完整性。如果一個值確實需要被外部設定,那它就不應該被設計為唯讀屬性。若要改變唯讀屬性的值,只能去改變它所依賴的來源屬性 (在此例中為 `Price` 或 `Quantity`)。

屬性類型比較表

屬性類型 優點 適用場景與限制
自動實作屬性 語法最簡潔,程式碼乾淨易讀。 適用:單純儲存資料,例如在資料模型類別 (Model) 中對應資料庫欄位。
限制:無法加入任何驗證或加工邏輯。
完整屬性 提供最高度的控制能力,可在 Get/Set 中執行任何邏輯。 適用:需要確保資料格式正確性、限制數值範圍、觸發其他事件等。
限制:需要手動宣告後端欄位,程式碼稍長。
唯讀屬性 保證資料不會被外部意外修改,安全性高。 適用:提供計算結果 (如 `FullName` 由 `FirstName` 和 `LastName` 組成)、組合字串、回傳物件的內部狀態。
限制:無法從外部寫入。

屬性 (Property) vs. 方法 (Method)

區分屬性與方法的使用時機是個常見的課題。雖然兩者有時能達到類似效果,但其設計哲學不同。延續販賣機的比喻:屬性代表販賣機的「特徵」,像是它的顏色、高度、商品價格;而方法則代表販賣機的「動作」,像是投幣、退款、加熱商品。

比較項目 屬性 (Property) 方法 (Method)
設計理念 代表物件的狀態特徵 (像名詞)。 代表物件執行的動作操作 (像動詞)。
呼叫語法 obj.Name = "New"
Dim n = obj.Name
obj.Calculate(x, y)
obj.Save()
效能期望 應是輕量級操作,速度快,不應有明顯副作用 (例如改變其他屬性的值)。 可以是耗時操作 (如 I/O、複雜計算),且可能改變物件狀態。
參數支援 不支援參數 (除了 Set 的 value 參數)。 支援多個參數,可以有多載版本。

一個實用的判斷準則

可以這樣思考:如果該成員描述的是「物件是什麼」,就用屬性 (例如:`Car.Color`);如果描述的是「物件能做什麼」,就用方法 (例如:`Car.StartEngine()`)。

進階屬性技巧

除了基本應用,屬性還提供了一些進階語法,讓程式碼的封裝更嚴謹、應用更靈活。

技巧一:在 Get/Set 上使用存取修飾詞 (Access Modifier)用於指定程式碼元素 (如屬性、方法) 可見性層級的關鍵字,例如 Public (公開)、Private (私有)、Protected (受保護) 等。

有時希望一個屬性對外是唯讀的,但對內 (在類別自己內部) 卻是可寫的。這時可以在 `Set` 程序上加上 `Private` 修飾詞。這在設定物件 ID 或內部狀態時非常有用,確保 ID 只能在物件建立時被設定一次,之後便無法從外部更改。

實際應用場景

常用於資料庫物件模型,其中主鍵 (Primary Key) 在物件從資料庫載入或新建立時設定,之後應保持不變,以防止資料不一致。

範例程式碼
Imports System ' 導入 System 命名空間

Public Class Order
    ' 對外公開讀取訂單 ID,但 Set 程序是私有的
    Public Property OrderId As String
        Get
        Private Set(value As String)
    End Property
    
    Public Property CustomerName As String
    Public Property OrderDate As DateTime
    
    ' 建構函式:在物件建立時執行的特殊方法
    Public Sub New()
        ' 在建構函式中,因為是在類別內部,所以可以呼叫私有的 Set 程序設定 ID
        Me.OrderId = Guid.NewGuid().ToString()
        Me.OrderDate = DateTime.Now
    End Sub
    
    ' 另一個建構函式,允許指定客戶名稱
    Public Sub New(customerName As String)
        Me.New() ' 呼叫無參數建構函式
        Me.CustomerName = customerName
    End Sub
End Class

Public Class Form1
    Private currentOrder As Order
    
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ' 建立一個 Order 物件,此時其建構函式會自動執行並設定 OrderId
        currentOrder = New Order(TextBox1.Text)
        
        ' 可以從外部讀取 OrderId
        Label1.Text = "訂單ID:" & currentOrder.OrderId
        Label2.Text = "客戶:" & currentOrder.CustomerName
        Label3.Text = "訂單日期:" & currentOrder.OrderDate.ToString("yyyy-MM-dd HH:mm:ss")
        
        ' 下面這行程式碼會產生編譯錯誤,因為 Set 程序是 Private
        ' currentOrder.OrderId = "NewID"
    End Sub
    
    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        ' 可以更新其他非私有屬性
        If currentOrder IsNot Nothing Then
            currentOrder.CustomerName = TextBox1.Text
            Label2.Text = "客戶:" & currentOrder.CustomerName
        End If
    End Sub
End Class
使用的控制項:
  • TextBox1:用於輸入客戶名稱。
  • Button1:用於建立新訂單。
  • Button2:用於更新客戶名稱。
  • Label1:用於顯示訂單 ID。
  • Label2:用於顯示客戶名稱。
  • Label3:用於顯示訂單日期。

技巧二:在屬性中引發事件 (Event)一種讓物件能夠通知其他物件某件事已經發生的機制。它是一種發佈-訂閱模式,當事件發生時 (發佈者),所有訂閱該事件的物件都會收到通知並執行對應的處理程序。 (UI 綁定關鍵)

在 WPF 或 WinForms 等 UI 開發中,當後端資料模型的屬性值變更時,需要一種機制來通知介面更新。這可以透過在 `Set` 程序中引發一個事件來達成,最常見的標準作法就是實作 `INotifyPropertyChanged` 介面.NET 提供的一個標準介面,定義了一個名為 PropertyChanged 的事件。當物件的某個屬性值變更時,可以引發此事件,通知所有訂閱者 (例如 UI 元素) 進行更新。

實際應用場景

在 MVVM (Model-View-ViewModel) 架構中,ViewModel 裡的屬性值改變時,需要自動更新 View (UI) 上的顯示。例如,當使用者名稱在 ViewModel 中被更新後,UI 上的文字方塊也要同步顯示新的名稱。

範例程式碼
' 需要匯入 System.ComponentModel 命名空間
Imports System.ComponentModel

' ViewModel 類別實作 INotifyPropertyChanged 介面
Public Class ViewModel
    Implements INotifyPropertyChanged
    
    ' 宣告此介面所要求的事件
    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
    
    ' 後端私有欄位
    Private _userName As String
    Private _userAge As Integer
    Private _isActive As Boolean
    
    Public Property UserName As String
        Get
            Return _userName
        End Get
        Set(value As String)
            ' 檢查傳入的值是否與目前的值不同,避免不必要的更新
            If _userName <> value Then
                ' 更新後端欄位的值
                _userName = value
                ' 引發事件通知 UI,參數 NameOf(UserName) 會安全地取得屬性名稱 "UserName"
                OnPropertyChanged(NameOf(UserName))
                ' 當名稱改變時,也更新顯示名稱
                OnPropertyChanged(NameOf(DisplayName))
            End If
        End Set
    End Property
    
    Public Property UserAge As Integer
        Get
            Return _userAge
        End Get
        Set(value As Integer)
            If _userAge <> value Then
                _userAge = value
                OnPropertyChanged(NameOf(UserAge))
                ' 年齡改變時,顯示名稱也會改變
                OnPropertyChanged(NameOf(DisplayName))
            End If
        End Set
    End Property
    
    Public Property IsActive As Boolean
        Get
            Return _isActive
        End Get
        Set(value As Boolean)
            If _isActive <> value Then
                _isActive = value
                OnPropertyChanged(NameOf(IsActive))
                OnPropertyChanged(NameOf(StatusText))
            End If
        End Set
    End Property
    
    ' 組合多個屬性的唯讀屬性
    Public ReadOnly Property DisplayName As String
        Get
            Return $"{UserName} ({UserAge}歲)"
        End Get
    End Property
    
    ' 根據狀態顯示不同文字
    Public ReadOnly Property StatusText As String
        Get
            Return If(IsActive, "使用中", "已停用")
        End Get
    End Property
    
    ' 輔助方法:引發 PropertyChanged 事件
    Protected Sub OnPropertyChanged(propertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub
End Class

Public Class Form1
    Private viewModel As New ViewModel()
    
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ' 訂閱 ViewModel 的 PropertyChanged 事件
        AddHandler viewModel.PropertyChanged, AddressOf ViewModel_PropertyChanged
        
        ' 設定初始值
        UpdateDisplay()
    End Sub
    
    Private Sub ViewModel_PropertyChanged(sender As Object, e As PropertyChangedEventArgs)
        ' 當 ViewModel 的任何屬性改變時,更新 UI 顯示
        UpdateDisplay()
    End Sub
    
    Private Sub UpdateDisplay()
        Label1.Text = "使用者:" & viewModel.UserName
        Label2.Text = "年齡:" & viewModel.UserAge.ToString()
        Label3.Text = "顯示名稱:" & viewModel.DisplayName
        Label4.Text = "狀態:" & viewModel.StatusText
    End Sub
    
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ' 更新使用者資料,會自動觸發 PropertyChanged 事件
        viewModel.UserName = TextBox1.Text
        
        Dim age As Integer
        If Integer.TryParse(TextBox2.Text, age) Then
            viewModel.UserAge = age
        End If
    End Sub
    
    Private Sub CheckBox1_CheckedChanged(sender As Object, e As EventArgs) Handles CheckBox1.CheckedChanged
        ' 更新活躍狀態
        viewModel.IsActive = CheckBox1.Checked
    End Sub
End Class
使用的控制項:
  • TextBox1:用於輸入使用者名稱。
  • TextBox2:用於輸入使用者年齡。
  • CheckBox1:用於設定使用者活躍狀態。
  • Button1:用於更新使用者資料。
  • Label1:用於顯示使用者名稱。
  • Label2:用於顯示使用者年齡。
  • Label3:用於顯示組合後的顯示名稱。
  • Label4:用於顯示使用者狀態。

技巧三:索引屬性 (Indexed Property)

VB.NET 支援索引屬性,讓物件可以像陣列一樣使用索引來存取資料。這在建立集合類別或需要透過鍵值對存取資料時非常有用。

範例程式碼
Imports System.Collections.Generic

Public Class StudentGrades
    Private grades As New Dictionary(Of String, Integer)
    
    ' 預設索引屬性,使用學生姓名作為索引
    Default Public Property Item(studentName As String) As Integer
        Get
            If grades.ContainsKey(studentName) Then
                Return grades(studentName)
            Else
                Return 0 ' 預設分數
            End If
        End Get
        Set(value As Integer)
            ' 限制分數範圍 0-100
            If value < 0 Then
                grades(studentName) = 0
            ElseIf value > 100 Then
                grades(studentName) = 100
            Else
                grades(studentName) = value
            End If
        End Set
    End Property
    
    ' 取得所有學生名單
    Public ReadOnly Property StudentNames As String()
        Get
            Dim names(grades.Count - 1) As String
            grades.Keys.CopyTo(names, 0)
            Return names
        End Get
    End Property
    
    ' 計算平均分數
    Public ReadOnly Property AverageGrade As Double
        Get
            If grades.Count = 0 Then Return 0
            Return grades.Values.Average()
        End Get
    End Property
End Class

Public Class Form1
    Private studentGrades As New StudentGrades()
    
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ' 使用索引屬性設定學生分數
        Dim studentName As String = TextBox1.Text
        Dim grade As Integer
        
        If Not String.IsNullOrWhiteSpace(studentName) AndAlso Integer.TryParse(TextBox2.Text, grade) Then
            ' 使用索引語法設定分數
            studentGrades(studentName) = grade
            UpdateDisplay()
        End If
    End Sub
    
    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        ' 查詢特定學生分數
        Dim studentName As String = TextBox1.Text
        If Not String.IsNullOrWhiteSpace(studentName) Then
            ' 使用索引語法取得分數
            Dim grade As Integer = studentGrades(studentName)
            Label3.Text = $"{studentName}的分數:{grade}"
        End If
    End Sub
    
    Private Sub UpdateDisplay()
        ' 清空列表
        ListBox1.Items.Clear()
        
        ' 顯示所有學生分數
        For Each studentName In studentGrades.StudentNames
            ListBox1.Items.Add($"{studentName}: {studentGrades(studentName)}分")
        Next
        
        ' 顯示平均分數
        Label1.Text = $"班級平均:{studentGrades.AverageGrade:F1}分"
    End Sub
End Class
使用的控制項:
  • TextBox1:用於輸入學生姓名。
  • TextBox2:用於輸入學生分數。
  • Button1:用於設定學生分數。
  • Button2:用於查詢學生分數。
  • ListBox1:用於顯示所有學生分數。
  • Label1:用於顯示班級平均分數。
  • Label3:用於顯示查詢結果。

屬性最佳實務建議

建議一:優先選擇自動實作屬性

除非你需要在 Get 或 Set 中加入驗證、計算或其他邏輯,否則都應該優先使用自動實作屬性。它不僅語法更簡潔,也更不容易出錯,是現代程式設計中推薦的迭代方式。

建議二:適當使用屬性命名慣例

屬性名稱應該使用名詞或名詞片語,並遵循 PascalCase 命名慣例。避免使用動詞,因為動詞更適合方法名稱。例如:`UserName`、`IsActive`、`TotalAmount` 是好的屬性名稱。

建議三:謹慎處理屬性中的例外

在屬性的 Get 存取子中拋出例外應該謹慎,因為屬性通常被期望是輕量級操作。如果必須拋出例外,應確保例外是有意義且可預期的。

注意事項:避免在屬性中執行耗時操作

屬性的 Get 存取子不應該執行耗時的操作,如檔案 I/O、網路請求或複雜計算。這些操作更適合放在方法中。如果屬性需要執行這類操作,考慮使用快取機制或改為方法。

綜合實戰範例

以下是一個綜合運用各種屬性技巧的完整範例,展示如何建立一個員工管理系統,包含資料驗證、計算屬性和事件通知。

範例:員工管理系統

Imports System.ComponentModel
Imports System.Text.RegularExpressions

Public Class Employee
    Implements INotifyPropertyChanged
    
    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
    
    ' 私有後端欄位
    Private _firstName As String
    Private _lastName As String
    Private _email As String
    Private _birthDate As DateTime
    Private _salary As Decimal
    Private _isActive As Boolean
    
    ' 員工ID - 對外唯讀,內部可設定
    Public Property EmployeeId As String
        Get
        Private Set(value As String)
    End Property
    
    ' 姓氏屬性 - 包含驗證
    Public Property FirstName As String
        Get
            Return _firstName
        End Get
        Set(value As String)
            If String.IsNullOrWhiteSpace(value) Then
                Throw New ArgumentException("姓氏不能為空")
            End If
            
            If _firstName <> value.Trim() Then
                _firstName = value.Trim()
                OnPropertyChanged(NameOf(FirstName))
                OnPropertyChanged(NameOf(FullName))
                OnPropertyChanged(NameOf(DisplayName))
            End If
        End Set
    End Property
    
    ' 名字屬性 - 包含驗證
    Public Property LastName As String
        Get
            Return _lastName
        End Get
        Set(value As String)
            If String.IsNullOrWhiteSpace(value) Then
                Throw New ArgumentException("名字不能為空")
            End If
            
            If _lastName <> value.Trim() Then
                _lastName = value.Trim()
                OnPropertyChanged(NameOf(LastName))
                OnPropertyChanged(NameOf(FullName))
                OnPropertyChanged(NameOf(DisplayName))
            End If
        End Set
    End Property
    
    ' Email屬性 - 包含格式驗證
    Public Property Email As String
        Get
            Return _email
        End Get
        Set(value As String)
            If Not String.IsNullOrWhiteSpace(value) Then
                ' 簡單的Email格式驗證
                Dim emailPattern As String = "^[^@\s]+@[^@\s]+\.[^@\s]+$"
                If Not Regex.IsMatch(value, emailPattern) Then
                    Throw New ArgumentException("Email格式不正確")
                End If
            End If
            
            If _email <> value Then
                _email = value
                OnPropertyChanged(NameOf(Email))
            End If
        End Set
    End Property
    
    ' 生日屬性 - 包含年齡驗證
    Public Property BirthDate As DateTime
        Get
            Return _birthDate
        End Get
        Set(value As DateTime)
            If value > DateTime.Today Then
                Throw New ArgumentException("生日不能是未來日期")
            End If
            
            If _birthDate <> value Then
                _birthDate = value
                OnPropertyChanged(NameOf(BirthDate))
                OnPropertyChanged(NameOf(Age))
                OnPropertyChanged(NameOf(DisplayName))
            End If
        End Set
    End Property
    
    ' 薪水屬性 - 包含範圍驗證
    Public Property Salary As Decimal
        Get
            Return _salary
        End Get
        Set(value As Decimal)
            If value < 0 Then
                Throw New ArgumentException("薪水不能為負數")
            End If
            
            If _salary <> value Then
                _salary = value
                OnPropertyChanged(NameOf(Salary))
                OnPropertyChanged(NameOf(AnnualSalary))
            End If
        End Set
    End Property
    
    ' 在職狀態
    Public Property IsActive As Boolean
        Get
            Return _isActive
        End Get
        Set(value As Boolean)
            If _isActive <> value Then
                _isActive = value
                OnPropertyChanged(NameOf(IsActive))
                OnPropertyChanged(NameOf(StatusText))
                OnPropertyChanged(NameOf(DisplayName))
            End If
        End Set
    End Property
    
    ' 計算屬性 - 全名
    Public ReadOnly Property FullName As String
        Get
            Return $"{FirstName} {LastName}"
        End Get
    End Property
    
    ' 計算屬性 - 年齡
    Public ReadOnly Property Age As Integer
        Get
            Dim today As DateTime = DateTime.Today
            Dim age As Integer = today.Year - BirthDate.Year
            If BirthDate.Date > today.AddYears(-age) Then
                age -= 1
            End If
            Return age
        End Get
    End Property
    
    ' 計算屬性 - 年薪
    Public ReadOnly Property AnnualSalary As Decimal
        Get
            Return Salary * 12
        End Get
    End Property
    
    ' 組合屬性 - 顯示名稱
    Public ReadOnly Property DisplayName As String
        Get
            Dim status As String = If(IsActive, "在職", "離職")
            Return $"{FullName} ({Age}歲) - {status}"
        End Get
    End Property
    
    ' 狀態文字
    Public ReadOnly Property StatusText As String
        Get
            Return If(IsActive, "在職中", "已離職")
        End Get
    End Property
    
    ' 建構函式
    Public Sub New()
        EmployeeId = Guid.NewGuid().ToString("N")(0..7).ToUpper() ' 8位英數字ID
        _birthDate = New DateTime(1990, 1, 1) ' 預設生日
        _isActive = True ' 預設為在職
    End Sub
    
    ' 帶參數的建構函式
    Public Sub New(firstName As String, lastName As String, email As String)
        Me.New()
        Me.FirstName = firstName
        Me.LastName = lastName
        Me.Email = email
    End Sub
    
    ' 引發PropertyChanged事件的輔助方法
    Protected Sub OnPropertyChanged(propertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub
End Class

Public Class Form1
    Private currentEmployee As Employee
    
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ' 建立示例員工
        Try
            currentEmployee = New Employee("張", "小明", "zhang.xiaoming@company.com")
            currentEmployee.BirthDate = New DateTime(1985, 6, 15)
            currentEmployee.Salary = 45000
            
            ' 訂閱PropertyChanged事件
            AddHandler currentEmployee.PropertyChanged, AddressOf Employee_PropertyChanged
            
            ' 初始顯示
            UpdateDisplay()
        Catch ex As Exception
            MessageBox.Show("初始化錯誤:" & ex.Message)
        End Try
    End Sub
    
    Private Sub Employee_PropertyChanged(sender As Object, e As PropertyChangedEventArgs)
        ' 當員工屬性改變時更新顯示
        UpdateDisplay()
    End Sub
    
    Private Sub UpdateDisplay()
        If currentEmployee Is Nothing Then Return
        
        Label1.Text = "員工ID:" & currentEmployee.EmployeeId
        Label2.Text = "姓名:" & currentEmployee.FullName
        Label3.Text = "Email:" & currentEmployee.Email
        Label4.Text = "年齡:" & currentEmployee.Age.ToString() & "歲"
        Label5.Text = "月薪:" & currentEmployee.Salary.ToString("C")
        Label6.Text = "年薪:" & currentEmployee.AnnualSalary.ToString("C")
        Label7.Text = "狀態:" & currentEmployee.StatusText
        Label8.Text = "顯示名稱:" & currentEmployee.DisplayName
    End Sub
    
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ' 更新員工資料
        Try
            If currentEmployee IsNot Nothing Then
                currentEmployee.FirstName = TextBox1.Text
                currentEmployee.LastName = TextBox2.Text
                currentEmployee.Email = TextBox3.Text
                
                Dim salary As Decimal
                If Decimal.TryParse(TextBox4.Text, salary) Then
                    currentEmployee.Salary = salary
                End If
                
                currentEmployee.BirthDate = DateTimePicker1.Value
                currentEmployee.IsActive = CheckBox1.Checked
            End If
        Catch ex As Exception
            MessageBox.Show("更新失敗:" & ex.Message)
        End Try
    End Sub
    
    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        ' 建立新員工
        Try
            currentEmployee = New Employee()
            AddHandler currentEmployee.PropertyChanged, AddressOf Employee_PropertyChanged
            UpdateDisplay()
            
            ' 清空輸入欄位
            TextBox1.Clear()
            TextBox2.Clear()
            TextBox3.Clear()
            TextBox4.Clear()
            DateTimePicker1.Value = DateTime.Today.AddYears(-30)
            CheckBox1.Checked = True
        Catch ex As Exception
            MessageBox.Show("建立員工失敗:" & ex.Message)
        End Try
    End Sub
End Class
使用的控制項:
  • TextBox1:用於輸入員工姓氏。
  • TextBox2:用於輸入員工名字。
  • TextBox3:用於輸入員工Email。
  • TextBox4:用於輸入員工薪水。
  • DateTimePicker1:用於選擇員工生日。
  • CheckBox1:用於設定員工在職狀態。
  • Button1:用於更新員工資料。
  • Button2:用於建立新員工。
  • Label1-Label8:用於顯示員工各項資訊。

這個範例展示了:

  • 使用私有 Set 存取子保護 EmployeeId
  • 在 Set 程序中進行資料驗證和格式化
  • 使用唯讀屬性提供計算結果
  • 實作 INotifyPropertyChanged 介面進行事件通知
  • 使用建構函式初始化物件狀態
  • 合理的例外處理機制