Введение

В этой статье речь пойдет о 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". Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.

Комментарии (29)


  1. a-tk
    14.01.2022 19:06
    +12

    Оказывается в Отусе плохих писателей больше чем один...


  1. KvanTTT
    14.01.2022 19:40
    +16

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

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


    1. holydel
      14.01.2022 20:24
      +2

      гуглится то легко, если знать, что гуглить, что такие штуки вообще есть. Мне этот небольшой список автора понравился. Про GetTempFileName и GetInvalidPathChars / GetInvalidFileNameChars узнал из этого поста. Кто-то что-то может себе найти, я думаю. А знакомые StringBuilder-ы можно просто промотать, благо новые штуки выделены жирным.


      1. KvanTTT
        14.01.2022 20:29
        +5

        Гуглится примерно так: Get temp file name .NET, Get invalid path chars .NET. Правильные результаты идут на первых строчках. Разве любой программист не использует Google/StackOverflow, когда сталкивается с чем-то незнакомым, чтобы не писать свой велосипед?


    1. skygtr99113
      15.01.2022 13:25
      +1

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


      1. dabrahabra
        17.01.2022 01:58

        А стали меньше гуглить?


  1. gotoxy
    14.01.2022 20:15
    +16

    Да сколько ж можно первооткрывать StringBuilder? Это давно уже вопрос собеса для джуна, а не озарение года. Собственно, как и Stopwatch. Да, это уже давно просто данность, хватит пихать это в подборки редкоиспользуемых фич. На дворе уже 2022, хватит "советов" из 2007.


    1. a-tk
      15.01.2022 09:59

      Дык команду думать переводчику никто не давал. Только план довели.


  1. WhiteBlackGoose
    14.01.2022 23:11
    +4

    Помимо сказанного, и помимо того, что автор так радостно рассказывает о ".NET Framework" в 2022, еще и более актуальную информацию стоило бы знать, а именно, CallerArgumentExpressionAttribute. Да и советовать использовать StopWatch не стоит. Это вот "по-быстренькому посмотреть" зачастую делается быстрее упомянутым BenchmarkDotNet. Либо профайлер.


    1. KvanTTT
      15.01.2022 15:27

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


      1. WhiteBlackGoose
        15.01.2022 15:32
        -1

        Я скорее имел ввиду автора поста, а не оригинальной статьи. Постится-то оно сейчас, в этом году).


      1. mvv-rus
        16.01.2022 03:17

        Переводчик мог бы постараться найти статью еще старей, было бы еще забавней
        Уже. В прошлом году OTUS находили статью про ASP.NET Web Pages. Поностальгировал. А то давненько я про этот антиквариат нигде не читал.


    1. Tangeman
      15.01.2022 15:47
      +3

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


      1. WhiteBlackGoose
        15.01.2022 15:58
        +1

        Бенчмарк — измерение производительности метода/функции/алгоритма. И как инструмент для замера времени — согласен, SW работает. А для бенмаркинга алгоритмов — не очень.


        На самом деле я часто вижу (в чатах, например) как люди делают какие-то бенчмарки на SW и делают на них выводы. Среди всех неправильно произведенных бенчмарков, абсолютно большинство было из-за того, что человек не понимает, как бенчить — как учесть целую гору факторов, время на jit компиляцию, паузы GC, tiered compilation, кеши, и наконец разброс измерений, и оверхед инструмента, и еще другие факторы.


        Думаю, что на sw можно сделать точные бенчмарки — но для этого недостаточно замерить время, нужно еще учеть перечисленные условия. А это за нас делает BenchmarkDotNet.


  1. propell-ant
    15.01.2022 00:23
    +2

    Да, StringBuilder порадовал.
    Насчет TPL - почитал статью автора про TPL, он не отдупляет. Пишет "используйте, детишки TPL в веб-проектах, не бойтесь".


  1. PahanMenski
    15.01.2022 08:49
    +6

    Метод Path.GetTempFileName() довольно таки опасный, особенно если, как советуют в этой статье, "не беспокоиться о засорении системы избыточными файлами". Он создает файл с именем tmpXXXX.tmp, где XXXX - шестнадцатеричное число, т.е. таких файлов может быть всего 65536. Так что, если не удалять их за собой, то в один прекрасный момент времени, все неаккуратно написанные приложения в системе, которые также используют этот метод, могут начать сбоить, пока пользователь не почистит их, например, через Очистку Диска. Если догадается, конечно...

    Сам лично наблюдал такой баг в одной из версий Visual Studio, в которой компиляция внезапно отваливалась, причем с ни о чем не говорящей ошибкой.


    1. DistortNeo
      15.01.2022 18:10
      +3

      Посмотрел его текущую реализацию.

      Unix: Из Path.GetTempFileName() в конечном итоге вызывается mkstemp, используя для шаблона 6 символов, на выходе получается такое:

      GetTempFileName = /tmp/tmpXafXB5.tmp

      Никаких ограничений на количество файлов нет.

      Windows: вызвает GetTempFileNameW. У него третий параметр определяет уникальность имени файла:

      If uUnique is zero, the function attempts to form a unique file name using the current system time. If the file already exists, the number is increased by one and the functions tests if this file already exists. This continues until a unique filename is found; the function creates a file by that name and closes it. Note that the function does not attempt to verify the uniqueness of the file name when uUnique is nonzero.

      Only the lower 16 bits of the uUnique parameter are used. This limits GetTempFileName to a maximum of 65,535 unique file names if the lpPathName and lpPrefixString parameters remain the same.

      Due to the algorithm used to generate file names, GetTempFileName can perform poorly when creating a large number of files with the same prefix. In such cases, it is recommended that you construct unique file names based on GUIDs.

      Увы, это проблема виндового API.


  1. marshinov
    15.01.2022 11:18
    +1

    Про деревья выражений лучше читать/смотреть здесь.


  1. skygtr99113
    15.01.2022 13:29

    DateTime.Now достаточно медленный способ получения времени, что бы засекать скорость кода. Конечно, если вы запускате длительную операцию, которая длится секунды, то этот способ покажет вам профит. Но если этоарифметика или скорость выделения памяти, то тут уже нужно считать такты процессора. Это не так просто. StopWatch как раз и позволяет это сделать. А лучше не придумывать велосипед, а запустить профайлер. Он и диаграммы построит и сразу станет понятно, а нужно ли вообще здесь оптимизировать.

    Ещё один лайфхак: если нужно много и часто получать отчёты времени, то можно зафиксировать время начала с помощью DateTime.Now, а потом к нему прибавлять значение StopWatch. Это будет довольно быстро.


    1. qw1
      16.01.2022 00:43
      +1

      Очень быстрый способ — это DateTime.UtcNow, он просто читает статическую переменную (которая в контексте процесса обновляется через 15-16 мс, отсюда и точность невысокая). А вот DateTime.Now применяет текущий часовой пояс к UtcNow, потому и медленный.


  1. KislyFan
    15.01.2022 13:53
    +1

    Тоже чтоли какой капитанский мануал написать ?


  1. aegoroff
    15.01.2022 15:00
    +3

    Лучше бы про Span и все такое написали, а не про StringBuilder


    1. a-tk
      15.01.2022 19:49
      +1

      Сегодня намного моднее string interpolation handlers


      1. aegoroff
        15.01.2022 22:22

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


        1. aegoroff
          15.01.2022 22:28
          +4

          1. a-tk
            16.01.2022 00:12
            +1

            Ага, недавно всё очень сильно поменялось.


  1. SLenik
    16.01.2022 00:10
    +3

    Думаю, эта статья достойна подробного комментария.

    1. Stopwatch

    Приведённый в начале рассказа код, в котором start и end вычисляются через как DateTime.Now в некоторые моменты времени и в некоторых странах не будет работать так, как ожидается.

    Дело в том, что DateTime.Now возвращает время с учётом текущего часового пояса. Каак следствие, вот тут автор приводит очень простой пример: если между start и end произошла смена часовых поясов, результат вычисления будет некорректным (потеряется час - или добавится лишний).

    Как мининимум следовало бы упомянуть DateTime.UtcNow.

    О третьем листинге: разница во времени в 15.625 миллисекунд связана с использованием в Windows по умолчанию частоты в 64Гц для отсчёта интервалов времени таймера. Попробуйте запустить этот же тест в Windows с запущенным Firefox - и вы увидите совершенно другие цифры. Лично я, правда, этого не делал, но у меня нет причин не верить одному авторитетному человеку.

    1. Библиотека распараллеливания задач

    В среднем этот фрагмент кода займет всего 1000 миллисекунд, что лучше аж в пять раз!

    Максимальное количество параллельных потоков, создаваемых в Parallel.ForEach, зависит от заданного свойства MaxDegreeOfParallelism. Минимальное же, вообще говоря, определяет механизм пула потоков. Это я к тому, что совсем непонятно, при каких условиях это "в среднем" получено - чтобы попробовать повторить эксперимент автора.

    ИМХО тут следовало бы также оставить ссылки на статьи и/или книги, где тема TPL раскрыта поглубже. Ну или хотя бы упомянуто ещё несколько важных моментов:

    • как прервать такой параллельный цикл (сделать в нём break)?

    • если внутри итерации цикла будет выброшено исключение, в каком виде оно "поймается" снаружи? Если в очередной итерации цикла выбросится исключение, будут ли выполняться оставшеся итерации?

    • Что такое потоки (Thread) и почему работа с некоторыми объектами возможна только из определённых потоков?

    Последнее - это я просмотрел "статью о TPL", на которую есть ссылка, и заметил, что там речь идёт об обработке класса Image из WPF. А многие объекты WPF ревностно следят, чтобы с ними работали только из UI потока.

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

    В тексте нигде нет упоминания стоимости использования деревьев выражений. В частности, вызываемый метод .Compile() это не просто "преобразование выражения обратно в делегат" (вольный перевод оригинального оригинального комментария из листингаconverts our expression back to a Func, который, кстати, почему-то не перевели в статье). Это компиляция силами dotnet-а данного метода. А компиляция требует прилично так времени по сравнению с вызовом обычного делегата или простым анализом дерева выражения.

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

    settings.Add(new Setting(nameof(Settings.EnableBugs),Settings.EnableBugs));

    Хоть бы на MSDN по этому вопросу ссылку дали.

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

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

    Однако не пытайтесь в разрабатываемом для кого-то приложении использовать такое логирование! Пишу из реального опыта: в районе начала 2019 года встреченное мною приложение с подобным же логированием при записи > 100 строк в секунду начинало дичайше тормозить на ОС Windows 10 1709. Даже на хороших SSD. Мы тогда выяснили, что причина была в Windows Defender. Может он после каждой модификации файла пересканировал его, может ещё за чем-то вносил задержку. Но в результате проблема решилась заменой подобного логовелосипеда на библиотеку NLog.

    1. Класс 'Path'

    @PahanMenski уже написал, что метод по-разному работает в разных ОС, поэтому копипастить его комментарий не буду.

    1. StringBuilder

    Тест некорректный: автор не учёл (или не знал?), что StringBuilder создаёт объект типа string и копирует в него все данные только при вызове sb.ToString().

    Так что для корректного сравнения перед вывом времени, но после цикла следовало записать куда-нибудь сформированную строку:

    var result = sb.ToString();

    Собственно, поэтому StringBuilder builder-ом и называется - потому, что реализует паттерн строитель (builder).

    Если очень поверхностно, то StringBuilder внутри содержит список "кусочков" (char[]) конструируемой строки. Плюс реализация менялась от версии к версии .NET. Мне очень понравилось описание от Steve Gordon, в нём три части - вот ссылка на первую.

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

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

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


  1. qw1
    16.01.2022 00:49

    это позволяет провайдеру LINQ просматривать Func, чтобы увидеть какой именно предикат был пропущен
    Очевидно, тут слово passed надо было перевести как «передан», а не как «пропущен».
    Path.Combine будет использовать разделитель каталогов, применимый к целевой операционной системе, а также позаботится об избыточных разделителях — то есть, если вы добавите каталог с '\' в конце к имени файла с '\' в начале, Path.Combine уберет один из них
    Это ошибка. Если добавлять к пути другой путь, начинающийся с '\' (или в linux — с '/'), первый путь будет отброшен и останется только второй. Логика, примерно как у команды cd
    Если после
    cd C:\Windows
    сделать
    cd Temp
    мы перейдём в C:\Windows\Temp

    Но если сделать
    cd \Temp
    мы перейдём в C:\Temp


    1. DistortNeo
      16.01.2022 01:01
      +2

      Очевидно, что автор ещё не знает про Path.Join.