6 не самых широко известных фич C#/.NET, которые вам стоило бы использовать

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Введение

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

1. Stopwatch

Начну я с того, чем мы будем пользоваться в дальнейшем, — со Stopwatch. Вполне вероятно, что на каком-то этапе разработки у вас появится причины начать профилировать фрагменты вашего кода, чтобы найти какие-либо узкие места в производительности. Хотя существует множество пакетов для проведения бенчмарков, которые вы можете использовать в своем коде (Benchmark.NET является одним из самых популярных), иногда вам просто нужно быстро проверить что-нибудь без особых заморочек. Я полагаю, что большинство людей сделают что-то вроде этого:

    var start = DateTime.Now;
    Thread.Sleep(2000); //Code you want to profile here
    var end = DateTime.Now;
    var duration = (int)(end - start).TotalMilliseconds;
    Console.WriteLine($"The operation took {duration} milliseconds");

И это будет работать — следующий способ скажет вам, что потребовалось ~2000 миллисекунд. Однако он не является рекомендуемым способом тестирования, поскольку DateTime.Now может не обеспечить необходимый уровень точности — DateTime.Now обычно требует примерно 15 миллисекунд. Чтобы продемонстрировать это, давайте рассмотрим очень надуманный пример ниже:

    var sleeps = new List<int>() { 5, 10, 15, 20 };
    foreach (var sleep in sleeps)
    {
        var start = DateTime.Now;
        Thread.Sleep(sleep);
        var end  = DateTime.Now;
        var duration = (int)(end - start).TotalMilliseconds;
        Console.WriteLine(duration);
    }

Результат, вероятно, будет меняться каждый раз, но вы, скорее всего, получите что-то вроде этого:

15
15
15
31 

Итак, мы убедились, что этот метод не очень точный, но что, если вам по большому счету все равно? Вы, конечно, можете продолжать использовать метод замера времени выполнения с DateTime.Now, но есть гораздо более приятная альтернатива- Stopwatch, который можно найти в пространстве имен System.Diagnostics. Этот подход намного удобнее, чем использовать DateTime.Now, выражает ваши намерения гораздо лаконичнее - и он намного точнее! Давайте изменим наш последний фрагмент кода, чтобы посмотреть на класс Stopwatch:

    var sleeps = new List<int>() { 5, 10, 15, 20 };
    foreach (var sleep in sleeps)
    {
        var sw = Stopwatch.StartNew();
        Thread.Sleep(sleep);
        Debug.WriteLine(sw.ElapsedMilliseconds);
    }

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

6
10
15
20

Но это уже намного лучше! Так что, учитывая простоту использования класса Stopwatch, я не вижу причин предпочесть ему “старомодный” способ.

2. Библиотека распараллеливания задач (TPL)

    var items = Enumerable.Range(0,100).ToList();
    var sw = Stopwatch.StartNew();
    foreach (var item in items)
    {
        Thread.Sleep(50);
    }
    Console.WriteLine($"Took {sw.ElapsedMilliseconds} milliseconds..."); 

Как можно не трудно догадаться, это займет примерно 5000 миллисекунд/5 секунд (100 * 50 = 5000). Теперь давайте взглянем на параллельную версию с использованием TPL…

    var items = Enumerable.Range(0,100).ToList();
    var sw = Stopwatch.StartNew();
    Parallel.ForEach(items, (item) => 
    {
        Thread.Sleep(50);                     
    });
    Console.WriteLine($"Took {sw.ElapsedMilliseconds} milliseconds..."); 

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

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

3. Деревья выражений

Деревья выражений (Expression Trees) — чрезвычайно мощный компонент .NET Framework, но они также являются одним из наиболее плохо понимаемых (неопытными программистами) компонентов. Мне потребовалось много времени, чтобы полностью понять их концепцию, и я все еще далек от экспертности в этом вопросе. По сути, они позволяют вам оборачивать лямбда-выражения, такие как Func<T> или Action<T>, и анализировать само лямбда-выражение. Я думаю, лучше всего будет проиллюстрировать это на примере — а в .NET Framework их недостатка нет, особенно в LINQ to SQL и Entity Framework.

Метод расширения 'Where' в LINQ to Objects принимает Func<T, int, bool> в качестве основного параметра — смотрите приведенный ниже код, который я украл из Reference Source (который содержит исходный код .NET)

    static IEnumerable<TSource> WhereIterator<TSource>(IEnumerable<TSource> source, Func<TSource, int, bool> predicate) 
    {
         int index = -1;
         foreach (TSource element in source) 
         {
              checked { index++; }
              if (predicate(element, index)) yield return element;
         }
    }

Тут все как и ожидалось — выполняется итерация по IEnumerable и возвращается то, что соответствует предикату. Однако очевидно, что это не будет работать в LINQ to SQL/Entity Framework — ему необходимо преобразовать ваш предикат в SQL! Так что сигнатура для версии IQueryable немного отличается...

    static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, int, bool>> predicate) 
    {
          return source.Provider.CreateQuery<TSource>( 
                    Expression.Call(
                        null,
                        GetMethodInfo(Queryable.Where, source, predicate),
                        new Expression[] { source.Expression, Expression.Quote(predicate) }
                        ));
    }

Если вас не смутит пугающая природа этого метода, вы заметите, что функция теперь принимает Func<T, int, bool>, обернутый в Expression (выражение) — это позволяет провайдеру LINQ просматривать Func, чтобы увидеть какой именно предикат был пропущен, и перевести его в SQL. По сути, Expressions позволяют вам проверять ваш код во время выполнения.

Давайте рассмотрим кое-что попроще — представьте, что у вас есть приведенный ниже код, который добавляет настройки в словарь (мы использовали это в реальном коде)

    var settings = new List<Setting>();
    settings.Add(new Setting("EnableBugs",Settings.EnableBugs));
    settings.Add(new Setting("EnableFluxCapacitor",Settings.EnableFluxCapacitor));

Надеюсь, здесь нетрудно разглядеть опасность/повторение — имя параметра в словаре представляет собой строку, и опечатка здесь может вызвать некоторые проблемы. К тому же это невероятно утомительно! Если мы создадим новый метод, который принимает Expression<Func<T>> (по сути, принимает лямбда-выражение, которое возвращает что-то), мы получаем имя переданной переменной!

    private Setting GetSetting<T>(Expression<Func<T>> expr)
    {
        var me = expr.Body as MemberExpression;
        if (me == null)
                 throw new ArgumentException("Invalid expression. It should be MemberExpression");
            var func = expr.Compile(); //This converts our expression back to a Func
        var value = func(); //Run the func to get the setting value
        return new Setting(me.Member.Name,value);
    }

Затем мы можем вызвать его следующим образом…

    var settings = new List<Setting>();
    settings.Add(GetSetting(() => Settings.EnableBugs));
    settings.Add(GetSetting(() => Settings.EnableFluxCapacitor));    

Намного лучше! Вы можете заметить, что в нашем методе GetSetting мне нужно проверить, передано ли выражение как 'MemberExpression' - это потому, что ничто не мешает вызывающему коду передать нам что-то вроде вызова метода или константы, где "именем члена" не будет.

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

4. Атрибуты с информацией о вызывающем объекте

Caller Information attributes были представлены миру в .NET Framework 4.0, и, хотя они в большинстве случаев бесполезны, они в полной мере проявляют себя при написании кода для логирования отладочной информации. Представим, что у вас есть такая незатейливая функция Log, как показано ниже:

    public static void Log(string text)
    {
       using (var writer = File.AppendText("log.txt"))
       {
           writer.WriteLine(text);
       }
    }

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

    public static void Log(string text)
    {
       using (var writer = File.AppendText("log.txt"))
       {
           writer.WriteLine($"{text} - {new StackTrace().GetFrame(1).GetMethod().Name});
       }
    }

И это работает — мы увидим “Main”, если я вызову эту функцию из своего метода Main. Однако это медленно и неэффективно, поскольку вы, по сути, фиксируете стек-трейс, как если бы было сгенерировано исключение. .NET Framework 4.0 вводит вышеупомянутые “атрибуты с информацией о вызывающем объекте”, которые позволяют фреймворку автоматически сообщать вашему методу информацию о том, что его вызывает, в частности путь к файлу, имя метода/свойства и номер строки. По сути, вы их используете, позволяя вашему методу принимать опциональные строковые параметры, которые вы помечаете атрибутом. Смотрите ниже, как я использую 3 доступных атрибута.

    public static void Log(string text,
    [CallerMemberName] string memberName = "",
    [CallerFilePath] string sourceFilePath = "",
    [CallerLineNumber] int sourceLineNumber = 0)
    {
        Console.WriteLine($"{text} - {sourceFilePath}/{memberName} (line {sourceLineNumber})");    
    }

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

5. Класс ‘Path’

Этот класс скорее всего все-таки достаточно известен, но я все еще вижу, как разработчики делают такие вещи, как получение расширения файла для имени файла вручную, при наличии встроенного класса Path с проверенные рабочими методами, которые делают эту работу за вас. Класс находится в пространстве имен System.IO и содержит множество полезных методов, которые сокращают объем стандартного кода, который вам необходимо писать. Многие из вас знакомы с такими методами, как Path.GetFileName и Path.GetExtension (которые работают именно так, как и следует из их названия), но я упомяну еще несколько менее непопулярных ниже

Path.Combine

Этот метод берет 2,3 или 4 пути и объединяем их в один. Обычно люди использую его, чтобы добавить имя файла к пути к каталогу, например, directoryPath + ”\” + filename . Проблема в том, что вы делаете предположение, что разделителем каталогов в системе, в которой работает ваше приложение, является '\'. А что, если приложение работает в Unix, который использует косую черту ('/') в качестве разделителя каталогов? Это становится все более серьезной проблемой, поскольку .NET Core позволяет запускать приложения .NET на гораздо большем количестве платформ. Path.Combine будет использовать разделитель каталогов, применимый к целевой операционной системе, а также позаботится об избыточных разделителях — то есть, если вы добавите каталог с '\' в конце к имени файла с '\' в начале, Path.Combine уберет один из них. Вы можете найти больше причин, по которым вам следовало бы использовать Path.Combine здесь.

Path.GetTempFileName

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

Хотя вы можете написать код для управления этим самостоятельно, это достаточно утомительно и чревато ошибками. Используйте безумно полезный метод Path.GetTempFileName из класса Path — он не принимает никаких параметров и создает пустой файл во временном каталоге, определяемом пользователем, и возвращает вам полный путь. Поскольку он находится во временном каталоге пользователей, Windows автоматически управляет им, и вам не нужно беспокоиться о засорении системы избыточными файлами. Смотрите эту статью для получения дополнительной информации об этом методе, а также связанного с ним 'GetTempPath'.

Path.GetInvalidPathChars / Path.GetInvalidFileNameChars

GetInvalidPathChars и его брат Path.GetInvalidFileNameChars, возвращает массив всех символов, которые недопустимы в качестве текущих путей/имен файлов в текущей системе. Я видел так много кода, который вручную удаляет некоторые из наиболее распространенных недопустимых символов, таких как кавычки, но не удаляет какие-либо другие недопустимые символы, что является бомбой замедленного действия. И в духе кроссплатформенной совместимости неверно предполагать, что то, что недопустимо в одной системе, будет так же недопустимо в другой. Моя единственная критика этих методов заключается в том, что они не обеспечивают способа проверки, содержит ли путь какой-либо из этих символов, что обычно требует написания нижеприведенных шаблонных методов:

    public static bool HasInvalidPathChars(string path)
    {
        if (path  == null)
            throw new ArgumentNullException(nameof(path));
        
        return path.IndexOfAny(Path.GetInvalidPathChars()) >= 0;
    }
        
    public static bool HasInvalidFileNameChars(string fileName)
    {
        if (fileName == null)
            throw new ArgumentNullException(nameof(fileName));
        
        return fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0;
    }

6. StringBuilder

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

Здесь на сцену выходит удачно названный класс StringBuilder, который позволяет выполнять конкатенацию с минимальными затратами на производительность. Он делает это довольно просто — на высоком уровне он хранит список каждого добавляемого вами символа и строит вашу строку только тогда, когда она вам действительно нужна. Для демонстрации того, как его использовать, а также о преимуществах производительности, смотрите приведенный ниже код, который тестирует производительность двух подходов к конкатенации (с использованием полезного класса Stopwatch, о котором я упоминал ранее)

    var sw = Stopwatch.StartNew();
    string test = "";
    for (int i = 0; i < 10000; i++)
    {
        test += $"test{i}{Environment.NewLine}";    
    }
    Console.WriteLine($"Took {sw.ElapsedMilliseconds} milliseconds to concatenate strings using string concatenation");
            
    sw = Stopwatch.StartNew();
    var sb = new StringBuilder();
    for (int i = 0; i < 10000; i++)
    {
        sb.Append($"test{i}");
        sb.AppendLine();
    }
    Console.WriteLine($"Took {sw.ElapsedMilliseconds} milliseconds to concatenate strings using StringBuilder");

Результаты этого теста приведены ниже:

Took 785 milliseconds to concatenate strings using the string concatenation
Took 3 milliseconds to concatenate strings using StringBuilder

Ого, это быстрее более чем в 250 раз! И это только 10000 конкатенаций — если вы создаете большой CSV-файл вручную (что в любом случае является плохой практикой, но давайте сейчас не будем об этом беспокоиться), эта цифра может быть больше. Если вы объединяете только пару строк, вероятно, будет ничего страшного, если вы совершите конкатенацию без использования StringBuilder, но, честно говоря, мне претит иметь привычку всегда использовать его — стоимость обновления StringBuilder, условно говоря, мизерная.

В моем примере кода я использую sb.Append, а затем sb.AppendLine — вы можете просто вызвать sb.AppendLine, пропустив свой текст, который вы хотите добавить, и он добавит новую строку в конце. Я хотел включить Append и AppendLine, чтобы было немного понятнее.

Заключение

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

Я был бы рад услышать про подобные фичи от тех, кто использует другие недостаточно используемые функции в .NET/C#, поэтому, пожалуйста, оставьте комментарий, если вы такие знаете!


Перевод подготовлен в рамках нового набора на специализацию "C# Developer". Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.

Источник: https://habr.com/ru/company/otus/blog/645699/


Интересные статьи

Интересные статьи

Несмотря на дефицит электронных компонентов, разные компании продолжают выпускать одноплатные компьютеры. За последние пару месяцев появилось несколько интересных моделей, которые вполне могут оказа...
Привет, Хабр! Недавно в нашем корпоративном блоге мы рассказали о выходе новой версии МойОфис 2021.02, в которой появились удобные инструменты для работы с формулами и математическими выражениями, а т...
Grafana Labs с гордостью представляет простую в эксплуатации, масштабируемую, рентабельную, распределенную систему трассировки: Tempo. Она разработана в качестве надежного хранилища, оп...
В обновлении «Сидней» Битрикс выпустил новый продукт в составе Битрикс24: магазины. Теперь в любом портале можно создать не только лендинг или многостраничный сайт, но даже интернет-магазин. С корзино...
Тема поиска работы за рубежом довольно популярна на хабре в последние годы. Однако если с профессиональными навыками у отечественных инженеров, как правило, все отлично, то уровень английског...