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

Постоянный доступ к одним и тем же данным

В приложении на C# довольно часто приходится многократно запрашивать базу данных не только для получения оперативной информации, необходимой для выполнения сценария использования, но и для получения информации, которая меняется нечасто. Мы&rsquo ; рассмотрим последний случай.

Представьте, что у вас есть таблица, в которой хранятся учетные данные для аутентификации в стороннем сервисе. Как правило, прежде чем воспользоваться сервисом, необходимо пройти аутентификацию. В вашем приложении, вероятно, есть процедура получения этих учетных данных из таблицы в базе данных. Это может не показаться проблемой, если процедура выполняется нечасто или всего несколько раз в день. Однако в более крупных случаях, когда вам нужно чаще использовать сторонние службы, это может со временем стать проблемой производительности вашего приложения. Учитывая, что учетные данные - это информация, которая обновляется нечасто, вы можете использовать кэш в памяти приложения&rsquo ;

В последних версиях .NET это можно реализовать довольно просто и быстро. Рассмотрим следующий сценарий.

public async Task<List<AccountSenders>> GetAccountSenders()
{
  return await _context.AccountSenders.ToListAsync();
}

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

public async Task<List<AccountSenders>?> GetAccountSenders(IMemoryCache _cache)
{
    string cacheKey = "AccountSenders";

    if (!_cache.TryGetValue(cacheKey, out List<AccountSenders>? accountSenders))
    {
        accountSenders = await _context.AccountSenders.ToListAsync();

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromMinutes(5));

         _cache.Set(cacheKey, accountSenders, cacheEntryOptions);
    }
    return accountSenders;
}

Вы, наверное, думаете, что больше кода не обязательно приведет к повышению производительности, но давайте заглянем за кулисы…

Здесь на помощь приходит кэширование. Оно представляет собой способ хранения данных в памяти, благодаря которому вам не нужно каждый раз их получать. И хотя поначалу его настройка может показаться немного сложнее, поверьте, в конечном счете это того стоит.

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

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

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

Плохой экземпляр HttpClient - молчаливый враг производительности

Рассматривая решение для связи с внешним сервисом по протоколу HTTP, мы ’почти наверняка’придем к выводу об использовании HttpClient для достижения этой цели. Однако что скрывается за созданием простого экземпляра HttpClient, который может стать молчаливым врагом производительности нашего приложения? Ну, почти все! Каждый раз, когда создается экземпляр HttpClient, открываются новые TCP-соединения, которые потребляют ресурсы и потенциально создают узкие места, особенно при высокой нагрузке.

public async Task<string> GetAsync(string url)
{
  using (var client = new HttpClient())
  {
    var response = await client.GetAsync(url);
    return await response.Content.ReadAsStringAsync();
  }
}

В приведенном выше коде, казалось бы, нет никаких проблем, просто простой экземпляр HttpClient выполняет запрос. Хотя его использование кажется простым, неправильное инстанцирование или неадекватное использование может стать первопричиной множества проблем с производительностью в вашем приложении.

Рекомендуется использовать HttpClientFactory для создания одного общего экземпляра HttpClient.

public async Task<string> GetAsync(string url)
{
  using (var client = _httpClientFactory.CreateClient())
  {
    var response = await client.GetAsync(url);
    return await response.Content.ReadAsStringAsync();
  }
}

С помощью HttpClientFactory вы можете создавать HTTP-клиенты, правильно настроенные и управляемые самим фреймворком. Эти клиенты, созданные HttpClientFactory, хранятся в пуле, поэтому новые клиенты не создаются каждый раз, когда они нужны. Более того, когда клиент больше не используется, он ’возвращается в пул и очищается для повторного использования.

Подводя итог, можно сказать, что неправильное инстанцирование HttpClient или его использование без надлежащей конфигурации может негативно сказаться на производительности вашего приложения. Использование HttpClientFactory и правильная настройка могут значительно увеличить скорость и эффективность ваших HTTP-запросов.

Конкатенация строк

Конкатенация строк в .NET может повлиять на производительность по сравнению с использованием StringBuilder из-за способа работы со строками в памяти.

Когда вы конкатенируете строки с помощью оператора + или String.Concat() метода, .NET создает новый объект строки каждый раз, когда происходит операция конкатенации. Это означает, что выделение памяти происходит часто, особенно в сценариях, где несколько операций конкатенации выполняются в циклах или при масштабных манипуляциях со строками. Каждый новый строковый объект требует выделения и удаления памяти, что может привести к фрагментации памяти и увеличению накладных расходов на сборку мусора.

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

Здесь&rsquo ; приведен пример, иллюстрирующий разницу:

// Using string concatenation
string result = "";
for (int i = 0; i < 10000; i++) {
    result += i.ToString();  // Each concatenation creates a new string object
}

// Using StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.Append(i.ToString());  // StringBuilder appends data to its internal buffer
}
string result = sb.ToString();  // Convert StringBuilder to string when needed

В приведенном выше примере многократное использование конкатенации строк (+=) внутри цикла создает новый объект строки для каждой операции конкатенации, что приводит к снижению производительности и увеличению объема используемой памяти. Однако использование метода StringBuilder's Append() эффективно добавляет данные во внутренний буфер, избегая ненужных выделений памяти и повышая производительность.

Подводя итог, можно сказать, что хотя конкатенация строк может показаться удобной, особенно для небольших операций, крайне важно использовать StringBuilder для больших операций со строками или в сценариях, где производительность имеет решающее значение для оптимизации использования памяти и производительности приложения в .NET.

Исключения и NULL

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

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

Пример:

try {
    // Code that might throw exceptions
} catch (Exception ex) {
    // Exception handling logic
}

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

Неправильное использование возврата Null

Возврат null вместо надлежащей обработки нулевых значений может привести к исключениям нулевой ссылки (NRE), когда к возвращаемому значению обращаются без надлежащей проверки нуля. Это может привести к ошибкам и неожиданному поведению приложения и требует дополнительных проверок на нулевые значения по всей кодовой базе, что может повлиять на производительность.

Пример:

public string GetCustomerName(int customerId) {
    // Some logic to fetch customer name
    if (customerExists) {
        return customerName;
    } else {
        return null; // Incorrect usage if caller does not handle null properly
    }
}

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

Для уменьшения этих проблем с производительностью:

  • Используйте исключения для исключительных условий, а не для обычного потока управления.
  • Минимизируйте использование исключений в критических по производительности разделах кода.
  • Разрешайте ожидаемые условия ошибок с помощью кодов возврата или других механизмов вместо исключений.
  • Убедитесь, что методы, возвращающие null, четко документируют возможность возврата нулевых значений, а вызывающие их лица обрабатывают их соответствующим образом.

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

Заключение

В сфере разработки приложений .NET нередко игнорируют вопросы производительности, полагая, что незначительные неэффективности не окажут существенного влияния на функциональность приложения. Однако такая самоуверенность может привести к постепенному накоплению неоптимальных практик, которые в совокупности со временем снижают производительность приложения. Хотя на начальном этапе приоритет других аспектов разработки может показаться несущественным, пренебрежение соображениями производительности может привести к серьезным последствиям по мере масштабирования приложения или увеличения его использования.

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

В конечном счете, долгосрочный успех и жизнеспособность приложения .NET зависят от коллективного усердия разработчиков в определении приоритетов производительности с самого начала. Применяя проактивный подход к оптимизации производительности и придерживаясь лучших практик, разработчики могут снизить риск падения производительности приложения. Благодаря согласованным усилиям по устранению узких мест в производительности и соблюдению стандартов оптимизации разработчики могут гарантировать, что их приложения .NET будут обеспечивать оптимальную производительность, масштабируемость и удобство работы с ними в условиях меняющихся требований и моделей использования.

Счастливого кодирования!