2025年10月3日 星期五

30.VB.NET 清單(List) 筆記 (精進篇)

VB.NET List(List(Of T))筆記(精進篇)

VB.NET List(List(Of T)) 筆記(精進篇)

List(Of T) 是 VB.NET 最常用的泛型集合之一。它可以保存同一種型別的多筆資料,並提供新增、插入、刪除、索引存取、查找、篩選、排序與輸出等操作。

List 的重點不是只把資料「放很多筆」,而是讓資料清單可以隨流程變動。名單、購物車、庫存項目、待辦事項、報表列資料、搜尋結果,都很適合先用 List 建立基本集合模型,再依需求做整理與顯示。

先理解 List 在做什麼

List(Of T):可動態調整大小、可用索引存取、並且限制元素型別的泛型集合。T 代表元素型別,例如 List(Of String) 只能放字串,List(Of ProductItem) 只能放 ProductItem 物件。

List 的核心特性

  • 型別安全:集合中的元素型別固定,不容易混入不相關資料。
  • 數量可變:可以依流程新增或刪除,不必先固定容量。
  • 保留順序:資料加入後會有排列順序,可用索引取得。
  • 允許重複:同一個值或同一類物件可以重複出現。
  • 方法完整:內建 AddInsertRemoveFindFindAllSort 等方法。
比較項目 Array List(Of T)
大小 建立後固定。 可動態增加或刪除元素。
索引存取 支援。 支援。
新增刪除 不方便,通常要重建資料。 有內建方法可直接操作。
適合情境 筆數固定的資料。 筆數會變動的資料清單。

基本語法

VB.NET
Dim taskNames As New List(Of String)()

taskNames.Add("整理報表")
taskNames.Add("列印標籤")
taskNames.Insert(0, "登入系統")

taskNames.Remove("列印標籤")

Dim firstTask As String = taskNames(0)
Dim taskCount As Integer = taskNames.Count

新增、插入、刪除與索引存取

場景一:活動報到序號清單

這個範例用活動報到序號示範 List 的基本生命週期。序號可以加入尾端,也可以插入指定位置;刪除時可依序號內容移除;顯示時用索引列出目前順序。

需要的主控項
  • TextBoxTicketCode:輸入報到序號。
  • NumericUpDownInsertIndex:輸入插入位置。
  • ButtonAddTicket:加入序號。
  • ButtonInsertTicket:插入序號。
  • ButtonRemoveTicket:刪除序號。
  • ListBoxTickets:顯示序號清單。
  • LabelTicketStatus:顯示狀態。
範例程式碼
VB.NET / Windows Forms
Public Class Form1
    Private ticketCodes As New List(Of String)()

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        NumericUpDownInsertIndex.Minimum = 0
        NumericUpDownInsertIndex.Maximum = 999
        LabelTicketStatus.Text = "尚未加入報到序號"
    End Sub

    Private Sub ButtonAddTicket_Click(sender As Object, e As EventArgs) Handles ButtonAddTicket.Click
        Dim code As String = TextBoxTicketCode.Text.Trim().ToUpper()

        If code = String.Empty Then
            LabelTicketStatus.Text = "請輸入報到序號"
            Return
        End If

        If ticketCodes.Contains(code) Then
            LabelTicketStatus.Text = "序號已存在"
            Return
        End If

        ticketCodes.Add(code)
        RefreshTicketList()
        LabelTicketStatus.Text = "已加入:" & code
    End Sub

    Private Sub ButtonInsertTicket_Click(sender As Object, e As EventArgs) Handles ButtonInsertTicket.Click
        Dim code As String = TextBoxTicketCode.Text.Trim().ToUpper()

        If code = String.Empty Then
            LabelTicketStatus.Text = "請輸入報到序號"
            Return
        End If

        If ticketCodes.Contains(code) Then
            LabelTicketStatus.Text = "序號已存在"
            Return
        End If

        Dim insertIndex As Integer = CInt(NumericUpDownInsertIndex.Value)

        If insertIndex > ticketCodes.Count Then
            insertIndex = ticketCodes.Count
        End If

        ticketCodes.Insert(insertIndex, code)
        RefreshTicketList()
        LabelTicketStatus.Text = "已插入:" & code & " / 位置:" & insertIndex.ToString()
    End Sub

    Private Sub ButtonRemoveTicket_Click(sender As Object, e As EventArgs) Handles ButtonRemoveTicket.Click
        Dim code As String = TextBoxTicketCode.Text.Trim().ToUpper()

        If code = String.Empty Then
            LabelTicketStatus.Text = "請輸入要刪除的序號"
            Return
        End If

        If ticketCodes.Remove(code) Then
            RefreshTicketList()
            LabelTicketStatus.Text = "已刪除:" & code
        Else
            LabelTicketStatus.Text = "找不到指定序號"
        End If
    End Sub

    Private Sub RefreshTicketList()
        ListBoxTickets.Items.Clear()

        For index As Integer = 0 To ticketCodes.Count - 1
            ListBoxTickets.Items.Add(index.ToString("00") & ":" & ticketCodes(index))
        Next
    End Sub
End Class
畫面輸出結果(依序加入 A103、B208,再插入 A001 到位置 0)
00:A001 01:A103 02:B208
邏輯解析
  • Add 會把資料加入清單尾端。
  • Insert 可把資料放到指定索引位置。
  • Remove 會依內容移除第一筆符合資料,並回傳是否成功。
  • 使用索引前要確認範圍,避免超出清單有效位置。

物件集合:讓每筆資料有多個欄位

場景二:課後材料包清單

實務資料通常不只是一個字串。材料包會有名稱、分類、數量與單價,適合用類別表示一筆資料,再用 List(Of MaterialPack) 保存多筆資料。

需要的主控項
  • TextBoxPackName:輸入材料包名稱。
  • TextBoxPackType:輸入分類。
  • TextBoxPackQty:輸入數量。
  • TextBoxPackPrice:輸入單價。
  • ButtonAddPack:加入材料包。
  • ListBoxPacks:顯示清單。
  • LabelPackStatus:顯示狀態。
範例程式碼
VB.NET / Windows Forms
Public Class MaterialPack
    Public Property PackName As String
    Public Property PackType As String
    Public Property Quantity As Integer
    Public Property UnitPrice As Decimal

    Public Sub New(packName As String, packType As String, quantity As Integer, unitPrice As Decimal)
        Me.PackName = packName
        Me.PackType = packType
        Me.Quantity = quantity
        Me.UnitPrice = unitPrice
    End Sub

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

    Public Function ToDisplayText() As String
        Return PackName & " / " & PackType & " / 數量:" & Quantity.ToString() &
               " / 小計:" & TotalAmount.ToString("N0")
    End Function
End Class

Public Class Form1
    Private packs As New List(Of MaterialPack)()

    Private Sub ButtonAddPack_Click(sender As Object, e As EventArgs) Handles ButtonAddPack.Click
        Dim packName As String = TextBoxPackName.Text.Trim()
        Dim packType As String = TextBoxPackType.Text.Trim()
        Dim quantity As Integer
        Dim unitPrice As Decimal

        If packName = String.Empty OrElse packType = String.Empty Then
            LabelPackStatus.Text = "名稱與分類不可空白"
            Return
        End If

        If Not Integer.TryParse(TextBoxPackQty.Text.Trim(), quantity) OrElse quantity <= 0 Then
            LabelPackStatus.Text = "數量格式錯誤"
            Return
        End If

        If Not Decimal.TryParse(TextBoxPackPrice.Text.Trim(), unitPrice) OrElse unitPrice < 0D Then
            LabelPackStatus.Text = "單價格式錯誤"
            Return
        End If

        packs.Add(New MaterialPack(packName, packType, quantity, unitPrice))
        RefreshPackList(packs)
        LabelPackStatus.Text = "材料包已加入"
    End Sub

    Private Sub RefreshPackList(source As List(Of MaterialPack))
        ListBoxPacks.Items.Clear()

        For Each pack As MaterialPack In source
            ListBoxPacks.Items.Add(pack.ToDisplayText())
        Next
    End Sub
End Class
畫面輸出結果(加入一筆材料包)
水彩體驗包 / 美術 / 數量:6 / 小計:900
邏輯解析
  • MaterialPack 代表一筆具有多個欄位的資料。
  • List(Of MaterialPack) 保存多筆材料包資料。
  • 顯示文字由物件自己的 ToDisplayText 方法產生,表單只負責把結果放到畫面。

查找、篩選與存在性判斷

方法 用途 適合情境
Find 找第一筆符合條件的元素。 找第一個符合關鍵字的項目。
FindAll 找出所有符合條件的元素。 篩選分類、庫存、狀態。
Exists 判斷是否至少有一筆符合。 檢查是否有缺貨、重複、異常。
IndexOf 找出元素位置。 簡單值清單的位置查找。

場景三:點心庫存搜尋面板

這個範例用點心庫存示範 FindFindAllExists。搜尋單筆時只顯示第一筆符合資料;篩選時會列出所有符合庫存門檻的項目。

需要的主控項
  • TextBoxSnackKeyword:輸入關鍵字。
  • NumericUpDownMinStock:設定最低庫存。
  • ButtonFindSnack:搜尋第一筆。
  • ButtonFilterSnack:依庫存篩選。
  • ButtonCheckEmptySnack:檢查是否有零庫存。
  • ListBoxSnackResult:顯示結果。
  • LabelSnackStatus:顯示狀態。
範例程式碼
VB.NET / Windows Forms
Public Class SnackItem
    Public Property SnackName As String
    Public Property CategoryName As String
    Public Property StockCount As Integer

    Public Sub New(snackName As String, categoryName As String, stockCount As Integer)
        Me.SnackName = snackName
        Me.CategoryName = categoryName
        Me.StockCount = stockCount
    End Sub

    Public Function ToDisplayText() As String
        Return SnackName & " / " & CategoryName & " / 庫存:" & StockCount.ToString()
    End Function
End Class

Public Class Form1
    Private snacks As New List(Of SnackItem) From {
        New SnackItem("海鹽餅乾", "餅乾", 18),
        New SnackItem("蜂蜜蛋糕", "甜點", 5),
        New SnackItem("黑糖麻糬", "甜點", 0),
        New SnackItem("烘焙堅果", "零食", 12),
        New SnackItem("蘋果果乾", "果乾", 7)
    }

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        NumericUpDownMinStock.Minimum = 0
        NumericUpDownMinStock.Maximum = 999
        RefreshSnackList(snacks)
    End Sub

    Private Sub ButtonFindSnack_Click(sender As Object, e As EventArgs) Handles ButtonFindSnack.Click
        Dim keyword As String = TextBoxSnackKeyword.Text.Trim()

        If keyword = String.Empty Then
            LabelSnackStatus.Text = "請輸入關鍵字"
            Return
        End If

        Dim found As SnackItem = snacks.Find(Function(item)
                                                 Return item.SnackName.Contains(keyword) OrElse
                                                        item.CategoryName.Contains(keyword)
                                             End Function)

        ListBoxSnackResult.Items.Clear()

        If found Is Nothing Then
            LabelSnackStatus.Text = "找不到符合資料"
            Return
        End If

        ListBoxSnackResult.Items.Add(found.ToDisplayText())
        LabelSnackStatus.Text = "已找到第一筆符合資料"
    End Sub

    Private Sub ButtonFilterSnack_Click(sender As Object, e As EventArgs) Handles ButtonFilterSnack.Click
        Dim minStock As Integer = CInt(NumericUpDownMinStock.Value)
        Dim result As List(Of SnackItem) = snacks.FindAll(Function(item) item.StockCount >= minStock)

        RefreshSnackList(result)
        LabelSnackStatus.Text = "符合筆數:" & result.Count.ToString()
    End Sub

    Private Sub ButtonCheckEmptySnack_Click(sender As Object, e As EventArgs) Handles ButtonCheckEmptySnack.Click
        Dim hasEmptyStock As Boolean = snacks.Exists(Function(item) item.StockCount = 0)

        If hasEmptyStock Then
            LabelSnackStatus.Text = "目前存在零庫存點心"
        Else
            LabelSnackStatus.Text = "所有點心皆有庫存"
        End If
    End Sub

    Private Sub RefreshSnackList(source As List(Of SnackItem))
        ListBoxSnackResult.Items.Clear()

        For Each item As SnackItem In source
            ListBoxSnackResult.Items.Add(item.ToDisplayText())
        Next
    End Sub
End Class
畫面輸出結果(最低庫存 = 10)
海鹽餅乾 / 餅乾 / 庫存:18 烘焙堅果 / 零食 / 庫存:12
邏輯解析
  • Find 只回傳第一筆符合條件的資料。
  • FindAll 會建立新的 List,內容是符合條件的元素。
  • Exists 只回答是否存在,不需要真的列出資料。
  • Lambda 條件讓查找規則可以直接寫在方法呼叫中。

排序、反轉與輸出

場景四:課程候補名單排序

List 可以直接排序,也可以先篩選後再排序。這個範例讓候補名單依登記時間、候補序號或姓名排序,並可輸出成一段文字。

需要的主控項
  • ComboBoxWaitSort:選擇排序方式。
  • ButtonSortWaitList:排序名單。
  • ButtonReverseWaitList:反轉目前順序。
  • ButtonExportWaitList:輸出名單文字。
  • ListBoxWaitList:顯示候補名單。
  • TextBoxWaitExport:顯示輸出文字,建議設定 Multiline=True
  • LabelWaitStatus:顯示狀態。
範例程式碼
VB.NET / Windows Forms
Imports System.Linq

Public Class WaitPerson
    Public Property QueueNo As Integer
    Public Property DisplayName As String
    Public Property RegisteredAt As DateTime

    Public Sub New(queueNo As Integer, displayName As String, registeredAt As DateTime)
        Me.QueueNo = queueNo
        Me.DisplayName = displayName
        Me.RegisteredAt = registeredAt
    End Sub

    Public Function ToDisplayText() As String
        Return QueueNo.ToString("000") & " / " & DisplayName & " / " &
               RegisteredAt.ToString("HH:mm")
    End Function
End Class

Public Class Form1
    Private waitList As New List(Of WaitPerson) From {
        New WaitPerson(12, "林小姐", New DateTime(2025, 4, 20, 9, 35, 0)),
        New WaitPerson(3, "陳先生", New DateTime(2025, 4, 20, 9, 10, 0)),
        New WaitPerson(8, "王同學", New DateTime(2025, 4, 20, 9, 22, 0))
    }

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ComboBoxWaitSort.Items.Clear()
        ComboBoxWaitSort.Items.Add("依候補序號")
        ComboBoxWaitSort.Items.Add("依登記時間")
        ComboBoxWaitSort.Items.Add("依姓名")
        ComboBoxWaitSort.SelectedIndex = 0
        RefreshWaitList()
    End Sub

    Private Sub ButtonSortWaitList_Click(sender As Object, e As EventArgs) Handles ButtonSortWaitList.Click
        Select Case ComboBoxWaitSort.SelectedIndex
            Case 0
                waitList.Sort(Function(a, b) a.QueueNo.CompareTo(b.QueueNo))
            Case 1
                waitList.Sort(Function(a, b) a.RegisteredAt.CompareTo(b.RegisteredAt))
            Case 2
                waitList.Sort(Function(a, b) String.Compare(a.DisplayName, b.DisplayName))
        End Select

        RefreshWaitList()
        LabelWaitStatus.Text = "排序完成"
    End Sub

    Private Sub ButtonReverseWaitList_Click(sender As Object, e As EventArgs) Handles ButtonReverseWaitList.Click
        waitList.Reverse()
        RefreshWaitList()
        LabelWaitStatus.Text = "已反轉目前順序"
    End Sub

    Private Sub ButtonExportWaitList_Click(sender As Object, e As EventArgs) Handles ButtonExportWaitList.Click
        Dim lines() As String = waitList.Select(Function(item) item.ToDisplayText()).ToArray()
        TextBoxWaitExport.Text = String.Join(Environment.NewLine, lines)
        LabelWaitStatus.Text = "名單文字已輸出"
    End Sub

    Private Sub RefreshWaitList()
        ListBoxWaitList.Items.Clear()

        For Each item As WaitPerson In waitList
            ListBoxWaitList.Items.Add(item.ToDisplayText())
        Next
    End Sub
End Class
畫面輸出結果(依候補序號排序)
003 / 陳先生 / 09:10 008 / 王同學 / 09:22 012 / 林小姐 / 09:35
邏輯解析
  • Sort 可搭配比較函式決定排序規則。
  • Reverse 是把目前順序反過來,不是重新套用排序條件。
  • Select(...).ToArray() 可把物件清單轉成字串陣列。
  • String.Join 適合把多筆資料組成可複製或匯出的文字。

參考型別元素的影響

重要觀念:List(Of T)T 是 Class 時,List 裡保存的是物件參考。取出某個元素後修改它的屬性,清單中的該物件也會看到變更。

場景五:借物單狀態更新

這個範例從 List 中找出指定借物單,直接修改該物件的狀態。因為清單保存的是同一個物件參考,所以重新整理畫面後狀態會變成「已歸還」。

需要的主控項
  • TextBoxBorrowNo:輸入借物單號。
  • ButtonReturnItem:更新為已歸還。
  • ListBoxBorrowList:顯示借物單。
  • LabelBorrowStatus:顯示狀態。
範例程式碼
VB.NET / Windows Forms
Public Class BorrowRecord
    Public Property BorrowNo As String
    Public Property ItemName As String
    Public Property Returned As Boolean

    Public Sub New(borrowNo As String, itemName As String, returned As Boolean)
        Me.BorrowNo = borrowNo
        Me.ItemName = itemName
        Me.Returned = returned
    End Sub

    Public Function ToDisplayText() As String
        Return BorrowNo & " / " & ItemName & " / " & If(Returned, "已歸還", "借出中")
    End Function
End Class

Public Class Form1
    Private borrowList As New List(Of BorrowRecord) From {
        New BorrowRecord("BR-001", "投影筆", False),
        New BorrowRecord("BR-002", "延長線", False),
        New BorrowRecord("BR-003", "白板筆組", True)
    }

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        RefreshBorrowList()
    End Sub

    Private Sub ButtonReturnItem_Click(sender As Object, e As EventArgs) Handles ButtonReturnItem.Click
        Dim targetNo As String = TextBoxBorrowNo.Text.Trim().ToUpper()

        If targetNo = String.Empty Then
            LabelBorrowStatus.Text = "請輸入借物單號"
            Return
        End If

        Dim record As BorrowRecord = borrowList.Find(Function(item) item.BorrowNo = targetNo)

        If record Is Nothing Then
            LabelBorrowStatus.Text = "找不到借物單"
            Return
        End If

        record.Returned = True
        RefreshBorrowList()
        LabelBorrowStatus.Text = "已更新歸還狀態"
    End Sub

    Private Sub RefreshBorrowList()
        ListBoxBorrowList.Items.Clear()

        For Each item As BorrowRecord In borrowList
            ListBoxBorrowList.Items.Add(item.ToDisplayText())
        Next
    End Sub
End Class
畫面輸出結果(輸入 BR-002 後更新)
BR-001 / 投影筆 / 借出中 BR-002 / 延長線 / 已歸還 BR-003 / 白板筆組 / 已歸還
邏輯解析
  • BorrowRecord 是 Class,所以清單保存的是物件參考。
  • record.Returned = True 修改的是清單中的同一個物件。
  • 若元素型別是 Structure,更新方式通常需要取出、修改、再寫回清單。

刪除資料與列舉限制

不要在 For Each 中直接修改同一個 List 結構

For Each 正在列舉清單時,若同時對同一個 List 呼叫 AddRemoveRemoveAt 改變集合結構,可能拋出 InvalidOperationException。需要刪除多筆資料時,可以使用倒序 For,或使用 RemoveAll

場景六:移除過期優惠券

這個範例示範兩種安全移除方式。若刪除條件簡單,可使用 RemoveAll;若刪除時還要同時記錄每一筆內容,可使用倒序 For

需要的主控項
  • ButtonRemoveExpired:移除過期優惠券。
  • ButtonRemoveExpiredWithLog:倒序移除並記錄。
  • ListBoxCouponList:顯示優惠券。
  • TextBoxRemoveLog:顯示移除紀錄,建議設定 Multiline=True
  • LabelCouponStatus:顯示狀態。
範例程式碼
VB.NET / Windows Forms
Public Class CouponInfo
    Public Property CouponCode As String
    Public Property ExpireDate As DateTime

    Public Sub New(couponCode As String, expireDate As DateTime)
        Me.CouponCode = couponCode
        Me.ExpireDate = expireDate.Date
    End Sub

    Public Function ToDisplayText() As String
        Return CouponCode & " / 到期日:" & ExpireDate.ToString("yyyy/MM/dd")
    End Function
End Class

Public Class Form1
    Private coupons As New List(Of CouponInfo)()

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ResetCoupons()
        RefreshCouponList()
    End Sub

    Private Sub ButtonRemoveExpired_Click(sender As Object, e As EventArgs) Handles ButtonRemoveExpired.Click
        Dim today As DateTime = DateTime.Today
        Dim removedCount As Integer = coupons.RemoveAll(Function(item) item.ExpireDate < today)

        RefreshCouponList()
        LabelCouponStatus.Text = "已移除過期優惠券:" & removedCount.ToString() & " 張"
    End Sub

    Private Sub ButtonRemoveExpiredWithLog_Click(sender As Object, e As EventArgs) Handles ButtonRemoveExpiredWithLog.Click
        Dim today As DateTime = DateTime.Today
        TextBoxRemoveLog.Clear()

        For index As Integer = coupons.Count - 1 To 0 Step -1
            If coupons(index).ExpireDate < today Then
                TextBoxRemoveLog.Text &= "移除:" & coupons(index).CouponCode & Environment.NewLine
                coupons.RemoveAt(index)
            End If
        Next

        RefreshCouponList()
        LabelCouponStatus.Text = "倒序移除完成"
    End Sub

    Private Sub ResetCoupons()
        coupons.Clear()
        coupons.Add(New CouponInfo("CP-A100", DateTime.Today.AddDays(-2)))
        coupons.Add(New CouponInfo("CP-B200", DateTime.Today.AddDays(5)))
        coupons.Add(New CouponInfo("CP-C300", DateTime.Today.AddDays(-1)))
        coupons.Add(New CouponInfo("CP-D400", DateTime.Today.AddDays(10)))
    End Sub

    Private Sub RefreshCouponList()
        ListBoxCouponList.Items.Clear()

        For Each item As CouponInfo In coupons
            ListBoxCouponList.Items.Add(item.ToDisplayText())
        Next
    End Sub
End Class
畫面輸出結果(移除過期後)
CP-B200 / 到期日:今天後 5 天 CP-D400 / 到期日:今天後 10 天
邏輯解析
  • RemoveAll 適合依條件一次刪除多筆資料。
  • 倒序 For 搭配 RemoveAt 可避免刪除後索引位移造成漏刪。
  • For Each 適合讀取,不適合在列舉中直接改變同一個 List 的結構。

List 與其他集合的選擇

集合型別 適合用途 判斷重點
List(Of T) 有順序、可重複、可索引的資料清單。 最通用的集合起點。
HashSet(Of T) 不允許重複值。 重複檢查比 List 更直接。
Dictionary(Of TKey, TValue) 用鍵快速找到對應值。 適合代碼查資料、編號查物件。
Queue(Of T) 先進先出流程。 適合排隊、任務佇列。
Stack(Of T) 後進先出流程。 適合復原、返回、暫存狀態。

選型提醒:List 很通用,但不代表所有集合需求都該使用 List。若重點是「唯一值」,可考慮 HashSet;若重點是「用代碼快速查資料」,可考慮 Dictionary;若重點是「依序排隊處理」,可考慮 Queue

綜合應用:小型庫存面板

場景七:飲品庫存管理

這個範例整合新增、查找、篩選、排序、統計與畫面重整。List 作為原始資料來源,畫面顯示可以依關鍵字和排序方式產生不同結果,但不破壞原本資料模型。

需要的主控項
  • TextBoxDrinkName:輸入飲品名稱。
  • TextBoxDrinkType:輸入分類。
  • TextBoxDrinkPrice:輸入單價。
  • TextBoxDrinkStock:輸入庫存。
  • TextBoxDrinkFilter:輸入篩選關鍵字。
  • ComboBoxDrinkSort:選擇排序方式。
  • ButtonAddDrink:新增飲品。
  • ButtonApplyDrinkView:套用篩選與排序。
  • ButtonDrinkSummary:更新摘要。
  • ListBoxDrinkList:顯示飲品清單。
  • LabelDrinkStatus:顯示狀態。
  • LabelDrinkSummary:顯示摘要。
範例程式碼
VB.NET / Windows Forms
Imports System.Linq

Public Class DrinkStockItem
    Public Property DrinkName As String
    Public Property DrinkType As String
    Public Property UnitPrice As Decimal
    Public Property StockCount As Integer

    Public Sub New(drinkName As String, drinkType As String, unitPrice As Decimal, stockCount As Integer)
        Me.DrinkName = drinkName
        Me.DrinkType = drinkType
        Me.UnitPrice = unitPrice
        Me.StockCount = stockCount
    End Sub

    Public ReadOnly Property StockAmount As Decimal
        Get
            Return UnitPrice * StockCount
        End Get
    End Property

    Public Function ToDisplayText() As String
        Return DrinkName & " / " & DrinkType & " / 單價:" & UnitPrice.ToString("N0") &
               " / 庫存:" & StockCount.ToString()
    End Function
End Class

Public Class Form1
    Private drinks As New List(Of DrinkStockItem) From {
        New DrinkStockItem("蜜香紅茶", "茶飲", 45D, 18),
        New DrinkStockItem("檸檬氣泡水", "氣泡飲", 55D, 9),
        New DrinkStockItem("燕麥拿鐵", "咖啡", 75D, 6),
        New DrinkStockItem("桂花烏龍", "茶飲", 50D, 0)
    }

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        ComboBoxDrinkSort.Items.Clear()
        ComboBoxDrinkSort.Items.Add("名稱")
        ComboBoxDrinkSort.Items.Add("價格由高到低")
        ComboBoxDrinkSort.Items.Add("庫存由低到高")
        ComboBoxDrinkSort.SelectedIndex = 0

        ApplyDrinkView()
        UpdateDrinkSummary()
    End Sub

    Private Sub ButtonAddDrink_Click(sender As Object, e As EventArgs) Handles ButtonAddDrink.Click
        Dim drinkName As String = TextBoxDrinkName.Text.Trim()
        Dim drinkType As String = TextBoxDrinkType.Text.Trim()
        Dim unitPrice As Decimal
        Dim stockCount As Integer

        If drinkName = String.Empty OrElse drinkType = String.Empty Then
            LabelDrinkStatus.Text = "名稱與分類不可空白"
            Return
        End If

        If Not Decimal.TryParse(TextBoxDrinkPrice.Text.Trim(), unitPrice) OrElse unitPrice < 0D Then
            LabelDrinkStatus.Text = "單價格式錯誤"
            Return
        End If

        If Not Integer.TryParse(TextBoxDrinkStock.Text.Trim(), stockCount) OrElse stockCount < 0 Then
            LabelDrinkStatus.Text = "庫存格式錯誤"
            Return
        End If

        drinks.Add(New DrinkStockItem(drinkName, drinkType, unitPrice, stockCount))
        ApplyDrinkView()
        UpdateDrinkSummary()
        LabelDrinkStatus.Text = "飲品已加入"
    End Sub

    Private Sub ButtonApplyDrinkView_Click(sender As Object, e As EventArgs) Handles ButtonApplyDrinkView.Click
        ApplyDrinkView()
        LabelDrinkStatus.Text = "已套用目前條件"
    End Sub

    Private Sub ButtonDrinkSummary_Click(sender As Object, e As EventArgs) Handles ButtonDrinkSummary.Click
        UpdateDrinkSummary()
        LabelDrinkStatus.Text = "摘要已更新"
    End Sub

    Private Sub ApplyDrinkView()
        Dim viewList As List(Of DrinkStockItem) = New List(Of DrinkStockItem)(drinks)
        Dim keyword As String = TextBoxDrinkFilter.Text.Trim()

        If keyword <> String.Empty Then
            viewList = viewList.FindAll(Function(item)
                                            Return item.DrinkName.Contains(keyword) OrElse
                                                   item.DrinkType.Contains(keyword)
                                        End Function)
        End If

        Select Case ComboBoxDrinkSort.SelectedIndex
            Case 0
                viewList.Sort(Function(a, b) String.Compare(a.DrinkName, b.DrinkName))
            Case 1
                viewList.Sort(Function(a, b) b.UnitPrice.CompareTo(a.UnitPrice))
            Case 2
                viewList.Sort(Function(a, b) a.StockCount.CompareTo(b.StockCount))
        End Select

        RefreshDrinkList(viewList)
    End Sub

    Private Sub RefreshDrinkList(source As List(Of DrinkStockItem))
        ListBoxDrinkList.Items.Clear()

        For Each item As DrinkStockItem In source
            ListBoxDrinkList.Items.Add(item.ToDisplayText())
        Next
    End Sub

    Private Sub UpdateDrinkSummary()
        Dim totalKinds As Integer = drinks.Count
        Dim totalStock As Integer = drinks.Sum(Function(item) item.StockCount)
        Dim totalAmount As Decimal = drinks.Sum(Function(item) item.StockAmount)
        Dim zeroStockKinds As Integer = drinks.Count(Function(item) item.StockCount = 0)

        LabelDrinkSummary.Text = "品項數:" & totalKinds.ToString() &
                                 " / 總庫存:" & totalStock.ToString() &
                                 " / 庫存總值:" & totalAmount.ToString("N0") &
                                 " / 零庫存:" & zeroStockKinds.ToString()
    End Sub
End Class
畫面輸出結果(初始摘要)
品項數:4 / 總庫存:33 / 庫存總值:1,920 / 零庫存:1
整合重點
  • drinks 是原始資料來源。
  • New List(Of DrinkStockItem)(drinks) 建立檢視用副本,避免排序時直接改變原始順序。
  • FindAll 負責篩選,Sort 負責排序。
  • SumCount 可快速完成摘要統計。

實務判斷與常見誤區

常見問題整理

  • 把 List 當成所有集合的唯一選擇:需要唯一性或鍵值查找時,可能更適合 HashSetDictionary
  • 索引沒有檢查範圍:使用 list(index) 前應確認 index >= 0index < list.Count
  • 在 For Each 中增刪同一個 List:容易造成列舉期間集合被修改的錯誤。
  • 排序直接改變原清單:Sort 會改變目前 List 的順序,若要保留原順序,先建立副本。
  • 忽略 Class 元素的參考行為:清單中的 Class 物件被取出後修改屬性,原清單中的物件也會變動。
  • 用 Contains 做複雜物件比對:物件比對需要定義相等規則,或改用 Exists 搭配指定條件。
需求 建議方法 原因
加入尾端 Add 最常見的新增方式。
插入指定位置 Insert 需要先確認索引範圍。
刪除符合條件多筆資料 RemoveAll 條件清楚時最簡潔。
找第一筆資料 Find 只需要一筆結果。
篩選多筆資料 FindAll 會回傳新的 List。
排序顯示 Sort 會改變目前 List 順序,必要時先複製。

重點整理

  1. List(Of T) 是可動態調整大小、可索引存取、具型別安全的泛型集合。
  2. List 適合有順序、可重複、筆數會變動的資料清單。
  3. Add 加到尾端,Insert 插入指定位置,Remove 依內容移除。
  4. Find 找第一筆,FindAll 找多筆,Exists 判斷是否存在。
  5. Sort 會改變目前 List 的順序;保留原順序時,應先建立副本。
  6. List 中若保存 Class 物件,取出後修改屬性會影響清單中的同一個物件。
  7. 列舉同一個 List 時,不應直接增刪該 List 結構;可改用 RemoveAll 或倒序 For
  8. 需要唯一性、鍵值查找或佇列流程時,應評估 HashSetDictionaryQueue