Hệ thống phân cấp, và mỗi bậc hứa hẹn điều gì
Các protocol collection là phần được thiết kế tốt nhất trong thư viện chuẩn Swift, và cũng là phần
người ta dùng nhiều nhất mà chẳng bao giờ tự tuân thủ. Hiểu được hệ thống phân cấp này bạn lời hai
lần: kiểu của bạn được tặng hàng trăm phương thức, và bạn cuối cùng cũng hiểu vì sao Array với
String lại hành xử như vậy.
Sequence — duyệt được ít nhất một lần. Lời hứa chỉ có thế.Collection — duyệt được nhiều lần, có chỉ mục, count hữu hạn.BidirectionalCollection — lùi được, nên last và reversed() rẻ.RandomAccessCollection — cộng trừ chỉ mục là O(1), nên count và shuffled() rẻ.MutableCollection — thay được phần tử (không chèn, không xoá).RangeReplaceableCollection — chèn và xoá được; đây là chỗ ở của append và remove(at:).Mỗi bậc là một lời hứa về chi phí, không chỉ về khả năng. Đó là điểm thiết kế đáng mang theo:
RandomAccessCollection không thêm phương thức nào mà bạn không tự viết được — nó hứa rằng chúng là
O(1), nhờ vậy các thuật toán generic mới chọn được chiến lược tốt hơn.
Cảnh báo
Một Sequence có thể chỉ duyệt được một lần. AsyncStream, một phản hồi mạng, Zip2Sequence
dựng trên iterator — duyệt lần thứ hai có thể chẳng còn gì. Nếu thuật toán của bạn cần duyệt hai
lượt, hãy ràng buộc vào Collection, đừng dừng ở Sequence.
Để một kiểu trở thành Collection, hãy cung cấp startIndex, endIndex, index(after:) và một
subscript. Bạn được tặng map, filter, reduce, first, contains, sorted, prefix,
dropLast và phần còn lại.
struct RingBuffer<Element> {
private var storage: [Element?]
private var head = 0
private(set) var count = 0
}
extension RingBuffer: Collection {
var startIndex: Int { 0 }
var endIndex: Int { count }
func index(after index: Int) -> Int { index + 1 }
subscript(position: Int) -> Element {
precondition(position < count, "chỉ mục vượt phạm vi")
return storage[(head + position) % storage.count]!
}
}
Toàn bộ phần tuân thủ chỉ có vậy, và RingBuffer giờ chạy được với for … in, với so khớp mẫu, với
sorted(), và với mọi hàm generic mà người khác đã viết cho Collection.
Để ý kiểu chỉ mục ở đây là Int, nhưng không nhất thiết phải thế — String.Index tồn tại chính vì
chỉ mục của một String là độ lệch byte, chỉ có nghĩa với đúng chuỗi đã sinh ra nó.
Dictionary.Index mờ đục cũng vì lý do ấy. Khi vùng lưu trữ của bạn không phải một mảng phẳng, một
kiểu chỉ mục mờ mới là lựa chọn trung thực.
Mỗi lần nối chuỗi map/filter là một lần cấp phát mảng mới:
let result = hugeArray.map(transform).filter(isInteresting).first
Đoạn đó dựng một mảng to bằng hugeArray, rồi dựng mảng thứ hai, rồi vứt cả hai đi để lấy đúng một
phần tử. lazy biến chuỗi đó thành một khung nhìn tính toán theo nhu cầu:
let result = hugeArray.lazy.map(transform).filter(isInteresting).first
Giờ transform chỉ chạy tới khi tìm được phần tử đầu tiên thoả mãn, và không cấp phát gì cả. Cái
giá là tính lười lan truyền và kiểu trở nên dài dòng (LazyFilterSequence<LazyMapSequence<…>>), và
nếu bạn duyệt một chuỗi lười hai lần thì phần việc cũng chạy hai lần. Hãy dùng nó khi cần thoát sớm
trên dữ liệu lớn; bỏ qua nó khi dù sao bạn cũng sẽ tiêu thụ hết.
prefix, suffix, dropFirst và subscript theo khoảng đều trả về slice, mà một slice giữ cho
toàn bộ vùng đệm gốc sống:
func firstLine(of file: [UInt8]) -> ArraySlice<UInt8> {
file.prefix { $0 != 0x0A }
}
Giữ slice đó là cả tệp nằm nguyên trong bộ nhớ. Nếu slice sẽ sống lâu hơn nguồn của nó, hãy sao
chép: Array(file.prefix(…)).
Cái bẫy còn lại của slice là chỉ mục. Slice giữ nguyên chỉ mục của nguồn, nên nó không bắt đầu từ 0:
let numbers = [10, 20, 30, 40]
let tail = numbers.dropFirst(2)
tail[0] // sập — startIndex của slice là 2
tail[tail.startIndex] // 30
Viết [0] lên bất cứ thứ gì có thể là slice là một lỗi đang chờ ngày đổi tên biến. Hãy dùng first,
startIndex, hoặc duyệt.