这与String
类型在Swift中的contains(_:)
工作方式以及 方法的工作方式有关。
“ 👩👩👧👦”是一个表情符号序列,它被表达为字符串中的一个可见字符。序列由Character
对象组成,并且同时由UnicodeScalar
对象组成。
如果检查字符串的字符数,将看到它由四个字符组成,而如果检查unicode标量数,它将显示不同的结果:
print("👩👩👧👦".characters.count) // 4
print("👩👩👧👦".unicodeScalars.count) // 7
现在,如果您解析这些字符并打印它们,您将看到看起来像普通字符的字符,但实际上,前三个字符在其字符中既包含表情符号,也包含零宽度的连接符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"]
如您所见,只有最后一个字符不包含零宽度的连接符,因此在使用该contains(_:)
方法时,它可以按预期工作。由于您没有与包含零宽度连接符的表情符号进行比较,因此该方法将找不到除最后一个字符以外的任何其他字符的匹配项。
要对此进行扩展,如果您创建一个String
由表情符号字符组成的表情符号,字符以零宽度的连接符结尾,并将其传递给该contains(_:)
方法,则其结果也将为false
。这与contains(_:)
和完全相同有关range(of:) != nil
,后者试图找到与给定参数的完全匹配。由于以零宽度连接符结尾的字符形成不完整的序列,因此该方法尝试在将以零宽度连接符结尾的字符组合为完整序列的同时找到参数的匹配项。这意味着在以下情况下,该方法将永远找不到匹配项:
- 该参数以零宽度连接符结尾,并且
- 要解析的字符串不包含不完整的序列(即,以零宽度的连接符结尾且不跟随兼容字符)。
展示:
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
但是,由于比较只是向前看,因此可以通过向后工作来在字符串中找到其他几个完整的序列:
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
最简单的解决方案是为该range(of:options:range:locale:)
方法提供特定的比较选项。该选项String.CompareOptions.literal
在逐个字符的等效值上执行比较。附带说明一下,此处的字符不是 Swift Character
,而是实例和比较字符串的UTF-16表示形式–但是,由于String
不允许格式错误的UTF-16,因此,这实际上等效于比较Unicode标量表示。
在这里,我已经重载了该Foundation
方法,因此,如果您需要原始方法,请将该方法重命名为:
extension String {
func contains(_ string: String) -> Bool {
return self.range(of: string, options: String.CompareOptions.literal) != nil
}
}
现在,该方法可以按每个字符使用,即使序列不完整也可以使用:
s.contains("👩") // true
s.contains("👩\u{200d}") // true
s.contains("\u{200d}") // true