角色?‍?‍?‍? (有两个女人、一个女孩和一个男孩的家庭)编码如下:

U+1F469 WOMAN,
‍U+200D ZWJ,
U+1F469 WOMAN,
U+200D ZWJ,
U+1F467 GIRL,
U+200D ZWJ,
U+1F466 BOY

所以它是非常有趣的编码;单元测试的完美目标.然而,Swift 似乎不知道如何治疗.我的意思是:

"?‍?‍?‍?".contains("?‍?‍?‍?") // true
"?‍?‍?‍?".contains("?") // false
"?‍?‍?‍?".contains("\u{200D}") // false
"?‍?‍?‍?".contains("?") // false
"?‍?‍?‍?".contains("?") // true

所以,Swift 说它包含自己(好)和一个男孩(好!).但它接着说,里面没有女性、女孩或零宽度的木匠.如果我把它当作一个包含What's happening here? Why does Swift know it contains a boy but not a woman or girl?个元素的子元素来对待,而其他人却无法理解.

This does not change if I use something like 100.


更令人困惑的是:

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

所以我得到了与字符数组相同的行为...这是非常恼人的,因为我知道数组是什么样子的.

This also does not change if I use something like 100.

推荐答案

这与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 为参数找到匹配项.这意味着如果出现以下情况,该方法将永远无法找到匹配项:

  1. 参数以零宽度的连接符结尾,并且
  2. 要分析的字符串不包含不完整的序列(即以零宽度连接符结尾,后面不跟兼容字符).

为了证明:

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.literalexact 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

Swift相关问答推荐

是否可以将字典项绑定到文本字段?

传递给SWIFT ResultBuilder的闭包中结果类型的对象

是否在字符串属性上搜索数组和子数组?

如何强制创建新的UIViewControllerRenatable|更新Make UIViewController上的视图

将NavigationSplitView更改为NavigationStack

如何在SwiftUI中做出适当的曲线?

如何判断设备是否为 Vision Pro - Xcode 15 Beta 4

仅当 boolean 为真时才实现方法(application:didReceiveRemoteNotification)

Swift - 给定一个像 30 的 Int 如何将范围数乘以 7?

阻止其他类型的键盘

Vapor Swift 如何配置客户端连接超时

如何在 SwiftUI 中实现 PageView?

将if let与逻辑或运算符一起使用

协议扩展中的where self是什么

Swift - 迭代 struct 对象时如何对其进行变异

在 Swift 中以编程方式更新约束的常量属性?

两个 Date 对象之间的所有日期 (Swift)

iOS:如何检测用户是否订阅了自动更新订阅

将 [String] 存储在 NSUserDefaults 中

如何在 iOS 上的 Swift 中获取 sharedApplication?