2025年8月27日 星期三

27.VB.NET 結構 (Structure) 筆記 (進階篇)

VB.NET 結構(Structure)筆記(進階篇)

VB.NET 結構(Structure) 筆記(進階篇)

Structure 是 VB.NET 中用來表示一組小型相關資料的值型別。它可以包含欄位、屬性、建構函式、方法、事件與介面實作,常用來描述座標、尺寸、範圍、金額組合、日期區間與簡單狀態。

Structure 的重點不是「比 Class 快」這麼單一,而是它具有值語意。將結構指定給另一個變數時,通常會複製整份值;修改副本不會改到原本資料。若資料很大、需要身份識別、需要共享狀態或需要繼承,通常應優先使用 Class

先理解 Structure 是什麼

Structure:一種值型別,用來把少量相關資料包成一個整體。它沒有物件身份的概念,重點在「值本身是否相同」,而不是「是否指向同一個物件」。

Structure 的核心特性

  • 值型別:指定或傳遞時,通常會複製整份值。
  • 適合小型資料:例如座標、尺寸、日期範圍、色彩分量。
  • 可包含成員:可宣告屬性、方法、建構函式與介面實作。
  • 不可繼承:Structure 不能繼承其他 Class 或 Structure,也不能被繼承。
  • 有預設值:未指定時,各欄位會是該型別的預設值。

記憶體提醒:Structure 是值型別,但不代表永遠直接放在堆疊。實際位置會受欄位、陣列、集合、裝箱等情境影響。實務判斷應以「值語意、複製成本、資料大小」為主,不要只用堆疊或堆積來判斷。

項目 Structure 實務理解
型別特性 值型別。 指定與傳遞時要注意複製。
資料意義 表示一組值。 適合座標、尺寸、範圍。
是否可繼承 不可繼承,也不可被繼承。 可實作介面,但不做繼承層級。
是否適合大量狀態 不適合。 大型或複雜狀態通常使用 Class。

定義 Structure 與建構函式

基本語法

VB.NET
Public Structure StructureName
    Public Property ValueA As Integer
    Public Property ValueB As Integer

    Public Sub New(valueA As Integer, valueB As Integer)
        Me.ValueA = valueA
        Me.ValueB = valueB
    End Sub
End Structure

Structure 可以定義帶參數的建構函式。建構函式結束前,所有欄位或自動屬性都應完成初始化。

場景一:自習座位位置

座位位置由區域、列號與座位號組成,本身只是小型資料,不需要物件身份,也不需要繼承,因此適合設計成 Structure。

需要的主控項
  • TextBoxArea:輸入區域代碼。
  • TextBoxRow:輸入列號。
  • TextBoxSeatNo:輸入座位號。
  • ButtonBuildSeat:建立座位位置。
  • LabelSeat:顯示結果。
範例程式碼
VB.NET / Windows Forms
Public Structure SeatPosition
    Public ReadOnly Property AreaCode As String
    Public ReadOnly Property RowNo As Integer
    Public ReadOnly Property SeatNo As Integer

    Public Sub New(areaCode As String, rowNo As Integer, seatNo As Integer)
        Me.AreaCode = areaCode.Trim().ToUpper()
        Me.RowNo = rowNo
        Me.SeatNo = seatNo
    End Sub

    Public Overrides Function ToString() As String
        Return AreaCode & "區-第" & RowNo.ToString() & "列-" & SeatNo.ToString("00") & "號"
    End Function
End Structure

Public Class Form1
    Private Sub ButtonBuildSeat_Click(sender As Object, e As EventArgs) Handles ButtonBuildSeat.Click
        Dim rowNo As Integer
        Dim seatNo As Integer

        If String.IsNullOrWhiteSpace(TextBoxArea.Text) Then
            LabelSeat.Text = "請輸入區域代碼"
            Return
        End If

        If Not Integer.TryParse(TextBoxRow.Text.Trim(), rowNo) OrElse rowNo <= 0 Then
            LabelSeat.Text = "列號必須大於 0"
            Return
        End If

        If Not Integer.TryParse(TextBoxSeatNo.Text.Trim(), seatNo) OrElse seatNo <= 0 Then
            LabelSeat.Text = "座位號必須大於 0"
            Return
        End If

        Dim seat As New SeatPosition(TextBoxArea.Text, rowNo, seatNo)
        LabelSeat.Text = "座位位置:" & seat.ToString()
    End Sub
End Class
畫面輸出結果(Area = b,Row = 3,SeatNo = 8)
座位位置:B區-第3列-08號
邏輯解析
  • SeatPosition 封裝區域、列號與座位號。
  • 屬性使用 ReadOnly,建立後不從外部任意修改。
  • ToString 負責把值整理成容易顯示的格式。

值型別複製行為

複製語意:Structure 指定給另一個變數時,通常會複製整份值。之後修改副本,不會影響原本的 Structure 變數。

場景二:包材尺寸複製後微調

這個範例先建立原始包材尺寸,再複製成另一個變數進行微調。修改副本寬度後,原始尺寸仍保持原值。

需要的主控項
  • ButtonCopySize:執行尺寸複製。
  • LabelCopyResult:顯示結果。
範例程式碼
VB.NET / Windows Forms
Public Structure PackageSize
    Public Property Width As Integer
    Public Property Height As Integer

    Public Sub New(width As Integer, height As Integer)
        Me.Width = width
        Me.Height = height
    End Sub

    Public Overrides Function ToString() As String
        Return Width.ToString() & " x " & Height.ToString()
    End Function
End Structure

Public Class Form1
    Private Sub ButtonCopySize_Click(sender As Object, e As EventArgs) Handles ButtonCopySize.Click
        Dim originalSize As New PackageSize(30, 20)
        Dim adjustedSize As PackageSize = originalSize

        adjustedSize.Width = 36

        LabelCopyResult.Text = "原始尺寸:" & originalSize.ToString() & vbCrLf &
                               "調整尺寸:" & adjustedSize.ToString()
    End Sub
End Class
畫面輸出結果(LabelCopyResult.Text)
原始尺寸:30 x 20 調整尺寸:36 x 20
邏輯解析
  • adjustedSize = originalSize 會複製 Structure 的值。
  • 修改 adjustedSize.Width 不會改到 originalSize.Width
  • 若 Structure 內含參考型別欄位,複製的是參考值;參考內部物件仍需另外小心。

屬性、方法與推導結果

場景三:寄物櫃尺寸判斷

Structure 可以把資料與簡單計算放在一起。寄物櫃尺寸由寬、高、深組成,體積與是否超大件都可以由這三個值推導出來。

需要的主控項
  • TextBoxWidth:輸入寬度。
  • TextBoxHeight:輸入高度。
  • TextBoxDepth:輸入深度。
  • ButtonCheckLockerSize:檢查尺寸。
  • LabelLockerSize:顯示結果。
範例程式碼
VB.NET / Windows Forms
Public Structure LockerSize
    Public ReadOnly Property Width As Integer
    Public ReadOnly Property Height As Integer
    Public ReadOnly Property Depth As Integer

    Public Sub New(width As Integer, height As Integer, depth As Integer)
        Me.Width = width
        Me.Height = height
        Me.Depth = depth
    End Sub

    Public ReadOnly Property Volume As Integer
        Get
            Return Width * Height * Depth
        End Get
    End Property

    Public Function IsOversize() As Boolean
        Return Volume > 60000
    End Function
End Structure

Public Class Form1
    Private Sub ButtonCheckLockerSize_Click(sender As Object, e As EventArgs) Handles ButtonCheckLockerSize.Click
        Dim width As Integer
        Dim height As Integer
        Dim depth As Integer

        If Not Integer.TryParse(TextBoxWidth.Text.Trim(), width) OrElse width <= 0 Then
            LabelLockerSize.Text = "寬度格式錯誤"
            Return
        End If

        If Not Integer.TryParse(TextBoxHeight.Text.Trim(), height) OrElse height <= 0 Then
            LabelLockerSize.Text = "高度格式錯誤"
            Return
        End If

        If Not Integer.TryParse(TextBoxDepth.Text.Trim(), depth) OrElse depth <= 0 Then
            LabelLockerSize.Text = "深度格式錯誤"
            Return
        End If

        Dim size As New LockerSize(width, height, depth)
        LabelLockerSize.Text = "體積:" & size.Volume.ToString("N0") & vbCrLf &
                               "尺寸判斷:" & If(size.IsOversize(), "超大件", "一般件")
    End Sub
End Class
畫面輸出結果(寬 40,高 50,深 35)
體積:70,000 尺寸判斷:超大件
邏輯解析
  • Volume 是由寬、高、深推導出的唯讀屬性。
  • IsOversize 是簡單判斷方法,依目前尺寸回傳 Boolean。
  • Structure 適合放輕量計算,不適合放資料庫查詢或長時間流程。

Structure 實作介面

場景四:展示牌面積計算

Structure 不能繼承其他類別,但可以實作介面。若多種形狀都需要提供面積,就可以定義共同介面,再由不同 Structure 實作。

需要的主控項
  • TextBoxBoardWidth:輸入展示牌寬度。
  • TextBoxBoardHeight:輸入展示牌高度。
  • ButtonShowBoardArea:計算面積。
  • LabelBoardArea:顯示結果。
範例程式碼
VB.NET / Windows Forms
Public Interface IAreaSource
    Function GetArea() As Decimal
End Interface

Public Structure DisplayBoard
    Implements IAreaSource

    Public ReadOnly Property Width As Decimal
    Public ReadOnly Property Height As Decimal

    Public Sub New(width As Decimal, height As Decimal)
        Me.Width = width
        Me.Height = height
    End Sub

    Public Function GetArea() As Decimal Implements IAreaSource.GetArea
        Return Width * Height
    End Function
End Structure

Public Class Form1
    Private Sub ButtonShowBoardArea_Click(sender As Object, e As EventArgs) Handles ButtonShowBoardArea.Click
        Dim width As Decimal
        Dim height As Decimal

        If Not Decimal.TryParse(TextBoxBoardWidth.Text.Trim(), width) OrElse width <= 0D Then
            LabelBoardArea.Text = "寬度格式錯誤"
            Return
        End If

        If Not Decimal.TryParse(TextBoxBoardHeight.Text.Trim(), height) OrElse height <= 0D Then
            LabelBoardArea.Text = "高度格式錯誤"
            Return
        End If

        Dim board As New DisplayBoard(width, height)
        Dim areaSource As IAreaSource = board

        LabelBoardArea.Text = "展示牌面積:" & areaSource.GetArea().ToString("N2")
    End Sub
End Class
畫面輸出結果(寬 2.4,高 1.5)
展示牌面積:3.60
邏輯解析
  • DisplayBoard 使用 Implements IAreaSource 實作介面。
  • 介面讓不同型別可以提供相同方法名稱。
  • Structure 指派給介面變數時可能產生裝箱,頻繁大量使用時要注意成本。

相等性比較

場景五:座位代碼相等比較

Structure 通常表示值,因此相等性常以欄位內容判斷。若需要使用 =<> 比較兩個結構,可自行定義運算子。

需要的主控項
  • ButtonCompareSeat:比較座位。
  • LabelCompareSeat:顯示結果。
範例程式碼
VB.NET / Windows Forms
Public Structure SeatKey
    Public ReadOnly Property AreaCode As String
    Public ReadOnly Property Number As Integer

    Public Sub New(areaCode As String, number As Integer)
        Me.AreaCode = areaCode.Trim().ToUpper()
        Me.Number = number
    End Sub

    Public Overrides Function Equals(obj As Object) As Boolean
        If TypeOf obj Is SeatKey Then
            Dim other As SeatKey = DirectCast(obj, SeatKey)
            Return AreaCode = other.AreaCode AndAlso Number = other.Number
        End If

        Return False
    End Function

    Public Overrides Function GetHashCode() As Integer
        Return AreaCode.GetHashCode() Xor Number.GetHashCode()
    End Function

    Public Shared Operator =(left As SeatKey, right As SeatKey) As Boolean
        Return left.Equals(right)
    End Operator

    Public Shared Operator <>(left As SeatKey, right As SeatKey) As Boolean
        Return Not left.Equals(right)
    End Operator
End Structure

Public Class Form1
    Private Sub ButtonCompareSeat_Click(sender As Object, e As EventArgs) Handles ButtonCompareSeat.Click
        Dim firstSeat As New SeatKey("a", 12)
        Dim secondSeat As New SeatKey("A", 12)

        If firstSeat = secondSeat Then
            LabelCompareSeat.Text = "座位相同"
        Else
            LabelCompareSeat.Text = "座位不同"
        End If
    End Sub
End Class
畫面輸出結果(LabelCompareSeat.Text)
座位相同
邏輯解析
  • 建構時把區域代碼統一轉成大寫。
  • Equals 負責定義相等規則。
  • 覆寫 Equals 時,也應一起覆寫 GetHashCode
  • =<> 運算子讓比較語法更直覺。

集合中的 Structure 更新

場景六:更新置物櫃狀態

Structure 放在集合中時,要注意取出的值通常是副本。安全做法是先取出、修改副本,再把修改後的值指定回集合。

需要的主控項
  • ButtonUpdateLocker:更新置物櫃。
  • ListBoxLocker:顯示置物櫃清單。
範例程式碼
VB.NET / Windows Forms
Public Structure LockerStatus
    Public Property LockerNo As String
    Public Property IsOccupied As Boolean

    Public Sub New(lockerNo As String, isOccupied As Boolean)
        Me.LockerNo = lockerNo
        Me.IsOccupied = isOccupied
    End Sub

    Public Overrides Function ToString() As String
        Return LockerNo & ":" & If(IsOccupied, "使用中", "空櫃")
    End Function
End Structure

Public Class Form1
    Private lockers As New List(Of LockerStatus) From {
        New LockerStatus("A01", False),
        New LockerStatus("A02", False),
        New LockerStatus("A03", False)
    }

    Private Sub ButtonUpdateLocker_Click(sender As Object, e As EventArgs) Handles ButtonUpdateLocker.Click
        Dim index As Integer = 1
        Dim item As LockerStatus = lockers(index)

        item.IsOccupied = True
        lockers(index) = item

        ListBoxLocker.Items.Clear()
        For Each locker As LockerStatus In lockers
            ListBoxLocker.Items.Add(locker.ToString())
        Next
    End Sub
End Class
畫面輸出結果(ListBoxLocker)
A01:空櫃 A02:使用中 A03:空櫃
邏輯解析
  • lockers(index) 取出的 Structure 可視為一份值。
  • 修改 item 後,需要再寫回 lockers(index)
  • 若資料會頻繁被修改,或需要共享同一個狀態,使用 Class 通常更自然。

Structure 與 Class 的選擇

判斷項目 適合 Structure 適合 Class
資料大小 小型資料,欄位少。 大型資料或欄位很多。
資料語意 重點是值本身。 重點是物件身份。
修改頻率 建立後少修改,或設計為不可變。 狀態會頻繁改變。
共享需求 不需要多處共享同一個實例狀態。 多處要操作同一個物件。
繼承設計 不需要繼承。 需要繼承、多型或複雜行為。

場景七:會員資料應使用 Class

會員資料通常有身份、歷史紀錄、狀態變化與多個流程共同操作,不適合只當成一組值來複製。這類資料使用 Class 會更合理。

範例程式碼
VB.NET
Public Class MemberAccount
    Public Property MemberNo As String
    Public Property DisplayName As String
    Public Property PointBalance As Integer

    Public Sub AddPoints(points As Integer)
        If points > 0 Then
            PointBalance += points
        End If
    End Sub
End Class
邏輯解析
  • 會員帳號有明確身份,不只是單純值。
  • 點數餘額會持續改變,適合保留同一個物件狀態。
  • 需要長期存在、共享與行為擴充時,Class 比 Structure 更合適。

實務判斷與常見誤區

常見問題整理

  • 把 Structure 當小型 Class:若資料需要身份、共享狀態或繼承,應使用 Class。
  • 設計過大的 Structure:欄位很多時,複製成本會變高。
  • 可變 Structure 太複雜:容易在指定、傳參數或集合中產生副本混淆。
  • 誤以為永遠在堆疊:值型別的重點是值語意,不是固定儲存位置。
  • 頻繁裝箱:Structure 轉成 Object 或介面時可能產生裝箱成本。
  • 忘記預設值:Structure 即使未呼叫自訂建構函式,也會有預設值。
需求 建議 原因
座標、尺寸、範圍 可考慮 Structure。 小型值資料,語意自然。
建立後不應修改 使用唯讀屬性。 降低副本修改造成的混淆。
需要可空狀態 使用 Nullable(Of T)T? 語法。 一般 Structure 變數本身有預設值。
需要身份與共享狀態 使用 Class。 參考型別更符合物件身份語意。
需要繼承與多型 使用 Class 或介面設計。 Structure 不支援繼承。

設計建議:Structure 最適合表示小型、完整、彼此相關、可以複製的值。若一份資料被修改後,其他地方也應看到同一份變化,通常代表它比較像物件,應使用 Class。

重點整理

  1. Structure 是值型別,適合表示小型相關資料。
  2. Structure 的重點是值語意,指定或傳遞時要注意複製。
  3. Structure 可以包含屬性、方法、建構函式與介面實作。
  4. Structure 不支援繼承,也不能被其他型別繼承。
  5. Structure 建立後若不需要修改,建議使用唯讀屬性降低混淆。
  6. 覆寫相等性時,應同時處理 EqualsGetHashCode
  7. Structure 放在集合中更新時,常見安全做法是取出、修改、再寫回。
  8. 需要身份、共享狀態、繼承或複雜行為時,應使用 Class