Grapheme, các khung nhìn, và vì sao count là O(n)
String là kiểu mà Swift từ chối nói dối bạn một cách lộ liễu nhất. Ở phần lớn ngôn ngữ, chuỗi là
một mảng đơn vị mã và length thì rẻ, trông có vẻ đúng, và sai với một phần lớn thế giới. Swift đưa
tính đúng đắn ấy ra mặt tiền và bắt bạn trả giá ngay từ đầu.
let flag = "🇻🇳"
flag.count // 1
flag.unicodeScalars.count // 2 — hai ký hiệu vùng
flag.utf16.count // 4
flag.utf8.count // 8
Cả bốn con số đều đúng; chúng trả lời bốn câu hỏi khác nhau. String duyệt theo cụm grapheme —
thứ mà người đọc gọi là một ký tự — nên count là O(n): phải đi hết chuỗi, vì một ký tự có thể dài
bao nhiêu byte cũng được.
Đó cũng là lý do String không có subscript bằng số nguyên. str[5] không thể là O(1), và Swift
không trao cho bạn một thao tác mà chi phí của nó giấu ngay giữa ban ngày:
let text = "Xin chào"
let index = text.index(text.startIndex, offsetBy: 4)
text[index] // "c"
text[text.startIndex..<index] // "Xin "
Hai chuỗi có thể chứa byte khác nhau mà vẫn bằng nhau:
let composed = "é" // U+00E9
let decomposed = "e\u{0301}" // U+0065 U+0301
composed == decomposed // true
composed.utf8.count // 2
decomposed.utf8.count // 3
Swift so sánh theo tương đương chuẩn tắc, nên văn bản hiển thị giống hệt nhau thì bằng nhau. Đó
gần như luôn là điều con người muốn nói. Nó cũng có nghĩa == trên chuỗi không phải memcmp, và tra
cứu Dictionary với khoá chuỗi có làm việc thật với Unicode.
Cảnh báo
Đừng bao giờ mang kết quả so sánh String của Swift qua một ranh giới đòi hỏi byte phải giống
hệt nhau — một mã băm, một chữ ký, một khoá chính trong cơ sở dữ liệu. Khi cần byte đúng nghĩa
byte, hãy so sánh Array(string.utf8).
Mỗi String phơi ra bốn khung nhìn trên cùng một vùng lưu trữ, và chọn đúng khung nhìn chính là
phần lớn ý nghĩa của cụm từ “tối ưu chuỗi” trong Swift:
characters (chính bản thân chuỗi) — cụm grapheme. Dùng cho mọi thứ hiển thị cho người.unicodeScalars — điểm mã Unicode. Dùng để phân loại ký tự, kiểm tra thuộc tính.utf16 — để giao tiếp với NSString, NSRange và phần lớn API C trên nền tảng Apple.utf8 — cho định dạng truyền tải, băm, và đọc ghi tệp. Từ Swift 5, đây là dạng lưu trữ gốc, nên
khung nhìn utf8 là miễn phí.func isASCIIDigitsOnly(_ text: String) -> Bool {
!text.utf8.isEmpty && text.utf8.allSatisfy { $0 >= 0x30 && $0 <= 0x39 }
}
Làm việc trên khung nhìn utf8 ở đây bỏ qua hoàn toàn việc tách grapheme: phép kiểm tra chỉ còn là
một lượt quét byte.
Cắt chuỗi cho ra một Substring, mà nó — giống ArraySlice — dùng chung vùng lưu trữ với nguồn:
func lastComponent(of path: String) -> String {
guard let slash = path.lastIndex(of: "/") else { return path }
return String(path[path.index(after: slash)...]) // cái String(...) này quan trọng
}
Không có phép chuyển đổi đó, bạn trả về một Substring ghim nguyên đường dẫn trong bộ nhớ. Hệ thống
kiểu ở đây hích bạn đi đúng hướng: các API lưu trữ văn bản đều nhận String, nên phép chuyển đổi
thường bị ép ngay tại ranh giới. Bên trong vòng lặp phân tích cú pháp, hãy ở lại với Substring và
chỉ chuyển đổi một lần ở cuối.
"\(value)" không phải phép màu, và cũng không phải description. Nó đi qua
ExpressibleByStringInterpolation, thứ mà bạn mở rộng được:
extension String.StringInterpolation {
mutating func appendInterpolation(_ value: Double, decimals: Int) {
appendLiteral(String(format: "%.\(decimals)f", value))
}
}
"π ≈ \(Double.pi, decimals: 3)" // "π ≈ 3.142"
Trình biên dịch biến từng đoạn thành một lời gọi appendLiteral hoặc appendInterpolation, nên mọi
thứ được phân giải tĩnh — một phép nội suy tuỳ biến không tốn hơn đoạn code bạn tự viết tay, mà lại
giữ được logic định dạng ra khỏi nơi gọi.