Điều này có liên quan đến cách thức String
hoạt động của kiểu này trong Swift và cách contains(_:)
thức hoạt động của phương thức.
'👩👩👧👦' là thứ được gọi là chuỗi biểu tượng cảm xúc, được hiển thị dưới dạng một ký tự hiển thị trong chuỗi. Trình tự được tạo thành từ Character
các đối tượng, đồng thời nó được tạo thành từ UnicodeScalar
các đối tượng.
Nếu bạn kiểm tra số lượng ký tự của chuỗi, bạn sẽ thấy rằng nó được tạo thành từ bốn ký tự, trong khi nếu bạn kiểm tra số vô hướng unicode, nó sẽ hiển thị cho bạn một kết quả khác:
print("👩👩👧👦".characters.count) // 4
print("👩👩👧👦".unicodeScalars.count) // 7
Bây giờ, nếu bạn phân tích các ký tự và in chúng, bạn sẽ thấy những gì trông giống như các ký tự bình thường, nhưng trên thực tế, ba ký tự đầu tiên chứa cả biểu tượng cảm xúc cũng như một phép nối có độ rộng bằng 0 trong chúng UnicodeScalarView
:
for char in "👩👩👧👦".characters {
print(char)
let scalars = String(char).unicodeScalars.map({ String($0.value, radix: 16) })
print(scalars)
}
// 👩
// ["1f469", "200d"]
// 👩
// ["1f469", "200d"]
// 👧
// ["1f467", "200d"]
// 👦
// ["1f466"]
Như bạn có thể thấy, chỉ có ký tự cuối cùng không chứa trình nối có độ rộng bằng không, vì vậy khi sử dụng contains(_:)
phương thức, nó hoạt động như bạn mong đợi. Vì bạn không so sánh với biểu tượng cảm xúc có chứa các phép nối có độ rộng bằng không, nên phương thức sẽ không tìm thấy kết quả khớp nào cho bất kỳ ngoại trừ ký tự cuối cùng.
Để mở rộng về điều này, nếu bạn tạo một String
ký tự bao gồm một ký tự biểu tượng cảm xúc kết thúc bằng một phép nối có độ rộng bằng 0 và chuyển nó sang contains(_:)
phương thức, nó cũng sẽ đánh giá false
. Điều này có liên quan đến việc contains(_:)
giống hệt như range(of:) != nil
, cố gắng tìm một kết quả khớp chính xác với đối số đã cho. Do các ký tự kết thúc bằng một phép nối có độ rộng bằng không tạo thành một chuỗi không hoàn chỉnh, nên phương thức cố gắng tìm một kết quả khớp cho đối số trong khi kết hợp các ký tự kết thúc với các phép nối có độ rộng bằng 0 thành một chuỗi hoàn chỉnh. Điều này có nghĩa là phương thức sẽ không bao giờ tìm thấy kết quả khớp nếu:
- đối số kết thúc bằng một phép nối có độ rộng bằng không và
- chuỗi phân tích cú pháp không chứa chuỗi không hoàn chỉnh (nghĩa là kết thúc bằng một phép nối có độ rộng bằng 0 và không được theo sau bởi một ký tự tương thích).
Để lam sang tỏ:
let s = "\u{1f469}\u{200d}\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}" // 👩👩👧👦
s.range(of: "\u{1f469}\u{200d}") != nil // false
s.range(of: "\u{1f469}\u{200d}\u{1f469}") != nil // false
Tuy nhiên, vì so sánh chỉ nhìn về phía trước, bạn có thể tìm thấy một số chuỗi hoàn chỉnh khác trong chuỗi bằng cách làm việc ngược lại:
s.range(of: "\u{1f466}") != nil // true
s.range(of: "\u{1f467}\u{200d}\u{1f466}") != nil // true
s.range(of: "\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}") != nil // true
// Same as the above:
s.contains("\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}") // true
Giải pháp đơn giản nhất là cung cấp một tùy chọn so sánh cụ thể cho range(of:options:range:locale:)
phương thức. Tùy chọn String.CompareOptions.literal
thực hiện so sánh trên một tương đương chính xác từng ký tự . Một lưu ý phụ, ý nghĩa của ký tự ở đây không phải là Swift Character
, mà là đại diện UTF-16 của cả thể hiện và chuỗi so sánh - tuy nhiên, vì String
không cho phép UTF-16 không đúng định dạng, điều này về cơ bản tương đương với việc so sánh vô hướng Unicode đại diện.
Ở đây tôi đã quá tải Foundation
phương thức, vì vậy nếu bạn cần phương thức ban đầu, hãy đổi tên phương thức này hoặc một cái gì đó:
extension String {
func contains(_ string: String) -> Bool {
return self.range(of: string, options: String.CompareOptions.literal) != nil
}
}
Bây giờ phương thức hoạt động như nó "nên" với mỗi ký tự, ngay cả với các chuỗi không hoàn chỉnh:
s.contains("👩") // true
s.contains("👩\u{200d}") // true
s.contains("\u{200d}") // true