Эффективная замена строк в потоке

Мне нужно заменить строку в потоковом ответе HTTP. Наивный способ сделать это был бы

using var reader = new StreamReader(input, leaveOpen: true);
var original = await reader.ReadToEndAsync();
var replaced = original.Replace(old, new, StringComparison.InvariantCultureIgnoreCase);
await output.WriteAsync(Encoding.UTF8.GetBytes(replaced));

Это требует очень много ресурсов и памяти, поскольку перед заменой строк необходимо считать полный ответ в память.

Я смотрел на System.IO.Pipelines и PipeReader. Хотя это дает мне эффективный доступ к потоку, оно работает на byte, что делает проблематичным преобразование в char при работе в Utf-8.

Один из методов, который я видел, — использовать ReadLineAsync в потоке чтения, но я не знаю, будет ли поток содержать какие-либо символы новой строки.

Другой метод, который я видел, — использование очереди, но даже он кажется неуклюжим.

Итак, мой вопрос: как лучше всего заменить текст потоком без чтения всего потока в памяти?

почему бы не прочитать его через буфер, например. это: Learn.microsoft.com/en-us/dotnet/api/… возвращаемое int сигнализирует, сколько символов было прочитано

Rand Random 25.03.2024 09:53

Учтите, что вам нужен буфер, который, возможно, в 2 раза превышает длину искомой строки, вы заполняете его, затем выполняете поиск, выдаете половину буфера, перемещаете вторую половину поверх первой и добавляете ко второй половине буфера, снова выполняете поиск. Таким образом, вы просматриваете данные в скользящем окне, которое в какой-то момент будет достаточно большим, чтобы вместить искомую строку, и будет содержать ее, если она будет найдена. Вы можете настроить размер буфера. Другой наивной реализацией было бы чтение и выдача по 1 символу за раз, и как только символ совпадает с началом искомой строки, буферизируйте длину и проверьте.

flackoverstow 25.03.2024 10:27

(Но будьте осторожны и вскоре вернитесь в односимвольный режим; если вы читаете одиночный символ в поисках «привет» в потоке «wellhellhellothere», как только вы найдете первый h, поместите в буфер 5 байтов и проверьте, повторите «привет», но не выводите все 5 за один раз, если это не так, потому что вы пропустите истинное приветствие, если выведете его h как часть 5)

flackoverstow 25.03.2024 11:16

У меня проблемы со скользящей частью. Вы просто копируете символ за символом в правильные позиции?

Lodewijk 25.03.2024 15:30

Какой смысл в «замене», если вы не читаете полный HTTP-ответ? Вы читаете ответ, находите токен и заменяете его. Вы имеете в виду «заменить» или «найти»? Оба имеют большое значение.

BionicCode 25.03.2024 19:47

@BionicCode Мне нужно прочитать полный ответ, чтобы заменить часть текста, но хранить для этого полный ответ в памяти не очень эффективно. Это критический путь приложения, поэтому мне нужно, чтобы оно работало эффективно.

Lodewijk 26.03.2024 08:52

Решение опубликую позже. Я не ожидаю какого-либо существенного улучшения производительности при замене токенов на лету, в отличие от их замены после получения результата. Вы не хотите возиться с размером блоков, которые вы читаете из потока. Ваша идея имеет смысл для бесконечных потоков, таких как потоки сокетов. Но для конечных потоков я не ожидаю каких-либо преимуществ.

BionicCode 26.03.2024 16:06

Я согласен с @BionicCode. Является ли ответ настолько огромным, что вы просто не можете позволить себе сохранить его в памяти на промежуточном этапе перед записью в output? В противном случае любое улучшение производительности будет практически незаметным.

Emperor Eto 27.03.2024 15:52
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
8
240
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Моя мысль:

  1. Прочитайте несколько символов из входного потока и заполните буфер.
  2. Найдите old[0] в буфере:
    • Если не найден, выведите весь буфер, затем перейдите к шагу 1.
    • Если они найдены, сравните оставшиеся символы в old:
      • Если найдена полная последовательность old, сначала выведите символы перед позицией old[0], затем выведите new. Вернитесь к шагу 2.
      • Если достигнут конец буфера, сначала выведите символы до позиции old[0], затем переместите оставшиеся символы в начало буфера. Перейдите к шагу 1.
      • Если символ не совпадает, вернитесь к шагу 2 и начните со следующего.

Не возникнет ли у них по-прежнему проблем с символами UTF-8 и более высокого порядка? Я думаю, им придется декодировать UTF-8 вручную, а также искать последовательность в стиле конечного автомата.

Emperor Eto 27.03.2024 15:48
Ответ принят как подходящий

Если у вас есть бесконечный Stream, например NetworkStream, замена строковых токенов «на лету» имела бы большой смысл. Но поскольку вы обрабатываете ограниченный поток, для которого вам нужен полный контент, такая фильтрация не имеет смысла из-за влияния на производительность.

Мой аргумент заключается в том, что вам придется использовать буферизацию. Размер буфера, конечно, ограничивает количество символов, которые вы можете обработать. Предполагая, что вы используете кольцевой буфер или очередь, вам придется удалить один символ, чтобы добавить новый. Это приводит к множеству недостатков по сравнению с обработкой всего контента.

За и против

Полноценный поиск и замена Буферизованный поиск и замена (в реальном времени) 👍 Один проход по всему диапазону 👎 несколько проходов по одному и тому же диапазону 👍 Расширенный поиск (например, возврат, просмотр вперед и т. д.) 👎 Поскольку символы удаляются в начале буфера, поисковая система теряет контекст. 👍 Поскольку у нас есть полный контекст, мы никогда не пропустим ни одного совпадения. 👎 Поскольку символы отбрасываются в начале буфера, в некоторых случаях искомый токен не помещается в буфер. Конечно, мы можем изменить размер буфера до длины = n * длина_токена. Однако это ограничение влияет на производительность, поскольку оптимальный размер буфера теперь ограничен размером токена. В худшем случае длина токена равна длине потока. В соответствии с алгоритмом поиска нам придется хранить вводимые данные в буфере до тех пор, пока мы не сопоставим все ключевые слова, а затем отбросить совпавшую часть. Если совпадений нет, размер буфера будет равен размеру потока. В сценариях потоковой передачи, например. сетевой сокет, мы даже заранее не знаем размер потока (ожидаем, что он будет бесконечным). В этом случае мы должны определить «случайный» размер буфера и рискнуть потерять совпадения, охватывающие несколько операций чтения буфера. Имеет смысл оценить ожидаемый ввод и ключи поиска для наихудших сценариев и переключиться на полноконтекстный поиск, чтобы избавиться от накладных расходов на управление буфером. Просто хочу подчеркнуть, что эффективность поиска в реальном времени во многом ограничивается вводом и ключами поиска (ожидаемый сценарий). Это не может быть быстрее, чем полноконтекстный поиск. Он потенциально потребляет меньше памяти при оптимальных обстоятельствах. 👍 Не нужно беспокоиться об оптимальном размере буфера для максимальной эффективности. 👎Размер буфера становится тем более важным, чем длиннее исходный контент. Слишком маленькие буферы приводят к слишком большому количеству сдвигов буфера и слишком большому количеству проходов в одном и том же диапазоне. Обратите внимание: ключевыми затратами исходной задачи поиска и замены являются поиск строки и замена/модификация, поэтому имеет смысл сократить количество сравнений до абсолютного минимума. 👎 Потребляет больше памяти. Однако в данном случае это не имеет значения, поскольку мы все равно сохраним полный ответ в памяти (клиента). И если мы используем соответствующую технику текстового поиска, мы избегаем всех дополнительных string выделений, которые происходят во время поиска и замены. 👍 Выделяется только размер буферов. Однако в данном случае это не имеет значения, поскольку мы все равно сохраним полный ответ в памяти (клиента). 👎 Не позволяет фильтровать поток в режиме реального времени (не актуально в данном случае)). Однако возможно гибридное решение, при котором мы будем фильтровать определенные части в режиме реального времени (например, преамбулу). 👍 Позволяет фильтровать поток в режиме реального времени, чтобы мы могли принимать решения на основе контента, например. прервать чтение (в данном случае не имеет значения).

На этом я останавливаюсь, так как поиск и замена наиболее релевантных затрат на производительность лучше подходят для решения с полным содержанием. Для поиска и замены в реальном времени нам, по сути, придется реализовать наш собственный алгоритм, который должен конкурировать с алгоритмами поиска и замены .NET. Нет проблем, но, учитывая затраченные усилия и конечный вариант использования, я бы не стал тратить на это время.

Эффективным решением может быть реализация пользовательского TextReader, расширенного StreamReader, который работает на основе StringBuilder для поиска и замены символов. Хотя StringBuilder предлагает значительное преимущество в производительности по сравнению с поиском и заменой строк, он не позволяет использовать сложные шаблоны поиска, такие как границы слов. Например, границы слов возможны только в том случае, если шаблон явно включает ограничивающие символы.

Например, замена «int» во входных данных «внутренний int» на «pat» дает «отцовский pat». Если мы хотим заменить только «int» на «internal int», нам придется использовать регулярное выражение. Поскольку регулярное выражение работает только с string, нам приходится платить эффективностью.

В следующем примере реализуется StringReplaceStreamReader, который расширяет TextReader и действует как специализированный StreamReader. Для лучшей производительности токены заменяются после прочтения всего потока.
Для краткости он поддерживает только методы ReadToEndAsync, Read и Peak.
Он поддерживает простой поиск, при котором шаблон поиска просто сопоставляется с входными данными (так называемый простой поиск).
Кроме того, он также поддерживает два варианта поиска и замены по регулярным выражениям для более расширенных сценариев поиска и замены.
Первый вариант основан на наборе пар ключ-значение, а второй вариант использует шаблон регулярного выражения, предоставленный вызывающей стороной.

Поскольку простой поиск включает в себя итерацию записей исходного словаря + несколько проходов (по одному для каждой записи), ожидается, что этот режим поиска будет самым медленным алгоритмом, хотя замена с использованием самого StringBuilder на самом деле выполняется быстрее. В этих обстоятельствах ожидается, что поиск и замена Regex будут значительно быстрее, чем простой поиск и замена с использованием StringBuilder, поскольку он может обрабатывать входные данные за один проход.

Поведение поиска StringReplaceStreamReader настраивается через конструктор.

Пример использования


private Dictionary<string, string> replacementTable;

private async Task ReplaceInStream(Stream sourceStream)
{
  this.replacementTable = new Dictionary<string, string>
  {
    { "private", "internal" },
    { "int", "BUTTON" },
    { "Read", "Not-To-Read" }
  };

  // Search and replace using simple search 
  // (slowest).
  await using var streamReader = new StringReplaceStreamReader(sourceStream, Encoding.Default, this.replacementTable, StringComparison.OrdinalIgnoreCase);
  string text = await streamReader.ReadToEndAsync();

  // Search and replace variant #1 using regex with key-value pairs instead of a regular expression pattern
  // for advanced scenarios (fast)
  await using var streamReader2 = new StringReplaceStreamReader(sourceStream, Encoding.Default, this.replacementTable, SearchMode.Regex);
  string text2 = await streamReader2.ReadToEndAsync();

  // Search and replace variant #2 using regex and regular expression pattern 
  // for advanced scenarios (fast).
  // The matchEvaluator callback actually provides the replcement value for each match.

  // Creates the following regular expression:
  // "\bprivate\b|\bint\b|\bRead\b"
  string searchPattern = this.replacementTable.Keys
    .Select(key => $@"\b{key}\b")
    .Aggregate((current, newValue) => $"{current}|{newValue}");

  await using var streamReader3 = new StringReplaceStreamReader(sourceStream, Encoding.Default, searchPattern, Replace);
  string text3 = await streamReader.ReadToEndAsync();
}


private string Replace(Match match) 
  => this.replacementTable.TryGetValue(match.Value, out string replacement) 
    ? replacement 
    : match.Value;

Выполнение

Режим поиска

public enum SearchMode
{
  Default = 0,
  Simple,
  Regex
}

StringReplaceStreamReader.cs

public class StringReplaceStreamReader : TextReader, IDisposable, IAsyncDisposable
{
  public Stream BaseStream { get; }
  public long Length => this.BaseStream.Length;
  public bool EndOfStream => this.BaseStream.Position == this.BaseStream.Length;

  public SearchMode SearchMode { get; }

  private const int DefaultCapacity = 4096;
  private readonly IDictionary<string, string> stringReplaceTable;
  private readonly StringComparison stringComparison;
  private readonly Encoding encoding;
  private readonly Decoder decoder;
  private readonly MatchEvaluator? matchEvaluator;
  private readonly Regex? regularExpression;
  private readonly byte[] byteBuffer;
  private readonly char[] charBuffer;

  public StringReplaceStreamReader(Stream stream, Encoding encoding, IDictionary<string, string> stringReplaceTable)
    : this(stream, encoding, stringReplaceTable, StringComparison.OrdinalIgnoreCase, SearchMode.Simple)
  {
  }

  public StringReplaceStreamReader(Stream stream, Encoding encoding, IDictionary<string, string> stringReplaceTable, SearchMode searchMode)
    : this(stream, encoding, stringReplaceTable, StringComparison.OrdinalIgnoreCase, searchMode)
  {
  }

  public StringReplaceStreamReader(Stream stream, Encoding encoding, IDictionary<string, string> stringReplaceTable, StringComparison stringComparison)
    : this(stream, encoding, stringReplaceTable, stringComparison, SearchMode.Simple)
  {
  }

  public StringReplaceStreamReader(Stream stream, Encoding encoding, IDictionary<string, string> stringReplaceTable, StringComparison stringComparison, SearchMode searchMode)
  {
    ArgumentNullException.ThrowIfNull(stream, nameof(stream));
    ArgumentNullException.ThrowIfNull(stringReplaceTable, nameof(stringReplaceTable));

    this.BaseStream = stream;
    this.encoding = encoding ?? Encoding.Default;
    this.decoder = this.encoding.GetDecoder();
    this.stringReplaceTable = stringReplaceTable;
    this.stringComparison = stringComparison;
    this.SearchMode = searchMode;
    this.regularExpression = null;
    this.matchEvaluator = ReplaceMatch;

    if (searchMode is SearchMode.Regex)
    {
      RegexOptions regexOptions = CreateDefaultRegexOptions(stringComparison);

      var searchPatternBuilder = new StringBuilder();
      foreach (KeyValuePair<string, string> entry in stringReplaceTable)
      {
        // Creates the following regular expression:
        // "\b[search_key]\b|\b[search_key]\b"
        string pattern = @$"\b{entry.Key}\b";
        searchPatternBuilder.Append(pattern);
        searchPatternBuilder.Append('|');
      }

      string searchPattern = searchPatternBuilder.ToString().TrimEnd('|');
      this.regularExpression = new Regex(searchPattern, regexOptions);
    }

    this.byteBuffer = new byte[StringReplaceStreamReader.DefaultCapacity];
    int charBufferSize = this.encoding.GetMaxCharCount(this.byteBuffer.Length);
    this.charBuffer = new char[charBufferSize];
  }

  public StringReplaceStreamReader(Stream stream, Encoding encoding, string searchAndReplacePattern, MatchEvaluator matchEvaluator)
    : this(stream, encoding, searchAndReplacePattern, matchEvaluator, RegexOptions.None)
  {
  }

  public StringReplaceStreamReader(Stream stream, Encoding encoding, string searchAndReplacePattern, MatchEvaluator matchEvaluator, RegexOptions regexOptions)
  {
    ArgumentNullException.ThrowIfNull(stream, nameof(stream));
    ArgumentNullException.ThrowIfNullOrWhiteSpace(searchAndReplacePattern, nameof(searchAndReplacePattern));
    ArgumentNullException.ThrowIfNull(matchEvaluator, nameof(matchEvaluator));

    this.BaseStream = stream;
    this.encoding = encoding ?? Encoding.Default;
    this.decoder = this.encoding.GetDecoder();
    this.matchEvaluator = matchEvaluator;
    this.SearchMode = SearchMode.Regex;
    this.stringReplaceTable = new Dictionary<string, string>();
    this.stringComparison = StringComparison.OrdinalIgnoreCase;

    if (regexOptions is RegexOptions.None)
    {
      regexOptions = CreateDefaultRegexOptions(stringComparison);
    }
    else if ((regexOptions & RegexOptions.Compiled) == 0)
    {
      regexOptions |= RegexOptions.Compiled;
    }

    this.regularExpression = new Regex(searchAndReplacePattern, regexOptions);
    this.byteBuffer = new byte[StringReplaceStreamReader.DefaultCapacity];
    int charBufferSize = this.encoding.GetMaxCharCount(this.byteBuffer.Length);
    this.charBuffer = new char[charBufferSize];
  }

  public override int Peek()
  {
    int value = Read();
    this.BaseStream.Seek(this.BaseStream.Position - 1, SeekOrigin.Begin);

    return value;
  }

  public override int Read()
    => this.BaseStream.ReadByte();

  public override Task<string> ReadToEndAsync()
    => ReadToEndAsync(CancellationToken.None);

  public override async Task<string> ReadToEndAsync(CancellationToken cancellationToken)
  {
    if (!this.BaseStream.CanRead)
    {
      throw new InvalidOperationException("Source stream is not readable.");
    }

    var textBuilder = new StringBuilder(StringReplaceStreamReader.DefaultCapacity);

    while (!this.EndOfStream)
    {
      cancellationToken.ThrowIfCancellationRequested();

      int bytesRead = await this.BaseStream.ReadAsync(buffer, 0, buffer.Length);
      bool flush = this.EndOfStream;
      int charsRead = this.decoder.GetChars(this.byteBuffer, 0, bytesRead, this.charBuffer, 0, flush);
      textBuilder.Append(charBuffer, 0, charsRead);
    }

    cancellationToken.ThrowIfCancellationRequested();
    SearchAndReplace(textBuilder, cancellationToken, out string result);

    return result;
  }

  public ValueTask DisposeAsync()
    => ((IAsyncDisposable)this.BaseStream).DisposeAsync();

  private void SearchAndReplace(StringBuilder textBuilder, CancellationToken cancellationToken, out string result)
  {
    cancellationToken.ThrowIfCancellationRequested();

    if (this.SearchMode is SearchMode.Simple or SearchMode.Default)
    {
      foreach (KeyValuePair<string, string> entry in this.stringReplaceTable)
      {
        cancellationToken.ThrowIfCancellationRequested();

        textBuilder.Replace(entry.Key, entry.Value);
      }

      result = textBuilder.ToString();
    }
    else if (this.SearchMode is SearchMode.Regex)
    {
      string input = textBuilder.ToString();
      result = this.regularExpression!.Replace(input, this.matchEvaluator!);
    }
    else
    {
      throw new NotImplementedException($"Search mode {this.SearchMode} is not implemented.");
    }
  }

  private string ReplaceMatch(Match match)
    => this.stringReplaceTable.TryGetValue(match.Value, out string replacement)
      ? replacement
      : match.Value;

  private RegexOptions CreateDefaultRegexOptions(StringComparison stringComparison)
  {
    RegexOptions regexOptions = RegexOptions.Multiline | RegexOptions.Compiled;
    if (stringComparison is StringComparison.CurrentCultureIgnoreCase or StringComparison.InvariantCultureIgnoreCase or StringComparison.OrdinalIgnoreCase)
    {
      regexOptions |= RegexOptions.IgnoreCase;
    }

    return regexOptions;
  }
}

Нет ли по-прежнему проблемы с UTF8, если конец буфера находится в середине многобайтового символа?

Emperor Eto 28.03.2024 00:53

Да, он неправильно обрабатывал многобайтовые символы. Сейчас это исправлено и улучшено. Спасибо за подсказку.

BionicCode 28.03.2024 02:59

Придирка: декодер никогда не назначается, но я думаю, что это так encoding.GetDecoder()

Lodewijk 28.03.2024 09:10

Спасибо за ваш подробный ответ. Почему бы вам просто не использовать StreamReader, чтобы выполнить декодирование за вас? Мне также нужно, чтобы поиск был нечувствителен к регистру, поэтому мне нужно использовать опцию медленного регулярного выражения. Я проверю это и сравню с тем, что я приготовил.

Lodewijk 28.03.2024 09:23

@Lodewijk Да, действительно, я пропустил его вставку. Теперь поле правильно инициализировано. Я не использовал StreamReader внутри, потому что изначально мне нужно было больше контроля над моментом завершения байтовых данных для создания string. Я также пытался найти решение для оперативного поиска и замены. Но затем я изменил алгоритм, отказался от поиска и замены «на лету» из-за плохой эффективности для вашего сценария.

BionicCode 28.03.2024 10:32

@Lodewijk Если вы хотите удалить функцию поиска и замены по словарю (простой поиск без регулярных выражений), я бы расширил StreamReader, а не обернул его. Затем переопределите члены ReadToEndAsync и ReadAsync. Вызовите базовую реализацию и обработайте результат. Но если вы хотите сохранить эту функцию, StreamReader — не лучший вариант, поскольку API возвращает только string.

BionicCode 28.03.2024 10:32

@Lodewijk Это связано с тем, что мы должны использовать StringBuilder для повышения эффективности поиска и замены (в частности, замена, поскольку string является неизменяемой, а замена символов экземпляра string означает, что мы всегда создаем новый экземпляр), мы не можем использовать StreamReader. Вместо этого нам придется реализовать обработку потока самостоятельно, чтобы иметь возможность работать исключительно с StringBuilder. Это значительно повышает производительность, по крайней мере, в отношении распределения памяти и, следовательно, скорости.

BionicCode 28.03.2024 10:32

@Lodewijk Мне придется пересмотреть свой ответ, если ваш ключевой вывод относительно ожидаемой производительности заключается в том, что Regex медленнее. Я пытался подчеркнуть, что обычно Regex будет намного медленнее, чем простой поиск и замена с использованием StringBuilder (для не слишком сложных шаблонов), но из-за итерации записей словаря и повторного анализа StringBuilder входной записи foreach, Regex должно быть значительно быстрее. Я был бы рад, если бы вы подтвердили то заявление, которое я сделал по результатам профилирования. В конце концов, вариант с регулярным выражением работает довольно быстро.

BionicCode 28.03.2024 10:33

@BionicCode да, я удивлен тем, насколько быстро работает regex.replace. Но поскольку для его использования вам нужно сбросить построитель строк в строку, я не думаю, что в моем случае это понадобится.

Lodewijk 28.03.2024 10:43

@Lodewijk Вы имеете в виду, что не хотите использовать регулярное выражение или поиск и замену StringBuilder? Я думаю, что в этом специальном сценарии регулярное выражение работает быстрее, за исключением того, что вы хотите выполнить поиск по одному ключевому слову. Тогда StringBuilder должен работать быстрее.

BionicCode 28.03.2024 11:06

Итак, я сравнил это. String.Replace работает быстрее, если чувствителен к регистру, но сильно замедляется при использовании IgnoreCase. Regex.Replace работает почти так же быстро, но выделяет больше памяти в куче. Потоковая передача через скользящий буфер и замена по ходу работы медленнее, чем Regex, но быстрее, чем String.Replace, а выделение памяти намного меньше. Поэтому мне нужно принять решение между памятью и скоростью :D

Lodewijk 29.03.2024 08:31

@Lodewijk Интересно. Вы также тестировали StringBuilder.Replace? Когда вы работаете с потоками, у вас есть возможность работать с массивами символов, чтобы полностью избежать выделения строк. StringBuilder должен играть важную роль, поскольку он представляет собой высокооптимизированный тип, с которым вам придется конкурировать при реализации поиска и замены строк вручную. Поиск в буфере сильно зависит от деталей реализации. Я очень сомневаюсь, что эта версия быстрее, чем строка. Замените на полном вводе, как вы сказали. Для меня это не имеет смысла.

BionicCode 29.03.2024 11:14

@Lodewijk Что касается принятия решений: на мой взгляд, оба фактора стоимости, память и скорость, в целом не имеют значения. Если вы не работаете на платформе с небольшим объемом памяти (например, микроконтроллер) или в контексте обработки данных в реальном времени, все, что имеет значение, — это пользовательский опыт (UX). При использовании приложения пользователь никогда не заботится о потреблении памяти (кроме оперативной памяти). Но пользователь очень не любит ждать.

BionicCode 29.03.2024 11:14

@Lodewijk Это делает время/скорость важнейшим фактором с точки зрения UX, связанного с производительностью. Таким образом, накопление затрат на скорость является более важным и всегда более актуальным, поскольку в 21 веке память дешева и ее много. Я всегда предпочитал скорость памяти.

BionicCode 29.03.2024 11:14

@Lodewijk Сложность ключа поиска также очень интересна. Например, заменить все проявления «мира» на «гармонию», но, например, если не составлять такие слова, как «мирный», то `string.Replace' становится сложным для эффективного использования. Проект теста должен охватывать такие сценарии, за исключением случаев, когда они не имеют отношения к вашему варианту использования.

BionicCode 29.03.2024 11:25

Другие вопросы по теме