swift - 检查单个 Unicode 标量的字符集会产生奇怪的行为

标签 swift foundation character-set

在使用 CharacterSet 时,我遇到了一个有趣的问题。从我目前收集到的信息来看,CharacterSet 是基于 UnicodeScalar 的;您可以使用标量对其进行初始化,并检查集合中是否包含标量。查询该集合以查明它是否包含一个 Character,其字形可能由多个 unicode 标量值组成,这没有意义。

我的问题出在我使用 😆 表情符号进行测试时,它是一个单一的 unicode 标量值(十进制为 128518)。由于这是一个单一的 unicode 标量值,我认为它会起作用,结果如下:

"😆" == UnicodeScalar(128518)! // true

// A few variations to show exactly what is being set up
let supersetA = CharacterSet(charactersIn: "😆")
let supersetB = CharacterSet(charactersIn: "A😆")
let supersetC = CharacterSet(charactersIn: UnicodeScalar(128518)!...UnicodeScalar(128518)!)
let supersetD = CharacterSet(charactersIn: UnicodeScalar(65)...UnicodeScalar(65)).union(CharacterSet(charactersIn: UnicodeScalar(128518)!...UnicodeScalar(128518)!))

supersetA.contains(UnicodeScalar(128518)!) // true
supersetB.contains(UnicodeScalar(128518)!) // false
supersetC.contains(UnicodeScalar(128518)!) // true
supersetD.contains(UnicodeScalar(128518)!) // false

如您所见,如果 CharacterSet 包含单个标量值(可能是由于优化),则检查有效,但在任何其他情况下它都不会按预期工作。

我找不到任何关于 CharacterSet 底层实现的信息,也找不到它是否以某种编码(即像 NSString 这样的 UTF-16)工作的信息,但作为 API处理很多 UnicodeScalar 我很惊讶它会这样失败,我不确定为什么会这样,或者如何进一步调查。

谁能解释为什么会这样?

最佳答案

CharacterSet 的源代码 is available, actually. contains 的来源是:

fileprivate func contains(_ member: Unicode.Scalar) -> Bool {
    switch _backing {
    case .immutable(let cs):
        return CFCharacterSetIsLongCharacterMember(cs, member.value)
    case .mutable(let cs):
        return CFCharacterSetIsLongCharacterMember(cs, member.value)
    }
}

所以它基本上只是调用 CFCharacterSetIsLongCharacterMemberis also available, although only for Yosemite 的源代码(El Cap 和 Sierra 的版本都说“即将推出”)。然而,优胜美地代码似乎与我在 Sierra 上的反汇编中看到的相符。无论如何,它的代码如下所示:

Boolean CFCharacterSetIsLongCharacterMember(CFCharacterSetRef theSet, UTF32Char theChar) {
    CFIndex length;
    UInt32 plane = (theChar >> 16);
    Boolean isAnnexInverted = false;
    Boolean isInverted;
    Boolean result = false;

    CF_OBJC_FUNCDISPATCHV(__kCFCharacterSetTypeID, Boolean, (NSCharacterSet *)theSet, longCharacterIsMember:(UTF32Char)theChar);

    __CFGenericValidateType(theSet, __kCFCharacterSetTypeID);

    if (plane) {
        CFCharacterSetRef annexPlane;

        if (__CFCSetIsBuiltin(theSet)) {
            isInverted = __CFCSetIsInverted(theSet);
            return (CFUniCharIsMemberOf(theChar, __CFCSetBuiltinType(theSet)) ? !isInverted : isInverted); 
        }

        isAnnexInverted = __CFCSetAnnexIsInverted(theSet);

        if ((annexPlane = __CFCSetGetAnnexPlaneCharacterSetNoAlloc(theSet, plane)) == NULL) {
            if (!__CFCSetHasNonBMPPlane(theSet) && __CFCSetIsRange(theSet)) {
                isInverted = __CFCSetIsInverted(theSet);
                length = __CFCSetRangeLength(theSet);
                return (length && __CFCSetRangeFirstChar(theSet) <= theChar && theChar < __CFCSetRangeFirstChar(theSet) + length ? !isInverted : isInverted);
            } else {
                return (isAnnexInverted ? true : false);
            }
        } else {
            theSet = annexPlane;
            theChar &= 0xFFFF;
        }
    }

    isInverted = __CFCSetIsInverted(theSet);

    switch (__CFCSetClassType(theSet)) {
        case __kCFCharSetClassBuiltin:
            result = (CFUniCharIsMemberOf(theChar, __CFCSetBuiltinType(theSet)) ? !isInverted : isInverted);
            break;

        case __kCFCharSetClassRange:
            length = __CFCSetRangeLength(theSet);
            result = (length && __CFCSetRangeFirstChar(theSet) <= theChar && theChar < __CFCSetRangeFirstChar(theSet) + length ? !isInverted : isInverted);
            break;

        case __kCFCharSetClassString:
            result = ((length = __CFCSetStringLength(theSet)) ? (__CFCSetBsearchUniChar(__CFCSetStringBuffer(theSet), length, theChar) ? !isInverted : isInverted) : isInverted);
            break;

        case __kCFCharSetClassBitmap:
            result = (__CFCSetCompactBitmapBits(theSet) ? (__CFCSetIsMemberBitmap(__CFCSetBitmapBits(theSet), theChar) ? true : false) : isInverted);
            break;

        case __kCFCharSetClassCompactBitmap:
            result = (__CFCSetCompactBitmapBits(theSet) ? (__CFCSetIsMemberInCompactBitmap(__CFCSetCompactBitmapBits(theSet), theChar) ? true : false) : isInverted);
            break;

        default:
            CFAssert1(0, __kCFLogAssertion, "%s: Internal inconsistency error: unknown character set type", __PRETTY_FUNCTION__); // We should never come here
            return false; // To make compiler happy
    }

    return (result ? !isAnnexInverted : isAnnexInverted);
}

这样我们就可以跟进,并弄清楚发生了什么。不幸的是,我们必须发挥我们的 x86_64 汇编技能才能做到这一点。但是不要害怕,因为我已经为你做了这件事,因为显然这是我在周五晚上做的娱乐事件。

一个有用的东西是数据结构:

struct __CFCharacterSet {
    CFRuntimeBase _base;
    CFHashCode _hashValue;
    union {
        struct {
            CFIndex _type;
        } _builtin;
        struct {
            UInt32 _firstChar;
            CFIndex _length;
        } _range;
        struct {
            UniChar *_buffer;
            CFIndex _length;
        } _string;
        struct {
            uint8_t *_bits;
        } _bitmap;
        struct {
            uint8_t *_cBits;
        } _compactBitmap;
   } _variants;
   CFCharSetAnnexStruct *_annex;
};

我们还需要知道 CFRuntimeBase 到底是什么:

typedef struct __CFRuntimeBase {
    uintptr_t _cfisa;
    uint8_t _cfinfo[4];
#if __LP64__
    uint32_t _rc;
#endif
} CFRuntimeBase;

你猜怎么着!还有一些我们需要的常量。

enum {
        __kCFCharSetClassTypeMask = 0x0070,
            __kCFCharSetClassBuiltin = 0x0000,
            __kCFCharSetClassRange = 0x0010,
            __kCFCharSetClassString = 0x0020,
            __kCFCharSetClassBitmap = 0x0030,
            __kCFCharSetClassSet = 0x0040,
            __kCFCharSetClassCompactBitmap = 0x0040,
    // irrelevant stuff redacted
};

然后我们可以中断 CFCharacterSetIsLongCharacterMember 并记录结构:

supersetA.contains(UnicodeScalar(128518)!)

(lldb) po [NSData dataWithBytes:$rdi length:48]
<21b3d2ad ffff1d00 90190000 02000000 00000000 00000000 06f60100 00000000 01000000 00000000 00000000 00000000>

根据上面的结构,我们可以弄清楚这个字符集是由什么组成的。在这种情况下,相关部分将是 CFRuntimeBasecfinfo 的第一个字节,即字节 9-12。其中的第一个字节 0x90 包含字符集的类型信息。它需要与 __kCFCharSetClassTypeMask 进行 AND 运算,得到 0x10,即 __kCFCharSetClassRange

对于这一行:

supersetB.contains(UnicodeScalar(128518)!)

结构是:

(lldb) po [NSData dataWithBytes:$rdi length:48]
<21b3d2ad ffff1d00 a0190000 02000000 00000000 00000000 9066f000 01000000 02000000 00000000 00000000 00000000>

这次字节 9 是 0xa0,它与掩码的 AND0x20__kCFCharSetClassString

此时 Monty Python Actor 正在尖叫“开始吧!”,所以让我们浏览一下 CFCharacterSetIsLongCharacterMember 的源代码,看看发生了什么。

跳过所有 CF_OBJC_FUNCDISPATCHV 废话,我们来到这一行:

if (plane) {

这显然在两种情况下都为真。下一个测试:

if (__CFCSetIsBuiltin(theSet)) {

这两种情况下的计算结果都是假的,因为两种类型都不是 __kCFCharSetClassBuiltin,所以我们跳过那个 block 。

isAnnexInverted = __CFCSetAnnexIsInverted(theSet);

在这两种情况下,_annex 指针都是空的(查看结构末尾的所有零),所以这是 false

出于同样的原因,此测试将为 true:

if ((annexPlane = __CFCSetGetAnnexPlaneCharacterSetNoAlloc(theSet, plane)) == NULL) {

带我们去:

if (!__CFCSetHasNonBMPPlane(theSet) && __CFCSetIsRange(theSet)) {

__CFCSetHasNonBMPPlane 宏检查 _annex,所以这是错误的。表情符号当然不在 BMP 平面中,所以这实际上对于两种情况来说似乎都是错误的,即使是返回正确结果的情况也是如此。

__CFCSetIsRange 检查我们的类型是否为 __kCFCharSetClassRange,这仅在第一次为真。所以这就是我们的分歧点。产生错误结果的第二次调用在下一行返回:

return (isAnnexInverted ? true : false);

并且由于附件为 NULL,导致 isAnnexInverted 为 false,因此返回 false。

至于如何修复它……好吧,我不会。但现在我们知道为什么会这样了。据我所知,主要问题是创建字符集时未填充 _annex 字段,并且由于附件似乎用于跟踪非 BMP 中的字符平面,我认为它应该存在于两个字符集。顺便说一句,如果您决定 file one,此信息可能会对错误报告有所帮助。 (我会针对 CoreFoundation 提交它,因为那是实际问题所在)。

关于swift - 检查单个 Unicode 标量的字符集会产生奇怪的行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46374069/

相关文章:

ios - 更改表格宽度时,Swift Tableview 会向上移动

ios - 点击 map View 上的标记时显示新的 View Controller ?

swift - 重新计算图表中不同比例的值

objective-c - NSNumber 字面量

database - 如何在 Firebird 中将字符集从 ISO8859_1 转换为 UTF8?

ios - fatal error : unexpectedly found nil while unwrapping an Optional value while retrieving default

Swift 4 Objective-C 运行时和转换为 NSObjectProtocol

swift - Swift 中的关键字协议(protocol)(大写 P)是什么

visual-studio - 关于 Visual Studio 中的 "Character set"选项