Есть ли что-то вроде Buffer .LastPositionOf? Найти последнее вхождение символа в буфер?

#c# #.net-core

#c# #.net-ядро

Вопрос:

У меня есть буфер типа ReadOnlySequence<byte> . Я хочу извлечь из него подпоследовательность (которая будет содержать 0 — n сообщений), зная, что каждое сообщение заканчивается 0x1c, 0x0d (как описано здесь ).

Я знаю, что у буфера есть метод расширения PositionOf, но он

Возвращает позицию первого вхождения item в ReadOnlySequence<T> .

и я ищу метод, который возвращает мне позицию последнего вхождения. Я попытался реализовать это самостоятельно, это то, что у меня есть до сих пор

 private SequencePosition? GetLastPosition(ReadOnlySequence<byte> buffer)
{
    // Do not modify the real buffer
    ReadOnlySequence<byte> temporaryBuffer = buffer;
    SequencePosition? lastPosition = null;

    do
    {
        /*
            Find the first occurence of the delimiters in the buffer
            This only takes a byte, what to do with the delimiters? { 0x1c, 0x0d }

        */
        SequencePosition? foundPosition = temporaryBuffer.PositionOf(???);

        // Is there still an occurence?
        if (foundPosition != null)
        {
            lastPosition = foundPosition;

            // cut off the sequence for the next run
            temporaryBuffer = temporaryBuffer.Slice(0, lastPosition.Value);
        }
        else
        {
            // this is required because otherwise this loop is infinite if lastPosition was set once
            break;
        }
    } while (lastPosition != null);

    return lastPosition;
}
  

Я борюсь с этим. Прежде всего PositionOf , метод принимает только a byte , но есть два разделителя, поэтому я должен передать a byte[] . Затем я думаю, что смогу оптимизировать цикл «как-нибудь».

Есть ли у вас какие-либо идеи, как найти последнее вхождение этих разделителей?

Комментарии:

1. Если вы хотите извлечь до n сообщений, каждое из которых заканчивается на этом разделителе, не означает ли это, что вы могли бы искать первое вхождение этого разделителя, извлекать сообщение до этой точки и повторять, а не искать последнее вхождение?

2. @JohnH Я думаю, что OP уже знает, как это сделать, но, как упоминалось в вопросе, но я думаю, что он ищет метод, который делает это под капотом.

3. И я думаю, что он не знает, как справиться, { 0x1c, 0x0d } когда PositionOf требуется только byte

4. Я немного обновил свой ответ, Олаф. Это может иметь или не иметь отношения к вам.

5. спасибо, я попробую 🙂

Ответ №1:

Я спустился в гигантскую кроличью нору, копаясь в этом, но мне удалось придумать метод расширения, который, я думаю, отвечает на ваш вопрос:

 using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;

public static class ReadOnlySequenceExtensions
{
    public static SequencePosition? LastPositionOf(
        this ReadOnlySequence<byte> source,
        byte[] delimiter)
    {
        if (delimiter == null)
        {
            throw new ArgumentNullException(nameof(delimiter));
        }
        if (!delimiter.Any())
        {
            throw new ArgumentException($"{nameof(delimiter)} is empty", nameof(delimiter));
        }

        var reader = new SequenceReader<byte>(source);
        var delimiterToFind = new ReadOnlySpan<byte>(delimiter);

        var delimiterFound = false;
        // Keep reading until we've consumed all delimiters
        while (reader.TryReadTo(out _, delimiterToFind, true))
        {
            delimiterFound = true;
        }

        if (!delimiterFound)
        {
            return null;
        }

        // If we got this far, we've consumed bytes up to,
        // and including, the last byte of the delimiter,
        // so we can use that to get the position of 
        // the starting byte of the delimiter
        return reader.Sequence.GetPosition(reader.Consumed - delimiter.Length);
    }
}
  

Вот также несколько тестовых примеров:

 var cases = new List<byte[]>
{
    // Case 1: Check an empty array
    new byte[0],
    // Case 2: Check an array with no delimiter
    new byte[] { 0xf },
    // Case 3: Check an array with part of the delimiter
    new byte[] { 0x1c },
    // Case 4: Check an array with the other part of the delimiter
    new byte[] { 0x0d },
    // Case 5: Check an array with the delimiter in the wrong order
    new byte[] { 0x0d, 0x1c },
    // Case 6: Check an array with a correct delimiter
    new byte[] { 0x1c, 0x0d },
    // Case 7: Check an array with a byte followed by a correct delimiter
    new byte[] { 0x1, 0x1c, 0x0d },
    // Case 8: Check an array with multiple correct delimiters
    new byte[] { 0x1, 0x1c, 0x0d, 0x2, 0x1c, 0x0d },
    // Case 9: Check an array with multiple correct delimiters
    // where the delimiter isn't the last byte
    new byte[] { 0x1, 0x1c, 0x0d, 0x2, 0x1c, 0x0d, 0x3 },
    // Case 10: Check an array with multiple sequential bytes of a delimiter
    new byte[] { 0x1, 0x1c, 0x0d, 0x2, 0x1c, 0x1c, 0x0d, 0x3 },
};

var delimiter = new byte[] { 0x1c, 0x0d };
foreach (var item in cases)
{
    var source = new ReadOnlySequence<byte>(item);
    var result = source.LastPositionOf(delimiter);
} // Put a breakpoint here and examine result
  

1 5 Все случаи корректно возвращаются null . 6 10 Все случаи корректно возвращают SequencePosition значение первому байту в разделителе (т. Е. В данном случае 0x1c ).

Я также попытался создать итеративную версию, которая выдавала бы позицию после нахождения разделителя, например:

 while (reader.TryReadTo(out _, delimiterToFind, true))
{
    yield return reader.Sequence.GetPosition(reader.Consumed - delimiter.Length);
}
  

Но SequenceReader<T> и ReadOnlySpan<T> не может использоваться в блоках итератора, поэтому я придумал AllPositionsOf вместо:

 public static IEnumerable<SequencePosition> AllPositionsOf(
    this ReadOnlySequence<byte> source,
    byte[] delimiter)
{
    if (delimiter == null)
    {
        throw new ArgumentNullException(nameof(delimiter));
    }
    if (!delimiter.Any())
    {
        throw new ArgumentException($"{nameof(delimiter)} is empty", nameof(delimiter));
    }

    var reader = new SequenceReader<byte>(source);
    var delimiterToFind = new ReadOnlySpan<byte>(delimiter);

    var results = new List<SequencePosition>();
    while (reader.TryReadTo(out _, delimiterToFind, true))
    {
        results.Add(reader.Sequence.GetPosition(reader.Consumed - delimiter.Length));
    }

    return results;
}
  

Тестовые примеры для этого тоже работают правильно.

Обновить

Теперь, когда я немного поспал и получил возможность подумать о вещах, я думаю, что вышесказанное можно улучшить по нескольким причинам:

  1. SequenceReader<T> имеет Rewind() метод, который заставляет меня думать SequenceReader<T> , предназначен для повторного использования
  2. SequenceReader<T> похоже, это сделано для упрощения работы с ReadOnlySequence<T> s в целом
  3. Создание метода расширения ReadOnlySequence<T> для использования a SequenceReader<T> для чтения из a ReadOnlySequence<T> кажется обратным

Учитывая вышесказанное, я думаю, что, вероятно, имеет смысл стараться избегать прямой работы с ReadOnlySequence<T> s, где это возможно, предпочитая и повторно SequenceReader<T> используя вместо этого. Итак, имея это в виду, вот другая версия LastPositionOf , которая теперь является методом расширения на SequenceReader<T> :

 public static class SequenceReaderExtensions
{
    /// <summary>
    /// Finds the last occurrence of a delimiter in a given sequence.
    /// </summary>
    /// <param name="reader">The reader to read from.</param>
    /// <param name="delimiter">The delimeter to look for.</param>
    /// <param name="rewind">If true, rewinds the reader to its position prior to this method being called.</param>
    /// <returns>A SequencePosition if a delimiter is found, otherwise null.</returns>
    public static SequencePosition? LastPositionOf(
        this ref SequenceReader<byte> reader,
        byte[] delimiter,
        bool rewind)
    {
        if (delimiter == null)
        {
            throw new ArgumentNullException(nameof(delimiter));
        }
        if (!delimiter.Any())
        {
            throw new ArgumentException($"{nameof(delimiter)} is empty", nameof(delimiter));
        }

        var delimiterToFind = new ReadOnlySpan<byte>(delimiter);
        var consumed = reader.Consumed;

        var delimiterFound = false;
        // Keep reading until we've consumed all delimiters
        while (reader.TryReadTo(out _, delimiterToFind, true))
        {
            delimiterFound = true;
        }

        if (!delimiterFound)
        {
            if (rewind)
            {
                reader.Rewind(reader.Consumed - consumed);
            }

            return null;
        }

        // If we got this far, we've consumed bytes up to,
        // and including, the last byte of the delimiter,
        // so we can use that to get the starting byte
        // of the delimiter
        var result = reader.Sequence.GetPosition(reader.Consumed - delimiter.Length);
        if (rewind)
        {
            reader.Rewind(reader.Consumed - consumed);
        }

        return resu<
    }
}
  

Приведенные выше тестовые примеры продолжают использоваться для этого, но теперь мы можем повторно использовать то же reader самое. Кроме того, это позволяет вам указать, хотите ли вы вернуться к исходной позиции reader перед вызовом.

Комментарии:

1. итак, вместо того, чтобы использовать ваш первый подход для последовательности, я мог бы теперь сделать что-то вроде SequenceReader<byte> sequenceReader = new SequenceReader<byte>(buffer); SequencePosition? lastPosition = sequenceReader.LastPositionOf(delimiters, false); права? По крайней мере, мой тест пройден 🙂