这与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)
}
// ?
// ["1f469", "200d"]
// ?
// ["1f469", "200d"]
// ?
// ["1f467", "200d"]
// ?
// ["1f466"]
如您所见,只有最后一个字符不包含零宽度的连接符,因此在使用contains(_:)
方法时,它的工作方式与您预期的一样.由于您没有与包含零宽度连接符的表情符号进行比较,因此该方法只能找到最后一个字符.
为了进一步扩展,如果创建一个String
,它由一个以零宽度连接符结尾的表情符号组成,并将其传递给contains(_:)
方法,它的计算结果也将为false
.这与contains(_:)
与range(of:) != nil
完全相同有关,后者试图找到与给定参数的完全匹配.由于以零宽度连接符结尾的字符形成了一个不完整的序列,因此该方法在将以零宽度连接符结尾的字符组合成一个完整序列的同时,try 为参数找到匹配项.这意味着如果出现以下情况,该方法将永远无法找到匹配项:
- 参数以零宽度的连接符结尾,并且
- 要分析的字符串不包含不完整的序列(即以零宽度连接符结尾,后面不跟兼容字符).
为了证明:
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
在exact character-by-character equivalence上执行比较.作为旁注,这里字符的意思是not,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