这与Swift中String
类型的工作方式以及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)
}
正如您所见,只有最后一个字符不包含零宽连接器,因此使用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
"".contains("\u{200D}")
现在会返回true,这是一个修复bug而非feature的结果。 - Kevin