ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Swift字符处理指南:从Character到Unicode的工程实践

Swift字符处理指南:从Character到Unicode的工程实践 1. Swift中的“字符”到底是个什么概念做iOS开发这么多年每次有新人问起Swift字符串处理我第一句想说的都是你先别急着用String先把Character搞明白。这个看起来不起眼的基础概念恰恰是Swift和其他语言差别最大的地方也是无数“诡异Bug”的根源。先抛一个最常见的反直觉场景你有一个字符串let str 你好‍‍下意识觉得它是4个字符对吧两个汉字加一个表情最多再加点什么。但如果你在Swift里数一下str.count结果会是3。这里面的关键就在于Character不是“一个可见符号”而是“一个用户感知到的字符”技术上叫扩展字素簇。这个设计是Swift作为现代语言的一个重大取舍。C语言里一个char就是一个字节Java里一个char是UTF-16的一个16位单元它们本质都在跟“编码单元”打交道。而Swift的Character对应的是人类阅读时自然感知到的“一个字”一个emoji、一个带声调的字母、一个由多个Unicode码点组合成的字符在用户眼里是一个字在Character层面就是一个元素。也正是因为这套设计Swift的字符串遍历几乎不会遇到“拆出半个字符”的尴尬。你拿for char in str去遍历拿到的每一个Character都是完整可展示的字符不会像某些语言那样遍历出一个孤立的表情符号中间段。但代价也很明显Character不是固定内存大小的它内部可能是1个Unicode标量也可能是多个标量组合。所以Swift的String不能像C数组那样按下标O(1)直接取字符。你想取第5个字符必须从头遍历这是Swift字符串性能话题里永远绕不开的一个底层原因。理解完“字符扩展字素簇”这个核心定义后面那些API为什么这么设计、有哪些坑、怎么优化就都顺理成章了。接下来的内容我会按实际开发中最常遇到的问题展开不是教科书式的罗列而是把踩过的坑和验证过的方法都写出来。2. Character、String与Unicode它们到底怎么协作2.1 扩展字素簇让“字符”回归人的直觉先认真聊一下扩展字素簇Extended Grapheme Cluster。这个东西不是Swift发明的Unicode标准里就有但Swift是主流语言里第一个把它作为Character默认定义的。我拿最常见的例子说明。表情符号“”这个字符实际上由两个Unicode码点组成一个是“”U1F44D另一个是“”的修饰符U1F3FD用来指定肤色。如果按码点数算这是2个码点按UTF-16编码单元算这甚至是4个16位值。但在任何正常用户眼里这就是一个字符一个竖大拇指的手颜色偏深。Swift的Character天然就是“一个”。你遍历字符串不会把肤色修饰符单独拆出来count也不会把组合序列拆开数。这种设计在处理用户输入、显示文本、做文本分析时几乎不会出错天然符合人的直觉。但这里就要注意第一个坑如果你从服务器拿数据某个字段按长度做校验比如“昵称不能超过10个字符”Swift的count显然不是你想的那个“长度”。abc的count是4但它在某些后端语言里按UTF-16算可能是6按字节算可能是10。这种跨端长度口径不一致的问题在涉及用户昵称、留言内容、订单备注等场景里经常暴雷。我的建议是凡是需要跟后端对齐的长度校验从一开始就约定好用Unicode码点数量还是UTF-16长度Swift这边分别用unicodeScalars.count和utf16.count拿到对应口径。2.2 Character和String不是父子关系而是“容器与元素”有个基本但容易混淆的点Character和String类型看起来很像但不是一个层级的东西。String是字符的集合Character是集合里的元素。Swift里你可以直接let c: Character a也可以用Character(a)把一个字符串转成字符前提是这个字符串确实只有一个字符。实操中我经常看到新手这样写let str Hello let first str[0] // 编译错误原因就是刚才说的String没有整数下标。它不能直接按整数索引因为索引本身对应的是字符边界不是等长的内存位置。这是设计使然不是API残缺。正确的姿势是用String.Indexlet str Hello let start str.startIndex let secondIndex str.index(start, offsetBy: 1) let secondChar str[secondIndex] // e这里的offsetBy: 1意思是“跨过1个Character”不是1个字节、也不是1个UTF-16单元。所以哪怕是这种多码点字符offsetBy照样一次跨过整个字符。2.3 编码单元视角等到不得不看字节时再看虽然Character很符合直觉但实际问题里你总要跟外部系统打交道——文件读写、网络传输、数据库存储这些全都绕不开字节。所以Swift提供了好几层“编码单元视图”我列个表方便对照视图元素类型对应Unicode概念典型使用场景str.countCharacter扩展字素簇界面展示、用户感知长度str.unicodeScalarsUnicode.Scalar码点跨端长度对齐、逐码点处理str.utf16UInt16UTF-16编码单元与Objective-C、Java、JS交互str.utf8UInt8UTF-8编码单元网络传输、文件存储、与C交互我自己的经验是日常业务逻辑能只用Character就只用Character直到你碰到底层协议、文件格式或者跨语言API再去接触utf8和utf16视图。过早陷入编码细节只会让代码变得又慢又绕。举个实际案例我之前处理过一个从C接口回调回来的字符串它是UTF-8字节流但因为中间被某个老旧的网关截断一个汉字被切成了半个。用String(data:encoding:)转换出来的结果是Optional(nil)整段数据解析失败。后来改成“容忍非法字节序列逐字节清洗”的方案先把非法字节替换掉再转String才解决问题。这种问题如果你一直停留在Character层面根本意识不到必须切到utf8视图去处理。3. 开发中的字符处理操作与踩坑记录3.1 字符转ASCII、整数值与进制转换Swift里拿字符对应的数值有好几个层面ASCII码、Unicode码点和UTF-16单元值容易搞混。如果你要的是ASCII码前提是字符属于ASCII范围let c: Character A let ascii c.asciiValue // Optional(65)asciiValue返回的是UInt8?因为ASCII只有128个值放UInt8绰绰有余。如果字符不是ASCII范围比如中文“中”的码点是U4E2DasciiValue返回nil。如果你要的是Unicode码点let scalar 中.unicodeScalars.first! let codePoint scalar.value // 20013也就是0x4E2D反过来从数值构造字符if let scalar Unicode.Scalar(20013) { let char Character(scalar) // 中 }我在处理“判断输入字符是否是数字/字母”这类需求时经常用Character的属性而不是手写正则let ch: Character 7 ch.isNumber // true ch.isLetter // false ch.isWhitespace // false ch.isUppercase // false这些属性内部走的都是Unicode规则对中文、阿拉伯数字、各种符号的处理都比手写ASCII区间判断稳妥得多。3.2 字符串与字符数组互相转换的几种方式很多从C或Java转过来的开发者习惯“用索引遍历字符串”到了Swift很不习惯。最常见的需求是把字符串拆成字符数组再处理然后拼回去。拆分let str Swift字符 let chars: [Character] Array(str) // [S, w, i, f, t, 字, 符]拼接let backToString String(chars) let joined chars.map(String.init).joined(separator: -) // S-w-i-f-t-字-符这类操作性能上有一个值得注意的点Array(str)会把所有Character搬到堆上如果字符串很长、又只需要访问其中几个字符这个开销就不划算。更推荐的做法是只在字符串上操作索引let str Swift字符 let first str[str.startIndex] let last str[str.index(before: str.endIndex)]我自己写业务代码时有个习惯需要连续处理大部分字符时用Array(str)直接了当只需要定位一两个字符时坚决用Index操作。这样代码既清晰又不浪费。还有一个容易让人困惑的APIstr.map { $0 }。String是Collectionmap后得到的元素是Character所以str.map { $0 }返回的也是[Character]和Array(str)等价。但读代码的人容易误解不如直接写Array(str)语义清晰。3.3 中文字符处理与乱码的根源处理中文时Swift的表现整体是很省心的因为Character天然按用户感知切分不会把一个汉字拆成两半。但乱码问题通常出现在转码环节尤其是跟外部系统的编码不一致。最常见的乱码链路是服务器返回UTF-8数据某个中间环节按GBK/GB2312解码了一次导致数据已经损坏。这种损坏是不可逆的Swift这边的String(data:encoding:)只能尽量探测不能凭空修复。排查乱码时我一般按这个顺序走先确认原始字节到底是什么编码。用Data把字节打出来看比如中文字符的UTF-8字节通常是E4 B8 80这种形态如果看到D6 D0 B1 EA这种大概率是GBK编码。确认编码后用正确的String.Encoding解码。Swift内置支持.utf8、.utf16、.isoLatin1、.windowsCP1252等但不内置GBK/GB2312。这种情况需要借助CFStringEncoding或者第三方库。如果字节已经被截断或替换那就只能做容错处理。String(decoding: data, as: UTF8.self)这种API会把非法序列替换成UFFFD那个符号而不是返回nil适合“先保证不崩再决定怎么清洗”。说句扎心的实话乱码问题90%不是Swift导致的是上游系统编码混乱造成的。Swift能做的只是优雅地暴露问题而不是变魔术。// GBK解码示例依赖CoreFoundation import CoreFoundation func decodeGBK(_ data: Data) - String? { let cfEncoding CFStringEncoding(CFStringEncodings.GB_18030_2000.rawValue) let cfString CFStringCreateWithBytes(nil, (data as NSData).bytes, data.count, cfEncoding, false) return cfString as String? }这段代码我在处理老系统对接时实测可用但注意它依赖CoreFoundation纯Swift服务端环境可能要换个思路。3.4 大小写转换与区域敏感性大小写转换看着简单但涉及区域时容易踩坑。最典型的是土耳其语环境下的“I”和“i”互相转换问题。土耳其语里大写“I”对应的小写是“ı”不带点的i而小写“i”对应的大写是“İ”带点的I。如果你的App面向的只是中文和英文用户大部分情况用默认的lowercased()和uppercased()没毛病但如果你做的是国际化产品又对文本做搜索、排序、去重就必须考虑这个区域差异。Swift的处理方式是通过Locale相关APIimport Foundation let str Istanbul let lower str.lowercased(with: Locale(identifier: tr_TR)) // 得到的是 ıstanbul不过说实话99%的国内App接触不到这种场景。我提它是因为做搜索功能时遇到过线上bug土耳其用户在搜索“I”时索引里的词条匹配不上排查半天才发现是区域化大小写问题。这个案例值得所有做国际化的团队引以为戒。3.5 处理子串Substring不是String别用错了Substring是Swift里一个特别容易踩坑的类型。它和String共享底层存储只是切了一段视图。好处是切子串不复制内存性能极好坏处是如果你把它存起来同时原来的大字符串还被别的变量持有那么那个大字符串的内存就释放不掉容易造成“看起来很小、实际占着大块内存”的情况。我曾经排查过一个内存异常问题某个页面循环处理几百KB的文本每次切出子串都放到数组里缓存结果内存峰值飙到几十MB。原因就是Substring一直引用着原始的几百KB字符串原始字符串又因为被引用无法释放。正确做法是需要短期使用的用Substring需要缓存或传递的转成Stringlet bigString ... let sub bigString.prefix(10) // Substring let realString String(sub) // 拷贝一份独立内存另外字符串切分也有两种常见的API容易混淆。split(separator:)返回的是[Substring]components(separatedBy:)返回的是[String]。前者是Swift原生、不依赖Foundation、性能好、返回的是视图后者是Foundation的API、会生成新字符串。在一次性解析场景比如解析CSV行、读取配置两者性能差异不大但如果切出来的片段要长期使用直接components(separatedBy:)拿到[String]更省事不用再逐个转。4. 字符串与字符转换的实战场景4.1 字符与C字符串的桥接CString、指针与窄字符Swift与C语言交互时字符串参数处理是个高频痛点。C接口需要的是char *或者const char *而Swift的String内部是Unicode要拿到C风格字符串必须做一次编码转换。String有一个cString相关的初始化方法和withCString方法它们是桥接C接口的主要工具。// 把Swift字符串转成UTF-8的C字符串指针并传给C函数 let str hello str.withCString { cPtr in some_c_function(cPtr) // cPtr是UnsafePointerCChar }这里有几个常踩的坑第一withCString默认编码是UTF-8所以如果C函数那边期望的是ASCII或者系统默认编码比如某些老Windows时代的API中文内容会变成多字节UTF-8序列C那边处理不好就是乱码。第二cPtr的生命周期只在闭包内有效不能存下来等到闭包外再用。我曾经把指针存到全局变量结果下次访问时数据已经变成垃圾值排查了很久才发现是生命周期问题。第三CChar在Swift里是Int8的别名不是UInt8。如果你需要跟“unsigned char”交互要做一次类型转换。标准做法是let bytes Array(str.utf8) // [UInt8] bytes.withUnsafeBufferPointer { buffer in some_c_function_unsigned(buffer.baseAddress) // 转成UnsafePointerUInt8 }“窄字符”这个热搜词我多说一句。它在C语言语境里通常指单字节字符char跟宽字符wchar_t通常是UTF-16或UTF-32相对应。Swift里本身没有窄/宽字符的概念所有Character都是Unicode字符。你只有在桥接C接口时才会遇到“对方要窄字符”的问题——此时就要注意把Swift字符串转成UTF-8字节是“窄字符串”转成UTF-16就是“宽字符串”。如果C接口声明的是const char*就要用UTF-8如果声明的是const wchar_t*则要转换成UTF-16然后通过withUnsafeBufferPointer传指针。4.2 日期转字符串与格式化输出里的字符细节日期转字符串看起来是DateFormatter的事不涉及字符处理。但实际开发里日期字符串里的字符顺序、分隔符、数字夹着文字经常需要做字符级操作。比如拿到2025-01-12 14:30:00要提取“年”“月”“日”这几个片段或者改成2025年1月12日14时30分00秒这种格式。最直接的方式是设置DateFormatter的dateFormat让格式化替你完成let formatter DateFormatter() formatter.locale Locale(identifier: zh_CN) formatter.dateFormat yyyy年M月d日HH时mm分ss秒 let dateString formatter.string(from: Date())但有一种常见需求是后端返回的日期是一个很长的字符串比如2025-01-12T14:30:0008:00你需要自己切出日期部分。我的做法是先split成数组再取前两段比用正则表达式直观let raw 2025-01-12T14:30:0008:00 let parts raw.split(separator: T).first ?? // 2025-01-12这个场景特别能体现Character和Substring的使用界限split得到的Substring马上被转成新String用于后续展示内存开销极小。还有一点要提防DateFormatter是很重的对象初始化代价高不要在循环里反复创建。正确做法是全局复用或者用static let缓存。我见过一个性能问题就是列表刷新时每个cell都创建一个DateFormatter导致滑动卡顿严重。4.3 去掉指定字符与过滤非法字符清理用户输入是每个App都躲不开的活儿。Swift里有两种思路一种是用replacingOccurrences(of:with:)替换另一种是用filter按Character条件过滤。前者适合已知具体字符后者适合按类别过滤。去掉所有空白字符let input hello \n world let cleaned input.filter { !$0.isWhitespace } // helloworld去掉非数字字符let input 电话: 138-1234-5678 let digits input.filter { $0.isNumber } // 13812345678去掉指定的一组字符let input a,b;c|d let cleaned input.filter { !,;|.contains($0) } // abcd这里,;|.contains($0)的意思是遍历原始字符串的每个Character如果目标字符串里不包含它就保留。这个写法简洁高效我经常在工具类里复用。用replacingOccurrences做批量替换时也可以结合正则let input abc123def456 let result input.replacingOccurrences(of: [0-9], with: , options: .regularExpression) // abcdef正则表达式的[0-9]是ASCII数字不包含中文数字“一二三”和全角数字“”。如果业务需要处理这些得单独拆出来判断。4.4 通过CharacterSet控制字符分类CharacterSet是Foundation里一个强大的字符集合工具。它不同于Swift标准库里的Character底层是“一组Unicode码点区间”用来做范围匹配和过滤特别方便。判断是否是字母或数字import Foundation let set CharacterSet.alphanumerics let input abc123 let isValid input.unicodeScalars.allSatisfy { set.contains($0) } // true清理字符串里所有非字母数字的字符let input hello, world! 2025. let filtered String(input.unicodeScalars.filter { CharacterSet.alphanumerics.contains($0) }) // helloworld2025注意这里必须用unicodeScalars而不是直接遍历Character因为CharacterSet.contains接收的是Unicode.Scalar。CharacterSet常用的预置集合集合含义典型用途.whitespacesAndNewlines空白和换行去除首尾空白.decimalDigits十进制数字提取数字.letters字母含中文等判断是否全部为字母.alphanumerics字母和数字过滤昵称非法字符.urlQueryAllowedURL查询允许的字符URL编码.controlCharacters控制字符清洗二进制文本我常用的一个技巧是用components(separatedBy:)加CharacterSet把字符串按任意分隔符拆开let input key1value1; key2value2; key3value3 let scanner CharacterSet(charactersIn: ; ) let parts input.components(separatedBy: scanner) // [key1value1, key2value2, key3value3]这个API有个小坑如果分隔符连续出现components会生成空字符串元素比如a;;b会拆成[a, , b]。用split(separator:)则默认会忽略连续分隔符产生的空元素两种行为差别在特定数据格式下会引发Bug用哪一个要想清楚。iPhone通讯录导出、CSV解析这类场景我都是先确认是否可能连续分隔符再选API。5. Swift并发场景下字符串与字符的安全问题swift并发安全这个热词在讨论字符串处理时也有相关风险值得单独拿出来讲。字符串是值类型Swift里大部分情况下拷贝是自动的线程间传递String天然安全。但有几类“貌似安全”的操作实际会踩并发或内存的坑。场景一可变状态的字符缓存。很多人喜欢用一个全局字典做字符串缓存比如按ID缓存处理好的显示文本。在Swift 5.5之后直接跨并发域修改全局Dictionary是编译器报错的必须用actor或者加锁。我第一次适配Swift并发时就被这个错误困住过// 编译错误全局可变状态跨并发域不安全 static var cache: [String: String] [:]正确做法是包一个actoractor StringCache { private var storage: [String: String] [:] func value(forKey key: String) - String? { return storage[key] } func setValue(_ value: String, forKey key: String) { storage[key] value } }场景二字符串桥接到C指针后又被其他线程改动。前面提到withCString的指针只能在闭包内使用。如果闭包内又开了并发任务去处理这个指针就会产生悬垂指针。解决办法是先把字符串转成Data或者不可变值再传给并发任务不要在并发任务里引用闭包捕获的指针。场景三Substring跨并发传递。Substring引用底层String存储如果这个存储本身是值类型跨并发传Substring在Swift 6严格并发检查下可能报错。我推荐直接转成String再传损失一点拷贝性能换确定性安全完全值得。有个判断原则我一直用着String和Character本身是值类型跨线程传递没问题但对它们的“引用”或“指针视图”进行操作就要格外警惕生命周期和并发边界。凡是拿不准的传一份新拷贝进去别共享底层存储。6. 常见问题排查手记字符处理十大坑整理这份清单的时候我是按自己真实踩坑频率排的序不是按API复杂度。每一条都有对应的代码教训值得收藏备用。坑1str.count在不同版本和平台上不一致这个坑在Swift 4刚改版时尤其严重。早期String继承Collection后count是按字符算还是按UTF-16算经历过一次变化现在稳定是按扩展字素簇计数。但如果你的代码里有str.length这种OC时代的写法在Swift里是编译不过的。NSAttributedString的length仍然是UTF-16单元数和str.count经常对不上这是多语言混编时最容易出矛盾的地方。坑2String.Index无法跨字符串使用String.Index是关联某个特定字符串的。把A字符串的索引拿到B字符串上用结果是未定义行为轻则错位重则崩溃。以下写法是错误示范let a hello let b world let idx a.index(a.startIndex, offsetBy: 1) let ch b[idx] // 危险索引与b不匹配坑3prefix和dropFirst返回的是视图能用是能用但prefix(3)返回的是Substring不是String。如果直接用它和另一个String比较Swift允许但会产生隐式转换如果你要存进缓存容易触发前面说的内存滞留问题。坑4range(of:)返回RangeString.Index?却经常被人当NSRange用正则匹配时尤其明显。NSRegularExpression使用的NSRange是UTF-16偏移量而Swift字符串范围是RangeString.Index两者不是同一坐标系。混用结果就是匹配位置错乱。正确转换方式let nsRange NSRange(range, in: str)或者反过来let range Range(nsRange, in: str)坑5用UILabel排版时character和“视觉宽度”不是一回事iiii和mmmm都是4个字符但显示宽度差一倍。如果需要按宽度截断不能用count要用size(withAttributes:)或boundingRect(with:options:attributes:)按实际排版宽度计算。坑6字符的isNumber和ASCII的isDigit语义不同½.isNumber返回true②.isNumber也可能返回true。如果这个结果用于数字解析或者表单校验就可能放行了一些意外字符。CharacterSet.decimalDigits则只包含数字0-9及部分十进制数字字符两者口径不同选择前先确认需求。坑7split与components对空段落的处理不同前面已详述。split(separator:)默认过滤空段components(separatedBy:)保留空段。处理CSV、Key-Value配置时务必确认数据里是否可能出现连续分隔符。坑8字符串插值导致的意外转义\(变量)里如果变量本身包含特殊字符插值结果不会帮你转义。比如拼SQL、拼HTML时用户输入的单引号、尖括号直接原样插入会造成注入或页面错乱。这种场景必须显式转义或使用参数化接口。坑9文件路径超过259字符报错Windows相关接口对长路径有历史限制macOS上一般没有这问题。但如果你用URL(fileURLWithPath:)处理从后端拼接来的超长路径部分API会抛异常。这种问题不是Swift能改的要么缩短路径要么分目录存储。坑10OLED屏、字体渲染场景下的字符映射这个词条下的“ASC2码”确实是个硬件领域的概念——在0.96寸OLED屏这类设备上英文字符通常要映射到8x16或6x8的ASCII点阵字模中文字符则要映射到16x16或更大的GBK点阵字模。这里的“字符”不是Unicode字符而是“字模索引”。如果你在嵌入式或硬件显示项目里做字符处理思路和纯软件完全不同——核心是“查表”不是“编码转换”。7. 性能考量大量字符操作时怎么优化字符串拼接在Swift里没有老Java里“号拼接极慢”的历史包袱因为String是值类型不可变拼接时会管理好底层存储。但也不是完全没有性能坑。第一个原则连续拼接用append或不要用中间变量反复连。var result for item in items { result item.name }这种写法Swift做了缓冲区优化比result result item.name好一些但仍有增长复制开销。更好的做法是预估容量var result String() result.reserveCapacity(items.count * 8) // 预估一个大概值第二个原则大批量字符级操作时尽量做单次遍历。比如要同时做“去空白、转小写、替换非法字符”不要分三步循环三遍合并成一次map或reduce更高效let cleaned input .lowercased() .filter { !$0.isWhitespace } .replacingOccurrences(of: -, with: _)第三个原则高频环境的字符串格式化优先用String(format:)而非多次插值。但注意String(format:)在iOS里格式化数字时受用户区域设置影响%.2f可能输出中文全角数字不会但分隔符确实可能变成逗号。如果希望固定用点号要显式指定Locale(identifier: en_US_POSIX)。我实际优化过一个日志模块原本每秒输出几十条日志每条都做日期格式化和字符串拼接用Instruments分析后发现大量时间花在DateFormatter初始化和字符串拼接的内存分配上。优化策略是缓存DateFormatter实例、预估容量、复用Data缓冲区最终耗时降了约60%。8. 真实项目中的字符处理设计规约走到这里我想分享一些从项目里沉淀下来的“字符处理规约”算是给团队新人立的规矩。这些规约不一定适合所有项目但都是“吃了亏之后换来的”值得参考。规约一全项目统一定义“字符长度”口径。业务层默认用Character个数与后端接口对接时用unicodeScalars.count与Objective-C和底层C库对接时用utf16.count。所有涉及长度校验的地方必须注明口径来源防止两个端各算各的。规约二统一封装字符过滤工具。不要在每个页面写各自的filter逻辑。我把常用的清洗封装成一个枚举加扩展enum TextSanitizer { static func phoneNumber(_ raw: String) - String { return raw.filter { $0.isNumber } } static func nickname(_ raw: String) - String { return raw.filter { CharacterSet.alphanumerics.contains($0.unicodeScalars.first!) } } static func removeControlChars(_ raw: String) - String { return raw.unicodeScalars.filter { !CharacterSet.controlCharacters.contains($0) }.map(String.init).joined() } }这样至少保证不同页面处理后的效果是一致的出问题也能在一个地方修补。规约三显式处理非法字符不做“银弹假设”。后端数据、用户输入、文件读取只要来源不可信就必须假设里面可能包含控制字符、非法UTF-8序列、异常长的组合字符。处理流程应该是解码容错 → 清洗 → 截断 → 输出。规约四所有C桥接函数统一走向量不用全局指针。我要求团队内部所有C函数调用字符串参数只在withCString或withUnsafeBufferPointer闭包内使用禁止把指针存到全局或者跨闭包回传。这是并发安全和内存安全的基本底线。这些年下来我处理过的字符相关Bug少说也有几十个从最普通的编码乱码到极端的内存滞留几乎每种都遇到过一次。Swift的字符模型在主流语言里算得上最贴近人直觉的但越是贴近直觉的设计底层隐藏的边界条件和性能特性就越多。希望这篇长文能帮你少走一些我之前走过的弯路。最后再说一句个人体会字符处理没有万能解面对具体问题先把“用户感知长度、码点数量、字节数量、显示宽度”这四个概念分开再看API选择思路就清晰了。
返回列表