从ReadOnlySequence <byte>解析UTF8字符串

时间:2019-07-10 22:34:11

标签: c# .net

如何从ReadOnlySequence解析UTF8字符串

ReadOnlySequence由多个部分组成,并且由于UTF8字符的长度可变,因此这些部分的中断可能在一个字符的中间。 因此,仅在部件上使用Encoding.UTF8.GetString()并将其组合到StringBuilder中是行不通的。

是否可以从ReadOnlySequence中解析UTF8字符串,而无需先将它们组合成一个数组。我宁愿避免在这里分配内存。

4 个答案:

答案 0 :(得分:2)

我们在这里要做的第一件事是测试序列是否实际上是单个跨度;如果是这样,我们可以极大地简化和优化。

一旦我们知道我们有一个多段(不连续)的缓冲区,我们可以采用两种方法:

  1. 将片段线性化为一个连续的缓冲区,可能是从ArrayPool.Shared中租用了一个超大的缓冲区,并在租用缓冲区的正确部分上使用了UTF8.GetString,或者
  2. 在编码上使用GetDecoder() API,并使用它填充新的字符串,这在较旧的框架上意味着覆盖新分配的字符串,或者在较新的框架中意味着使用string.Create API

第一个选项简单得多,但是涉及一些内存复制操作(除了字符串之外,没有其他分配):

public static string GetString(in this ReadOnlySequence<byte> payload,
    Encoding encoding = null)
{
    encoding ??= Encoding.UTF8;
    return payload.IsSingleSegment ? encoding.GetString(payload.FirstSpan)
        : GetStringSlow(payload, encoding);

    static string GetStringSlow(in ReadOnlySequence<byte> payload, Encoding encoding)
    {
        // linearize
        int length = checked((int)payload.Length);
        var oversized = ArrayPool<byte>.Shared.Rent(length);
        try
        {
            payload.CopyTo(oversized);
            return encoding.GetString(oversized, 0, length);
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(oversized);
        }
    }
}

答案 1 :(得分:1)

您可以使用Decoder。可能是这样的:

var decoder = Encoding.UTF8.GetDecoder();
var sb = new StringBuilder();
var processed = 0L;
var total = bytes.Length;
foreach (var i in bytes)
{
    processed += i.Length;
    var isLast = processed == total;
    var span = i.Span;
    var charCount = decoder.GetCharCount(span, isLast);
    Span<char> buffer = stackalloc char[charCount];
    decoder.GetChars(span, buffer, isLast);
    sb.Append(buffer);
}

来自the docs:

  

Decoder.GetChars方法将字节的顺序块转换为字符的顺序块,其方式类似于此类的GetChars方法。但是,解码器维护调用之间的状态信息,因此它可以正确解码跨越块的字节序列。解码器还在数据块的末尾保留尾随字节,并在下一个解码操作中使用尾随字节。因此,GetDecoder和GetEncoder对于网络传输和文件操作很有用,因为这些操作通常处理数据块而不是完整的数据流。

当然StringBuilder会引入一个新的分配源,但是如果有问题,您可以用其他类型的缓冲区替换它。

答案 2 :(得分:1)

.NET 5.0 似乎引入了 EncodingExtensions.GetString 来解决这个问题。

<块引用>

使用指定的编码将指定的 ReadOnlySequence 解码为字符串。

using System.Text;

string message = EncodingExtensions.GetString(Encoding.UTF8, buffer);

答案 3 :(得分:0)

警告:未经测试

我对官方回答做了改进:

  • 打包为扩展方法
  • 通过分配高估的字符数组来消除对StringBuilder的需求
  • 通过使用单个大数组,将GetCharCount找到的总字符相加,并移动目标跨度切片,消除了额外的GetChars步骤的需要
  • 重命名一些变量。 preProcessedBytes对我来说尤其重要,IMO直到导出字符后才对它们进行处理。
  • 使用stringLengthEstimate参数,以便可以将协议使用字符串长度(就字符数而言)作为标头存储在UTF8字节之前的协议

这是源代码:

/// <summary>
/// Parses UTF8 characters in the ReadOnlySequence
/// </summary>
/// <param name="slice">Aligned slice of ReadOnlySequence that contains the UTF8 string bytes. Use slice before calling this function to ensure you have an aligned slice.</param>
/// <param name="stringLengthEstimate">The amount of characters in the final string. You should use a header before the string bytes for the best accuracy. If you are not sure -1 means that the most pessimistic estimate will be used: slice.Length</param>
/// <returns>a string parsed from the bytes in the ReadOnlySequence</returns>
public static string ParseAsUTF8String(this ReadOnlySequence<byte> slice, int stringLengthEstimate = -1)
{
    if (stringLengthEstimate == -1)
        stringLengthEstimate = (int)slice.Length; //overestimate
    var decoder = Encoding.UTF8.GetDecoder();
    var preProcessedBytes = 0;
    var processedCharacters = 0;
    Span<char> characterSpan = stackalloc char[stringLengthEstimate]; 
    foreach (var memory in slice)
    {
        preProcessedBytes += memory.Length;
        var isLast = (preProcessedBytes == slice.Length);
        var emptyCharSlice = characterSpan.Slice(processedCharacters, characterSpan.Length - processedCharacters);
        var charCount = decoder.GetChars(memory.Span, emptyCharSlice, isLast);
        processedCharacters += charCount;
    }
    var finalCharacters = characterSpan.Slice(0, processedCharacters);
    return new string(finalCharacters);
}