角色(有两个女人,一个女孩和一个男孩的家庭)编码如下:
U+1F469
WOMAN
,
?U+200D
ZWJ
,
U+1F469
WOMAN
,
U+200D
ZWJ
,
U+1F467
GIRL
,
U+200D
ZWJ
,
U+1F466
BOY
所以它非常有趣地编码; 单元测试的完美目标.然而,斯威夫特似乎不知道如何对待它.这就是我的意思:
"???".contains("???") // true "???".contains("") // false "???".contains("\u{200D}") // false "???".contains("") // false "???".contains("") // true
所以,斯威夫特说它包含自己(好)和一个男孩(好!).但它说它不包含女人,女孩或零宽度木匠.这里发生了什么事?斯威夫特为什么知道它包含一个男孩而不是女人或女孩?我能理解它是否将它视为一个单一的角色并且只识别它包含它自己,但事实上它有一个子组件,没有其他人困惑我.
如果我使用类似的东西,这不会改变"".characters.first!
.
更令人困惑的是:
let manual = "\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}" Array(manual.characters) // ["?", "?", "?", ""]
即使我将ZWJ放在那里,它们也不会反映在字符数组中.接下来是一个小小的说法:
manual.contains("") // false manual.contains("") // false manual.contains("") // true
所以我得到了与字符数组相同的行为...这是非常烦人的,因为我知道数组的样子.
如果我使用类似的东西,这也不会改变"".characters.first!
.
这与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
第一个问题是你正在与基金会建立联系contains
(Swift String
不是a Collection
),所以这就是NSString
行为,我不相信把握Emoji就像Swift一样强大.也就是说,Swift我认为现在正在实施Unicode 8,这也需要在Unicode 10中围绕这种情况进行修改(因此,当它们实现Unicode 10时,这可能都会发生变化;我还没有考虑是否会这样做).
为简化起见,让我们摆脱Foundation,并使用Swift,它提供更明确的视图.我们将从角色开始:
"???".characters.forEach { print($0) } ? ? ?
好.这就是我们的预期.但这是谎言.让我们看看这些角色究竟是什么.
"???".characters.forEach { print(String($0).unicodeScalars.map{$0}) } ["\u{0001F469}", "\u{200D}"] ["\u{0001F469}", "\u{200D}"] ["\u{0001F467}", "\u{200D}"] ["\u{0001F466}"]
啊......就是这样["ZWJ", "ZWJ", "ZWJ", ""]
.这让一切都变得更加清晰.不是此列表的成员(它是"ZWJ"),但是是成员.
问题在于它Character
是一个"字形集群",它将事物组合在一起(如附加ZWJ).你真正寻找的是一个unicode标量.这完全符合您的预期:
"???".unicodeScalars.contains("") // true "???".unicodeScalars.contains("\u{200D}") // true "???".unicodeScalars.contains("") // true "???".unicodeScalars.contains("") // true
当然,我们也可以寻找那里的实际角色:
"???".characters.contains("\u{200D}") // true
(这大大复制了Ben Leggiero的观点.我在注意到他已经回答之前发布了这个.留下以防任何人都清楚.)
似乎Swift认为a ZWJ
是一个扩展的字形集群,其前面有一个字符.我们可以在将字符数组映射到它们时看到unicodeScalars
:
Array(manual.characters).map { $0.description.unicodeScalars }
这将从LLDB打印以下内容:
? 4 elements ? 0 : StringUnicodeScalarView("?") - 0 : "\u{0001F469}" - 1 : "\u{200D}" ? 1 : StringUnicodeScalarView("?") - 0 : "\u{0001F469}" - 1 : "\u{200D}" ? 2 : StringUnicodeScalarView("?") - 0 : "\u{0001F467}" - 1 : "\u{200D}" ? 3 : StringUnicodeScalarView("") - 0 : "\u{0001F466}"
此外,.contains
组将字形集群扩展为单个字符.例如,取出hangul字符?
,?
和?
(结合起来使韩语单词为"one" :) ???
:
"\u{1112}\u{1161}\u{11AB}".contains("\u{1112}") // false
找不到这个,?
因为三个代码点被分组为一个作为一个字符的集群.同样,\u{1F469}\u{200D}
(WOMAN
ZWJ
)是一个集群,作为一个字符.
其他答案讨论了Swift的作用,但没有详细说明原因.
你期望"Å"等于"Å"吗?我希望你会的.
其中一个是带有组合器的字母,另一个是单个组合字符.您可以向基本角色添加许多不同的组合器,而人类仍然会将其视为单个角色.为了处理这种差异,创建了一个字母的概念来表示人类将考虑一个字符而不管使用的代码点.
现在,短信服务已经组合字符成图形表情符号多年:)
→ . So various emoji were added to Unicode.
These services also started combining emoji together into composite emoji.
There of course is no reasonable way to encode all possible combinations into individual codepoints, so The Unicode Consortium decided to expand on the concept of graphemes to encompass these composite characters.
What this boils down to is "???"
如果你试图在字形级别使用它,那么归结为应该被视为单个"字形集群",正如Swift默认情况下那样.
如果你想检查它是否包含""
它的一部分,那么你应该降低到较低的水平.
我不知道Swift语法,所以这里有一些Perl 6,它对Unicode有类似的支持.
(Perl 6支持Unicode版本9,因此可能存在差异)
say "\c[family: woman woman girl boy]" eq "???"; # True
# .contains is a Str method only, in Perl 6
say "???".contains("???") # True
say "???".contains(""); # False
say "???".contains("\x[200D]"); # False
# comb with no arguments splits a Str into graphemes
my @graphemes = "???".comb;
say @graphemes.elems; # 1
让我们下降一个级别
# look at it as a list of NFC codepoints
my @components := "???".NFC;
say @components.elems; # 7
say @components.grep("".ord).Bool; # True
say @components.grep("\x[200D]".ord).Bool; # True
say @components.grep(0x200D).Bool; # True
降低到这个水平会让事情变得更难.
my @match = "???".ords;
my $l = @match.elems;
say @components.rotor( $l => 1-$l ).grep(@match).Bool; # True
我认为.contains
在Swift中使这更容易,但这并不意味着没有其他事情变得更加困难.
例如,在此级别工作可以更容易地将字符串意外地拆分到复合字符的中间.
你无意中问的是为什么这种更高级别的表示不像低级表示那样工作.答案当然是,它不应该.
如果你问自己" 为什么这必须如此复杂 ",答案当然是" 人类 ".
Swift 4.0更新
字符串在swift 4更新中收到大量修订,如SE-0163中所述.表示两种不同结构的演示使用了两个表情符号.两者都与一系列表情符号相结合.
is the combination of two emoji,
和
???
是四个表情符号的组合,连接零宽度的连接器.格式是?joiner?joiner?joiner
1.计数
在swift 4.0中.表情符号被计为字形簇.每个表情符号都计为1. count属性也可直接用于字符串.所以你可以这样直接调用它.
"".count // 1. Not available on swift 3 "???".count // 1. Not available on swift 3
字符串的字符数组也被计为swift 4.0中的字形簇,因此以下两个代码都打印1.这两个表情符号是表情符号序列的示例,其中几个表情符号组合在一起,count
它们之间有或没有零宽度连接符.在swift 3.0中,这种字符串的字符数组分隔出每个表情符号,并产生一个包含多个元素(表情符号)的数组.在此过程中忽略了连接器.但是在swift 4.0中,字符数组将所有表情符号视为一个整体.所以任何表情符号都将是1.
"".characters.count // 1. In swift 3, this prints 2 "???".characters.count // 1. In swift 3, this prints 4
\u{200d}
在swift 4中保持不变.它在给定的字符串中提供唯一的Unicode字符.
"".unicodeScalars.count // 2. Combination of two emoji "???".unicodeScalars.count // 7. Combination of four emoji with joiner between them
2.包含
在swift 4.0中,unicodeScalars
方法忽略了表情符号中的零宽度连接符.因此,对于四个表情符号组件中的任何一个,它都返回true,contains
如果检查了连接器,则返回false.但是,在swift 3.0中,连接器不会被忽略,并与前面的表情符号结合使用.因此,当您检查是否"???"
包含前三个组件表情符号时,结果将为false
"".contains("") // true "".contains("") // true "???".contains("???") // true "???".contains("") // true. In swift 3, this prints false "???".contains("\u{200D}") // false "???".contains("") // true. In swift 3, this prints false "???".contains("") // true