【问题标题】:In Swift, how can I find the byte length of a UTF8 string within a larger buffer在 Swift 中,如何在更大的缓冲区中找到 UTF8 字符串的字节长度
【发布时间】:2021-12-29 02:25:25
【问题描述】:

我有一个带有编码的 UTF8 字符串和其他类型的序列化值的 Data 对象。这个问题的第一个版本假设 UTF8 有一个内置的字符串终止,但它没有。

一大块 UTF8 字符与一大块 ascii 字节具有相同的问题。字符串的长度必须通过在某处显式存储长度或使用终止符(如 NUL/0)来处理。

如果使用终止符,则必须限制字符串内容,使其不包含终止符值。这将使您的代码不适合编码所有合法的 Swift 字符串,但根据应用程序,这可能没问题。

【问题讨论】:

  • 您可以简单地获取str.utf8.count 属性
  • 所以我猜,rawData 可以是someContenNULLsomeOtherContentNULL?并且您想从中解码 2 刺,解码然后从缓冲区中删除 someContentNULL 并继续?那么,在rawData 中找到NULL 呢?
  • Swift 字符串可以包含NUL 字节——这里的原始方法是不够的,因为你只会得到一个带有NUL 字节的字符串。
  • 这能回答你的问题吗? Calculate the size in bytes of a Swift String

标签: swift string utf-8 character-encoding


【解决方案1】:

Swift 字符串可以包含NUL 字节(例如"Hello\u{0000}world!" 是有效的String),因此假设您的字符串以NUL 字节结尾,那么您的方法都不够。

相反,您可能希望采用@Larme 作为评论发布的方法:首先拆分数据,然后从这些切片创建字符串。

如果你的分隔符确实是一个NUL 字节,这可以很简单

import Foundation

func decode(_ data: Data, separator: UInt8) -> [String] {
    data.split(separator: separator).map { String(decoding: $0, as: UTF8.self) }
}

let data = Data("Hello, world!\u{00}Following string.\u{00}And another one!".utf8)

print(decode(data, separator: 0x00))
// => ["Hello, world!", "Following string.", "And another one!"]

这里的split(separator:) 方法是Sequence.split(separator:maxSplits:omittingEmptySubsequences:),它采用单个Sequence.Element 的分隔符——在本例中为单个UInt8omittingEmptySubsequences 默认为 true,所以

  • 如果空字符串是有效输入并且您需要处理它们,请确保传入false。否则,
  • 如果您的分隔符是 N NUL 连续字节,则此方法仍然适用:您将得到 N - 1 空拆分,所有拆分都将被丢弃

或者,如果您不想急切地预先分割整个缓冲区(例如,您可能正在寻找一个指示停止处理的标记值),您可以通过循环遍历缓冲区来分段分割缓冲区,然后使用Data.prefix(while:)获取由分隔符终止的前缀:

import Foundation

func process(_ data: Data, separator: UInt8, using action: (String) -> Bool) {
    var slice = data[...]
    while !slice.isEmpty {
        let substring = String(decoding: slice.prefix(while: { $0 != separator }), as: UTF8.self)
        if !action(substring) {
            break
        }
        
        slice = slice.dropFirst(substring.utf8.count + 1)
    }
}

let data = Data("Hello, world!\u{00}Following string.\u{00}And another one!".utf8)
process(data, separator: 0x00) { string in
    print(string)
    return true // continue
}

如果您的分隔符更复杂(例如,多个不同的字符长),您仍然可以使用Data 方法来查找您的分隔符序列的实例并自己拆分它们:

import Foundation

func decode(_ data: Data, separator: String) -> [String] {
    // `firstRange(of:)` below takes a type conforming to `DataProtocol`.
    // `String.UTF8View` doesn't conform, but `Array` does. This copy should
    // be cheap if the separator is small.
    let separatorBytes = Array(separator.utf8)
    var strings = [String]()
    
    // Slicing the data will give cheap no-copy views into it.
    // This first slice is the full data blob.
    var slice = data[...]

    // As long as there's an instance of `separator` in the  data...
    while let separatorRange = slice.firstRange(of: separatorBytes) {
        // ... pull out all of the bytes before it into a String...
        strings.append(String(decoding: slice[..<separatorRange.lowerBound], as: UTF8.self))

        // ... and skip past the separator to keep looking for more.
        slice = slice[separatorRange.upperBound...]
    }
    
    // If there are no separators, in the string, or the last string is not
    // terminated with a separator itself, pull out the remaining contents.
    if !slice.isEmpty {
        strings.append(String(decoding: slice, as: UTF8.self))
    }
    
    return strings
}

let separator = "\u{00}\u{20}\u{00}"
let data = Data("Hello, world!\(separator)Following string.\(separator)And another one!".utf8)
print(decode(data, separator: separator))
// => ["Hello, world!", "Following string.", "And another one!"]

【讨论】:

  • 我认为将 omittingEmptySubsequences 设置为 false 很重要。有空字符串似乎很常见,在我的应用程序中我绝对需要它。所以缓冲区开头的零字节必须被解析为空字符串。
  • @ChrisQuenelle 好点,这是一个完全有效的用例。更新了答案以反映这一点
【解决方案2】:

@Itai 的回答很棒,有很多额外的细节,但我想创建一个简短的摘要。

这是我最终得到的代码:

let buffer: Data = ...
let pos: Int = ...
let separator = UInt8(0x00)
let s = buffer[pos...].split(
    separator: separator,
    maxSplits: 1,
    omittingEmptySubsequences: false).map { 
        data in
        String(decoding: data, as: UTF8.self)
    }[0]
pos += s.utf8.count + 1

注意:如果缓冲区数据在当前位置有一个零,请务必使用 omittingEmptySubsequences 选项以返回空字符串。

注意:小心使用 Data.suffix。它总是创建一个相对于原始后备存储的新数据对象。例如:

let data: Data = ...
let d1 = data.suffix(from: 10)
let d2 = d1.suffix(from: 10)
// d1 and d2 will have the same data.

这就是为什么我选择使用保持整数位置变量的方法。

【讨论】:

  • 如果您希望以这种方式拆分缓冲区,您也可以将split(separator:maxSplits:omittingEmptySubsequences:) 替换为Data.prefix(while:),从Sequence.prefix(while:) 精炼:buffer[pos...].prefix(while: { $0 != separator }) 将为您提供字节范围,直到分隔符,不需要像数组一样对待结果
  • 在性能方面,我希望 Data.prefix(while:) 可以与 Data.split(maxSplits:1) 相媲美。我认为任何一个都不会导致复制字节。但我同意在可读性方面,使用 Data.prefix 稍微好一点。
【解决方案3】:

试试这个解决方案

str.utf8.count

【讨论】:

  • 这只是几个小时前@Leo Dabus 评论的副本。
  • 不工作
  • @GopiDonga 如果没有 OP 的验证,你怎么知道?请edit 解释以改进您的答案。
  • 这是我倾向于的方法。这就是我最初问题中解决方案#1的意思。我不知道 String 实现是否会重新计算 UTF8 编码以计算字节数。将 UTF8 转换为 String 时,记住计数似乎更有效。
  • @ChrisQuenelle UTF8View.count 如果字符串已经由 UTF-8 支持(O(1) 访问),则具有快速路径,否则,每次都必须转换为 UTF-8计算。在您的情况下,字符串保证为 UTF-8,但如果不是这种情况(isContiguousUTF8false),您需要多次使用该值,并且您的字符串很长,您可能想要缓存结果。
猜你喜欢
  • 2018-11-24
  • 2023-03-16
  • 1970-01-01
  • 1970-01-01
  • 2013-12-22
  • 1970-01-01
  • 2014-12-31
  • 2022-01-20
相关资源
最近更新 更多