檔案列表裡有「報告2」和「報告10」,重新整理後,兩個同名項目又交換了位置。問題不一定出在介面更新,也可能是比較函式沒有定義完整的順序。在雲端 Mac 上執行 iOS 測試時,把排序邏輯從檢視層抽出來,先用小樣本驗收規則,比對著錄影猜測更容易重現問題。
先明確定義排序契約
本文採用一套檔案列表規則:名稱由小到大排序,數字按自然順序比較,忽略大小寫與重音;名稱比較結果相等時,再按持久化的業務識別碼由小到大排序。比較時使用固定的地區設定,避免測試結果隨機器設定改變。
這不是所有產品都適用的預設答案。使用者的通訊錄可能需要依目前語言排序;跨裝置共用的檔案列表則可能要求一致的順序。先確定需求,再撰寫實作,不要把「看起來順眼」當成驗收標準。
穩定識別碼必須在重新整理前後保持不變。陣列索引、接收時間,以及每次建立的新 UUID,都不能用來決定同名項目的先後順序。資料匯入時也應拒絕重複識別碼,否則連最後一道排序鍵都無法區分項目。
穩定排序只能保留輸入中相等項目的相對順序,無法修正伺服器每次傳回不同順序的問題。
將比較函式獨立為純函式
將以下檔案加入應用程式目標。範例固定使用 en_US_POSIX,代表這裡選擇了固定的比較策略,並不表示它適合所有語言的名稱顯示需求。
import Foundation
struct ListEntry {
let id: String
let name: String
}
func entryLess(_ lhs: ListEntry, _ rhs: ListEntry) -> Bool {
let order = lhs.name.compare(
rhs.name,
options: [.numeric, .caseInsensitive, .diacriticInsensitive],
range: nil,
locale: Locale(identifier: "en_US_POSIX")
)
if order != .orderedSame {
return order == .orderedAscending
}
return lhs.id < rhs.id
}
不要在比較過程中讀取網路狀態、修改計數器,或查詢可能變動的使用者設定。一次排序會多次比較同一物件,結果必須一致。也不要用「小於或等於」實作比較函式:物件不能排在自己之前。
名稱比較結果等價,不代表是同一個業務物件。「Resume」和「résumé」在目前策略下可能被歸為同一組,但不能因此合併紀錄。排序、搜尋和去除重複資料,是三種不同的契約。
用小樣本固定關鍵行為
在單元測試檔案中匯入應用程式模組,再加入測試類別。程式碼應放在能存取上述型別的測試目標內;將 ListSortingTests 中的樣本擴充為自己的檔案名稱即可。
import XCTest
final class ListSortingTests: XCTestCase {
let items = [
ListEntry(id: "d", name: "Report10"),
ListEntry(id: "b", name: "report2"),
ListEntry(id: "a", name: "REPORT2"),
ListEntry(id: "c", name: "Report1")
]
func testNaturalOrderAndTies() {
XCTAssertEqual(
items.sorted(by: entryLess).map(\.id),
["c", "a", "b", "d"]
)
}
func testComparatorProperties() {
for a in items {
XCTAssertFalse(entryLess(a, a))
for b in items {
if entryLess(a, b) {
XCTAssertFalse(entryLess(b, a))
}
for c in items {
if entryLess(a, b) && entryLess(b, c) {
XCTAssertTrue(entryLess(a, c))
}
}
}
}
}
func testInputOrderDoesNotChangeOutput() {
let expected = items.sorted(by: entryLess).map(\.id)
XCTAssertEqual(
Array(items.reversed()).sorted(by: entryLess).map(\.id),
expected
)
let rotated = Array(items.dropFirst()) + [items[0]]
XCTAssertEqual(
rotated.sorted(by: entryLess).map(\.id),
expected
)
}
}
第一個測試驗收數字順序與同名項目的排序。第二個檢查反自反性、非對稱性與傳遞性,能找出臨時拼湊多種規則所造成的矛盾。第三個改變輸入順序,驗證結果不依賴資料的接收順序。
三層迴圈適合十幾個具代表性的樣本,不要直接套用到完整的正式環境列表。大型資料集應另外測量排序時間,不能把性質檢查誤當成效能基準測試。
補齊 Unicode 與空值的邊界情況
接著加入空名稱、前導零、中文名稱、組合重音和表情符號序列。預組字元與「字母加組合重音」可能顯示成相同的樣子;不能用字串長度判斷它們的顯示順序。
對於「檔案02」和「檔案2」,不要自行假設數字比較的結果。先明確定義產品是否將它們視為同一組,再於選定的執行環境中驗證。如果必須區分前導零,可以在名稱比較結果等價後,加入明確的次級名稱規則,最後仍以業務識別碼決定順序。
缺少名稱的情況應在模型轉換階段統一處理,例如轉換為空字串,或定義「缺少名稱的項目排在最後」的分支。不要在比較函式中隨機補入預設值。降冪排序也應重新定義排序鍵的方向,不能直接將升冪比較結果取反;取反會讓相等物件被判定為「小於」。
在雲端執行並記錄失敗證據
在雲端 Mac 的測試工作中,固定 Xcode、模擬器執行環境與比較所用的地區設定,透過既有的單元測試入口執行這些測試。保存提交識別碼、工具鏈版本、失敗時的輸入,以及實際輸出的識別碼序列,不需要記錄真實客戶的檔案名稱。
先驗收純函式,再驗收介面:連續重新整理兩次,確認同名項目不會交換位置;刪除一個項目後再插入,確認其餘項目的相對順序符合規則。介面層必須重用同一個比較函式,不能另外撰寫一套排序邏輯。
固定地區設定不代表系統的定序規則永遠不變。升級執行環境後,應重新檢查具代表性的語言樣本;如果差異來自系統規則,就決定要接受變更,還是採用自訂排序鍵,而不是直接更新所有快照。
最終交付內容應包含排序契約、代表性樣本、比較函式的性質檢查,以及重新整理的驗收結果。這樣下次順序改變時,就能判斷是資料、規則還是執行環境發生變化,而不是又從截圖開始猜測。
常見問題
排序演算法穩定,就能避免同名項目跳動嗎?
不能。穩定排序保留的是輸入中的相對順序;若伺服器回傳順序改變,同名項目仍會移動。應以持久且唯一的業務識別碼作為最後的排序鍵。
固定比較地區後,可以永久沿用同一份排序快照嗎?
不宜。固定地區能排除使用者設定差異,但系統字串定序規則仍可能隨執行環境更新。升級時應重新檢查代表性資料與比較函式的性質。