สิ่งนี้เกี่ยวข้องกับวิธีการ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 ที่มีรูปแบบไม่ถูกต้อง การแสดง
ที่นี่ฉันได้ใช้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