Ma to związek z tym, jak String
typ działa w Swift i jak contains(_:)
działa metoda.
„👩👩👧👦” to tak zwana sekwencja emoji, która jest renderowana jako jeden widoczny znak w ciągu. Sekwencja składa się z Character
obiektów, a jednocześnie składa się z UnicodeScalar
obiektów.
Jeśli sprawdzisz liczbę znaków ciągu, zobaczysz, że składa się on z czterech znaków, a jeśli sprawdzisz liczbę skalarną Unicode, wyświetli się inny wynik:
print("👩👩👧👦".characters.count) // 4
print("👩👩👧👦".unicodeScalars.count) // 7
Teraz, gdy przeanalizujesz znaki i wydrukujesz je, zobaczysz coś, co wydaje się normalne, ale w rzeczywistości trzy pierwsze znaki zawierają zarówno emoji, jak i łącznik o zerowej szerokości 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"]
Jak widać, tylko ostatni znak nie zawiera łącznika o zerowej szerokości, więc przy użyciu tej contains(_:)
metody działa tak, jak można się spodziewać. Ponieważ nie porównujesz z emoji zawierającymi łączniki o zerowej szerokości, metoda nie znajdzie dopasowania dla żadnego oprócz ostatniego znaku.
Aby rozwinąć tę kwestię, jeśli utworzysz String
kompozycję składającą się ze znaku emoji kończącego się łącznikiem o zerowej szerokości i przekażesz ją do contains(_:)
metody, to również oceni to false
. Ma to związek z contains(_:)
byciem dokładnie takim samym jak range(of:) != nil
, który próbuje znaleźć dokładne dopasowanie do podanego argumentu. Ponieważ znaki kończące się łącznikiem o zerowej szerokości tworzą niekompletną sekwencję, metoda próbuje znaleźć dopasowanie dla argumentu, łącząc znaki kończące się łącznikami o zerowej szerokości w pełną sekwencję. Oznacza to, że metoda nigdy nie znajdzie dopasowania, jeśli:
- argument kończy się łącznikiem o zerowej szerokości, oraz
- ciąg do analizy nie zawiera niekompletnej sekwencji (tzn. kończy się łącznikiem o zerowej szerokości i nie następuje po nim zgodny znak).
Aby zademonstrować:
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
Ponieważ jednak porównanie tylko patrzy w przyszłość, można znaleźć kilka innych pełnych sekwencji w ciągu, pracując wstecz:
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
Najłatwiejszym rozwiązaniem byłoby zapewnienie konkretnej opcji porównania z range(of:options:range:locale:)
metodą. Opcja String.CompareOptions.literal
wykonuje porównanie na podstawie dokładnej równoważności znak po znaku . Na marginesie, co oznaczało po znaku Oto nie Swift Character
, ale UTF-16 reprezentacji obu instancji i porównania String - jednak, ponieważ String
nie pozwala zniekształcone UTF-16, to jest w zasadzie równoważne porównując skalarne Unicode reprezentacja.
Tutaj przeciążyłem Foundation
metodę, więc jeśli potrzebujesz oryginalnej, zmień nazwę tej lub innej:
extension String {
func contains(_ string: String) -> Bool {
return self.range(of: string, options: String.CompareOptions.literal) != nil
}
}
Teraz metoda działa tak, jak „powinna” z każdym znakiem, nawet z niekompletnymi sekwencjami:
s.contains("👩") // true
s.contains("👩\u{200d}") // true
s.contains("\u{200d}") // true