2025年8月9日 星期六

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

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

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

Property 是類別對外提供的資料入口。外部看起來像是在讀寫欄位,但類別內部可以在讀取或寫入時加入驗證、格式整理、計算結果、變更通知與存取權限控制。

屬性的重點不是把欄位換一種寫法,而是把資料規則放回類別內部。若資料可以被外部任意改寫,物件狀態很容易失控;若使用屬性,就能讓資料在進入物件前先被檢查與整理。

先理解 Property 在做什麼

Property:提供受控制的讀取與寫入介面。Get 負責回傳資料,Set 負責接收外部指定的值,必要時可進行驗證、修正或通知。

屬性的核心價值

  • 封裝欄位:外部不直接接觸內部儲存欄位。
  • 保護資料:Set 中檢查空白、範圍、格式。
  • 提供推導值:Get 中由其他資料計算結果。
  • 控制寫入權限:可做成唯讀或 Private Set
  • 回報變更:可搭配 INotifyPropertyChanged 通知畫面或其他程式。
成員 用途 實務理解
Get 讀取屬性值。 回傳欄位值或計算結果。
Set 寫入屬性值。 value 是外部指定的新值。
後端欄位 實際保存資料。 通常宣告成 Private
ReadOnly 只允許讀取。 適合計算結果或不可外部修改的狀態。

基本結構

VB.NET
Private _displayName As String

Public Property DisplayName As String
    Get
        Return _displayName
    End Get
    Set(value As String)
        _displayName = value.Trim()
    End Set
End Property

自動實作屬性

場景一:保存閱讀座位資料

若資料只是單純保存,不需要驗證、不需要額外計算,也不需要在寫入時做特殊處理,就可以使用自動實作屬性。

需要的主控項
  • TextBoxSeatCode:輸入座位代碼。
  • TextBoxAreaName:輸入區域名稱。
  • ButtonShowSeat:顯示座位資料。
  • LabelSeatResult:顯示結果。
範例程式碼
VB.NET / Windows Forms
Public Class ReadingSeat
    Public Property SeatCode As String
    Public Property AreaName As String
End Class

Public Class Form1
    Private currentSeat As New ReadingSeat()

    Private Sub ButtonShowSeat_Click(sender As Object, e As EventArgs) Handles ButtonShowSeat.Click
        currentSeat.SeatCode = TextBoxSeatCode.Text.Trim()
        currentSeat.AreaName = TextBoxAreaName.Text.Trim()

        LabelSeatResult.Text = "座位:" & currentSeat.AreaName & " / " & currentSeat.SeatCode
    End Sub
End Class
畫面輸出結果(AreaName = 靜音區,SeatCode = A-12)
座位:靜音區 / A-12
邏輯解析
  • SeatCodeAreaName 只是單純保存資料。
  • 自動實作屬性由編譯器建立背後儲存欄位。
  • 若後續需要限制座位格式,就應改成完整屬性。

完整屬性:在 Set 中加入規則

場景二:限制活動報名人數

完整屬性適合在寫入時清理資料、限制範圍或拒絕不合法內容。這個範例把姓名空白修正為未填寫,並把報名人數限制在 1 到 6 人。

需要的主控項
  • TextBoxGuestName:輸入報名名稱。
  • TextBoxPeopleCount:輸入報名人數。
  • ButtonCheckSignup:建立報名資料。
  • LabelSignupResult:顯示結果。
範例程式碼
VB.NET / Windows Forms
Public Class WorkshopSignup
    Private _guestName As String = "未填寫"
    Private _peopleCount As Integer = 1

    Public Property GuestName As String
        Get
            Return _guestName
        End Get
        Set(value As String)
            Dim cleanedName As String = value.Trim()

            If cleanedName = String.Empty Then
                _guestName = "未填寫"
            Else
                _guestName = cleanedName
            End If
        End Set
    End Property

    Public Property PeopleCount As Integer
        Get
            Return _peopleCount
        End Get
        Set(value As Integer)
            If value < 1 Then
                _peopleCount = 1
            ElseIf value > 6 Then
                _peopleCount = 6
            Else
                _peopleCount = value
            End If
        End Set
    End Property
End Class

Public Class Form1
    Private signup As New WorkshopSignup()

    Private Sub ButtonCheckSignup_Click(sender As Object, e As EventArgs) Handles ButtonCheckSignup.Click
        signup.GuestName = TextBoxGuestName.Text

        Dim countValue As Integer
        If Integer.TryParse(TextBoxPeopleCount.Text.Trim(), countValue) Then
            signup.PeopleCount = countValue
        Else
            signup.PeopleCount = 1
        End If

        LabelSignupResult.Text = "報名名稱:" & signup.GuestName & vbCrLf &
                                 "報名人數:" & signup.PeopleCount.ToString()
    End Sub
End Class
畫面輸出結果(GuestName = 林小姐,PeopleCount = 9)
報名名稱:林小姐 報名人數:6
邏輯解析
  • GuestNameSet 中移除前後空白。
  • PeopleCountSet 中限制人數上限。
  • 規則放在屬性內,外部指定資料時會自動套用同一套檢查。

唯讀屬性:回傳推導結果

場景三:列印工作費用計算

某些值不應由外部直接指定,而是由其他屬性推導而來。例如總費用應由頁數、單頁費用與裝訂費計算,不應讓外部任意寫入。

需要的主控項
  • TextBoxPageCount:輸入頁數。
  • ButtonCalculatePrint:計算列印費。
  • LabelPrintResult:顯示結果。
範例程式碼
VB.NET / Windows Forms
Public Class PrintJob
    Public Property PageCount As Integer
    Public Property PricePerPage As Decimal = 1.5D
    Public Property BindingFee As Decimal = 12D

    Public ReadOnly Property TotalAmount As Decimal
        Get
            Return PageCount * PricePerPage + BindingFee
        End Get
    End Property
End Class

Public Class Form1
    Private job As New PrintJob()

    Private Sub ButtonCalculatePrint_Click(sender As Object, e As EventArgs) Handles ButtonCalculatePrint.Click
        Dim pages As Integer

        If Not Integer.TryParse(TextBoxPageCount.Text.Trim(), pages) OrElse pages <= 0 Then
            LabelPrintResult.Text = "請輸入正確頁數"
            Return
        End If

        job.PageCount = pages
        LabelPrintResult.Text = "頁數:" & job.PageCount.ToString() & vbCrLf &
                                "總費用:" & job.TotalAmount.ToString("N1") & " 元"
    End Sub
End Class
畫面輸出結果(PageCount = 20)
頁數:20 總費用:42.0 元
邏輯解析
  • TotalAmount 沒有 Set,外部不能直接指定。
  • 每次讀取 TotalAmount 時,都會依目前屬性重新計算。
  • 推導值使用唯讀屬性,可避免資料來源互相矛盾。

Private Set:外部可讀,內部可寫

場景四:取件單號與建立時間

系統產生的單號與建立時間通常可以被外部讀取,但不應被外部任意修改。這類資料適合使用 Private Set

需要的主控項
  • ButtonCreatePickup:建立取件單。
  • LabelPickupResult:顯示取件資料。
範例程式碼
VB.NET / Windows Forms
Public Class PickupTicket
    Public Property TicketNo As String
        Get
            Return _ticketNo
        End Get
        Private Set(value As String)
            _ticketNo = value
        End Set
    End Property

    Public Property CreatedAt As DateTime
        Get
            Return _createdAt
        End Get
        Private Set(value As DateTime)
            _createdAt = value
        End Set
    End Property

    Private _ticketNo As String
    Private _createdAt As DateTime

    Public Sub New()
        CreatedAt = DateTime.Now
        TicketNo = "P" & CreatedAt.ToString("yyyyMMddHHmmss")
    End Sub
End Class

Public Class Form1
    Private currentTicket As PickupTicket

    Private Sub ButtonCreatePickup_Click(sender As Object, e As EventArgs) Handles ButtonCreatePickup.Click
        currentTicket = New PickupTicket()

        LabelPickupResult.Text = "取件單號:" & currentTicket.TicketNo & vbCrLf &
                                 "建立時間:" & currentTicket.CreatedAt.ToString("yyyy/MM/dd HH:mm:ss")
    End Sub
End Class
畫面輸出結果
取件單號:依執行時間產生 建立時間:依執行當下時間顯示
邏輯解析
  • TicketNoCreatedAt 對外可讀。
  • Private Set 代表只有類別內部能設定值。
  • 系統產生的資料使用 Private Set,可避免外部誤改。

INotifyPropertyChanged:屬性變更通知

場景五:點餐項目變更後更新摘要

當屬性改變時,畫面或其他程式可能需要同步更新。INotifyPropertyChanged 提供標準事件,讓屬性變更可以被外部接收到。

需要的主控項
  • TextBoxItemName:輸入品項名稱。
  • TextBoxQuantity:輸入數量。
  • TextBoxUnitPrice:輸入單價。
  • ButtonUpdateOrder:更新訂單。
  • LabelOrderSummary:顯示摘要。
  • LabelOrderTotal:顯示總金額。
範例程式碼
VB.NET / Windows Forms
Imports System.ComponentModel

Public Class OrderLineViewModel
    Implements INotifyPropertyChanged

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Private _itemName As String = "未選擇"
    Private _quantity As Integer = 1
    Private _unitPrice As Decimal = 0D

    Public Property ItemName As String
        Get
            Return _itemName
        End Get
        Set(value As String)
            Dim newName As String = value.Trim()
            If newName = String.Empty Then newName = "未選擇"

            If _itemName <> newName Then
                _itemName = newName
                RaiseChanged(NameOf(ItemName))
                RaiseChanged(NameOf(SummaryText))
            End If
        End Set
    End Property

    Public Property Quantity As Integer
        Get
            Return _quantity
        End Get
        Set(value As Integer)
            Dim newQuantity As Integer = Math.Max(1, value)

            If _quantity <> newQuantity Then
                _quantity = newQuantity
                RaiseChanged(NameOf(Quantity))
                RaiseChanged(NameOf(TotalAmount))
                RaiseChanged(NameOf(SummaryText))
            End If
        End Set
    End Property

    Public Property UnitPrice As Decimal
        Get
            Return _unitPrice
        End Get
        Set(value As Decimal)
            Dim newPrice As Decimal = Math.Max(0D, value)

            If _unitPrice <> newPrice Then
                _unitPrice = newPrice
                RaiseChanged(NameOf(UnitPrice))
                RaiseChanged(NameOf(TotalAmount))
                RaiseChanged(NameOf(SummaryText))
            End If
        End Set
    End Property

    Public ReadOnly Property TotalAmount As Decimal
        Get
            Return Quantity * UnitPrice
        End Get
    End Property

    Public ReadOnly Property SummaryText As String
        Get
            Return ItemName & " x " & Quantity.ToString()
        End Get
    End Property

    Private Sub RaiseChanged(ByVal propertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    End Sub
End Class

Public Class Form1
    Private line As New OrderLineViewModel()

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        AddHandler line.PropertyChanged, AddressOf LineChanged
    End Sub

    Private Sub ButtonUpdateOrder_Click(sender As Object, e As EventArgs) Handles ButtonUpdateOrder.Click
        line.ItemName = TextBoxItemName.Text

        Dim quantityValue As Integer
        If Integer.TryParse(TextBoxQuantity.Text.Trim(), quantityValue) Then
            line.Quantity = quantityValue
        End If

        Dim priceValue As Decimal
        If Decimal.TryParse(TextBoxUnitPrice.Text.Trim(), priceValue) Then
            line.UnitPrice = priceValue
        End If
    End Sub

    Private Sub LineChanged(sender As Object, e As PropertyChangedEventArgs)
        LabelOrderSummary.Text = line.SummaryText
        LabelOrderTotal.Text = "總金額:" & line.TotalAmount.ToString("N0") & " 元"
    End Sub
End Class
畫面輸出結果(ItemName = 咖啡豆,Quantity = 3,UnitPrice = 280)
咖啡豆 x 3 總金額:840 元
邏輯解析
  • INotifyPropertyChanged 讓屬性變更可被外部知道。
  • QuantityUnitPrice 改變時,也要通知 TotalAmount 改變。
  • 推導屬性本身沒有儲存值,但依賴的資料改變時,仍應發出通知。

預設索引屬性

場景六:用置物櫃編號讀寫備註

若物件本身像一個集合,可以使用 Default Public Property 建立預設索引屬性。呼叫時可寫成 lockerNotes("B014")

需要的主控項
  • TextBoxLockerNo:輸入置物櫃編號。
  • TextBoxLockerNote:輸入備註。
  • ButtonSaveNote:儲存備註。
  • LabelNoteResult:顯示結果。
範例程式碼
VB.NET / Windows Forms
Public Class LockerNoteBook
    Private notes As New Dictionary(Of String, String)()

    Default Public Property Item(ByVal lockerNo As String) As String
        Get
            Dim key As String = NormalizeKey(lockerNo)

            If notes.ContainsKey(key) Then
                Return notes(key)
            End If

            Return "尚無備註"
        End Get
        Set(value As String)
            Dim key As String = NormalizeKey(lockerNo)
            Dim noteText As String = value.Trim()

            If noteText = String.Empty Then
                notes.Remove(key)
            Else
                notes(key) = noteText
            End If
        End Set
    End Property

    Private Function NormalizeKey(ByVal lockerNo As String) As String
        Return lockerNo.Trim().ToUpper()
    End Function
End Class

Public Class Form1
    Private lockerNotes As New LockerNoteBook()

    Private Sub ButtonSaveNote_Click(sender As Object, e As EventArgs) Handles ButtonSaveNote.Click
        Dim lockerNo As String = TextBoxLockerNo.Text.Trim()

        If lockerNo = String.Empty Then
            LabelNoteResult.Text = "請輸入置物櫃編號"
            Return
        End If

        lockerNotes(lockerNo) = TextBoxLockerNote.Text
        LabelNoteResult.Text = lockerNo.ToUpper() & ":" & lockerNotes(lockerNo)
    End Sub
End Class
畫面輸出結果(LockerNo = b014,Note = 需清潔)
B014:需清潔
邏輯解析
  • Default Public Property Item(...) 讓物件可以用索引方式存取。
  • Get 找不到備註時回傳預設文字。
  • Set 收到空白備註時移除該筆資料。

屬性與方法的差異

比較項目 Property Method
語意 描述資料、狀態、特徵。 描述動作、流程、行為。
呼叫感受 像讀取或設定資料。 像執行一件事情。
適合內容 輕量驗證、格式整理、推導結果。 儲存檔案、查詢資料庫、寄信、長時間計算。
常見名稱 TotalAmountIsActive Save()LoadData()SendMail()

屬性不適合放重型流程

  • 不要在屬性中做檔案 I/O:讀取屬性不應造成長時間等待。
  • 不要在屬性中呼叫網路服務:屬性看起來像資料,不應暗藏昂貴操作。
  • 不要在 Get 中改變主要狀態:讀取屬性應盡量避免造成副作用。
  • 動作語意應使用方法:例如儲存、寄送、重新載入,應寫成 SubFunction

實務判斷與常見誤區

需求 建議寫法 原因
單純保存資料 自動實作屬性。 程式簡短,語意清楚。
寫入時要檢查 完整屬性。 可在 Set 中集中驗證。
由其他值計算 唯讀屬性。 避免外部指定矛盾結果。
外部可讀不可寫 Private Set 適合系統產生的單號與時間。
屬性變更要通知畫面 INotifyPropertyChanged 可集中處理資料變更事件。

封裝原則:欄位通常設為 Private,外部透過屬性存取。這樣資料規則會集中在類別中,不會散落在多個按鈕事件或表單流程裡。

重點整理

  1. Property 是類別對外提供的受控制資料入口。
  2. 自動實作屬性適合單純保存資料。
  3. 完整屬性適合在 Set 中加入驗證、清理與限制。
  4. 唯讀屬性適合回傳由其他資料推導出的結果。
  5. Private Set 適合外部可讀、內部才可改的資料。
  6. INotifyPropertyChanged 可在屬性變更時發出通知。
  7. 預設索引屬性適合讓物件像集合一樣用索引存取。
  8. 屬性應保持輕量,重型流程與明確動作應使用方法。