VB.NET 屬性 (Property) 筆記 (完整篇)
在 VB.NET 中,Property (屬性) 可以想成一台精密的「智慧型自動販賣機販賣機對外提供簡單的投幣窗口,讓外部程式碼可以設定或取得物件的狀態,內部卻能執行驗證、加工等複雜邏輯。」。它對外提供簡單的存取窗口,讓外部程式碼可以設定或取得物件的狀態,內部卻能執行驗證、加工等複雜邏輯。其中,Get 程序就像是販賣機的「取貨口Get 程序專門負責取出商品,回傳物件的目前狀態。」,專門負責取出商品,回傳物件的目前狀態;而 Set 程序則是「投幣口Set 程序在接收外部傳入的值的同時,可以自動驗證、判斷,完成一系列內部檢查後才更新狀態。」,在接收外部傳入的值的同時,可以自動驗證、判斷,完成一系列內部檢查後才更新狀態。這個過程體現了物件導向中封裝 (Encapsulation)一種將物件的內部狀態(資料)和操作這些狀態的方法(程式碼)捆綁在一起的技術。它對外界隱藏了物件的內部實作細節,只提供一個公開的介面進行互動,從而提高程式碼的安全性、模組性和可維護性。原則。
認識屬性 (Property)
屬性 (Property): 屬性的核心目的,是為類別內部私有的資料欄位 (Field)在類別或結構中直接宣告的變數,通常宣告為 Private,用於儲存物件的內部狀態。屬性 (Property) 就是為了控制對這些欄位的存取而設計的。,提供一個安全且受控的公開管道。它透過 Get 和 Set 存取子 (Accessor),將「資料的儲存」與「資料的存取邏輯」分離開來,讓資料的操作更加安全與靈活。
- Get 程序 (取貨口): 當程式碼需要「讀取」屬性的值時觸發。此程序必須回傳與屬性相同資料類型的值。
- Set 程序 (投幣口): 當程式碼需要「寫入」新值給屬性時觸發。此程序會接收一個名為 value 的隱含參數,其資料類型與屬性相同。
屬性的核心優勢
- 資料驗證在設定屬性值前,可以檢查資料是否符合特定條件,例如數值範圍、字串格式等。:可以在存入資料前進行格式檢查、範圍限制等驗證。
- 資料加工可以對輸入的資料進行處理,例如去除空白、轉換大小寫、格式化等。:可以對輸入的資料進行處理,例如去除空白、轉換大小寫等。
- 動態計算屬性的值可以根據其他屬性即時計算,而不需要預先儲存。:屬性值可以根據其他資料即時計算,不一定要儲存在記憶體中。
- 存取控制可以設定某些屬性為唯讀或限制存取權限,保護重要資料不被意外修改。:可以設定唯讀屬性或限制存取權限,保護重要資料。
屬性的三種核心類型
根據需求的不同,屬性的實作方式可以從極簡到複雜,以下是三種核心的應用場景整理。
類型一:自動實作屬性 (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 介面進行事件通知
- 使用建構函式初始化物件狀態
- 合理的例外處理機制