第 8 篇:翻译 NSData——自带 Base64、类型别名与重载冲突
本系列第 8 篇。上一篇翻完了数值类型
NSNumber。这一篇轮到二进制数据NSData——它本身只有 220 行,但因为需要 Base64 编解码,不得不先把上游那个 2000+ 行的Base64.java也处理掉。翻译过程中又撞上仓颉的类型别名、构造函数重载、StringBuilder缺失重载等一系列「看着不起眼但编译就是不过」的问题。
一、NSData 是什么
NSData 是字节缓冲的包装类,对齐 Apple Foundation 的同名概念。plist 里的 <data> 标签就是它——内容以 Base64 编码存储。上游 NSData.java 提供:
- 三种构造:字节数组、Base64 字符串、文件路径
bytes()/length()基础访问getBase64EncodedData()编码回 Base64equals/hashCode/clone/compareTo全套toXML/toASCII/toASCIIGnuStep输出
整体 220 行,不算复杂。但它依赖 Base64 编解码——上游自带了一个 2125 行的 Base64.java。
二、Base64:自己写还是用标准库?
仓颉有 stdx.encoding.base64,但需要额外安装 stdx 扩展包,当前环境没装(CANGJIE_STDX_PATH 未设置)。考虑到共建要求「拿到就能编」,我决定自行实现一个最简 Base64,只保留 NSData 需要的编码/解码子集。
实现要点
- 编码
encodeBytes(source: Array<Byte>): String:经典的 3 字节 → 4 字符查表法,不足 3 字节时用=填充。 - 解码
decode(s: String): Array<Byte>:建一张 128 项的 DECODABET 查找表,把 ASCII 码映射回 6 位值。空白符标记 -5(跳过),=填充符标记 -2(当 0 处理,最后减去填充字节数),非法字符标记 -1(抛异常)。
💡 如果后续 stdx 可用,可以把
Base64.cj替换成stdx.encoding.base64的封装,公开接口不变。
三、连环踩坑实录
坑 1:String[index] 返回的是 Byte,不是 Rune
Java 的 str.charAt(i) 返回字符。仓颉的 String 是 UTF-8 字节序列,str[i] 返回的是第 i 个字节(Byte),不是第 i 个字符。
要对字符操作,必须用 .toRuneArray() 转成 Array<Rune> 再遍历。这在 Base64 编码时特别容易踩坑:
// 错误:ALPHABET 是 String,ALPHABET[i] 返回 Byte
out.append(ALPHABET[index]) // StringBuilder 没有 append(Byte)!
// 正确:先转 Rune 数组
private static let ALPHABET_RUNES: Array<Rune> = ALPHABET.toRuneArray()
out.append(ALPHABET_RUNES[index]) // Rune 有 append 重载 ✓坑 2:Byte(expr) 不能当构造函数用
Java 里 (byte) value 是类型转换。仓颉里 Byte 是 UInt8 的类型别名,但 Byte(intExpr) 会报「no matching function for operator '()'」。正确写法是用 UInt8(expr):
// 错误
out[pos] = Byte((combined >> 16) & 255)
// 正确
out[pos] = UInt8((combined >> 16) & 255)坑 3:Rune 不是数值类型
想拿字符的 ASCII 码值参与计算,Java 里 int code = c 就行。仓颉的 Rune 不是数值类型,不能直接 Int64(rune)——编译器会报「the expression for numeric type conversion must have a numeric type」。必须先转 UInt32:
// 错误
let code = Int64(c) // c 是 Rune
// 正确
let code = Int64(UInt32(c))坑 4:字段名和方法名不能相同
上游有字段 bytes 和方法 bytes()。仓颉不允许同名——编译报「redefinition of declaration 'bytes'」。我把字段改名为 data,方法保持 bytes() 不变(对外接口一致)。
坑 5:两个 init(String) 重载冲突
上游有 NSData(String base64) 和 NSData(String path) 两个构造。仓颉的参数名不算签名的一部分,两个 init(String) 就是重载冲突。解决方案:文件构造改为静态工厂方法 NSData.fromFile(path):
public static func fromFile(path: String): NSData {
NSData(File.readFrom(path))
}坑 6:File 在 std.fs,不是 std.io
看名字直觉 import std.io.*,但仓颉的 File 类在 std.fs 包里。导错包直接报「undeclared identifier 'File'」。
坑 7:Base64 填充符 = 的处理
一开始把 = 在 DECODABET 里标记为 -1(和非法字符一样),结果解码 "SGVsbG8=" 时直接抛异常。正确做法是给 = 一个独立标记 -2,解码时当 0 参与位运算,最后从输出长度里减去填充字节数:
// 截断到实际写入长度(减去填充字节数)
outPosn = outPosn - paddingCount四、验证:13 个测试,30/30 全绿
针对字节构造、Base64 构造(含去空白)、编解码往返、空数据、比较、克隆、相等、XML 输出、类名、toString、非法 Base64 抛异常都写了用例:
[ PASSED ] testBytesConstructor
[ PASSED ] testBase64Constructor
[ PASSED ] testBase64ConstructorWithWhitespace
[ PASSED ] testBase64RoundTrip
[ PASSED ] testGetBase64EncodedData
[ PASSED ] testEmptyData
[ PASSED ] testCompare
[ PASSED ] testClone
[ PASSED ] testEquals
[ PASSED ] testXmlOutput
[ PASSED ] testClassName
[ PASSED ] testToString
[ PASSED ] testInvalidBase64Throws
Summary: TOTAL: 30, PASSED: 30, FAILED: 0五、这一步的收获
String[index]返回 Byte:仓颉字符串是 UTF-8 字节序列,按字符操作要用toRuneArray()。StringBuilder没有append(Byte/UInt8):需要先把字节转成 Rune 或 String 再 append。Byte(expr)不行,用UInt8(expr):类型别名在某些场景不能直接当构造函数用。Rune不是数值类型:要取码点参与运算,必须UInt32(rune)中转。- 字段名 ≠ 方法名:仓颉不允许同名,对外接口优先,内部字段改名。
- 参数名不算重载签名:
init(base64: String)和init(path: String)冲突,用静态工厂方法解决。 - Base64 填充符要单独处理:不能和非法字符用同一个标记值。
下一篇(待更新):翻译日期类型 NSDate——看看仓颉怎么处理时间戳和日期格式化。