5 советов для C#-программистов, которые вы, наверняка, уже знаете +6


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

Примечание для всех читателей

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

1. Не считывайте Http-ответ во временную строку

Недавно я наткнулся на твит Дэвида Фаулера в твиттере, в котором он критиковал считывание Http-ответа в строку — шаблон, который, по его наблюдениям, используется слишком многими разработчиками. После чего я предпринял небольшое расследование, чтобы выяснить, почему эта практика может быть плохой. Как я понял, в этом замешано выделение памяти под временные объекты (temporary allocation), а чем больше лишних аллокаций, тем больше работы для сборщика мусора, что может неблагоприятно повлиять на работу вашего приложения. Но это еще не все, при аллокации такого рода возможны еще большие проблемы.

Ответы на HTTP-запросы иногда могут быть очень большими по размеру и даже превышать лимит в 85 КБ. Если они превышают этот лимит в 85 КБ, они будут аллоцированы в LOH (large object heap — куча больших объектов), что является достаточно веским поводом, чтобы избегать излишних аллокаций этого типа. Если хотите узнать больше об опасностях кучи больших объектов и почему вы должны стараться избегать ее, когда это возможно, то почитайте эту статью о том, как работает куча больших объектов.

Ниже приведен пример того, как можно избежать чтения ответа на Http-запрос во временную строку для десериализации. В этом примере для обработки и сериализации используется JSON.NET.

“Это пример кода с сайта документации JSON.NET с небольшими изменениями”

var client = new HttpClient();
using (var s = await client.GetStreamAsync("http://www.nerdlife.me"))
using (var sr = new StreamReader(s))
using (var reader = new JsonTextReader(sr))
{
    var serializer = new JsonSerializer();
    // Читаем json-ответ на запрос из потока
    // Размер Json не имеет значения, потому что HTTP-запроса считывается по частям 
    var p = serializer.Deserialize<Person>(reader);
}

2. Заменяйте временные/ненужные коллекции на Yield

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

[TestMethod]
        public void TestWithoutYield()
        {
            // Отображение степеней двойки до восьмой степени:
            foreach (int i in PowerWithoutYield(2, 8))
            {
                Debug.Write("{0} ", i);
            }
           
        }
public static IEnumerable<int> PowerWithoutYield(int number, int exponent)
    {
        int result = 1;
        var results = new List<int>();
        for (int i = 0; i < exponent; i++)
        {
            result = result * number;
            results.Add(result);
        }
        return results;
    }

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

Что мы можем здесь сделать, так это использовать yield, чтобы избавиться от необходимости в этом промежуточном временном объекте, которые нужны для создания результирующего списка (var results = new List<int>()).

Давайте порефакторим приведенный выше фрагмент кода с использованием Yield. Приведенный выше пример кода изменится следующим образом.

Пример кода взят с сайта MSDN.

[TestMethod]
        public void TestWithYield()
        {
            // Отображение степеней двойки до восьмой степени:
            foreach (int i in PowerWithYield(2, 8))
            {
                Debug.Write("{0} ", i);
            }

        }
public static IEnumerable<int> PowerWithYield(int number, int exponent)
    {
        int result = 1;
        for (int i = 0; i < exponent; i++)
        {
            result = result * number;
            yield return result;
        }
    }

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

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

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

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

  1. Вот ссылка на пример без Yield.

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

Чтобы продемонстрировать разницу, я создал еще один гист, который содержит декомпилированный код примера с Yield. Как вы можете увидеть в этом гисте, метод c Yield преобразуется в StateMachine, которая реализует интерфейс IEnumerable, а затем вызывает метод GetEnumerator для получения энумератора и с помощью MoveNext итерирует по значениям, которые будут возвращены методом Yield. Таким образом, мы избавились от необходимости выделения памяти для промежуточного/временного списка для хранения возвращаемых результатов.

  1. Вот ссылка на пример с Yield.

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

Я раскрыл только одно из преимуществ, которые ‘yield’ может предложить в некоторых сценариях. Настоятельно рекомендую вам продолжить самостоятельные исследования по этой теме. Я оставлю вам кое-что, что может побудить вас узнать больше о yield и о том, как он работает в C#.

Как вы думаете, глядя на этот код, будет ли Unit Test пройден или провалится с unhandeled exception?

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace CSharpTipsProject
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void GeEvenNumbersWhereYieldReturnThrowException()
        {
            var numbersToFilter = Enumerable.Range(1, 20);
            var evenNumbers = GetEvenNumbersWithYield(numbersToFilter);
            Assert.IsNotNull(evenNumbers);
        }

        private IEnumerable<int> GetEvenNumbersWithYield(IEnumerable<int> numbers)
        {
            Debug.WriteLine("Filtering Even Numbers");
            throw new Exception("Exception in get even numbers.");
            foreach (var num in numbers)
            {
                Debug.WriteLine("Processing input number: "+num);
                if (num % 2 == 0)
                {
                    yield return num;
                }
            }
        }
    }
}

3. Помечайте устаревший код атрибутом Obsolete

Вы когда-либо сталкивались с ситуацией, когда у вас есть некоторые функции, доступные в библиотеке или приложении, которые вы еще не готовы удалить полностью, но хотите воспрепятствовать их использованию?

Если да, то вы наверняка уже знаете, что C# предоставляет атрибут, который позволяет декорировать методы, что компилятор предупреждал, что они скоро устареют, тем самым мотивируя отказаться от их использования.

Если вам достаточно просто вывести предупреждение пользователю с помощью удобного для него сообщения, вы можете использовать что-то вроде того, что показано ниже. Когда пользователь будет компилировать код, компилятор выдаст предупреждение.

Для предупреждения:

[Obsolete("This method has been superseded by the IsInUse() method")]
public void NotInUse()

Если вы хотите полностью запретить использование данной функциональности, вы можете передать атрибуту Obsolete второй параметр true, что полностью остановит компиляцию кода. Код, использующий устаревший метод, не будет скомпилирован и выдаст ошибку.

Для ошибки:

[Obsolete("This method has been superseded by the IsInUse() method", true)]
public void NotInUse()

4. Сохраняйте стек-трейс при перебрасывании исключений

Я более чем уверен, что вы писали в своем приложении фрагменты кода, которые содержат блоки try и catch. Часто внутри блока catch, вместо непосредственной обработки исключения, его данные считываются, а само исключение перебрасывается, чтобы вы могли передать с ним стек-трейс и обработать это исключение в каком-то другом централизованном месте в вашем приложении.

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

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
namespace RethrowExample
{
    public class ExceptionThrowingFactory
    {
        //Метод, который не сохранит стек-трейс
        public void NoStackTrace()
        {
            try
            {
                FirstLevelDeep();
            }
            catch (Exception ex) //Типичный паттерн try catch
            {
                Debug.WriteLine($"Exception with message: {ex.Message}");
                throw ex;
            }
        }
        //Метод, сохраняющий стек-трейс
        public void WithStackTrace()
        {
            try
            {
                FirstLevelDeep();
            }
            catch (Exception ex) //Типичный паттерн try catch
            {
                Debug.WriteLine($"Exception with message: {ex.Message}");
                throw;
            }
        }
        //Пустой метод для создания стека вызовов с тремя вложениями
        public void FirstLevelDeep()
        {
            SecondLevelDeep();
        }
        public void SecondLevelDeep()
        {
            ThreeLevelDeep();
        }
        public void ThreeLevelDeep()
        {
            throw new Exception("Just a demo exception.");
        }
    }
}

Теперь мы будем вызывать оба метода NoStackTrace и WithStackTrace из класса нашей фабрики, обернув их в модульный тест.

 [TestClass]
    public class TestStackTrace
    {
        [TestMethod]
        public void TestStackTraceLostForExceptions()
        {
            var factory = new ExceptionThrowingFactory();
            factory.NoStackTrace();
        }

        [TestMethod]
        public void TestStackTracePreservedForExceptions()
        {
            var factory = new ExceptionThrowingFactory();
            factory.WithStackTrace();
        }
    }

Давайте подебажим оба эти теста один за другим. Сначала мы выполняем тестовый пример TestStackTraceLostForExceptions и смотрим на полученный StackTrace.

//Исходный стек для неправильного метода
Result StackTrace:	
at RethrowExample.ExceptionThrowingFactory.NoStackTrace() in C:\Users\ranje\Documents\Visual Studio 2017\Projects\RethrowExample\RethrowExample\ExceptionThrowingFactory.cs:line 20
   at RethrowExample.TestStackTrace.TestStackTraceLostForExceptions() in C:\Users\ranje\Documents\Visual Studio 2017\Projects\RethrowExample\RethrowExample\TestStackTrace.cs:line 12

Как мы видим, StackTrace говорит нам, что исключение произошло в методе NoStackTrace(), т.е. мы потеряли исходную информацию о стеке вызовов, откуда возникло исключение.

Теперь давайте подебажим второй тестовый пример TestStackTracePreservedForExceptions и посмотрим на наличие StackTrace в захваченных исключениях.

Result StackTrace:	
at RethrowExample.ExceptionThrowingFactory.ThreeLevelDeep() in C:\Users\ranje\Documents\Visual Studio 2017\Projects\RethrowExample\RethrowExample\ExceptionThrowingFactory.cs:line 48
   at RethrowExample.ExceptionThrowingFactory.SecondLevelDeep() in C:\Users\ranje\Documents\Visual Studio 2017\Projects\RethrowExample\RethrowExample\ExceptionThrowingFactory.cs:line 43
   at RethrowExample.ExceptionThrowingFactory.FirstLevelDeep() in C:\Users\ranje\Documents\Visual Studio 2017\Projects\RethrowExample\RethrowExample\ExceptionThrowingFactory.cs:line 38
   at RethrowExample.ExceptionThrowingFactory.WithStackTrace() in C:\Users\ranje\Documents\Visual Studio 2017\Projects\RethrowExample\RethrowExample\ExceptionThrowingFactory.cs:line 27
   at RethrowExample.TestStackTrace.TestStackTracePreservedForExceptions() in C:\Users\ranje\Documents\Visual Studio 2017\Projects\RethrowExample\RethrowExample\TestStackTrace.cs:line 19

Теперь мы видим, что стек-трейс содержит полную информацию о стеке вызовов из места, откуда возникло исключение.

Подытожим продемонстрированное выше:

//Используя что-то вроде этого, 
//мы потеряем информацию из стек-трейсе исходного исключения
...
catch (Exception ex) 
            {
                Debug.WriteLine($"Exception with message: {ex.Message}");
                throw ex;
            }
// Используя только ключевое слово throw, мы сохраним исходный стек-трейс
...
catch (Exception ex) 
            {
                Debug.WriteLine($"Exception with message: {ex.Message}");
                throw;
            }

5. Используйте свой собственный сериализатор и десериализатор для классов, использующих JSON.NET

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

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

Самый быстрый способ чтения и записи JSON — использовать JsonTextReader/JsonTextWriter напрямую для сериализации типов вручную. Использование модуля чтения или записи напрямую избавляет от любых накладных расходов сериализатора, таких как рефлексия — (информация с сайта документации JSON.NET).

Ниже приведен пример написания пользовательского сериализатора с использованием JSON.NET. Здесь мы используем JsonTextReader для чтения строковых данных и парсинга их в объект C# типа Car.

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

public class Car {
    public string Make {get;set;}
    public string Model {get;set;}
    public string Year {get;set;}
    //остальные поля в том же духе...
} 
//Пример десериализации строки Car Object в объект
private static Car DeserializeCar(string carInfo)
        {
            using (var reader = new JsonTextReader(new StringReader(carInfo)))
            {
                string model;
                var make = model = string.Empty;
                int year = 0;
                var currentProperty = string.Empty;

                while (reader.Read())
                {
                    if (reader.Value != null)
                    {
                        if (reader.TokenType == JsonToken.PropertyName)
                            currentProperty = reader.Value.ToString();

                        if (reader.TokenType == JsonToken.Integer && currentProperty == "Year")
                            year = Int32.Parse(reader.Value.ToString());

                        if (reader.TokenType == JsonToken.String && currentProperty == "Make")
                            make = reader.Value.ToString();

                        if (reader.TokenType == JsonToken.String && currentProperty == "Model")
                            model = reader.Value.ToString();
                        // Обработка других полей
                    }

                }
                return  new Car(make, model, year);
            }
        }

//Пример ручной сериализации объекта Car с использованием метода расширения
public static string ConvertToJson(this Car c)
{
    var sw = new StringWriter();
    var writer = new JsonTextWriter(sw);
    //Start writing serialized object
    writer.WriteStartObject();
    //Write the first property name
    writer.WritePropertyName("make");
    //Write the property value
    writer.WriteValue(p.Make);
    writer.WritePropertyName("model");
    writer.WriteValue(p.Model);
    writer.WritePropertyName("yodel");
    writer.WriteValue(p.Year);
    //Заканчиваем запись объекта, добавляя закрывающую фигурную скобку.
    writer.WriteEndObject();
    //Возвращаем получившуюся сериализованную строку
    return sw.ToString();
}

В заключение

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


Материал подготовлен в рамках специализации "Developer C#".

Всех желающих приглашаем на бесплатное demo-занятие «Делаем программу интерактивной». На занятии будем работать с выводом и вводом данных в консоли: ввод данных и их преобразование в другие типы, форматирование вывода, работа с классом Console. Если интересно — записывайтесь.




Комментарии (4):

  1. Alekseyz
    /#23967471 / +6

    В любой непонятной ситуации пиши свой собственный сериализатор

  2. Free_ze
    /#23968975 / +4

    К слову, System.Text.Json теперь умеет генерировать сериализаторы.

  3. Krey
    /#23971313 / +2

    Ну, это лучше чем совет использовать StringBuilder :)

  4. Filliny
    /#23973887

    всегда использовал throw вместо trow ex... тем более IDE только trow и предлагает)