﻿<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
	<channel>
		<title>.net, C# and programming (RU)</title>
		<link>https://blog.rogatnev.net/</link>
		<description>.net, C# and programming</description>
		<copyright>Copyright © 2026</copyright>
		<managingEditor>Sergey Rogatnev</managingEditor>
		<pubDate>Sun, 26 Apr 2026 15:23:08 GMT</pubDate>
		<lastBuildDate>Sun, 26 Apr 2026 15:23:08 GMT</lastBuildDate>
		<item>
			<title>Удаляем пробелы из строки</title>
			<link>https://blog.rogatnev.net/posts/ru/2025/11/Remove-Whitespaces.html</link>
			<description>Рассматриваем разные подходы к удалению пробельных символов из строк C# - сравнение методов.</description>
			<author>Сергей Рогатнев</author>
			<guid>https://blog.rogatnev.net/posts/ru/2025/11/Remove-Whitespaces.html</guid>
			<pubDate>Thu, 20 Nov 2025 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;Еще одна популярная задача при работе со строками — удалить из них пробельные символы.
Можно представить, что нам нужно очистить пользовательский ввод (удалить пробелы вначале и конце строк в имени) или минифицировать json-объект.
.NET предоставляет нам несколько возможностей для решения этой задачи, давайте рассмотрим самые популярные и попробуем найти наиболее эффективные.
Заодно проверим, какие изменения произошли в новой версии .NET 10.&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;Для нашего примера будем считать пробельными символами сам пробел (&lt;code&gt; &lt;/code&gt;), табуляцию (&lt;code&gt;\t&lt;/code&gt;) и перевод строки (&lt;code&gt;\n&lt;/code&gt;).
На самом деле их больше, но для нашей задачи ограничимся только этими как самыми популярными.&lt;/p&gt;
&lt;p&gt;Для бенчмарка я буду использовать самые новые и одни из самых старых версий фреймворка.
Вряд ли для новых проектов выберут .NET Framework 4.8 для старта нового проекта, но интересно увидеть, как развивалась платформа во времени и что можно получить с переходом на новые версии.&lt;/p&gt;
&lt;h1 id="replace"&gt;Replace&lt;/h1&gt;
&lt;p&gt;Начнем с самого простого метода — &lt;code&gt;Replace&lt;/code&gt;. Он может заменить нам все вхождения какого-то символа или подстроки на другой символ или подстроку.
Для наших целей мы можем заменить пробельные символы на пустую строку (&lt;code&gt;string.Empty&lt;/code&gt;). Но есть большой недостаток - этот метод не может заменить все пробельные символы сразу, поэтому мы вынуждены вызывать его для каждого пробельного символа:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;private static string Replace(string s)
{
    return s.Replace(" ", string.Empty).Replace("\t", string.Empty).Replace("\n", string.Empty);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Посмотрим, как этот метод ведет себя в разных версиях .NET.&lt;/p&gt;
&lt;p&gt;Полный код бенчмарка и все результаты вы найдете в конце статьи.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;| Method           | Runtime            |        Mean |      Median | Allocated |
|------------------|--------------------|------------:|------------:|----------:|
| Replace          | .NET Framework 4.8 | 230.9093 ns | 227.7122 ns |     233 B |
| Replace          | .NET Core 3.1      | 158.6589 ns | 158.1439 ns |     216 B |
| Replace          | .NET 8.0           | 178.9700 ns | 177.7641 ns |     216 B |
| Replace          | .NET 10.0          | 157.4850 ns | 156.3763 ns |     216 B |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Если посмотреть на размер итоговой строки, то мы тратим примерно в 3 раза больше памяти.
Очевидно, из-за того, что мы 3 раза вызываем метод &lt;code&gt;Replace()&lt;/code&gt;, который создаёт новую строку. Из неё мы опять удаляем символы, что опять создаёт новую строку.
И мы видим незначительное улучшение с каждой новой версией .NET.&lt;/p&gt;
&lt;p&gt;И если в версии .NET 4.8 реализация метода &lt;code&gt;Replace()&lt;/code&gt; находится внутри Common Language Runtime (CLR):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;[SecuritySafeCritical]
[MethodImpl(MethodImplOptions.InternalCall)]
private extern string ReplaceInternal(string oldValue, string newValue);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;То уже начиная с .NET Core 3.1 используется собственная реализация на указателях, а позже — с использованием типа &lt;code&gt;Span&lt;/code&gt; и векторных инструкций.&lt;/p&gt;
&lt;p&gt;К недостатку этого метода можно отнести то, что нужно явно перечислить все пробельные символы. А их, на самом деле, не так мало.
Каждый новый символ - это лишний вызов метода &lt;code&gt;Replace&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Наверняка, должны быть более оптимальные решения.&lt;/p&gt;
&lt;h1 id="regex"&gt;Regex&lt;/h1&gt;
&lt;p&gt;Регулярные выражения — универсальный способ решения для многих задач. И здесь он тоже применим.
Как я говорил в предыдущем методе, мы не можем с помощью метода &lt;code&gt;Replace()&lt;/code&gt; заменить все пробельные символы. А вот с помощью регулярных выражений можем:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;private static readonly Regex EmptySpacesCompiled = new Regex(@"\s+", RegexOptions.Compiled);

private static string RegexCompiled(string s)
{
    return EmptySpacesCompiled.Replace(s, string.Empty);
}

private static readonly Regex EmptySpacesGenerated = GenerateRegex();

[GeneratedRegex(@"\s+")]
private static partial Regex GenerateRegex();

private static string RegexGenerated(string s)
{
    return EmptySpacesGenerated.Replace(s, string.Empty);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;\s&lt;/code&gt; - специальное выражение, которое объединяет широкий набор пробельных символов, в том числе &lt;code&gt;\t&lt;/code&gt;, &lt;code&gt;\n&lt;/code&gt;, &lt;code&gt; &lt;/code&gt;.
Поэтому расширим задачу и дальше переходим к более общему варианту, где под пробельными понимаются все символы, которые &lt;code&gt;char.IsWhiteSpace&lt;/code&gt; или &lt;code&gt;\s&lt;/code&gt; считают пробельными.
Таким образом, мы не пропустим ничего, как в случае с методом &lt;code&gt;Replace&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Мы уже знаем по &lt;a href="xref:2025-06-07-String-of-Digits"&gt;предыдущей статье&lt;/a&gt;, опция &lt;code&gt;Compiled&lt;/code&gt; положительно сказывается на производительности, будем указывать её.
Заодно проверим, как работает &lt;code&gt;GeneratedRegex&lt;/code&gt; в этом случае.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;| Method         | Runtime            |        Mean |      Median | Allocated |
|----------------|--------------------|------------:|------------:|----------:|
| RegexCompiled  | .NET Framework 4.8 | 872.0395 ns | 856.0633 ns |    1115 B |
| RegexGenerated | .NET Framework 4.8 |         N/A |         N/A |       N/A |
| RegexCompiled  | .NET Core 3.1      | 709.4504 ns | 696.5562 ns |     896 B |
| RegexGenerated | .NET Core 3.1      |         N/A |         N/A |       N/A |
| RegexCompiled  | .NET 8.0           | 202.2732 ns | 202.0966 ns |      64 B |
| RegexGenerated | .NET 8.0           | 191.2238 ns | 188.5849 ns |      64 B |
| RegexCompiled  | .NET 10.0          | 183.2023 ns | 181.1374 ns |      64 B |
| RegexGenerated | .NET 10.0          | 138.8752 ns | 138.8915 ns |      64 B |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Если в старых версиях фреймворка это решение не выдерживает сравнения даже с примитивным &lt;code&gt;Replace()&lt;/code&gt;, то в 8-й и 10-й версии мы видим значительное улучшение.&lt;/p&gt;
&lt;p&gt;А самое интересное — обратите внимание на потребление памяти. Всего 64 байта — итоговая строка. Это минимум, который можно получить в данном случае.
Дело в том, что в новых версиях .NET &lt;code&gt;Regex&lt;/code&gt; собирает итоговую строку из "кусочков" исходной без использования промежуточных состояний.
За счет этого удаётся избежать дополнительной аллокации.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/remove-whitespaces/replace-regex.png" alt="Сравнение методов Replace и Regex"&gt;&lt;/p&gt;
&lt;p&gt;Удивительно, насколько оптимизированными стали регулярные выражения - мы видим кратный рост производительности и уменьшение использования памяти.&lt;/p&gt;
&lt;p&gt;Но попробуем поискать более быстрый способ очистки строк.&lt;/p&gt;
&lt;h1 id="split-concat"&gt;Split + Concat&lt;/h1&gt;
&lt;p&gt;Еще один из способов удалить все пробелы — выделить из строки только непрерывные последовательности не пробельных символов, а потом собрать их в новую строку.&lt;/p&gt;
&lt;p&gt;Это можно сделать с помощью метода &lt;code&gt;Split&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;private static string SplitConcat(string s)
{
    var parts = s.Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
    return string.Concat(parts);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Передавая &lt;code&gt;null&lt;/code&gt; в качестве первого параметра мы как раз хотим разделить нашу исходную строку по пробельным символам с использованием метода &lt;code&gt;char.IsWhiteSpace&lt;/code&gt;.
Этот метод работает с более широким набором пробельных символов, чем те, что мы описывали вначале.
И указываем опцию &lt;code&gt;RemoveEmptyEntries&lt;/code&gt; чтобы в результирующем массиве &lt;code&gt;parts&lt;/code&gt; не оказалось пустых строк.&lt;/p&gt;
&lt;p&gt;Проведем бенчмарк и сравним с предыдущими результатами. Для наглядности оставим только последние версии фреймворка.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;| Method         | Runtime            |        Mean |      Median | Allocated |
|----------------|--------------------|------------:|------------:|----------:|
| Replace        | .NET 8.0           | 178.9700 ns | 177.7641 ns |     216 B |
| RegexCompiled  | .NET 8.0           | 202.2732 ns | 202.0966 ns |      64 B |
| RegexGenerated | .NET 8.0           | 191.2238 ns | 188.5849 ns |      64 B |
| SplitConcat    | .NET 8.0           |  97.6117 ns |  97.6992 ns |     376 B |
| Replace        | .NET 10.0          | 157.4850 ns | 156.3763 ns |     216 B |
| RegexCompiled  | .NET 10.0          | 183.2023 ns | 181.1374 ns |      64 B |
| RegexGenerated | .NET 10.0          | 138.8752 ns | 138.8915 ns |      64 B |
| SplitConcat    | .NET 10.0          |  90.2844 ns |  90.6640 ns |     376 B |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/remove-whitespaces/split-concat.png" alt="Сравнение всех методов со Split+Concat"&gt;&lt;/p&gt;
&lt;p&gt;На данный момент этот способ является самым производительным во всех фреймворках.
Но, к сожалению, он потребляет неприлично много памяти. Даже больше, чем трехкратный вызов метода &lt;code&gt;Replace()&lt;/code&gt;.
И большая часть этих затрат приходится на вызов метода &lt;code&gt;Split()&lt;/code&gt;. Как мы уже знаем, результирующая строка занимает всего 64 байта, а значит 312 байт мы тратим на массив &lt;code&gt;parts&lt;/code&gt;.
Это большой симптом того, что мы выбрали некорректный способ для реализации задачи. Ведь действительно, сам по себе массив &lt;code&gt;parts&lt;/code&gt; нам не нужен.
Это только промежуточный результат, который мы потом должны объединить в строку, а сам массив отбросить.&lt;/p&gt;
&lt;h1 id="linq"&gt;LINQ&lt;/h1&gt;
&lt;p&gt;Мы можем ускориться и по возможности не создавать лишних объектов.
Еще один популярный вариант:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;private static string Linq(string s)
{
    return new string(s.Where(c =&amp;gt; !char.IsWhiteSpace(c)).ToArray());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Берем из нашей исходной строки только не пробельные символы и создаём из них новую строку.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;| Method           | Runtime            |        Mean |      Median | Allocated |
|------------------|--------------------|------------:|------------:|----------:|
| Linq             | .NET Framework 4.8 | 455.1868 ns | 457.0586 ns |     449 B |
| Linq             | .NET Core 3.1      | 331.2851 ns | 331.8324 ns |     448 B |
| Linq             | .NET 8.0           | 166.4657 ns | 165.9890 ns |     448 B |
| Linq             | .NET 10.0          |  70.9343 ns |  71.1085 ns |     192 B |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;В общей сводной таблице он есть, но в рекомендациях не рассматривается из‑за аллокаций.
На это влияет необходимость создания какого-то промежуточного массива для символов без пробелов.&lt;/p&gt;
&lt;h1 id="array-buffer"&gt;Array buffer&lt;/h1&gt;
&lt;p&gt;Если нам всё равно нужно создавать какой-то массив, сделаем это сами заранее.
Подготовим массив символов длинной исходной строки, заполним его символами без пробелов и создадим из него новую строку:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;private static string Buffer(string s)
{
    var buffer = new char[s.Length];
    var index = 0;

    foreach (var c in s)
    {
        if (!char.IsWhiteSpace(c))
        {
            buffer[index++] = c;
        }
    }

    return new string(buffer, 0, index);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;И посмотрим на результаты:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;| Method           | Runtime            |        Mean |      Median | Allocated |
|------------------|--------------------|------------:|------------:|----------:|
| Buffer           | .NET Framework 4.8 |  72.8121 ns |  73.0219 ns |     168 B |
| Buffer           | .NET Core 3.1      |  56.3518 ns |  55.7937 ns |     160 B |
| Buffer           | .NET 8.0           |  38.5226 ns |  38.5226 ns |     160 B |
| Buffer           | .NET 10.0          |  31.0592 ns |  31.1676 ns |     160 B |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Выглядит впечатляюще - лучший метод по производительности на данный момент:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/remove-whitespaces/array-buffer.png" alt="Производительность метода Buffer"&gt;&lt;/p&gt;
&lt;p&gt;А главное, мы видим стабильное потребление памяти: только затраты на буфер и новую строку.&lt;/p&gt;
&lt;p&gt;Но есть еще один трюк, который нам поможет улучшить результаты.&lt;/p&gt;
&lt;h1 id="stackalloc-array-buffer"&gt;Stackalloc array buffer&lt;/h1&gt;
&lt;p&gt;Как мы знаем, такие типы данных как массивы, хранятся в управляемой куче (как раз эти данные попадают в метрику &lt;code&gt;Allocated&lt;/code&gt; в бенчмарках).
И доступ к этим данным несколько медленнее, чем к тем, что хранятся на стеке.&lt;/p&gt;
&lt;p&gt;Обычно мы не управляем тем, где будут размещаться данные, но в .NET есть специальное выражение &lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/stackalloc"&gt;&lt;code&gt;stackalloc&lt;/code&gt;&lt;/a&gt;, с помощью него можно выделить блок памяти прямо на стеке:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;private static unsafe string StackallocBuffer(string s)
{
    var buffer = stackalloc char[s.Length];
    var index = 0;

    foreach (var c in s)
    {
        if (!char.IsWhiteSpace(c))
        {
            buffer[index++] = c;
        }
    }

    return new string(buffer, 0, index);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;И запустим бенчмарк:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;| Method           | Runtime            |        Mean |      Median | Allocated |
|------------------|--------------------|------------:|------------:|----------:|
| StackallocBuffer | .NET Framework 4.8 |  72.5095 ns |  70.6909 ns |      72 B |
| StackallocBuffer | .NET Core 3.1      |  50.3178 ns |  49.4475 ns |      64 B |
| StackallocBuffer | .NET 8.0           |  32.9556 ns |  32.7873 ns |      64 B |
| StackallocBuffer | .NET 10.0          |  26.7175 ns |  26.4192 ns |      64 B |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Теперь наш временный массив &lt;code&gt;buffer&lt;/code&gt; создаётся не в общей куче, от чего получается еще небольшая прибавка в производительности и минимальное потребление памяти.&lt;/p&gt;
&lt;p&gt;Но нужно учитывать, что размер стека ограничен и очень большие массивы создать не получится - получим &lt;code&gt;StackOverflowException&lt;/code&gt;. Размер стека по-умолчанию равен 1 МБ и нужно учитывать это ограничение.
Для коротких строк (десятки-сотни символов) это безопасно, для потенциально больших строк лучше оставить вариант с кучей.&lt;/p&gt;
&lt;h1 id="section"&gt;Заключение&lt;/h1&gt;
&lt;p&gt;Как обычно мы рассмотрели самые популярные способы очистки строк от пробельных символов. Все они дают одинаковый результат, но используют разные механизмы.
Если эта операция является частотной в вашем сценарии, то посмотрите, насколько оптимальный вариант вы используете.
Виден значительный прогресс в каждой версии .NET и, в отличие от проверки строки на числа, здесь нет каких-то подводных камней.&lt;/p&gt;
&lt;p&gt;Еще в общем бенчмарке есть вариант с использованием &lt;code&gt;StringBuilder&lt;/code&gt;, но фактически он очень похож на решение с &lt;code&gt;Buffer&lt;/code&gt; - также проходит строку один раз и накапливает символы без пробелов во внутренний буфер, поэтому подробно я про него не пишу, но можно посмотреть на результаты в таблице.&lt;/p&gt;
&lt;p&gt;Рекомендации:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Если для вас важна простота решения и его читабельность, то подходят разные варианты &lt;code&gt;Regex&lt;/code&gt; и метод &lt;code&gt;Replace&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Для "горячих" мест используйте &lt;code&gt;Buffer&lt;/code&gt; или &lt;code&gt;StackallocBuffer&lt;/code&gt; в зависимости от ограничений на длину строки. Обычно это какие-то парсеры, логирование, нормализация входящих запросов и т.п.&lt;/li&gt;
&lt;li&gt;Для старых версий фреймворка избегайте использования регулярных выражений для простых шаблонов - это будет не эффективно.&lt;/li&gt;
&lt;/ul&gt;
&lt;details&gt;
&lt;summary&gt;Полный код бенчмарка. Нажмите, чтобы развернуть.&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace WhitespacesBenchmark
{
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net10_0)]
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.NetCoreApp31)]
[SimpleJob(RuntimeMoniker.Net48)]
[HideColumns("Job", "Error", "StdDev", "Gen0")]
public partial class RemoveWhitespacesBenchmark
{
        private const string Value = " \tString  \t with \n\n\t\t whitespaces \t";

        [Benchmark]
        public string Replace()
        {
            return Replace(Value);
        }

        private static string Replace(string s)
        {
            return s.Replace(" ", string.Empty).Replace("\t", string.Empty).Replace("\n", string.Empty);
        }

        [Benchmark]
        public string SplitConcat()
        {
            return SplitConcat(Value);
        }

        private static string SplitConcat(string s)
        {
            var parts = s.Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
            return string.Concat(parts);
        }

        [Benchmark]
        public string Linq()
        {
            return Linq(Value);
        }

        private static string Linq(string s)
        {
            return new string(s.Where(c =&amp;gt; !char.IsWhiteSpace(c)).ToArray());
        }

        private static readonly Regex EmptySpacesCompiled = new Regex(@"\s+", RegexOptions.Compiled);

#if NET7_0_OR_GREATER
        private static readonly Regex EmptySpacesGenerated = GenerateRegex();

        [GeneratedRegex(@"\s+")]
        private static partial Regex GenerateRegex();
#endif

        [Benchmark]
        public string RegexCompiled()
        {
            return RegexCompiled(Value);
        }

        private static string RegexCompiled(string s)
        {
            return EmptySpacesCompiled.Replace(s, string.Empty);
        }

        [Benchmark]
        public string RegexGenerated()
        {
            return RegexGenerated(Value);
        }

        private static string RegexGenerated(string s)
        {
#if NET7_0_OR_GREATER
            return EmptySpacesGenerated.Replace(s, string.Empty);
#else
            return s;
#endif
        }

        [Benchmark]
        public string StringBuilder()
        {
            return StringBuilder(Value);
        }

        private static string StringBuilder(string s)
        {
            var sb = new StringBuilder(s.Length);

            foreach (var c in s)
            {
                if (!char.IsWhiteSpace(c))
                {
                    sb.Append(c);
                }
            }

            return sb.ToString();
        }

        [Benchmark]
        public string Buffer()
        {
            return Buffer(Value);
        }

        private static string Buffer(string s)
        {
            var buffer = new char[s.Length];
            var index = 0;

            foreach (var c in s)
            {
                if (!char.IsWhiteSpace(c))
                {
                    buffer[index++] = c;
                }
            }

            return new string(buffer, 0, index);
        }

        [Benchmark]
        public string StackallocBuffer()
        {
            return StackallocBuffer(Value);
        }

        private static unsafe string StackallocBuffer(string s)
        {
            var buffer = stackalloc char[s.Length];
            var index = 0;

            foreach (var c in s)
            {
                if (!char.IsWhiteSpace(c))
                {
                    buffer[index++] = c;
                }
            }

            return new string(buffer, 0, index);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;Все результаты бенчмарков. Нажмите, чтобы развернуть.&lt;/summary&gt;
&lt;pre&gt;&lt;code&gt;BenchmarkDotNet v0.15.6, Windows 10 (10.0.19045.6456/22H2/2022Update)
AMD Ryzen 7 7840H with Radeon 780M Graphics 3.80GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 10.0.100
[Host]             : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4
.NET 10.0          : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4
.NET 8.0           : .NET 8.0.22 (8.0.22, 8.0.2225.52707), X64 RyuJIT x86-64-v4
.NET Core 3.1      : .NET Core 3.1.32 (3.1.32, 4.700.22.55902), X64 RyuJIT VectorSize=256
.NET Framework 4.8 : .NET Framework 4.8.1 (4.8.9310.0), X64 RyuJIT VectorSize=256


| Method           | Runtime            |        Mean |      Median | Allocated |
|------------------|--------------------|------------:|------------:|----------:|
| Replace          | .NET Framework 4.8 | 230.9093 ns | 227.7122 ns |     233 B |
| SplitConcat      | .NET Framework 4.8 | 164.1033 ns | 161.6289 ns |     610 B |
| Linq             | .NET Framework 4.8 | 455.1868 ns | 457.0586 ns |     449 B |
| RegexCompiled    | .NET Framework 4.8 | 872.0395 ns | 856.0633 ns |    1115 B |
| RegexGenerated   | .NET Framework 4.8 |         N/A |         N/A |       N/A |
| StringBuilder    | .NET Framework 4.8 | 115.0854 ns | 115.5498 ns |     217 B |
| Buffer           | .NET Framework 4.8 |  72.8121 ns |  73.0219 ns |     168 B |
| StackallocBuffer | .NET Framework 4.8 |  72.5095 ns |  70.6909 ns |      72 B |
| Replace          | .NET Core 3.1      | 158.6589 ns | 158.1439 ns |     216 B |
| SplitConcat      | .NET Core 3.1      | 122.7668 ns | 120.7053 ns |     376 B |
| Linq             | .NET Core 3.1      | 331.2851 ns | 331.8324 ns |     448 B |
| RegexCompiled    | .NET Core 3.1      | 709.4504 ns | 696.5562 ns |     896 B |
| RegexGenerated   | .NET Core 3.1      |         N/A |         N/A |       N/A |
| StringBuilder    | .NET Core 3.1      |  88.3582 ns |  88.6698 ns |     208 B |
| Buffer           | .NET Core 3.1      |  56.3518 ns |  55.7937 ns |     160 B |
| StackallocBuffer | .NET Core 3.1      |  50.3178 ns |  49.4475 ns |      64 B |
| Replace          | .NET 8.0           | 178.9700 ns | 177.7641 ns |     216 B |
| SplitConcat      | .NET 8.0           |  97.6117 ns |  97.6992 ns |     376 B |
| Linq             | .NET 8.0           | 166.4657 ns | 165.9890 ns |     448 B |
| RegexCompiled    | .NET 8.0           | 202.2732 ns | 202.0966 ns |      64 B |
| RegexGenerated   | .NET 8.0           | 191.2238 ns | 188.5849 ns |      64 B |
| StringBuilder    | .NET 8.0           |  50.1718 ns |  50.4215 ns |     208 B |
| Buffer           | .NET 8.0           |  38.5226 ns |  38.5226 ns |     160 B |
| StackallocBuffer | .NET 8.0           |  32.9556 ns |  32.7873 ns |      64 B |
| Replace          | .NET 10.0          | 157.4850 ns | 156.3763 ns |     216 B |
| SplitConcat      | .NET 10.0          |  90.2844 ns |  90.6640 ns |     376 B |
| Linq             | .NET 10.0          |  70.9343 ns |  71.1085 ns |     192 B |
| RegexCompiled    | .NET 10.0          | 183.2023 ns | 181.1374 ns |      64 B |
| RegexGenerated   | .NET 10.0          | 138.8752 ns | 138.8915 ns |      64 B |
| StringBuilder    | .NET 10.0          |  37.4249 ns |  37.6158 ns |     208 B |
| Buffer           | .NET 10.0          |  31.0592 ns |  31.1676 ns |     160 B |
| StackallocBuffer | .NET 10.0          |  26.7175 ns |  26.4192 ns |      64 B |
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Отметка &lt;code&gt;N/A&lt;/code&gt; ставится для методов, которые не существуют в данной версии фреймворка.&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Инициализатор коллекций: List&lt;T&gt;</title>
			<link>https://blog.rogatnev.net/posts/ru/2025/09/List-initializer.html</link>
			<description>Оцените влияние предварительной инициализации списка на производительность и работу с памятью в приложении.</description>
			<author>Сергей Рогатнев</author>
			<guid>https://blog.rogatnev.net/posts/ru/2025/09/List-initializer.html</guid>
			<pubDate>Sat, 27 Sep 2025 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;Мы все привыкли писать &lt;code&gt;new List&amp;lt;int&amp;gt; { 1, 2, 3, 4 }&lt;/code&gt; или &lt;code&gt;new int[] { 1, 2, 3, 4}&lt;/code&gt;, чтобы инициализировать коллекции какими-то значениями. Синтаксически это выглядит похоже, но поведение отличается, и вам следует быть осторожными, если вы заботитесь о производительности.&lt;/p&gt;
&lt;!--more--&gt;
&lt;h1 id="section"&gt;Массив&lt;/h1&gt;
&lt;p&gt;Как мы знаем, &lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/arrays"&gt;массивы&lt;/a&gt; содержат последовательность элементов фиксированного размера. После создания размер нельзя изменить в течение всего времени жизни массива.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var array = new int[] { 1, 2, 3, 4};
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id="listt"&gt;List&amp;lt;T&amp;gt;&lt;/h1&gt;
&lt;p&gt;Когда мы не знаем конечный размер коллекции или нам нужно добавлять/удалять элементы в течение ее жизненного цикла, подходит использование тип &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1?view=net-8.0"&gt;&lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var list = new List&amp;lt;int&amp;gt;();
list.Add(1);
list.Add(2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;На первый взгляд может показаться, что мы всегда можем использовать список вместо массива — у него есть все возможности массива, но его также можно динамически изменять.
Но чтобы решить, использовать ли список, нам нужно больше узнать о его внутренней структуре.&lt;/p&gt;
&lt;p&gt;Часть исходного кода:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public class List&amp;lt;T&amp;gt; : IList&amp;lt;T&amp;gt;, IList, IReadOnlyList&amp;lt;T&amp;gt;
{
    private const int DefaultCapacity = 4;

    internal T[] _items;
    internal int _size;

    private static readonly T[] s_emptyArray = new T[0];

    // Constructs a List. The list is initially empty and has a capacity
    // of zero. Upon adding the first element to the list the capacity is
    // increased to DefaultCapacity, and then increased in multiples of two
    // as required.
    public List()
    {
        _items = s_emptyArray;
    }
    ...

    public int Capacity
    {
        get =&amp;gt; _items.Length;
        set {...}
    }

    public int Count =&amp;gt; _size;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;_items&lt;/code&gt; - внутренний массив для хранения элементов;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_size&lt;/code&gt; - количество элементов в массиве и общий размер списка;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Capacity&lt;/code&gt; - размер массива &lt;code&gt;_items&lt;/code&gt; и максимальное количество элементов, которые могут в него поместиться без изменения размера.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="section-1"&gt;Изменение размера списка&lt;/h2&gt;
&lt;p&gt;Проще говоря, можно сказать, что список — это массив, который может изменять размер при необходимости.&lt;/p&gt;
&lt;p&gt;Каждый раз, когда мы пытаемся добавить еще один элемент, список проверяет, достаточно ли в &lt;code&gt;_items&lt;/code&gt; свободного места, в противном случае он устанавливает новую емкость:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;internal void Grow(int capacity)
{
    ...
    int newCapacity = _items.Length == 0 ? DefaultCapacity : 2 * _items.Length;
    ...
    Capacity = newCapacity;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Давайте посмотрим поближе на свойство &lt;code&gt;Capacity&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// Gets and sets the capacity of this list.  The capacity is the size of
// the internal array used to hold items.  When set, the internal
// array of the list is reallocated to the given capacity.
public int Capacity
{
    get =&amp;gt; _items.Length;
    set
    {
        if (value &amp;lt; _size)
        {
            ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity);
        }

        if (value != _items.Length)
        {
            if (value &amp;gt; 0)
            {
                T[] newItems = new T[value];
                if (_size &amp;gt; 0)
                {
                    Array.Copy(_items, newItems, _size);
                }
                _items = newItems;
            }
            else
            {
                _items = s_emptyArray;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Итак, что мы видим? Изначально каждый список создается с пустым внутренним массивом. После добавления первого элемента список создает новый массив на 4 элемента (&lt;code&gt;DefaultCapacity&lt;/code&gt; равно 4). И когда текущий массив исчерпан, создается новый с удвоенным размером, и все элементы копируются.&lt;/p&gt;
&lt;h1 id="section-2"&gt;Производительность&lt;/h1&gt;
&lt;p&gt;Что происходит, когда мы создаем новый список и инициализируем его значениями?&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var list = new List&amp;lt;int&amp;gt; { 1, 2, 3, 4, 5 };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Это выглядит как инициализация массива, но работает совершенно по-другому. Согласно &lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers#collection-initializers"&gt;документации&lt;/a&gt;, любой тип, который реализует &lt;code&gt;IEnumerable&lt;/code&gt; и имеет метод &lt;code&gt;Add&lt;/code&gt; может использоваться с инициализатором коллекции.&lt;/p&gt;
&lt;p&gt;Таким образом, предыдущий пример — это просто краткая форма последовательных вызовов метода &lt;code&gt;Add&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var list = new List&amp;lt;int&amp;gt;();
list.Add(1);
list.Add(2);
list.Add(3);
list.Add(4);
list.Add(5);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Компилятор просто преобразует короткий инициализатор коллекции и автоматически добавляет необходимые вызовы. И это может вызвать снижение производительности.&lt;/p&gt;
&lt;p&gt;Давайте подробнее рассмотрим процесс добавления 5 элементов:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Перед первым вызовом &lt;code&gt;Add&lt;/code&gt; список пуст, внутренний массив пуст.&lt;/li&gt;
&lt;li&gt;Первый добавленный элемент создает новый внутренний массив на 4 элемента.&lt;/li&gt;
&lt;li&gt;Элементы 2, 3, 4 при добавлении ничего не меняют.&lt;/li&gt;
&lt;li&gt;Когда мы добавляем пятый элемент, внутренний массив заполнен и требует изменения размера. Создается новый массив размером 8, все элементы копируются из предыдущего массива, и добавляется пятый элемент.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;В итоге у нас есть список с 5 элементами и внутренний массив на 8 элементов. При этом мы создали 2 массива, и конечный тратит 37.5% своего пространства впустую.
Как вы могли догадаться, создание новых массивов и копирование элементов приводит к выделению памяти и занимает дополнительное время.&lt;/p&gt;
&lt;p&gt;Это может стать неприятным сюрпризом в критически важных местах. Есть ли у нас решение? Да!&lt;/p&gt;
&lt;h2 id="capacity"&gt;Capacity&lt;/h2&gt;
&lt;p&gt;Если мы знаем или предполагаем конечный размер списка, мы можем создать его с начальной емкостью (&lt;code&gt;Capacity&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var list = new List&amp;lt;int&amp;gt;(5);
list.Add(1);
list.Add(2);
list.Add(3);
list.Add(4);
list.Add(5);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Или&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var list = new List&amp;lt;int&amp;gt;(5) { 1, 2, 3, 4, 5 };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Теперь мы сразу создаем один внутренний массив на 5 элементов и больше нет никаких ненужных выделений памяти.&lt;/p&gt;
&lt;h2 id="section-3"&gt;Бенчмарк&lt;/h2&gt;
&lt;p&gt;Давайте сравним создание списков с начальной емкостью и без нее.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Код бенчмарка. Нажмите, чтобы развернуть.&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace ListBenchmark
{
    [ShortRunJob(RuntimeMoniker.Net48)]
    [ShortRunJob(RuntimeMoniker.NetCoreApp31)]
    [ShortRunJob(RuntimeMoniker.Net80)]
    [ShortRunJob(RuntimeMoniker.Net10_0)]
    [MemoryDiagnoser]
    [HideColumns("Job", "Error", "StdDev", "Gen0")]
    public class InitListBenchmark
    {
        [BenchmarkCategory("One")]
        [Benchmark]
        public List&amp;lt;int&amp;gt; InitList1()
        {
            return new List&amp;lt;int&amp;gt; {1};
        }

        [BenchmarkCategory("One")]
        [Benchmark]
        public List&amp;lt;int&amp;gt; InitListWithSize1()
        {
            return new List&amp;lt;int&amp;gt;(1) {1};
        }

        [BenchmarkCategory("Five")]
        [Benchmark]
        public List&amp;lt;int&amp;gt; InitList5()
        {
            return new List&amp;lt;int&amp;gt; {1, 2, 3, 4, 5};
        }
        
        [BenchmarkCategory("Five")]
        [Benchmark]
        public List&amp;lt;int&amp;gt; InitListWithSize5()
        {
            return new List&amp;lt;int&amp;gt;(5) {1, 2, 3, 4, 5};
        }
        
        [BenchmarkCategory("Ten")]
        [Benchmark]
        public List&amp;lt;int&amp;gt; InitList10()
        {
            return new List&amp;lt;int&amp;gt; {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
        }
        
        [BenchmarkCategory("Ten")]
        [Benchmark]
        public List&amp;lt;int&amp;gt; InitListWithSize10()
        {
            return new List&amp;lt;int&amp;gt;(10) {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;BenchmarkDotNet v0.15.6, Windows 10 (10.0.19045.6456/22H2/2022Update)
AMD Ryzen 7 7840H with Radeon 780M Graphics 3.80GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 10.0.100
[Host]                      : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4
ShortRun-.NET 10.0          : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4
ShortRun-.NET 8.0           : .NET 8.0.22 (8.0.22, 8.0.2225.52707), X64 RyuJIT x86-64-v4
ShortRun-.NET Core 3.1      : .NET Core 3.1.32 (3.1.32, 4.700.22.55902), X64 RyuJIT VectorSize=256
ShortRun-.NET Framework 4.8 : .NET Framework 4.8.1 (4.8.9310.0), X64 RyuJIT VectorSize=256

IterationCount=3  LaunchCount=1  WarmupCount=3

| Method             | Runtime            |      Mean | Allocated |
|--------------------|--------------------|----------:|----------:|
| InitList1          | .NET Framework 4.8 | 13.904 ns |      80 B |
| InitListWithSize1  | .NET Framework 4.8 |  6.658 ns |      72 B |
| InitList1          | .NET Core 3.1      | 11.091 ns |      72 B |
| InitListWithSize1  | .NET Core 3.1      |  7.407 ns |      64 B |
| InitList1          | .NET 8.0           | 10.084 ns |      72 B |
| InitListWithSize1  | .NET 8.0           |  6.838 ns |      64 B |
| InitList1          | .NET 10.0          |  8.170 ns |      72 B |
| InitListWithSize1  | .NET 10.0          |  6.638 ns |      64 B |
| InitList5          | .NET Framework 4.8 | 31.298 ns |     136 B |
| InitListWithSize5  | .NET Framework 4.8 | 12.013 ns |      88 B |
| InitList5          | .NET Core 3.1      | 26.466 ns |     128 B |
| InitListWithSize5  | .NET Core 3.1      |  9.446 ns |      80 B |
| InitList5          | .NET 8.0           | 23.714 ns |     128 B |
| InitListWithSize5  | .NET 8.0           | 15.587 ns |      80 B |
| InitList5          | .NET 10.0          | 20.002 ns |     128 B |
| InitListWithSize5  | .NET 10.0          |  8.712 ns |      80 B |
| InitList10         | .NET Framework 4.8 | 53.488 ns |     225 B |
| InitListWithSize10 | .NET Framework 4.8 | 18.185 ns |     104 B |
| InitList10         | .NET Core 3.1      | 44.371 ns |     216 B |
| InitListWithSize10 | .NET Core 3.1      | 12.496 ns |      96 B |
| InitList10         | .NET 8.0           | 38.707 ns |     216 B |
| InitListWithSize10 | .NET 8.0           | 12.024 ns |      96 B |
| InitList10         | .NET 10.0          | 33.854 ns |     216 B |
| InitListWithSize10 | .NET 10.0          | 15.822 ns |      96 B |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Когда мы устанавливаем начальную емкость, это не приводит к ненужным выделениям памяти, и мы видим лучшую производительность.
И нет избыточного трафика памяти. Чем больше элементов мы добавляем в список, тем большую разницу мы видим в бенчмарках.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/list/memory-allocation.png" alt="Разница в используемой памяти"&gt;&lt;/p&gt;
&lt;h2 id="section-4"&gt;Анализатор&lt;/h2&gt;
&lt;p&gt;Если производительность критична для вашего проекта, вы должны обращать внимание на эти ситуации. Автоматическая диагностика может вам помочь.
Я поддерживаю набор диагностических инструментов на основе Roslyn - &lt;a href="https://www.nuget.org/packages/Collections.Analyzer"&gt;Collections.Analyzer&lt;/a&gt;, и теперь он может обнаруживать списки с инициализатором коллекции и без начальной емкости.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/list/list-initializer.gif" alt="Автоматическое указание первоначального размера List"&gt;&lt;/p&gt;
&lt;p&gt;Важно понимать, что анализатор устанавливает только начальный размер коллекции, чтобы избежать лишних аллокаций на начальном этапе заполнения коллекции.
Он не может предсказать, как изменится размер коллекции в будущем.&lt;/p&gt;
&lt;h1 id="section-5"&gt;Рекомендации&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Если вам нужна статическая коллекция, которую вы не будете изменять (добавлять или удалять элементы) — используйте массивы.&lt;/li&gt;
&lt;li&gt;Если вы создаете список и точно знаете его будущий размер — установите начальную емкость с этим размером.&lt;/li&gt;
&lt;li&gt;Если вы создаете список и не знаете его будущий размер — установите ожидаемый размер.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="section-6"&gt;Ссылки&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers#collection-initializers"&gt;Collection initializers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Backs/Collections.Analyzer"&gt;Collections.Analyzer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Паттерны: Спецификация</title>
			<link>https://blog.rogatnev.net/posts/ru/2025/08/Patterns-Specification.html</link>
			<description>Реализация паттерна спецификация на C# и Entity Framework. Централизация бизнес-логики, улучшение тестируемости и независимость от слоя хранения.</description>
			<author>Сергей Рогатнев</author>
			<guid>https://blog.rogatnev.net/posts/ru/2025/08/Patterns-Specification.html</guid>
			<pubDate>Fri, 29 Aug 2025 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;Паттерн Спецификация (Specification) объединяет в себе доменный подход к построению приложений и Entity Framework.
Этот паттерн создан для управления бизнес-правилами и соединяет наш код с ними. Эта статья показывает пример реализации этого паттерна на базе Entity Framework.&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;Для начала опишем нашу модель:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public sealed class User
{
    public User()
    {
        Contacts = new List&amp;lt;Contact&amp;gt;();
    }
    public int Id { get; set; }

    public string Login { get; set; }

    public bool IsActive { get; set; }

    public ICollection&amp;lt;Contact&amp;gt; Contacts { get; }
}

public abstract class Contact
{
    public int Id { get; set; }

    public bool IsActive { get; set; }
}

public class Phone : Contact
{
    public string PhoneCode { get; set; }
    public string PhoneNumber { get; set; }
}

public class Email : Contact
{
    public string Value { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Она достаточно простая, но подходит нам в качестве примера.
Теперь добавим немного магии Entity Framework для создания контекста данных:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public sealed class Context : DbContext
{
    public Context(DbContextOptions builderOptions)
        :base(builderOptions)
    {
        
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity&amp;lt;User&amp;gt;().HasMany(o =&amp;gt; o.Contacts).WithOne();
        modelBuilder.Entity&amp;lt;Contact&amp;gt;()
            .HasDiscriminator&amp;lt;int&amp;gt;(&amp;quot;ContactType&amp;quot;)
            .HasValue&amp;lt;Phone&amp;gt;(1)
            .HasValue&amp;lt;Email&amp;gt;(2);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Теперь мы можем использовать SQL Server в качестве хранилища.&lt;/p&gt;
&lt;p&gt;Хорошо спроектированные приложения разделяют хранилище и доменную модель, поэтому мы добавим репозиторий для нашего класса &lt;code&gt;User&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public interface IUserRepository
{
    void Add(User user);
    User Get(int id);
}

public sealed class UserRepository : IUserRepository
{
    private readonly Context _context;

    public UserRepository(Context context)
    {
        _context = context;
    }

    public void Add(User user)
    {
        _context.Set&amp;lt;User&amp;gt;().Add(user);
    }

    public User Get(int id)
    {
        return _context.Set&amp;lt;User&amp;gt;().FirstOrDefault(o =&amp;gt; o.Id == id);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Сейчас это выглядит как стандартное приложение, ничего особенного.
Самое интересное начинается, когда нам требуется реализовать различные запросы в наше хранилище.
Давайте рассмотрим самые распространенные способы.&lt;/p&gt;
&lt;h1 id="section"&gt;Фильтры&lt;/h1&gt;
&lt;p&gt;Для примера давайте создадим несколько фильтров:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Найти всех пользователей с активными телефонами&lt;/li&gt;
&lt;li&gt;Найти всех пользователей по префиксу логина&lt;/li&gt;
&lt;li&gt;Найти всех пользователей по телефонному номеру&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="iqueryable"&gt;Путь IQueryable&lt;/h1&gt;
&lt;p&gt;Самый простой способ - предоставить метод репозитория, который возвращает &lt;code&gt;IQueryable&amp;lt;User&amp;gt;&lt;/code&gt; и позволить разработчикам запрашивать любые данные:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public interface IUserRepository
{
    void Add(User user);
    User Get(int id);
    IQueryable&amp;lt;User&amp;gt; Query();
}

public sealed class UserRepository : IUserRepository
{
    private readonly Context _context;

    public UserRepository(Context context)
    {
        _context = context;
    }

    public void Add(User user)
    {
        _context.Set&amp;lt;User&amp;gt;().Add(user);
    }

    public User Get(int id)
    {
        return _context.Set&amp;lt;User&amp;gt;().FirstOrDefault(o =&amp;gt; o.Id == id);
    }

    public IQueryable&amp;lt;User&amp;gt; Query()
    {
        return _context.Set&amp;lt;User&amp;gt;();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;С методом &lt;code&gt;Query&lt;/code&gt; мы можем реализовать наши фильтры:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public static void Search(IUserRepository repository)
{
    var users1 = repository.Query()
        .Where(o =&amp;gt; o.Contacts.Any(c =&amp;gt; c is Phone &amp;amp;&amp;amp; c.IsActive))
        .ToArray();

    var users2 = repository.Query()
        .Where(o =&amp;gt; o.Login.StartsWith(&amp;quot;log&amp;quot;))
        .ToArray();

    var users3 = repository.Query()
        .Where(o =&amp;gt; o.Contacts.Any(c =&amp;gt; c is Phone &amp;amp;&amp;amp; (c as Phone).PhoneNumber == &amp;quot;111-22-33&amp;quot;))
        .ToArray();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Это быстрое и простое решение, но имеющее серьезные недостатки:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Нам нужно повторять код фильтра, если нужно его переиспользовать в другом месте. Можно сделать изменение в одном месте и забыть в другом — вот они, недостатки дублирования кода.&lt;/li&gt;
&lt;li&gt;Бизнес-логика (наши фильтры) распределены по коду. Мы не можем просто выписать все правила, т.к. они находятся в разных частях приложения.&lt;/li&gt;
&lt;li&gt;Сложно тестировать. Мы не можем проверить фильтры отдельно от хранилища.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="section-1"&gt;Методы расширения&lt;/h1&gt;
&lt;p&gt;Можно попробовать устранить недостатки предыдущего решения и собрать все фильтры в одном классе в виде методов расширений:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public static class UserQueryExtensions
{
    public static IEnumerable&amp;lt;User&amp;gt; FindWithActivePhones(this IQueryable&amp;lt;User&amp;gt; query)
    {
        return query.Where(o =&amp;gt; o.Contacts.Any(c =&amp;gt; c is Phone &amp;amp;&amp;amp; c.IsActive))
            .AsEnumerable();
    }

    public static IEnumerable&amp;lt;User&amp;gt; FindWithLoginPrefix(this IQueryable&amp;lt;User&amp;gt; query, string prefix)
    {
        return query.Where(o =&amp;gt; o.Login.StartsWith(prefix))
            .AsEnumerable();
    }

    public static IEnumerable&amp;lt;User&amp;gt; FindWithPhoneNumber(this IQueryable&amp;lt;User&amp;gt; query, string phoneNumber)
    {
        return query.Where(o =&amp;gt; o.Contacts.Any(c =&amp;gt; c is Phone &amp;amp;&amp;amp; (c as Phone).PhoneNumber == phoneNumber))
            .AsEnumerable();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public static void Search(IUserRepository repository)
{
    var users1 = repository.Query().FindWithActivePhones();

    var users2 = repository.Query().FindWithLoginPrefix(&amp;quot;log&amp;quot;);

    var users3 = repository.Query().FindWithPhoneNumber(&amp;quot;111-22-33&amp;quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Выглядит лучше, теперь вся бизнес-логика (фильтры) находятся в одном месте и их можно протестировать:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;[Test]
public void FindWithActivePhonesTest()
{
    var user = new User
    {
        Id = 1,
        Login = &amp;quot;login&amp;quot;
    };
    user.Contacts.Add(new Phone { IsActive = true, PhoneNumber = &amp;quot;123-321&amp;quot; });
    
    var queryable = new[] { user }.AsQueryable();

    var result = queryable.FindWithActivePhones();

    CollectionAssert.AreEquivalent(new[] { 1 }, result.Select(o =&amp;gt; o.Id));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Но наш репозиторий всё еще содержит метод &lt;code&gt;Query&lt;/code&gt;, что нехорошо, это может привести к неконтролируемому использованию.
Можно переместить всю логику внутрь репозитория:&lt;/p&gt;
&lt;h1 id="section-2"&gt;Фильтры в репозитории&lt;/h1&gt;
&lt;p&gt;Добавим все фильтр-методы в сам репозиторий:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public interface IUserRepository
{
    void Add(User user);
    User Get(int id);
    IEnumerable&amp;lt;User&amp;gt; FindWithActivePhones();
    IEnumerable&amp;lt;User&amp;gt; FindWithLoginPrefix(string prefix);
    IEnumerable&amp;lt;User&amp;gt; FindWithPhoneNumber(string phoneNumber);
}

public sealed class UserRepository : IUserRepository
{
    private readonly Context _context;

    public UserRepository(Context context)
    {
        _context = context;
    }

    public void Add(User user)
    {
        _context.Set&amp;lt;User&amp;gt;().Add(user);
    }

    public User Get(int id)
    {
        return _context.Set&amp;lt;User&amp;gt;().FirstOrDefault(o =&amp;gt; o.Id == id);
    }

    public IEnumerable&amp;lt;User&amp;gt; FindWithActivePhones()
    {
        return _context.Set&amp;lt;User&amp;gt;().Where(o =&amp;gt; o.Contacts.Any(c =&amp;gt; c is Phone &amp;amp;&amp;amp; c.IsActive))
            .AsEnumerable();
    }

    public IEnumerable&amp;lt;User&amp;gt; FindWithLoginPrefix(string prefix)
    {
        return _context.Set&amp;lt;User&amp;gt;().Where(o =&amp;gt; o.Login.StartsWith(prefix))
            .AsEnumerable();
    }

    public IEnumerable&amp;lt;User&amp;gt; FindWithPhoneNumber(string phoneNumber)
    {
        return _context.Set&amp;lt;User&amp;gt;().Where(o =&amp;gt; o.Contacts.Any(c =&amp;gt; c is Phone &amp;amp;&amp;amp; (c as Phone).PhoneNumber == phoneNumber))
            .AsEnumerable();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public static void Search(IUserRepository repository)
{
    var users1 = repository.FindWithActivePhones();

    var users2 = repository.FindWithLoginPrefix(&amp;quot;log&amp;quot;);

    var users3 = repository.FindWithPhoneNumber(&amp;quot;111-22-33&amp;quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Мы избавились от опасного метода &lt;code&gt;Query&lt;/code&gt;. Но всё еще есть сложности с тестированием — методы сильно связаны с хранилищем и Entity Framework.
Так же, наши фильтры могут использоваться только для доступа к хранилищу. А что, если мы хотим проверить вновь создаваемые сущности этими бизнес-правилами?
Сейчас это невозможно.&lt;/p&gt;
&lt;p&gt;На самом деле, мы бы могли остановиться на этом решении, его достаточно в 99% случаев.&lt;/p&gt;
&lt;p&gt;Но если вы используете Domain-Driven development (DDD), Entity Framework (или другую ORM) и активно используете юнит-тесты, то вам может подойти следующий подход.&lt;/p&gt;
&lt;h1 id="section-3"&gt;Спецификации&lt;/h1&gt;
&lt;p&gt;Вначале давайте опишем требования, которые для нас важны:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Хранить бизнес-правила в одном месте и не дублировать их&lt;/li&gt;
&lt;li&gt;Фильтры отделены от хранилища (могут быть использованы вне репозитория)&lt;/li&gt;
&lt;li&gt;Фильтры можно тестировать отдельно&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ключевая сущность паттерна - это &lt;code&gt;спецификация&lt;/code&gt;, специальный объект с бизнес-правилом.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public interface ISpecification&amp;lt;T&amp;gt;
{
    Expression&amp;lt;Func&amp;lt;T, bool&amp;gt;&amp;gt; IsSatisfiedBy { get; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Где &lt;code&gt;T&lt;/code&gt; - наша бизнес сущность (&lt;code&gt;User&lt;/code&gt; как в примерах выше).
В репозиторий добавляется метод, который работает со спецификацией:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public interface IUserRepository
{
    void Add(User user);
    User Get(int id);
    IEnumerable&amp;lt;User&amp;gt; Get(ISpecification&amp;lt;User&amp;gt; specification);
}

public sealed class UserRepository : IUserRepository
{
    private readonly Context _context;

    public UserRepository(Context context)
    {
        _context = context;
    }

    public void Add(User user)
    {
        _context.Set&amp;lt;User&amp;gt;().Add(user);
    }

    public User Get(int id)
    {
        return _context.Set&amp;lt;User&amp;gt;().FirstOrDefault(o =&amp;gt; o.Id == id);
    }

    public IEnumerable&amp;lt;User&amp;gt; Get(ISpecification&amp;lt;User&amp;gt; specification)
    {
        return _context.Set&amp;lt;User&amp;gt;()
            .Where(specification.IsSatisfiedBy)
            .AsEnumerable();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Entity Framework позволяет нам использовать концепцию LINQ to SQL через методы расширения.
Нам интересен метод &lt;code&gt;Where&lt;/code&gt; с предикатом фильтра:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public static IQueryable&amp;lt;TSource&amp;gt; Where&amp;lt;TSource&amp;gt;(this IQueryable&amp;lt;TSource&amp;gt; source, Expression&amp;lt;Func&amp;lt;TSource, bool&amp;gt;&amp;gt; predicate)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Реализуем наши бизнес-правила (спецификации):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public class ActivePhonesSpecification : ISpecification&amp;lt;User&amp;gt;
{
	public Expression&amp;lt;Func&amp;lt;User, bool&amp;gt;&amp;gt; IsSatisfiedBy =&amp;gt;
		o =&amp;gt; o.Contacts.Any(c =&amp;gt; c is Phone &amp;amp;&amp;amp; c.IsActive);
}

public class LoginPrefixSpecification : ISpecification&amp;lt;User&amp;gt;
{
	public LoginPrefixSpecification(string prefix)
	{
		IsSatisfiedBy = o =&amp;gt; o.Login.StartsWith(prefix);
	}
	public Expression&amp;lt;Func&amp;lt;User, bool&amp;gt;&amp;gt; IsSatisfiedBy { get; }
}

public class PhoneNumberSpecification:ISpecification&amp;lt;User&amp;gt;
{
	public PhoneNumberSpecification(string phoneNumber)
	{
		IsSatisfiedBy = o =&amp;gt; o.Contacts.Any(c =&amp;gt; c is Phone &amp;amp;&amp;amp; (c as Phone).PhoneNumber == phoneNumber);
	}
	public Expression&amp;lt;Func&amp;lt;User, bool&amp;gt;&amp;gt; IsSatisfiedBy { get; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Мы описали все правила в одном месте и они отделены от хранилища - работают только с доменной сущностью.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public static void Search(IUserRepository repository)
{
	var users1 = repository.Get(new ActivePhonesSpecification());

	var users2 = repository.Get(new LoginPrefixSpecification(&amp;quot;log&amp;quot;));

	var users3 = repository.Get(new PhoneNumberSpecification(&amp;quot;111-22-33&amp;quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Тестирование:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;[Test]
public void FindWithActivePhonesTest()
{
	var user = new User
	{
		Id = 1,
		Login = &amp;quot;login&amp;quot;
	};
	user.Contacts.Add(new Phone { IsActive = true, PhoneNumber = &amp;quot;123-321&amp;quot; });


	var queryable = new[] { user }.AsQueryable();

	var specification = new ActivePhonesSpecification();
	var result = queryable.Where(specification.IsSatisfiedBy);

	CollectionAssert.AreEquivalent(new[] { 1 }, result.Select(o =&amp;gt; o.Id));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Работа с правилами без хранилища:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;[Test]
public void FindWithActivePhonesTest()
{
	var user = new User
	{
		Id = 1,
		Login = &amp;quot;login&amp;quot;
	};
	user.Contacts.Add(new Phone { IsActive = true, PhoneNumber = &amp;quot;123-321&amp;quot; });

	var specification = new ActivePhonesSpecification();

	Assert.IsTrue(specification.IsSatisfiedBy.Compile().Invoke(user));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Как улучшение, мы можем скомпилировать метод из выражения &lt;code&gt;IsSatisfiedBy&lt;/code&gt;.&lt;/p&gt;
&lt;h1 id="section-4"&gt;Заключение&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;DDD описывает модели, репозитории и правила&lt;/li&gt;
&lt;li&gt;Entity Framework и LINQ позволяют организовать простой доступ к данным&lt;/li&gt;
&lt;li&gt;Спецификации хранят правила отдельно от уровня работы с данными&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="section-5"&gt;Ссылки&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/en-us/ef/core/"&gt;Entity Framework Core&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/en-us/previous-versions/msp-n-p/ff649690(v=pandp.10)"&gt;The Repository Pattern&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice"&gt;Design a DDD-oriented microservice&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Dapper: как кеширование может принести вред</title>
			<link>https://blog.rogatnev.net/posts/ru/2025/08/Dapper.html</link>
			<description>Исследование проблемы высокого потребления памяти при использовании кеширования в Dapper</description>
			<author>Сергей Рогатнев</author>
			<guid>https://blog.rogatnev.net/posts/ru/2025/08/Dapper.html</guid>
			<pubDate>Sun, 24 Aug 2025 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;Dapper — популярная библиотека, которая позволяет делать маппинг объектов из базы данных в типы C#.
В отличие от Entity Framework не является полноценной ORM, но пользуется большой популярностью за счет своей минималистичности.
В этой статье я расскажу, как поведение по-умолчанию может привести к значительному росту потребления памяти.&lt;/p&gt;
&lt;!--more--&gt;
&lt;h1 id="section"&gt;Профилирование&lt;/h1&gt;
&lt;p&gt;Эта история началась с одного приложения на работе. Метрики показывали, что со временем оно потребляло всё больше и больше памяти, хотя явных причин на то не было.
Я собрал дамп приложения и открыл его в профайлере.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/dapper/memory_profiler.png" alt="Memory profiler"&gt;&lt;/p&gt;
&lt;p&gt;Проблема была видна практически сразу — приложение держало в памяти 2 гигабайта каких-то строк, связанных с Dapper.
Сами строки содержали в себе тело SQL-запроса. Давайте посмотрим, с какими объектами в памяти они ассоциированы.&lt;/p&gt;
&lt;h1 id="dapper"&gt;Внутренности Dapper&lt;/h1&gt;
&lt;p&gt;Профайлер указывает нам на тип &lt;code&gt;SqlMapper.Identity&lt;/code&gt; и связанынй с ним &lt;code&gt;SqlMapper.CacheInfo&lt;/code&gt;. По названию очевидно, что это своего рода кеширование.
Давайте ближе взглянем на создание типа &lt;code&gt;SqlMapper.Identity&lt;/code&gt; в процессе выполнения запроса:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;  private static async Task&amp;lt;int&amp;gt; ExecuteImplAsync(
    IDbConnection cnn,
    CommandDefinition command,
    object? param)
  {
    SqlMapper.CacheInfo cacheInfo = SqlMapper.GetCacheInfo(new SqlMapper.Identity(command.CommandText, new CommandType?(command.CommandTypeDirect), cnn, (Type) null, param?.GetType()), param, command.AddToCache);
    // ...
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Как мы видим, в самом начале метода мы пытаемся получить что-то из кэша используя для этого в качестве ключа &lt;code&gt;SqlMapper.Identity&lt;/code&gt;,
который в свою очередь содержит полный текст запроса &lt;code&gt;command.CommandText&lt;/code&gt; и другие параметры.&lt;/p&gt;
&lt;p&gt;Хранится в кеше тип &lt;code&gt;SqlMapper.CacheInfo&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;  private class CacheInfo
  {
    private int hitCount;
    public SqlMapper.DeserializerState Deserializer { get; set; }
    public Func&amp;lt;DbDataReader, object&amp;gt;[]? OtherDeserializers { get; set; }
    public Action&amp;lt;IDbCommand, object?&amp;gt;? ParamReader { get; set; }
    public int GetHitCount() =&amp;gt; Interlocked.CompareExchange(ref this.hitCount, 0, 0);
    public void RecordHit() =&amp;gt; Interlocked.Increment(ref this.hitCount);
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Это набор разных десериализаторов ассоциированных с конкретным SQL-запросом, что позволяет Dapper переиспользовать объекты для однотипных запросов ускоряя материализацию данных.&lt;/p&gt;
&lt;p&gt;Довольно разумная оптимизация. Но почему же наше приложение держит такой большой кэш?&lt;/p&gt;
&lt;h1 id="sql-json"&gt;SQl + json = 🚩&lt;/h1&gt;
&lt;p&gt;Проблема в том, как именно мы составляем SQL запрос. Например, метод получения пользователей по списку идентификаторов выглядит так:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public async Task&amp;lt;ICollection&amp;lt;string&amp;gt;&amp;gt; SearchAsync(
    ThemeSearchRequest searchRequest, 
    int skip = 0,
    int take = int.MaxValue, 
    TimeSpan? timeout = null)
{
    var selectScript = @$"SELECT theme_base 
                          FROM [dbo].Themes 
                          {searchRequest.ToWhereExpression()}
                          GROUP BY theme_base
                          ORDER BY MAX(count_base) DESC, theme_base
                          OFFSET {skip} ROWS 
                          FETCH NEXT {take} ROWS ONLY";
    
        await using var connection = new SqlConnection(this.connectionString);
        
        var result = (await connection.QueryAsync&amp;lt;string&amp;gt;(
            selectScript,
            searchRequest.ToParametersWithValues(),
            commandTimeout: (int)(timeout?.TotalSeconds ?? DefaultTimeout.TotalSeconds))).AsList();
        
        return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Метод &lt;code&gt;SearchAsync&lt;/code&gt; формирует &lt;strong&gt;динамический&lt;/strong&gt; запрос к базе — какие-то поисковые условия, пагинация. И в этом и есть проблема.
Почти каждый запрос будет уникальным, и согласно базовой логике Dapper он будет кешироваться для быстрой материализации в будущем.&lt;/p&gt;
&lt;p&gt;Об этом же написано в &lt;a href="https://github.com/DapperLib/Dapper#limitations-and-caveats"&gt;документации&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Dapper caches information about every query it runs, this allows it to materialize objects quickly and process parameters quickly. The current implementation caches this information in a ConcurrentDictionary object. Statements that are only used once are routinely flushed from this cache. Still, if you are generating SQL strings on the fly without using parameters it is possible you may hit memory issues.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id="dapper-1"&gt;Кеширование Dapper&lt;/h1&gt;
&lt;p&gt;Давайте подробнее рассмотрим механизм кеширование в Dapper. Мы уже выяснили, что каждый SQL-запрос используется в качестве ключа и сохраняет дополнительную информацию для быстрой материализации объектов.
Будет ли этот кэш расти бесконечно? Давайте взглянем на код &lt;code&gt;SqlMapper&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;private static bool TryGetQueryCache(Identity key, [NotNullWhen(true)] out CacheInfo? value)
{
    if (_queryCache.TryGetValue(key, out value!))
    {
        value.RecordHit();
        return true;
    }
    value = null;
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Метод &lt;code&gt;TryGetQueryCache&lt;/code&gt; ищет данные в кэше и в случае успеха увеличивает счетчик попаданий — количество раз, котоыре данные возвращались по этому ключу.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;private const int COLLECT_PER_ITEMS = 1000;
private static void SetQueryCache(Identity key, CacheInfo value)
{
    if (Interlocked.Increment(ref collect) == COLLECT_PER_ITEMS)
    {
        CollectCacheGarbage();
    }
    _queryCache[key] = value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Метод &lt;code&gt;SetQueryCache&lt;/code&gt; пытается добавить данные в кэш, пока он не достиг размера в 1000 элементов.
В случае его достижения вызывается метод &lt;code&gt;CollectCacheGarbage()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;private const int COLLECT_HIT_COUNT_MIN = 0;
private static void CollectCacheGarbage()
{
    try
    {
        foreach (var pair in _queryCache)
        {
            if (pair.Value.GetHitCount() &amp;lt;= COLLECT_HIT_COUNT_MIN)
            {
                _queryCache.TryRemove(pair.Key, out var _);
            }
        }
    }
    finally
    {
        Interlocked.Exchange(ref collect, 0);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Он пытается удалить все закешированные элементы, к которым ни разу не обращались. При этом, метод &lt;code&gt;GetHitCount()&lt;/code&gt; обнуляет внутренний счетчик попаданий:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public int GetHitCount() { return Interlocked.CompareExchange(ref hitCount, 0, 0); }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Таким образом, после запуска &lt;code&gt;CollectCacheGarbage()&lt;/code&gt; значение всех &lt;code&gt;hitCount&lt;/code&gt; обнуляется, что позволяет найти неиспользуемые элементы при следующей очистке.
Но в случае активного использования динамических SQL-запросов велика вероятность, что все запросы будут использоваться по 1 разу и постоянно будут вытесняться из кэша.&lt;/p&gt;
&lt;p&gt;Использование одноразовых запросов бьет не только по памяти вашего приложения, но и создаёт дополнительную нагрузку на SQL-сервер.
Ему придется парсить, компилировать и строить план выполнения для каждого нового запроса.&lt;/p&gt;
&lt;h1 id="section-1"&gt;Решение&lt;/h1&gt;
&lt;p&gt;Корректным вариантом решения здесь было бы использование параметризованных, тогда один запрос будет переиспользоваться множество раз, в том числе и на SQL-сервере:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public async Task&amp;lt;ICollection&amp;lt;TaskRules&amp;gt;&amp;gt; GetTaskRulesAsync(long taskId)
{
    var sql = $@"SELECT * FROM TaskRules WHERE TaskID = @taskId";
    await using var connection = new SqlConnection(this.connectionString);
    await connection.OpenAsync();
    var result = await connection
        .QueryAsync&amp;lt;TaskRules&amp;gt;(sql, new { taskId }, commandTimeout: 180);

    return result.AsList();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;В методе &lt;code&gt;GetTaskRulesAsync&lt;/code&gt; используется параметризованный SQL-запрос, где выбираются данные по &lt;code&gt;@taskId&lt;/code&gt;.
Это позволяет эффективно использовать кеширование на всех уровнях — в приложении и SQL-сервере.&lt;/p&gt;
&lt;p&gt;Быстрым решением может быть отключение кеширования для конкретного запроса — это можно сделать через управление &lt;code&gt;CommandFlags&lt;/code&gt; при создании команды:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;new CommandDefinition(sql, CommandFlags.NoCache)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Таким образом, "одноразовые" запросы не будут попадать в кэш и засорять память.&lt;/p&gt;
&lt;h1 id="section-2"&gt;Заключение&lt;/h1&gt;
&lt;p&gt;Многие решения пытаются оптимизировать часто-используемые сценарии за счет строгих правил или каких-то своих эвристик.
Такие важные правила обычно описаны где-то на видном месте. В случае в Dapper — сразу в Readme.md.&lt;/p&gt;
&lt;h1 id="section-3"&gt;Рекомендации&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Проводите периодическое профилирование, смотрите, как приложения используют память. Например, с помощью &lt;a href="https://www.jetbrains.com/dotmemory/"&gt;dotMemory&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Для работы с SQL используйте параметризованные запросы&lt;/li&gt;
&lt;li&gt;Если какое-либо решение использует кэширование, убедитесь, что оно полезно для вас.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Пагинация: как правильно поделить данные по страницам</title>
			<link>https://blog.rogatnev.net/posts/ru/2025/07/Paging.html</link>
			<description>Элегантный способ подсчета количества страниц для организации пагинации</description>
			<author>Сергей Рогатнев</author>
			<guid>https://blog.rogatnev.net/posts/ru/2025/07/Paging.html</guid>
			<pubDate>Sat, 12 Jul 2025 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;Работая с большими списками данных, мы разбиваем их на страницы — так и пользователю удобнее, и не нагружает нашу систему.
А значит вы наверняка решали задачу определения общего количества страниц. Давайте покажу вам один простой и элегантный способ.&lt;/p&gt;
&lt;!--more--&gt;
&lt;h1 id="section"&gt;Пагинация&lt;/h1&gt;
&lt;p&gt;В классическом виде задача звучит так: у нас есть всего &lt;code&gt;TotalCount&lt;/code&gt; записей в базе данных, нужно вывести их постранично по &lt;code&gt;BatchSize&lt;/code&gt; на страницу.
Нужно определить количество страниц - &lt;code&gt;PageCount&lt;/code&gt;. Для этого нужно поделить &lt;code&gt;TotalCount&lt;/code&gt; на &lt;code&gt;BatchSize&lt;/code&gt; и если результат получился дробным, то округлить его вверх.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;| TotalCount | BatchSize | TotalCount/BatchSize | PageCount |
|------------|-----------|----------------------|----------:|
| 90         | 10        | 9                    |         9 |
| 99         | 10        | 9.9                  |        10 |
| 100        | 10        | 10                   |        10 |
| 101        | 10        | 10.1                 |        11 |
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="math.ceiling"&gt;Math.Ceiling&lt;/h3&gt;
&lt;p&gt;В C# можно воспользоваться методом &lt;a href="https://learn.microsoft.com/ru-ru/dotnet/api/system.math.ceiling?view=net-9.0"&gt;&lt;code&gt;Math.Ceiling&lt;/code&gt;&lt;/a&gt; - он округляет вещественное число до ближайшего целого, которое больше либо равно переданному аргументу.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var PageCount = (int)Math.Ceiling((double)TotalCount / BatchSize);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Мы получим корректное значение. Но как вы заметили, нам вначале нужно преобразовать наши целые числа &lt;code&gt;TotalCount&lt;/code&gt;, &lt;code&gt;BatchSize&lt;/code&gt; в вещественные, вызвать метод, а потом обратно преобразовать результат в целочисленный.&lt;/p&gt;
&lt;h3 id="section-1"&gt;Остаток от деления&lt;/h3&gt;
&lt;p&gt;Можно заметить, если &lt;code&gt;TotalCount&lt;/code&gt; делится нацело на &lt;code&gt;BatchSize&lt;/code&gt;, то результатом является просто частное &lt;code&gt;TotalCount/BatchSize&lt;/code&gt;, иначе нужно добавить 1 к результату.
Преобразуем нашу формулу:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var PageCount = TotalCount / BatchSize + (TotalCount % BatchSize == 0 ? 0 : 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Мы избавились от преобразований типа и вызова метода, но добавился один тернарный оператор. Можно ли сделать еще проще?
Оказывается, можно.&lt;/p&gt;
&lt;h3 id="section-2"&gt;Целочисленное деление&lt;/h3&gt;
&lt;p&gt;Как мы знаем, целочисленное деление всегда даёт округление результата к меньшему либо равному значению.
Давайте воспользуемся этой особенностью и рассмотрим следующее выражение:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var Add = (BatchSize - 1) / BatchSize;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Числитель у нас всегда меньше знаменателя, поэтому результат целочисленного деления всегда будет равен &lt;code&gt;0&lt;/code&gt;.
Добавим это выражение к частному &lt;code&gt;TotalCount / BatchSize&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var PageCount = TotalCount / BatchSize + (BatchSize - 1) / BatchSize;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;По правилам целочисленной арифметики второе слагаемое всегда равно &lt;code&gt;0&lt;/code&gt; и оно не влияет на сумму.
Давайте внесем его под общий знаменатель:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var PageCount = (TotalCount + BatchSize - 1) / BatchSize;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Это и есть наша элегантная формула деления с округлением вверх. Почему она работает?&lt;/p&gt;
&lt;p&gt;Давайте рассмотрим 2 случая:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TotalCount&lt;/code&gt; делится нацело на &lt;code&gt;BatchSize&lt;/code&gt;, тогда &lt;code&gt;TotalCount / BatchSize&lt;/code&gt; даст нужный нам &lt;code&gt;PageCount&lt;/code&gt;, а &lt;code&gt;(BatchSize - 1) / BatchSize&lt;/code&gt; даст &lt;code&gt;0&lt;/code&gt; и не повлияет на результат.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TotalCount&lt;/code&gt; при делении на &lt;code&gt;BatchSize&lt;/code&gt; даёт остаток &lt;code&gt;R&lt;/code&gt;, тогда &lt;code&gt;TotalCount / BatchSize&lt;/code&gt; даст значение &lt;code&gt;PageCount - 1&lt;/code&gt;. И у нас остаётся выражение &lt;code&gt;(R + BatchSize - 1) / BatchSize&lt;/code&gt;, где &lt;code&gt;R&lt;/code&gt; по определению всегда больше &lt;code&gt;1&lt;/code&gt; и меньше &lt;code&gt;BatchSize&lt;/code&gt;.
При любых значениях &lt;code&gt;R&lt;/code&gt; результат целочисленного деления будет равен &lt;code&gt;1&lt;/code&gt;. Таким образом, в итоге получим значение &lt;code&gt;PageCount&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="section-3"&gt;Бенчмарк&lt;/h3&gt;
&lt;p&gt;Давайте ряди любопытства сравним все 3 способа.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Код бенчмарка. Нажмите, чтобы развернуть.&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;using BenchmarkDotNet.Attributes;

namespace Paging;

public class Benchmark
{
    [Params(99)]
    public int TotalCount;

    [Params(10)]
    public int BatchSize;

    [Params(10000)]
    public int Iterations;

    [Benchmark]
    public int MathCeiling()
    {
        var result = 0;
        for (int i = 0; i &amp;lt; Iterations; i++)
            result += (int)Math.Ceiling((double)TotalCount / BatchSize);
        return result;
    }

    [Benchmark]
    public int Mod()
    {
        var result = 0;
        for (int i = 0; i &amp;lt; Iterations; i++)
            result += TotalCount / BatchSize + (TotalCount % BatchSize == 0 ? 0 : 1);
        return result;
    }

    [Benchmark]
    public int Div()
    {
        var result = 0;
        for (int i = 0; i &amp;lt; Iterations; i++)
            result += (TotalCount + BatchSize - 1) / BatchSize;
        return result;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;p&gt;Результаты:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BenchmarkDotNet v0.15.2, Windows 10 (10.0.19045.6093/22H2/2022Update)
AMD Ryzen 7 7840H with Radeon 780M Graphics 3.80GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.412
  [Host]     : .NET 8.0.18 (8.0.1825.31117), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  DefaultJob : .NET 8.0.18 (8.0.1825.31117), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI


| Method      | TotalCount | BatchSize | Iterations |     Mean |     Error |    StdDev |
|-------------|------------|-----------|------------|---------:|----------:|----------:|
| MathCeiling | 99         | 10        | 10000      | 2.298 us | 0.0042 us | 0.0037 us |
| Mod         | 99         | 10        | 10000      | 4.517 us | 0.0597 us | 0.0559 us |
| Div         | 99         | 10        | 10000      | 2.285 us | 0.0455 us | 0.0425 us |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Еще одно подтверждение бессмысленности этих измерений — это необходимость нескольких тысяч итераций, иначе значения неотличимы от нуля.
Тем не менее удивительно, что первый и третий варианты показали практически идентичный результат.&lt;/p&gt;
&lt;h1 id="section-4"&gt;Заключение&lt;/h1&gt;
&lt;p&gt;Данная формула — хорошая демонстрация особенностей целочисленной арифметики, которая немного отличается от обычной.
Можно пользоваться этими возможностями, но не в ущерб понятности.&lt;/p&gt;
&lt;p&gt;И да, важно, что данная формула работает только для положительных чисел.&lt;/p&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Строка из чисел</title>
			<link>https://blog.rogatnev.net/posts/ru/2025/06/String-of-Digits.html</link>
			<description>Рассматриваем разные подходы к проверке цифровых строк в C# - сравнение методов, эффективность и готовые примеры.</description>
			<author>Сергей Рогатнев</author>
			<guid>https://blog.rogatnev.net/posts/ru/2025/06/String-of-Digits.html</guid>
			<pubDate>Sat, 07 Jun 2025 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;Популярная задача — определить, состоит ли строка только из числовых символов. Например, нужно проверить, что пользователь правильно ввел номер телефона, индекс или ИНН организации.
Сделать это можно несколькими способами, которые отличаются своей эффективностью. Давайте рассмотрим самые популярные из них.&lt;/p&gt;
&lt;!--more--&gt;
&lt;h1 id="regex"&gt;Regex&lt;/h1&gt;
&lt;p&gt;Наверное, это наиболее часто встречающийся способ. И действительно, что может быть проще использования регулярного выражения вида &lt;code&gt;^[0-9]*$&lt;/code&gt; (или &lt;code&gt;^\d*$&lt;/code&gt;)?&lt;/p&gt;
&lt;p&gt;Ниже представлена наивная реализация проверки с помощью регулярного выражения:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;Regex regex = new Regex("^[0-9]*$");
var value = "123456789000";
var isValid = regex.IsMatch(value);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Возможно, вы уже видите здесь проблему. Такая реализация годится только для одноразового запуска. В промышленном коде, где вы проверяете сотни тысяч строк, такое решение будет не эффективным.
.NET предоставляет возможность скомпилировать регулярное выражение во время выполнения при вызове конструктора, для это нужно использовать опцию &lt;code&gt;RegexOptions.Compiled&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;Regex regex = new Regex("^[0-9]*$", RegexOptions.Compiled);
var value = "123456789000";
var isValid = regex.IsMatch(value);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;При вызове конструктора с этой опцией будет сгенерирован IL-код, который будет вызываться через &lt;code&gt;DynamicMethod&lt;/code&gt; внутри &lt;code&gt;Regex.IsMatch&lt;/code&gt;, что будет быстрее, чем обычная обработка регулярного выражения.
Минусом же будет более долгое создание объекта &lt;code&gt;Regex&lt;/code&gt; за счет затрат времени на компиляцию в рантайме, но это быстро окупается при многократном использовании.&lt;/p&gt;
&lt;p&gt;Давайте сравним производительность двух вариантов.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Код бенчмарка. Нажмите, чтобы развернуть.&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net90)]
[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.NetCoreApp31)]
[SimpleJob(RuntimeMoniker.Net48)]
[HideColumns("Job", "Error", "StdDev", "Gen0")]
public partial class DigitsBenchmarks
{
    private static string value = "123456789000";

    private static readonly Regex regex = new Regex("^[0-9]*$");
    private static readonly Regex compiledRegex = new Regex("^[0-9]*$", RegexOptions.Compiled);

    [Benchmark]
    public bool Regex()
    {
        return regex.IsMatch(value);
    }
    
    [Benchmark]
    public bool CompiledRegex()
    {
        return compiledRegex.IsMatch(value);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;p&gt;Результаты:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Median&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Allocated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Regex&lt;/td&gt;
&lt;td&gt;.NET Framework 4.8&lt;/td&gt;
&lt;td style="text-align: right;"&gt;165.4417 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;166.2537 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CompiledRegex&lt;/td&gt;
&lt;td&gt;.NET Framework 4.8&lt;/td&gt;
&lt;td style="text-align: right;"&gt;115.9377 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;115.9720 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regex&lt;/td&gt;
&lt;td&gt;.NET Core 3.1&lt;/td&gt;
&lt;td style="text-align: right;"&gt;118.1540 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;118.1887 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CompiledRegex&lt;/td&gt;
&lt;td&gt;.NET Core 3.1&lt;/td&gt;
&lt;td style="text-align: right;"&gt;89.7392 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;89.6514 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regex&lt;/td&gt;
&lt;td&gt;.NET 6.0&lt;/td&gt;
&lt;td style="text-align: right;"&gt;57.8247 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;57.8031 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CompiledRegex&lt;/td&gt;
&lt;td&gt;.NET 6.0&lt;/td&gt;
&lt;td style="text-align: right;"&gt;21.2952 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;21.2616 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regex&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td style="text-align: right;"&gt;47.2579 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;47.3506 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CompiledRegex&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td style="text-align: right;"&gt;24.2419 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;24.2547 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/string-of-digits/regex_compiled.png" alt="Regex &amp;amp; Compiled Regex"&gt;&lt;/p&gt;
&lt;p&gt;Преимущество использования скомпилированных выражений довольно наглядно. Так же с каждой новой версией .NET виден вклад разработчиков в производительность.
Это еще один аргумент в пользу обновления на современные версии фреймворка.&lt;/p&gt;
&lt;h1 id="regex-source-generators"&gt;Regex source generators&lt;/h1&gt;
&lt;p&gt;Повторюсь, что компиляция регулярных выражений имеет один недостаток — создание объекта &lt;code&gt;Regex&lt;/code&gt; в рантайме будет занимать какое-то время. Можно ли от этого избавиться?
Начиная с .NET 7 такая возможность появляется благодаря &lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/#source-generators"&gt;генераторам кода&lt;/a&gt;. Строго говоря, они появились в .NET 5, но решение для регулярных выражений было реализовано только в седьмой версии.
Генераторы кода позволяют создавать C#-код на этапе &lt;strong&gt;компиляции&lt;/strong&gt;. А значит его можно просматривать и дебажить так, словно это ваш собственный код.
И регулярные выражения можно превратить в C#-код на этапе компиляции! В .NET для этого реализован специальный атрибут &lt;code&gt;GeneratedRegex&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;namespace DigitBenchmark
{
    public partial class DigitsBenchmarks
    {
        private static readonly Regex generatedRegex = GenerateRegex();
        
        [GeneratedRegex("^[0-9]*$")]
        private static partial Regex GenerateRegex();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Давайте разберемся, что здесь происходит.
Во-первых, нам нужно пометить наш класс &lt;code&gt;DigitsBenchmarks&lt;/code&gt; как &lt;code&gt;partial&lt;/code&gt;, т.к. часть сгенерированного кода для этого класса будет находиться в другом файле.
Дальше нам нужно создать &lt;code&gt;partial&lt;/code&gt;-метод, который будет возвращать объект типа &lt;code&gt;Regex&lt;/code&gt; и пометить его атрибутом &lt;code&gt;GeneratedRegex&lt;/code&gt; с указанием шаблона регулярного выражения.
Опцию &lt;code&gt;RegexOptions.Compiled&lt;/code&gt; указывать не нужно, она будет проигнорирована. Далее поймете почему.&lt;/p&gt;
&lt;p&gt;Реализация метода &lt;code&gt;GenerateRegex&lt;/code&gt; будет находиться в другом файле. Его можно найти в проекте и посмотреть исходный код:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;namespace DigitBenchmark
{
    partial class DigitsBenchmarks
    {
        /// &amp;lt;remarks&amp;gt;
        /// Pattern:&amp;lt;br/&amp;gt;
        /// &amp;lt;code&amp;gt;^[0-9]*$&amp;lt;/code&amp;gt;&amp;lt;br/&amp;gt;
        /// Explanation:&amp;lt;br/&amp;gt;
        /// &amp;lt;code&amp;gt;
        /// ○ Match if at the beginning of the string.&amp;lt;br/&amp;gt;
        /// ○ Match a character in the set [0-9] atomically any number of times.&amp;lt;br/&amp;gt;
        /// ○ Match if at the end of the string or if before an ending newline.&amp;lt;br/&amp;gt;
        /// &amp;lt;/code&amp;gt;
        /// &amp;lt;/remarks&amp;gt;
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Text.RegularExpressions.Generator", "8.0.12.21506")]
        private static partial global::System.Text.RegularExpressions.Regex GenerateRegex() =&amp;gt; global::System.Text.RegularExpressions.Generated.GenerateRegex_0.Instance;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Как видите, автоматически был создан файл с тем же классом и содержащий реализацию нашего метода по генерации регулярного выражения.
А дальше мы можем пользоваться этим объектом &lt;code&gt;Regex&lt;/code&gt; как обычно. Так как это настоящий C#-код, то генерировать в рантайме ничего не нужно, поэтому и указывать опцию &lt;code&gt;RegexOptions.Compiled&lt;/code&gt; нет смысла.&lt;/p&gt;
&lt;p&gt;Какое преимущество мы от этого получим? В моих бенчмарках нет версий .NET 7 и 8, сравним производительность по последней на данный момент:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Median&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Allocated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Regex&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td style="text-align: right;"&gt;47.2579 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;47.3506 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CompiledRegex&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td style="text-align: right;"&gt;24.2419 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;24.2547 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GeneratedRegex&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td style="text-align: right;"&gt;17.2548 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;17.2603 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Видим, что время сократилось почти на 30%! Компилятор имеет намного больше возможностей для оптимизации исходного кода на этапе компиляции, чем в рантайме.&lt;/p&gt;
&lt;h1 id="char.isdigit"&gt;char.IsDigit&lt;/h1&gt;
&lt;p&gt;Еще один популярный способ — использование статического метода &lt;code&gt;char.IsDigit&lt;/code&gt; в сочетании с LINQ-методом &lt;code&gt;All&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var value = "123456789000";
var isValid = value.All(char.IsDigit);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Давайте проверим, насколько хорош этот метод с точки зрения производительности.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Код бенчмарка. Нажмите, чтобы развернуть.&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net90)]
[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.NetCoreApp31)]
[SimpleJob(RuntimeMoniker.Net48)]
[HideColumns("Job", "Error", "StdDev", "Gen0")]
public partial class DigitsBenchmarks
{
    [Benchmark]
    public bool LinqCharIsDigit()
    {
        return value.All(char.IsDigit);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;p&gt;Результаты:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Median&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Allocated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LinqCharIsDigit&lt;/td&gt;
&lt;td&gt;.NET Framework 4.8&lt;/td&gt;
&lt;td style="text-align: right;"&gt;92.1679 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;92.2549 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;96 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LinqCharIsDigit&lt;/td&gt;
&lt;td&gt;.NET Core 3.1&lt;/td&gt;
&lt;td style="text-align: right;"&gt;72.0987 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;72.6419 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;96 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LinqCharIsDigit&lt;/td&gt;
&lt;td&gt;.NET 6.0&lt;/td&gt;
&lt;td style="text-align: right;"&gt;74.2609 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;74.4256 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;96 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LinqCharIsDigit&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td style="text-align: right;"&gt;31.0294 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;31.0501 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;32 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;И сравним этот способ с предыдущими решениями.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/string-of-digits/regex_isdigit.png" alt="Regex vs char.IsDigit"&gt;&lt;/p&gt;
&lt;p&gt;Если в старых версиях фреймворка проверки через LINQ и метод &lt;code&gt;IsDigit&lt;/code&gt; имеют преимущество над регулярными выражениями, то позже мы видим, что такая реализация начинает проигрывать.&lt;/p&gt;
&lt;p&gt;Так же, обратите внимание, что каждый вызов такого метода приводит к выделению какого-то количества дополнительной памяти. Это значение складывается из 2-х составляющих:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Создание лямбда-выражения в параметре метода &lt;code&gt;All(c =&amp;gt; char.IsDigit(c))&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Создание итератора внутри метода &lt;code&gt;All&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Примечательно, что в версии .NET 9 выделяется в 3 раза меньше памяти.
До .NET 9 метод &lt;code&gt;All&lt;/code&gt; был очень простым и состоял из цикла &lt;code&gt;foreach&lt;/code&gt; с условием:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public static bool All&amp;lt;TSource&amp;gt;(this IEnumerable&amp;lt;TSource&amp;gt; source, Func&amp;lt;TSource, bool&amp;gt; predicate)
{
  //...
  foreach (TSource source1 in source)
  {
    if (!predicate(source1))
      return false;
  }
  return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Но в версии .NET 9 была добавлена важная оптимизация:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public static bool All&amp;lt;TSource&amp;gt;(this IEnumerable&amp;lt;TSource&amp;gt; source, Func&amp;lt;TSource, bool&amp;gt; predicate)
{
  //...
  ReadOnlySpan&amp;lt;TSource&amp;gt; span;
  if (source.TryGetSpan&amp;lt;TSource&amp;gt;(out span))
  {
    ReadOnlySpan&amp;lt;TSource&amp;gt; readOnlySpan = span;
    for (int index = 0; index &amp;lt; readOnlySpan.Length; ++index)
    {
      TSource source1 = readOnlySpan[index];
      if (!predicate(source1))
        return false;
    }
  }
  else
  {
    foreach (TSource source2 in source)
    {
      if (!predicate(source2))
        return false;
    }
  }
  return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Вместо безусловного цикла &lt;code&gt;foreach&lt;/code&gt; метод &lt;code&gt;All&lt;/code&gt; пробует получить из источника &lt;code&gt;ReadOnlySpan&lt;/code&gt; - безопасный для чтения непрерывный блок памяти.
И дальше используется простой цикл &lt;code&gt;for&lt;/code&gt;, который не приводит к созданию итератора. Тем самым уменьшая количество дополнительной памяти.
Полностью избавиться от этого можно переписав метод &lt;code&gt;All&lt;/code&gt; на обычный цикл:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public bool ForIsDigit()
{
    for (var i = 0; i &amp;lt; value.Length; i++)
    {
        if (!char.IsDigit(value[i]))
            return false;
    }

    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Помимо отсутствия лишнего memory-traffic данное решение является очень быстрым.
&lt;img src="https://blog.rogatnev.net/img/string-of-digits/linq_for.png" alt="LINQ vs for"&gt;&lt;/p&gt;
&lt;h1 id="section"&gt;Что такое число?&lt;/h1&gt;
&lt;p&gt;Казалось бы, мы нашли оптимальное решение для проверки строки на наличие только чисел.
Но предлагаю вам попробовать угадать, что выведет следующий код:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;Console.WriteLine(char.IsDigit('0'));
Console.WriteLine(char.IsDigit('a'));
Console.WriteLine(char.IsDigit('٨'));
Console.WriteLine(char.IsDigit('৯'));
&lt;/code&gt;&lt;/pre&gt;
&lt;details&gt;
&lt;summary&gt;Нажмите, чтобы узнать ответ.&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;Console.WriteLine(char.IsDigit('0')); //True
Console.WriteLine(char.IsDigit('a')); //False
Console.WriteLine(char.IsDigit('٨')); //True
Console.WriteLine(char.IsDigit('৯')); //True
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;p&gt;Думаю, вы удивлены результатом. Но в этом нет ничего необычного, метод &lt;code&gt;IsDigit&lt;/code&gt; считает числами не только привычные нам символы из множества &lt;code&gt;0-9&lt;/code&gt;, но и все остальные символы, которые в кодировке Unicode относятся к числам. А их на самом деле много.
Это может быть проблемой, если вы опираетесь на такую проверку в своём бизнес-коде.&lt;/p&gt;
&lt;p&gt;Думаю, это послужило причиной появления нового метода &lt;code&gt;char.IsAsciiDigit&lt;/code&gt; начиная с .NET 7. Вот он уже действительно проверяет только символы из множества &lt;code&gt;0-9&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Его реализация очень похожа на ручную проверку каждого символа в цикле, давай сравним оба варианта:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;[Benchmark]
public bool ForCompare()
{
    for (var i = 0; i &amp;lt; value.Length; i++)
    {
        if (value[i] &amp;lt; '0' || value[i] &amp;gt; '9')
            return false;
    }

    return true;
}

[Benchmark]
public bool ForIsAsciiDigit()
{
    for (var i = 0; i &amp;lt; value.Length; i++)
    {
        if (!char.IsAsciiDigit(value[i]))
            return false;
    }

    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Median&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Allocated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ForCompare&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td style="text-align: right;"&gt;4.8587 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;4.8656 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ForIsAsciiDigit&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td style="text-align: right;"&gt;4.7515 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;4.4976 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Оба метода показывают эквивалентные результаты.&lt;/p&gt;
&lt;h1 id="section-1"&gt;Заключение&lt;/h1&gt;
&lt;p&gt;Мы рассмотрели несколько разных способов проверить строку, состоит ли она только из чисел или нет.
И важно понимать особенности работы некоторых из них, так как помимо банальных проблем производительности можно получить ошибки в бизнес-логике, если недостаточно качественно проверять входные данные.&lt;/p&gt;
&lt;p&gt;Рекомендации:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Если вы пишете свои приложения под .NET 7 или выше, то используйте сгенерированные регулярные выражения. В противном случае указывайте опцию &lt;code&gt;RegexOptions.Compiled&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Если вы пишете свои приложения под .NET 7 используйте метод &lt;code&gt;char.IsAsciiDigit&lt;/code&gt; для проверки символов. В противном случае лучше написать проверку самому.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="section-2"&gt;Ссылки&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/standard/base-types/compilation-and-reuse-in-regular-expressions"&gt;Компиляция и генерация регулярных выражений&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.char.isdigit?view=net-9.0"&gt;char.IsDigit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.char.isasciidigit?view=net-9.0"&gt;char.IsAsciiDigit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;details&gt;
&lt;summary&gt;Полный код бенчмарка. Нажмите, чтобы развернуть.&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;using System.Linq;
using System.Text.RegularExpressions;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace DigitBenchmark
{
    [MemoryDiagnoser]
    [SimpleJob(RuntimeMoniker.Net90)]
    [SimpleJob(RuntimeMoniker.Net60)]
    [SimpleJob(RuntimeMoniker.NetCoreApp31)]
    [SimpleJob(RuntimeMoniker.Net48)]
    [HideColumns("Job", "Error", "StdDev", "Gen0")]
    public partial class DigitsBenchmarks
    {
        private static string value = "123456789000";

        private static readonly Regex regex = new Regex("^[0-9]*$");
        private static readonly Regex compiledRegex = new Regex("^[0-9]*$", RegexOptions.Compiled);
        private static readonly Regex generatedRegex = GenerateRegex();

        [GeneratedRegex("^[0-9]*$")]
        private static partial Regex GenerateRegex();

        [Benchmark]
        public bool Regex()
        {
            return regex.IsMatch(value);
        }
        
        [Benchmark]
        public bool CompiledRegex()
        {
            return compiledRegex.IsMatch(value);
        }

        [Benchmark]
        public bool GeneratedRegex()
        {
            return generatedRegex.IsMatch(value);
        }

        [Benchmark]
        public bool LinqCharIsDigit()
        {
            return value.All(char.IsDigit);
        }

        [Benchmark]
        public bool ForCompare()
        {
            for (var i = 0; i &amp;lt; value.Length; i++)
            {
                if (value[i] &amp;lt; '0' || value[i] &amp;gt; '9')
                    return false;
            }

            return true;
        }

        [Benchmark]
        public bool ForIsDigit()
        {
            for (var i = 0; i &amp;lt; value.Length; i++)
            {
                if (!char.IsDigit(value[i]))
                    return false;
            }

            return true;
        }

        [Benchmark]
        public bool ForIsAsciiDigit()
        {
            for (var i = 0; i &amp;lt; value.Length; i++)
            {
                if (!char.IsAsciiDigit(value[i]))
                    return false;
            }

            return true;
        }

        [Benchmark]
        public bool LinqCharIsAsciiDigit()
        {
            return value.All(char.IsAsciiDigit);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;Все результаты бенчмарков. Нажмите, чтобы развернуть.&lt;/summary&gt;
&lt;pre&gt;&lt;code&gt;BenchmarkDotNet v0.15.0, Windows 10 (10.0.19045.5917/22H2/2022Update)
AMD Ryzen 7 7840H with Radeon 780M Graphics 3.80GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 9.0.204
  [Host]             : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  .NET 6.0           : .NET 6.0.36 (6.0.3624.51421), X64 RyuJIT AVX2
  .NET 9.0           : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  .NET Core 3.1      : .NET Core 3.1.32 (CoreCLR 4.700.22.55902, CoreFX 4.700.22.56512), X64 RyuJIT AVX2
  .NET Framework 4.8 : .NET Framework 4.8.1 (4.8.9310.0), X64 RyuJIT VectorSize=256

| Method               | Runtime            |        Mean |      Median | Allocated |
|----------------------|--------------------|------------:|------------:|----------:|
| Regex                | .NET Framework 4.8 | 165.4417 ns | 166.2537 ns |         - |
| CompiledRegex        | .NET Framework 4.8 | 115.9377 ns | 115.9720 ns |         - |
| GeneratedRegex       | .NET Framework 4.8 |         N/A |         N/A |         - |
| LinqCharIsDigit      | .NET Framework 4.8 |  92.1679 ns |  92.2549 ns |      96 B |
| ForCompare           | .NET Framework 4.8 |   4.8703 ns |   4.8602 ns |         - |
| ForIsDigit           | .NET Framework 4.8 |   8.3920 ns |   8.3952 ns |         - |
| ForIsAsciiDigit      | .NET Framework 4.8 |         N/A |         N/A |         - |
| LinqCharIsAsciiDigit | .NET Framework 4.8 |         N/A |         N/A |         - |
| Regex                | .NET Core 3.1      | 118.1540 ns | 118.1887 ns |         - |
| CompiledRegex        | .NET Core 3.1      |  89.7392 ns |  89.6514 ns |         - |
| GeneratedRegex       | .NET Core 3.1      |         N/A |         N/A |         - |
| LinqCharIsDigit      | .NET Core 3.1      |  72.0987 ns |  72.6419 ns |      96 B |
| ForCompare           | .NET Core 3.1      |   5.3070 ns |   5.3077 ns |         - |
| ForIsDigit           | .NET Core 3.1      |   9.0998 ns |   9.1106 ns |         - |
| ForIsAsciiDigit      | .NET Core 3.1      |         N/A |         N/A |         - |
| LinqCharIsAsciiDigit | .NET Core 3.1      |         N/A |         N/A |         - |
| Regex                | .NET 6.0           |  57.8247 ns |  57.8031 ns |         - |
| CompiledRegex        | .NET 6.0           |  21.2952 ns |  21.2616 ns |         - |
| GeneratedRegex       | .NET 6.0           |         N/A |         N/A |         - |
| LinqCharIsDigit      | .NET 6.0           |  74.2609 ns |  74.4256 ns |      96 B |
| ForCompare           | .NET 6.0           |   5.6198 ns |   5.5922 ns |         - |
| ForIsDigit           | .NET 6.0           |  10.3160 ns |  10.1789 ns |         - |
| ForIsAsciiDigit      | .NET 6.0           |         N/A |         N/A |         - |
| LinqCharIsAsciiDigit | .NET 6.0           |         N/A |         N/A |         - |
| Regex                | .NET 9.0           |  47.2579 ns |  47.3506 ns |         - |
| CompiledRegex        | .NET 9.0           |  24.2419 ns |  24.2547 ns |         - |
| GeneratedRegex       | .NET 9.0           |  17.2548 ns |  17.2603 ns |         - |
| LinqCharIsDigit      | .NET 9.0           |  31.0294 ns |  31.0501 ns |      32 B |
| ForCompare           | .NET 9.0           |   4.8587 ns |   4.8656 ns |         - |
| ForIsDigit           | .NET 9.0           |   7.1207 ns |   7.7792 ns |         - |
| ForIsAsciiDigit      | .NET 9.0           |   4.7515 ns |   4.4976 ns |         - |
| LinqCharIsAsciiDigit | .NET 9.0           |  32.6861 ns |  32.5483 ns |      32 B |
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Отметка &lt;code&gt;N/A&lt;/code&gt; ставится для методов, которые не существуют в данной версии фреймворка.&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Any() vs Count: часть 2</title>
			<link>https://blog.rogatnev.net/posts/ru/2025/05/Any-vs-Count-part-2.html</link>
			<description>Вторая часть глубокого анализ методов Any() и Count в LINQ - когда что использовать, как выбрать лучший вариант для производительности.</description>
			<author>Сергей Рогатнев</author>
			<guid>https://blog.rogatnev.net/posts/ru/2025/05/Any-vs-Count-part-2.html</guid>
			<pubDate>Tue, 06 May 2025 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;В &lt;a href="xref:2018-06-17-LINQ-Any-vs-Count"&gt;первой части&lt;/a&gt; мы сравнивали методы &lt;code&gt;Any()&lt;/code&gt; и &lt;code&gt;Count&lt;/code&gt; для различных коллекций и предложили вариант оптимизаций.
Прошло достаточно времени, чтобы провести повторное сравнение.&lt;/p&gt;
&lt;!--more--&gt;
&lt;h1 id="array"&gt;Array&lt;/h1&gt;
&lt;p&gt;Начнем с самого простого типа для коллекции - массива.
Проверим, есть ли какая-то разница между вызовами &lt;code&gt;array.Any()&lt;/code&gt; и &lt;code&gt;array.Length != 0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Здесь и далее будет использоваться эта конфигурация:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BenchmarkDotNet v0.14.0, Windows 10 (10.0.19045.5796/22H2/2022Update)
AMD Ryzen 7 7840H with Radeon 780M Graphics, 1 CPU, 16 logical and 8 physical cores
.NET SDK 9.0.203
  [Host]             : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  .NET 5.0           : .NET 5.0.17 (5.0.1722.21314), X64 RyuJIT AVX2
  .NET 6.0           : .NET 6.0.36 (6.0.3624.51421), X64 RyuJIT AVX2
  .NET 8.0           : .NET 8.0.15 (8.0.1525.16413), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  .NET 9.0           : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  .NET Core 3.1      : .NET Core 3.1.32 (CoreCLR 4.700.22.55902, CoreFX 4.700.22.56512), X64 RyuJIT AVX2
  .NET Framework 4.8 : .NET Framework 4.8.1 (4.8.9310.0), X64 RyuJIT VectorSize=256

&lt;/code&gt;&lt;/pre&gt;
&lt;details&gt;
&lt;summary&gt;Код бенчмарка. Нажмите, чтобы развернуть.&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace AnyVsCount
{
    [MemoryDiagnoser]
    [SimpleJob(RuntimeMoniker.Net90)]
    [SimpleJob(RuntimeMoniker.Net80)]
    [SimpleJob(RuntimeMoniker.Net60)]
    [SimpleJob(RuntimeMoniker.Net50)]
    [SimpleJob(RuntimeMoniker.NetCoreApp31)]
    [SimpleJob(RuntimeMoniker.Net48)]
    [HideColumns("Job", "Error", "StdDev", "Gen0")]
    public class ArrayBenchmark
    {
        [Params(10, 10000)]
        public int N;

        private int[] array;

        [GlobalSetup]
        public void SetUp()
        {
            array = new int[N];
        }

        [Benchmark]
        public bool ArrayAny()
        {
            return array.Any();
        }

        [Benchmark]
        public bool ArrayCount()
        {
            return array.Length != 0;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;h3 id="section"&gt;Результаты&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Median&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Allocated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ArrayAny&lt;/td&gt;
&lt;td&gt;.NET Framework 4.8&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;8.7962 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;8.8213 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;32 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayCount&lt;/td&gt;
&lt;td&gt;.NET Framework 4.8&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0073 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0028 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayAny&lt;/td&gt;
&lt;td&gt;.NET Core 3.1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;8.0450 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;8.0462 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;32 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayCount&lt;/td&gt;
&lt;td&gt;.NET Core 3.1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0433 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0399 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayAny&lt;/td&gt;
&lt;td&gt;.NET 5.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6.2080 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6.1852 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayCount&lt;/td&gt;
&lt;td&gt;.NET 5.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0030 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0000 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayAny&lt;/td&gt;
&lt;td&gt;.NET 6.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6.0673 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6.0671 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayCount&lt;/td&gt;
&lt;td&gt;.NET 6.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0135 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0135 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayAny&lt;/td&gt;
&lt;td&gt;.NET 8.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5.0980 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5.2591 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayCount&lt;/td&gt;
&lt;td&gt;.NET 8.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0000 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0000 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayAny&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.1643 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.1216 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayCount&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0030 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0034 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayAny&lt;/td&gt;
&lt;td&gt;.NET Framework 4.8&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;9.0093 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;9.0043 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;32 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayCount&lt;/td&gt;
&lt;td&gt;.NET Framework 4.8&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0123 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0137 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayAny&lt;/td&gt;
&lt;td&gt;.NET Core 3.1&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;7.9624 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;7.7794 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;32 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayCount&lt;/td&gt;
&lt;td&gt;.NET Core 3.1&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0311 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0295 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayAny&lt;/td&gt;
&lt;td&gt;.NET 5.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;4.8357 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;4.8352 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayCount&lt;/td&gt;
&lt;td&gt;.NET 5.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0008 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0007 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayAny&lt;/td&gt;
&lt;td&gt;.NET 6.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6.0913 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6.0747 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayCount&lt;/td&gt;
&lt;td&gt;.NET 6.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0147 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0153 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayAny&lt;/td&gt;
&lt;td&gt;.NET 8.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;4.7691 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;4.7521 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayCount&lt;/td&gt;
&lt;td&gt;.NET 8.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0110 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0078 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayAny&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.2933 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.2906 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ArrayCount&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0121 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.0109 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;Значения близкие к нулю (например, 0.0000 ns) следует считать пренебрежимо малыми — они находятся в пределах погрешности измерений.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Легенда ко всем таблицам:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  N         : Value of the 'N' parameter
  Mean      : Arithmetic mean of all measurements
  Median    : Value separating the higher half of all measurements (50th percentile)
  Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
  1 ns      : 1 Nanosecond (0.000000001 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Можно заметить, что для массива результаты практически не зависят от размера, поэтому дальше будет рассматривать только результаты для &lt;code&gt;N = 10000&lt;/code&gt;.
Дальше мы видим, что прямой вызов свойства &lt;code&gt;Length&lt;/code&gt; у массива минимум на 2 порядка быстрее вызова метода &lt;code&gt;Any()&lt;/code&gt; и кажется, что нет никакого смысла его использовать.
Но зачастую мы работаем не с коллекциями напрямую, а с обобщенным кодом:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public bool IsEmpty&amp;lt;T&amp;gt;(IEnumerable&amp;lt;T&amp;gt; collection)
{
    return !collection.Any();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Поэтому продолжим наше исследование и внимательнее посмотрим, почему получается такая разница в методе &lt;code&gt;Any()&lt;/code&gt; в разных версиях фреймворка.
Видно, что от .NET 4.8 до .NET 9.0 время выполнения метода &lt;code&gt;Any()&lt;/code&gt; уменьшилось в 4 раза:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/any-part2/any1.png" alt="Method Any performance"&gt;&lt;/p&gt;
&lt;p&gt;Разница между последней версией классического фреймворка и .NET Core незначительная. Давайте посмотрим на реализацию метода &lt;code&gt;Any()&lt;/code&gt; внутри .NET 4.8 и .NET Core 3.1 (они совпадают):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public static bool Any&amp;lt;TSource&amp;gt;(this IEnumerable&amp;lt;TSource&amp;gt; source)
{
    if (source == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
    }

    using (IEnumerator&amp;lt;TSource&amp;gt; e = source.GetEnumerator())
    {
        return e.MoveNext();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Как видим, чтобы определить, есть ли в последовательности хотя бы один элемент, создаётся итератор по этой последовательности и вызывается метод &lt;code&gt;MoveNext()&lt;/code&gt;.
На это же нам намекали 32 байта выделяемой памяти в бенчмарке - это как раз затраты на создание итератора. Что же изменилось в .NET 5?
Давайте опять посмотрим на код:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public static bool Any&amp;lt;TSource&amp;gt;(this IEnumerable&amp;lt;TSource&amp;gt; source)
{
    if (source == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
    }

    if (source is ICollection&amp;lt;TSource&amp;gt; collectionoft)
    {
        return collectionoft.Count != 0;
    }
    else if (source is IIListProvider&amp;lt;TSource&amp;gt; listProv)
    {
        int count = listProv.GetCount(onlyIfCheap: true);
        if (count &amp;gt;= 0)
        {
            return count != 0;
        }
    }
    else if (source is ICollection collection)
    {
        return collection.Count != 0;
    }

    using (IEnumerator&amp;lt;TSource&amp;gt; e = source.GetEnumerator())
    {
        return e.MoveNext();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Решение подозрительно похоже на то, что мы использовали в &lt;a href="xref:2018-06-16-LINQ-Any-vs-Count"&gt;первой части&lt;/a&gt;.
Вначале проверяется, реализует ли перечисление интерфейс &lt;code&gt;ICollection&amp;lt;T&amp;gt;&lt;/code&gt;, у которого есть свойство &lt;code&gt;Count&lt;/code&gt;. А дальше мы видим использование нового интерфейса &lt;code&gt;IIListProvider&amp;lt;TSource&amp;gt;&lt;/code&gt; (внутренний интерфейс .NET, оптимизирующий операции LINQ за счёт избегания перечисления элементов):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;internal interface IIListProvider&amp;lt;TElement&amp;gt; : IEnumerable&amp;lt;TElement&amp;gt;
{
    TElement[] ToArray();

    List&amp;lt;TElement&amp;gt; ToList();

    int GetCount(bool onlyIfCheap);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Этот интерфейс - часть масштабной переработки LINQ, которая позволила добиться значительной оптимизации заметной даже на простом методе &lt;code&gt;Any()&lt;/code&gt;.
Любопытно, что в .NET 6 производительность несколько ухудшилась, хотя реализация метода &lt;code&gt;Any()&lt;/code&gt; не изменилась.&lt;/p&gt;
&lt;p&gt;В восьмой версии .NET код, который вычисляет размер перечисления (не выполняя то самое перечисление) вынесли в отдельный метод &lt;code&gt;TryGetNonEnumeratedCount()&lt;/code&gt;.
Этот метод работает за константное время, но не всегда может вернуть значение.&lt;/p&gt;
&lt;p&gt;В девятой версии .NET концепция использования &lt;code&gt;IIListProvider&amp;lt;TSource&amp;gt;&lt;/code&gt; получила развитие и LINQ-методы были переработаны с использованием нового класса &lt;code&gt;Iterator&amp;lt;TSource&amp;gt;&lt;/code&gt;, что позволило еще улучшить производительность.&lt;/p&gt;
&lt;p&gt;Как видим, напрашивающееся решение с проверкой типа перечисления и свойства &lt;code&gt;Count&lt;/code&gt; было реализовано в новых версиях .NET.
Будет ли это эффективно для всех коллекций?&lt;/p&gt;
&lt;h1 id="concurrentdictionary"&gt;ConcurrentDictionary&lt;/h1&gt;
&lt;p&gt;Давайте рассмотрим самую распространенную потокобезопасную коллекцию - &lt;code&gt;ConcurrentDictionary&amp;lt;TKey, TValue&amp;gt;&lt;/code&gt;.
Кажется, что с новой реализацией могут быть проблемы.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Код бенчмарка. Нажмите, чтобы развернуть.&lt;/summary&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;using System.Collections.Concurrent;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace AnyVsCount
{
    [MemoryDiagnoser]
    [SimpleJob(RuntimeMoniker.Net90)]
    [SimpleJob(RuntimeMoniker.Net80)]
    [SimpleJob(RuntimeMoniker.Net60)]
    [SimpleJob(RuntimeMoniker.Net50)]
    [SimpleJob(RuntimeMoniker.NetCoreApp31)]
    [SimpleJob(RuntimeMoniker.Net48)]
    [HideColumns("Job", "Error", "StdDev", "Gen0")]
    public class ConcurrentDictionaryBenchmark
    {
        [Params(10, 10000)]
        public int N;

        private ConcurrentDictionary&amp;lt;int, string&amp;gt; dictionary;

        [GlobalSetup]
        public void SetUp()
        {
            dictionary = new ConcurrentDictionary&amp;lt;int, string&amp;gt;();

            for (int i = 0; i &amp;lt; N; i++)
            {
                dictionary[i] = i.ToString();
            }
        }

        [Benchmark]
        public bool DictionaryAny()
        {
            return dictionary.Any();
        }

        [Benchmark]
        public bool DictionaryCount()
        {
            return dictionary.Count != 0;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;h3 id="section-1"&gt;Результаты&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Median&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Allocated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryAny&lt;/td&gt;
&lt;td&gt;.NET Framework 4.8&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;18.48 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;18.47 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;64 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryCount&lt;/td&gt;
&lt;td&gt;.NET Framework 4.8&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;110.77 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;110.75 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryAny&lt;/td&gt;
&lt;td&gt;.NET Core 3.1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;18.21 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;18.17 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;64 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryCount&lt;/td&gt;
&lt;td&gt;.NET Core 3.1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;108.27 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;110.12 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryAny&lt;/td&gt;
&lt;td&gt;.NET 5.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;92.98 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;92.81 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryCount&lt;/td&gt;
&lt;td&gt;.NET 5.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;97.02 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;96.42 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryAny&lt;/td&gt;
&lt;td&gt;.NET 6.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;87.96 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;87.79 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryCount&lt;/td&gt;
&lt;td&gt;.NET 6.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;88.35 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;88.40 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryAny&lt;/td&gt;
&lt;td&gt;.NET 8.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;76.10 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;76.08 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryCount&lt;/td&gt;
&lt;td&gt;.NET 8.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;72.26 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;72.27 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryAny&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;83.02 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;83.52 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryCount&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;80.43 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;79.34 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryAny&lt;/td&gt;
&lt;td&gt;.NET Framework 4.8&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;18.87 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;18.71 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;64 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryCount&lt;/td&gt;
&lt;td&gt;.NET Framework 4.8&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6,870.38 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6,859.65 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryAny&lt;/td&gt;
&lt;td&gt;.NET Core 3.1&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;18.04 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;18.06 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;64 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryCount&lt;/td&gt;
&lt;td&gt;.NET Core 3.1&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;7,000.55 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;7,000.95 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryAny&lt;/td&gt;
&lt;td&gt;.NET 5.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5,877.30 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5,877.50 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryCount&lt;/td&gt;
&lt;td&gt;.NET 5.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5,962.47 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5,958.17 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryAny&lt;/td&gt;
&lt;td&gt;.NET 6.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5,993.15 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5,994.17 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryCount&lt;/td&gt;
&lt;td&gt;.NET 6.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6,030.51 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6,029.08 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryAny&lt;/td&gt;
&lt;td&gt;.NET 8.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5,082.70 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5,082.69 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryCount&lt;/td&gt;
&lt;td&gt;.NET 8.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5,370.34 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5,368.76 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryAny&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5,917.83 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5,917.32 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryCount&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5,847.50 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;5,848.04 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Уже видны некоторые особенности:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Время выполнения зависит от размера коллекции.&lt;/li&gt;
&lt;li&gt;В старых версиях фреймворка метод &lt;code&gt;Any()&lt;/code&gt; был намного быстрее.&lt;/li&gt;
&lt;li&gt;В новых версиях фреймворка метод &lt;code&gt;Any()&lt;/code&gt; сровнялся по производительности с &lt;code&gt;Count&lt;/code&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/any-part2/any2.png" alt="ConcurrentDictionary Any performance"&gt;&lt;/p&gt;
&lt;p&gt;Давайте посмотрим реализацию свойства &lt;code&gt;Count&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public int Count
{
    get
    {
        int locksAcquired = 0;
        try
        {
            AcquireAllLocks(ref locksAcquired);

            return GetCountNoLocks();
        }
        finally
        {
            ReleaseLocks(locksAcquired);
        }
    }
}

private void AcquireAllLocks(ref int locksAcquired)
{
    //...

    // First, acquire lock 0, then acquire the rest. _tables won't change after acquiring lock 0.
    AcquireFirstLock(ref locksAcquired);
    AcquirePostFirstLock(_tables, ref locksAcquired);
    Debug.Assert(locksAcquired == _tables._locks.Length);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Подсчет всех элементов требует получения блокировок на специальный внутренний массив &lt;code&gt;_tables._locks&lt;/code&gt;. Каждый элемент этого массива блокирует часть словаря.
Таким образом, начиная с .NET 5, метод &lt;code&gt;Any()&lt;/code&gt; для &lt;code&gt;ConcurrentDictionary&lt;/code&gt; использует свойство &lt;code&gt;Count&lt;/code&gt; через &lt;code&gt;ICollection&amp;lt;T&amp;gt;&lt;/code&gt;, что приводит к таким же блокировкам, как и при прямом вызове &lt;code&gt;Count&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="any-isempty"&gt;Any() и IsEmpty&lt;/h3&gt;
&lt;p&gt;Давайте восстановим реализацию &lt;code&gt;Any()&lt;/code&gt; с итератором и проверим производительность этого метода:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;[Benchmark]
public bool DictionaryEnumerator()
{
    using (var enumerator = dictionary.GetEnumerator())
    {
        return enumerator.MoveNext();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Median&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Allocated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryEnumerator&lt;/td&gt;
&lt;td&gt;.NET Framework 4.8&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;15.51 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;15.52 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;64 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryEnumerator&lt;/td&gt;
&lt;td&gt;.NET Core 3.1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;14.60 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;14.60 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;64 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryEnumerator&lt;/td&gt;
&lt;td&gt;.NET 5.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;15.16 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;15.14 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;64 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryEnumerator&lt;/td&gt;
&lt;td&gt;.NET 6.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;18.60 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;18.67 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;64 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryEnumerator&lt;/td&gt;
&lt;td&gt;.NET 8.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;12.97 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;12.96 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;64 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryEnumerator&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;12.80 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;12.81 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;64 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryEnumerator&lt;/td&gt;
&lt;td&gt;.NET Framework 4.8&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;15.41 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;15.38 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;64 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryEnumerator&lt;/td&gt;
&lt;td&gt;.NET Core 3.1&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;15.03 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;14.96 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;64 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryEnumerator&lt;/td&gt;
&lt;td&gt;.NET 5.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;15.90 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;15.92 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;64 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryEnumerator&lt;/td&gt;
&lt;td&gt;.NET 6.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;18.50 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;18.50 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;64 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryEnumerator&lt;/td&gt;
&lt;td&gt;.NET 8.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;13.38 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;12.92 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;64 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryEnumerator&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;12.97 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;12.53 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;64 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Видим, что результаты не зависят от количества элементов и практически не изменились в новых версиях .NET.
Но мы расходуем немного памяти на создание итератора каждый раз.&lt;/p&gt;
&lt;p&gt;Но можно воспользоваться специализированным свойством коллекции - &lt;code&gt;IsEmpty&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;[Benchmark]
public bool DictionaryIsEmpty()
{
    return !dictionary.IsEmpty;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Median&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Allocated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryIsEmpty&lt;/td&gt;
&lt;td&gt;.NET Framework 4.8&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;99.023 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;98.709 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryIsEmpty&lt;/td&gt;
&lt;td&gt;.NET Core 3.1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.313 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.313 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryIsEmpty&lt;/td&gt;
&lt;td&gt;.NET 5.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.545 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.545 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryIsEmpty&lt;/td&gt;
&lt;td&gt;.NET 6.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.247 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.283 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryIsEmpty&lt;/td&gt;
&lt;td&gt;.NET 8.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.567 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.568 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryIsEmpty&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;10.917 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;10.902 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryIsEmpty&lt;/td&gt;
&lt;td&gt;.NET Framework 4.8&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6,027.991 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6,026.648 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryIsEmpty&lt;/td&gt;
&lt;td&gt;.NET Core 3.1&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.320 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.315 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryIsEmpty&lt;/td&gt;
&lt;td&gt;.NET 5.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.513 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.512 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryIsEmpty&lt;/td&gt;
&lt;td&gt;.NET 6.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.756 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.759 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryIsEmpty&lt;/td&gt;
&lt;td&gt;.NET 8.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.673 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.673 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DictionaryIsEmpty&lt;/td&gt;
&lt;td&gt;.NET 9.0&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.801 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3.804 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Видим, что уже начиная с .NET Core 3.1 реализация этого свойства не зависит от размера коллекции и в случае не пустых коллекций вызов этого свойства не является блокирующим.
Для .NET 4.8 мы получили результаты аналогичные вызову свойства &lt;code&gt;Count&lt;/code&gt; - так же используется блокировка на всю коллекцию.
Для .NET 9 результаты получились несколько хуже, но у меня нет быстрого ответа, почему так произошло.&lt;/p&gt;
&lt;h1 id="section-2"&gt;Заключение&lt;/h1&gt;
&lt;p&gt;Реализация метода &lt;code&gt;Any()&lt;/code&gt; сегодня достаточно оптимизирована, чтобы его использовать в обобщенном коде, но еще есть задел на его использование в потокобезопасных коллекциях.&lt;/p&gt;
&lt;h1 id="section-3"&gt;Рекомендации&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Используйте свойства &lt;code&gt;Count&lt;/code&gt; для простых коллекций, если вам важна производительность.&lt;/li&gt;
&lt;li&gt;Используйте метод &lt;code&gt;Any()&lt;/code&gt; для обобщенного кода и &lt;code&gt;IEnumerable&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Используйте свойство &lt;code&gt;IsEmpty&lt;/code&gt;, если коллекции его поддерживают, начиная с .NET Core 3.1.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>nvarchar(max) vs nvarchar(N)</title>
			<link>https://blog.rogatnev.net/posts/ru/2025/04/nvarchar(max)-vs-nvarchar(N).html</link>
			<description>Рассматриваем разницу между типами nvarchar(max) vs nvarchar(N) в MS SQL Server - что использовать в разных случаях и как это влияет на производительность</description>
			<author>Сергей Рогатнев</author>
			<guid>https://blog.rogatnev.net/posts/ru/2025/04/nvarchar(max)-vs-nvarchar(N).html</guid>
			<pubDate>Mon, 07 Apr 2025 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;MS SQL Server предоставляет нам на выбор несколько типов данных для хранения строк. Самый популярный - &lt;code&gt;nvarchar(N)&lt;/code&gt; (где 1 ≤ N ≤ 4000) или &lt;code&gt;nvarchar(max)&lt;/code&gt;, который позволяет хранить строковые данные в кодировке Юникод. При этом, часто прикладные разработчики переносят опыт использования типа &lt;code&gt;string&lt;/code&gt; из языка приложения (C# или Java, например) в базы данных и не задумываясь выбирают тип &lt;code&gt;nvarchar(max)&lt;/code&gt;, позволяющий хранить строки размером до 2ГБ. Но базы данных хранят и работают с данными совершенно другим способом. В этой статье я расскажу и покажу на практике, к чему может приводить неоправданное использование типа &lt;code&gt;nvarchar(max)&lt;/code&gt;.&lt;/p&gt;
&lt;!--more--&gt;
&lt;h1 id="section"&gt;Подготовка&lt;/h1&gt;
&lt;p&gt;Давайте создадим 3 таблицы, хранящие текстовые данные:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE [dbo].[nv100]
(
	[Id] [int] NOT NULL,
	[CreateDate] [datetime] NOT NULL,
	[Comment] [nvarchar](100) NOT NULL,
	CONSTRAINT [PK_nv100_Id] PRIMARY KEY CLUSTERED ([Id] ASC)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE [dbo].[nv1000]
(
	[Id] [int] NOT NULL,
	[CreateDate] [datetime] NOT NULL,
	[Comment] [nvarchar](1000) NOT NULL,
	CONSTRAINT [PK_nv1000_Id] PRIMARY KEY CLUSTERED ([Id] ASC)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE [dbo].[nvmax]
(
	[Id] [int] NOT NULL,
	[CreateDate] [datetime] NOT NULL,
	[Comment] [nvarchar](max) NOT NULL,
	CONSTRAINT [PK_nvmax_Id] PRIMARY KEY CLUSTERED ([Id] ASC)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Таблицы отличаются типом колонки &lt;code&gt;Comment&lt;/code&gt;. Заполним эти таблицы 200 000 строками подобного вида:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Id          CreateDate              Comment
----------- ----------------------- ----------
1           2025-01-01 00:00:01.000 1
2           2025-01-01 00:00:02.000 2
3           2025-01-01 00:00:03.000 3
4           2025-01-01 00:00:04.000 4
5           2025-01-01 00:00:05.000 5
6           2025-01-01 00:00:06.000 6
7           2025-01-01 00:00:07.000 7
8           2025-01-01 00:00:08.000 8
9           2025-01-01 00:00:09.000 9
10          2025-01-01 00:00:10.000 10
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id="section-1"&gt;Тестируем выборки&lt;/h1&gt;
&lt;p&gt;Просто выберем все данные из таблиц и посмотрим на план выполнения:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select *
from nv100

select *
from nv1000

select *
from nvmax
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;На первый взгляд мы получаем 3 одинаковых плана:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/nvarchar/query1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Давайте посмотрим на единственную стрелочку в плане первого запроса:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/nvarchar/query1_1.png" alt="nvarchar(100)"&gt;&lt;/p&gt;
&lt;p&gt;Мы видим, что SQL Server правильно определил количество строк, которые необходимо вернуть - 200 000. Это предсказуемо, т.к. мы выбираем все данные из таблицы.
Более интересный показатель &lt;code&gt;Estimated Row Size&lt;/code&gt; - предполагаемый размер одной строки в байтах. Из чего он складывается? Это метаданные, которые есть в каждой строке и непосредственно сами данные. В нашем случае 4 байта отводится под идентификатор типа &lt;code&gt;int&lt;/code&gt;, 8 байт для поля типа &lt;code&gt;datetime&lt;/code&gt; и из &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/data-types/nchar-and-nvarchar-transact-sql?view=sql-server-ver16"&gt;документации&lt;/a&gt; мы знаем, что тип &lt;code&gt;nvarchar(n)&lt;/code&gt; занимает 2*n байт. Для колонки с типом &lt;code&gt;nvarchar(100)&lt;/code&gt; будет 200 байт. 4 + 8 + 200 = 212 байт. Почему же SQL Server оценивает размер в 123 байта?&lt;/p&gt;
&lt;p&gt;При составлении плана запроса сервер делает оценку по полю &lt;code&gt;nvarchar(n)&lt;/code&gt; как половину от максимально возможного количества байт. Т.е. 100 байт для первой таблицы. Таким образом, предполагаемый объем занимаемых данных в одной строке - 4 + 8 + 100 = 112 байт. Еще несколько байт занимают метаданные. Давайте посмотрим на план запроса для второй таблицы:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/nvarchar/query1_2.png" alt="nvarchar(1000)"&gt;&lt;/p&gt;
&lt;p&gt;Видим, что &lt;code&gt;Estimated Row Size&lt;/code&gt; вырос до 1023 байт, из которых на данные отводится теперь 4 + 8 + 1000 = 1012 байт, остальное - опять метаданные. А что же будет для типа &lt;code&gt;nvarchar(max)&lt;/code&gt;? Данный тип данных &lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/data-types/nchar-and-nvarchar-transact-sql?view=sql-server-ver16"&gt;позволяет хранить до 2ГБ&lt;/a&gt;. Неужели, каждая строка будет получать оценку в 1ГБ данных? На самом деле нет, SQL Server оценивает такие данные в 4000 байт:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/nvarchar/query1_3.png" alt="nvarchar(max)"&gt;&lt;/p&gt;
&lt;p&gt;Если мы исключим колонку &lt;code&gt;Comment&lt;/code&gt; из выборки, то SQL Server будет делать точные оценки:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select Id, CreateDate
from nvmax
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/nvarchar/query1_4.png" alt="nvarchar(max)"&gt;&lt;/p&gt;
&lt;h1 id="section-2"&gt;Сортировка&lt;/h1&gt;
&lt;p&gt;Как мы знаем, перед выполнением запроса SQL Server пытается оставить оптимальный план - то, как именно получить данные, какие алгоритмы использовать. На составление плана влияет множество параметров: размеры таблиц, наличие индексов, типы данных и многое другое... Один из таких влияющих параметров - &lt;code&gt;Estimated Row Size&lt;/code&gt;. Опираясь на него SQL Server определяет, сколько оперативной памяти нужно выделить под выполнение определенного запроса и нужно ли использовать &lt;code&gt;TempDB&lt;/code&gt; (например, для сортировки больших данных).&lt;/p&gt;
&lt;p&gt;Давайте проверим это на примере и отсортируем нашу выборку по комментарию:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select *
from nv100
order by Comment desc

select *
from nv1000
order by Comment desc

select *
from nvmax
order by Comment desc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;И посмотрим на план выполнения:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/nvarchar/query2.png" alt="order by Comment desc"&gt;&lt;/p&gt;
&lt;p&gt;Получаются очень похожие планы, но обратите внимание на восклицательный знак возле оператора &lt;code&gt;SELECT&lt;/code&gt; во втором запросе - там явно есть какая-то проблема:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/nvarchar/query2_1.png" alt="nvarchar(1000)"&gt;&lt;/p&gt;
&lt;p&gt;Мы видим предупреждение типа &lt;code&gt;Excessive Grant&lt;/code&gt;. Основываясь на оценках объема данных SQL Server запросил для выполнения запроса 295616 килобайт (~300 мегабайт), а фактически было использовано всего 14584 килобайт (~15 мегабайт), т.е. в 20 раз меньше.&lt;/p&gt;
&lt;p&gt;Еще хуже ситуация с третьим запросом:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/nvarchar/query2_2.png" alt="nvarchar(max)"&gt;&lt;/p&gt;
&lt;p&gt;Запросив 1122496 килобайт (~1.1 гигабайт), было использовано всего 19960 килобайт (~20 мегабайт) - в 56 раз меньше. Фактически, мы заставили SQL Server выделить лишний гигабайт памяти, который никак не был использован. Как минимум, это не полезно, а в худшем случае - может отнять ресурсы у более полезного запроса.&lt;/p&gt;
&lt;h1 id="section-3"&gt;Фильтрация&lt;/h1&gt;
&lt;p&gt;Давайте сделаем выборку с фильтрацией по полю &lt;code&gt;Comment&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select *
from nv100
where Comment = N'12345'

select *
from nv1000
where Comment = N'12345'

select *
from nvmax
where Comment = N'12345'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;И традиционно посмотрим на план выполнения:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.rogatnev.net/img/nvarchar/query3.png" alt="Filter by comment"&gt;&lt;/p&gt;
&lt;p&gt;Во всех случаях мы полностью сканируем кластерный индекс, но бросается в глаза, что в случае с таблицей &lt;code&gt;nvmax&lt;/code&gt; появляется дополнительная операция &lt;code&gt;Filter&lt;/code&gt;. Почему так? Причина в способе хранения данных. Данные типа &lt;code&gt;nvarchar(max)&lt;/code&gt; могут храниться двумя разными способами:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IN_ROW_DATA&lt;/code&gt; - если данные меньше или равны 8000 байт, то они хранятся непосредственно в строке таблицы.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LOB_DATA&lt;/code&gt; - если данные превышают 8000 байт, то они хранятся в отдельных страницах внутри файлов базы данных. В строке остаётся только указатель на эти данные.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;В нашей колонке &lt;code&gt;Comment&lt;/code&gt; данные значительно меньше 8000 байт, но SQL Server мог по другим причинам разместить данные в &lt;code&gt;LOB_DATA&lt;/code&gt;. Давайте проверим, где именно располагаются данные:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT
	t.Name as TableName,
	a.type_desc as StorageType
FROM sys.tables t
JOIN sys.columns c ON t.object_id = c.object_id
JOIN sys.partitions p ON t.object_id = p.object_id
JOIN sys.allocation_units a ON p.partition_id = a.container_id
WHERE c.system_type_id = 231 -- nvarchar(max)
  AND a.type_desc IN ('IN_ROW_DATA', 'LOB_DATA')
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Результат запроса:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TableName  StorageType
---------- -------------
nv100      IN_ROW_DATA
nv1000     IN_ROW_DATA
nvmax      IN_ROW_DATA
nvmax      LOB_DATA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Видим, что для таблиц &lt;code&gt;nv100&lt;/code&gt; и &lt;code&gt;nv1000&lt;/code&gt; данные хранятся только в режиме &lt;code&gt;IN_ROW_DATA&lt;/code&gt;. Для таблицы &lt;code&gt;nvmax&lt;/code&gt; используются оба варианта (&lt;code&gt;IN_ROW_DATA&lt;/code&gt;, &lt;code&gt;LOB_DATA&lt;/code&gt;).
Возвращаясь к запросу с фильтрацией, такое расположение данных приводит к тому, что серверу предварительно требуется извлечь данные, а только потом применить фильтрацию. От этого в плане запроса мы видим дополнительную операцию &lt;code&gt;Filter&lt;/code&gt;.&lt;/p&gt;
&lt;h1 id="section-4"&gt;Индексы&lt;/h1&gt;
&lt;p&gt;Да, поиск по строке, не самый частый сценарий. А если это и требуется, то логично создать индекс. Но тут очевидное ограничение, на колонку с типом &lt;code&gt;nvarchar(max)&lt;/code&gt; нельзя создать индекс. Более того, при попытке создать индекс на колонку с типом &lt;code&gt;nvarchar(1000)&lt;/code&gt; мы получим предупреждение:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE NONCLUSTERED INDEX [IX_nv1000_Comment] ON [dbo].[nv1000]
(
	[Comment] ASC
)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Warning! The maximum key length for a nonclustered index is 1700 bytes. The index 'IX_nv1000_Comment' has maximum length of 2000 bytes. For some combination of large values, the insert/update operation will fail.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Индекс будет работать только для определенных данных. При попытке вставить строку максимальной длины мы получим ошибку:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;insert into nv1000
values(-1, GETUTCDATE(), REPLICATE(N'A',1000))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Msg 1946, Level 16, State 3, Line 1
Operation failed. The index entry of length 2000 bytes for the index 'IX_nv1000_Comment' exceeds the maximum length of 1700 bytes for nonclustered indexes.
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id="section-5"&gt;Сравнение&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;nvarchar(100)&lt;/th&gt;
&lt;th&gt;nvarchar(1000)&lt;/th&gt;
&lt;th&gt;nvarchar(max)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Хранение данных&lt;/td&gt;
&lt;td&gt;IN_ROW_DATA&lt;/td&gt;
&lt;td&gt;IN_ROW_DATA&lt;/td&gt;
&lt;td&gt;IN_ROW_DATA/LOB_DATA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Оценка размера строки&lt;/td&gt;
&lt;td&gt;100 байт&lt;/td&gt;
&lt;td&gt;1000 байт&lt;/td&gt;
&lt;td&gt;4000 байт&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Индекс&lt;/td&gt;
&lt;td&gt;Да&lt;/td&gt;
&lt;td&gt;Да, с ограничениями&lt;/td&gt;
&lt;td&gt;Нет&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1 id="section-6"&gt;Выводы&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Число &lt;code&gt;n&lt;/code&gt; в определении типа &lt;code&gt;nvarchar(n)&lt;/code&gt; - не просто ограничение максимальной длины строки. Это важная информация, которая помогает SQL Server'у выбирать наиболее оптимальный план запроса.&lt;/li&gt;
&lt;li&gt;Использование &lt;code&gt;nvarchar(max)&lt;/code&gt; должно быть оправдано и использоваться для действительно больших строк. Скорее всего, ваша бизнес-логика ограничивает максимальную длину строки в приложении. Если это пользовательский ввод или API - там наверняка будут ограничения. Перенесите эти ограничения на базу данных.&lt;/li&gt;
&lt;li&gt;Оптимальным размером для типа &lt;code&gt;nvarchar(n)&lt;/code&gt; будет удвоенная средняя длина хранящихся строк.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="section-7"&gt;Ссылки&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/ru-ru/sql/t-sql/data-types/nchar-and-nvarchar-transact-sql?view=sql-server-ver16"&gt;nchar и nvarchar (Transact-SQL)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>LINQ: Any() vs Count</title>
			<link>https://blog.rogatnev.net/posts/ru/2018/06/LINQ-Any-vs-Count.html</link>
			<description>Исследование методов Any() и Count для классического .NET Framework для разных коллекций</description>
			<author>Сергей Рогатнев</author>
			<guid>https://blog.rogatnev.net/posts/ru/2018/06/LINQ-Any-vs-Count.html</guid>
			<pubDate>Sun, 17 Jun 2018 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;Эта статья началась с обсуждения на Stack Overflow: &lt;a href="https://stackoverflow.com/questions/49663052/why-linq-method-any-does-not-check-count"&gt;Why LINQ method Any does not check Count?&lt;/a&gt;. Здесь мы сравним производительность методов &lt;code&gt;Any&lt;/code&gt; и &lt;code&gt;Count != 0&lt;/code&gt;.&lt;/p&gt;
&lt;!--more--&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;UPD:&lt;/strong&gt; Вышла вторая часть статьи: &lt;a href="xref:2025-05-06-Any-vs-Count-part-2"&gt;Any() vs Count: часть 2&lt;/a&gt;, где сравниваются реализации в новых версиях .NET.
Эта статья только о .NET 4.7.1.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Сначала добавим наш собственный метод-расширения, который реализует нужную логику сравнения:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public static class EnumerableExtensions
{
    public static bool CustomAny&amp;lt;TSource&amp;gt;(this IEnumerable&amp;lt;TSource&amp;gt; source)
    {
        if (source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }

        if (source is ICollection&amp;lt;TSource&amp;gt; collection)
        {
            return collection.Count != 0;
        }

        using (var enumerator = source.GetEnumerator())
        {
            if (enumerator.MoveNext())
                return true;
        }

        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Идея: для коллекций, реализующих &lt;code&gt;ICollection&amp;lt;T&amp;gt;&lt;/code&gt;, использовать свойство &lt;code&gt;Count&lt;/code&gt;, чтобы избежать создания энумератора. Но будет ли это быстрее стандартного &lt;code&gt;Any()&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;Используем &lt;a href="https://github.com/dotnet/BenchmarkDotNet"&gt;BenchmarkDotNet&lt;/a&gt; для тестирования.&lt;/p&gt;
&lt;p&gt;Проверим на следующих коллекциях:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;List&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Array&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HashSet&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Dictionary&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Stack&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Queue&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SortedList&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SortedSet&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Каждая коллекция содержит 1000 элементов. Сравниваем &lt;code&gt;Any()&lt;/code&gt; и &lt;code&gt;CustomAny()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;[CategoriesColumn]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
public class SimpleCollections
{
    [Params(1000)]
    public int _size;

    private List&amp;lt;int&amp;gt; _list;
    private int[] _array;
    private HashSet&amp;lt;int&amp;gt; _hashSet;
    private Dictionary&amp;lt;int, int&amp;gt; _dictionary;
    private Stack&amp;lt;int&amp;gt; _stack;
    private Queue&amp;lt;int&amp;gt; _queue;
    private SortedList&amp;lt;int, int&amp;gt; _sortedList;
    private SortedSet&amp;lt;int&amp;gt; _sortedSet;

    [GlobalSetup]
    public void SetUp()
    {
        _list = new List&amp;lt;int&amp;gt;();
        _array = new int[_size];
        _hashSet = new HashSet&amp;lt;int&amp;gt;();
        _dictionary = new Dictionary&amp;lt;int, int&amp;gt;();
        _stack = new Stack&amp;lt;int&amp;gt;();
        _queue = new Queue&amp;lt;int&amp;gt;();
        _sortedList = new SortedList&amp;lt;int, int&amp;gt;();
        _sortedSet = new SortedSet&amp;lt;int&amp;gt;();

        for (int i = 0; i &amp;lt; _size; i++)
        {
            _list.Add(i);
            _array[i] = i;
            _hashSet.Add(i);
            _dictionary[i] = i;
            _stack.Push(i);
            _queue.Enqueue(i);
            _sortedList.Add(i, i);
            _sortedSet.Add(i);
        }
    }
    
    [Benchmark(Baseline = true, Description = "Any"), BenchmarkCategory("List")]
    public bool ListAny()
    {
        return _list.Any();
    }

    [Benchmark(Description = "Custom"), BenchmarkCategory("List")]
    public bool ListCount()
    {
        return _list.CustomAny();
    }
    
    ///other collections
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id="section"&gt;Первые результаты&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
Intel Core i7-2670QM CPU 2.20GHz (Sandy Bridge), 1 CPU, 8 logical and 4 physical cores
Frequency=2143565 Hz, Resolution=466.5126 ns, Timer=TSC
  [Host]     : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3110.0
  DefaultJob : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3110.0
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: center;"&gt;Method&lt;/th&gt;
&lt;th style="text-align: center;"&gt;Categories&lt;/th&gt;
&lt;th style="text-align: center;"&gt;_size&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Median&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Scaled&lt;/th&gt;
&lt;th style="text-align: right;"&gt;ScaledSD&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;List&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;38.88 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;38.32 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;List&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;16.49 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;16.50 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.43&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;Array&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;27.46 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;27.39 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;Array&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;51.21 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;50.60 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.87&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;HashSet&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;39.87 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;39.89 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;HashSet&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;15.06 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;15.01 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.38&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;Dictionary&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;51.14 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;50.83 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;Dictionary&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;17.41 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;17.36 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.34&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;Stack&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;42.10 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;42.29 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;Stack&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;49.41 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;48.60 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.17&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;Queue&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;44.68 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;44.41 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;Queue&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;50.68 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;50.66 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.14&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;SortedList&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;42.89 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;42.55 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;SortedList&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;17.48 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;17.17 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.41&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;SortedSet&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;220.19 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;219.66 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;SortedSet&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;18.46 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;18.34 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.08&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Здесь и далее испльзуется легенда:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  Categories : Collection type
  _size      : Number of elements in collection
  Mean       : Arithmetic mean of all measurements
  Error      : Half of 99.9% confidence interval
  StdDev     : Standard deviation of all measurements
  Median     : Value separating the higher half of all measurements (50th percentile)
  Scaled     : Mean(CurrentBenchmark) / Mean(BaselineBenchmark)
  ScaledSD   : Standard deviation of ratio of distribution of [CurrentBenchmark] and [BaselineBenchmark]
  1 ns       : 1 Nanosecond (0.000000001 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Как мы видим, новый метод с проверкой &lt;code&gt;Count&lt;/code&gt; быстрее или сравним по производительности со стандартным &lt;code&gt;Any()&lt;/code&gt; за исключением нескольких коллекций.&lt;/p&gt;
&lt;h2 id="section-1"&gt;Почему массив замедлился?&lt;/h2&gt;
&lt;p&gt;Хотя массивы реализуют &lt;code&gt;ICollection&amp;lt;T&amp;gt;&lt;/code&gt;, проверка типа &lt;code&gt;source is ICollection&amp;lt;T&amp;gt;&lt;/code&gt; в .NET работает медленнее для них. Решение — добавить отдельную проверку на массив:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public static class EnumerableExtensions
{
    public static bool CustomAny&amp;lt;TSource&amp;gt;(this IEnumerable&amp;lt;TSource&amp;gt; source)
    {
        // ... остальная логика ...

        if (source is TSource[] array)
        {
            return array.Length != 0;
        }

        // ... остальная логика ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;После доработки результаты для массива улучшились:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: center;"&gt;Method&lt;/th&gt;
&lt;th style="text-align: center;"&gt;Categories&lt;/th&gt;
&lt;th style="text-align: center;"&gt;_size&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Scaled&lt;/th&gt;
&lt;th style="text-align: right;"&gt;ScaledSD&lt;/th&gt;
&lt;th style="text-align: right;"&gt;ScaledSD&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;Array&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;27.11 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;Array&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;18.87 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.70&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.02&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.05&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="stack-and-queue"&gt;Stack and Queue&lt;/h2&gt;
&lt;p&gt;Проблема: &lt;code&gt;Stack&amp;lt;T&amp;gt;&lt;/code&gt; и &lt;code&gt;Queue&amp;lt;T&amp;gt;&lt;/code&gt; не реализуют &lt;code&gt;ICollection&amp;lt;T&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public class Stack&amp;lt;T&amp;gt; : IEnumerable&amp;lt;T&amp;gt;, IEnumerable, ICollection, IReadOnlyCollection&amp;lt;T&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public class Queue&amp;lt;T&amp;gt; : IEnumerable&amp;lt;T&amp;gt;, IEnumerable, ICollection, IReadOnlyCollection&amp;lt;T&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Но имеют необобщенный &lt;code&gt;ICollection&lt;/code&gt;. Добавим проверку:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public static class EnumerableExtensions
{
    public static bool CustomAny&amp;lt;TSource&amp;gt;(this IEnumerable&amp;lt;TSource&amp;gt; source)
    {
        // ... остальная логика ...

        if (source is ICollection baseCollection)
        {
            return baseCollection.Count != 0;
        }

        // ... остальная логика ...

        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Результаты после оптимизации:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: center;"&gt;Method&lt;/th&gt;
&lt;th style="text-align: center;"&gt;Categories&lt;/th&gt;
&lt;th style="text-align: center;"&gt;_size&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Scaled&lt;/th&gt;
&lt;th style="text-align: right;"&gt;ScaledSD&lt;/th&gt;
&lt;th style="text-align: right;"&gt;ScaledSD&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;Stack&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;39.56 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;Stack&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;31.66 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.80&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.01&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;Queue&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;42.90 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;Queue&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;31.69 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.74&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.04&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="sortedset"&gt;Загадка SortedSet&lt;/h2&gt;
&lt;p&gt;Почему Any() для SortedSet такой медленный? Всё дело в реализации его энумератора.&lt;/p&gt;
&lt;p&gt;Метод &lt;code&gt;GetEnumerator&lt;/code&gt; создаёт структуру типа &lt;code&gt;Enumerator&lt;/code&gt;, которая принимает &lt;code&gt;SortedSet&amp;lt;T&amp;gt;&lt;/code&gt; в качестве параметра конструктора:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;internal Enumerator(SortedSet&amp;lt;T&amp;gt; set)
{
  this.tree = set;
  this.tree.VersionCheck();
  this.version = this.tree.version;
  this.stack = new Stack&amp;lt;SortedSet&amp;lt;T&amp;gt;.Node&amp;gt;(2 * SortedSet&amp;lt;T&amp;gt;.log2(set.Count + 1));
  this.current = (SortedSet&amp;lt;T&amp;gt;.Node) null;
  this.reverse = false;
  this.siInfo = (SerializationInfo) null;
  this.Intialize();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Взглянем на метод &lt;code&gt;Intialize&lt;/code&gt; (с опечаткой в названии):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;private void Intialize()
{
  this.current = (SortedSet&amp;lt;T&amp;gt;.Node) null;
  SortedSet&amp;lt;T&amp;gt;.Node node1 = this.tree.root;
  while (node1 != null)
  {
    SortedSet&amp;lt;T&amp;gt;.Node node2 = this.reverse ? node1.Right : node1.Left;
    SortedSet&amp;lt;T&amp;gt;.Node node3 = this.reverse ? node1.Left : node1.Right;
    if (this.tree.IsWithinRange(node1.Item))
    {
      this.stack.Push(node1);
      node1 = node2;
    }
    else
      node1 = node2 == null || !this.tree.IsWithinRange(node2.Item) ? node3 : node2;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Класс &lt;code&gt;SortedSet&amp;lt;T&amp;gt;&lt;/code&gt; построен на бинарном дереве, а метод &lt;code&gt;Intialize&lt;/code&gt; обходит все узлы дерева и помещает элементы в стек.
Таким образом, при создании энумератора для &lt;code&gt;SortedSet&lt;/code&gt;, его внутренняя реализация фактически формирует новую коллекцию со всеми элементами.
Чем больше элементов, тем больше времени тратится на это копирование:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: center;"&gt;Method&lt;/th&gt;
&lt;th style="text-align: center;"&gt;_size&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Scaled&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;230.64 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;22.23 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;288.27 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;21.72 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;100000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;343.54 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;100000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;21.70 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.06&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Мы проверили наиболее популярные коллекции и можно сказать, что мы должны использовать свойство &lt;code&gt;Count&lt;/code&gt; вместо вызова метода &lt;code&gt;Enumerable.Any()&lt;/code&gt;.
Но всегда ли безопасно использовать &lt;code&gt;Count&lt;/code&gt;?
Есть ли коллекции, где вычисление свойства &lt;code&gt;Count&lt;/code&gt; занимает больше времени, чем создание энумератора? Вы будете удивлены.&lt;/p&gt;
&lt;h1 id="concurrent-collections"&gt;Concurrent collections&lt;/h1&gt;
&lt;p&gt;Все коллекции до этого были не потокобезопасными - они не могут быть использованы в многопоточном коде без дополнительных синхронизаций.
Чтобы этого избежать, был создан отдельный набор потокобезопасных коллекций.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ConcurrentBag&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ConcurrentDictionary&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ConcurrentQueue&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ConcurrentStack&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Давайте для теста заполним эти коллекции из разных потоков симулируя многопоточную работу:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;[GlobalSetup]
public void SetUp()
{
    _bag = new ConcurrentBag&amp;lt;int&amp;gt;();
    _dictionary = new ConcurrentDictionary&amp;lt;int, int&amp;gt;();
    _queue = new ConcurrentQueue&amp;lt;int&amp;gt;();
    _stack = new ConcurrentStack&amp;lt;int&amp;gt;();

    var tasksCount = 10;
    var batch = _size / tasksCount;

    var tasks = new Task[tasksCount];

    for (int i = 0; i &amp;lt; tasksCount; i++)
    {
        var task = i;

        tasks[task] = Task.Run(() =&amp;gt;
        {
            var from = task * batch;
            var to = (task + 1) * batch;

            for (int j = from; j &amp;lt; to; j++)
            {
                _bag.Add(j);
                _dictionary[j] = j;
                _queue.Enqueue(j);
                _stack.Push(j);
            }
        });
    }

    Task.WaitAll(tasks);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id="section-2"&gt;Результаты&lt;/h1&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: center;"&gt;Method&lt;/th&gt;
&lt;th style="text-align: center;"&gt;Categories&lt;/th&gt;
&lt;th style="text-align: center;"&gt;_size&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Scaled&lt;/th&gt;
&lt;th style="text-align: right;"&gt;ScaledSD&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;ConcurrentBag&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;7,065.75 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;ConcurrentBag&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;267.89 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.04&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;ConcurrentDictionary&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;39.29 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;ConcurrentDictionary&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6,319.42 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;160.88&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;ConcurrentStack&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;28.08 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;ConcurrentStack&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3,179.18 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;113.23&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;ConcurrentQueue&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;72.12 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;ConcurrentQueue&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;48.28 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.67&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.02&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Получили неожиданные результаты. Где-то &lt;code&gt;Any()&lt;/code&gt; очень медленный, а где-то наоборот.&lt;/p&gt;
&lt;h2 id="concurrentbag"&gt;ConcurrentBag&lt;/h2&gt;
&lt;p&gt;Метод &lt;code&gt;GetEnumerator&lt;/code&gt; работает аналогично как и в &lt;code&gt;SortedSet&lt;/code&gt;. Он блокирует исходную коллекцию и копирует все элементы в новый список:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;private List&amp;lt;T&amp;gt; ToList()
{
  List&amp;lt;T&amp;gt; objList = new List&amp;lt;T&amp;gt;();
  for (ConcurrentBag&amp;lt;T&amp;gt;.ThreadLocalList threadLocalList = this.m_headList; threadLocalList != null; threadLocalList = threadLocalList.m_nextList)
  {
    for (ConcurrentBag&amp;lt;T&amp;gt;.Node node = threadLocalList.m_head; node != null; node = node.m_next)
      objList.Add(node.m_value);
  }
  return objList;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Свойство &lt;code&gt;Count&lt;/code&gt; блокирует коллекцию и суммирует внутренние счетчики количества элементов:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;private int GetCountInternal()
{
  int num = 0;
  for (ConcurrentBag&amp;lt;T&amp;gt;.ThreadLocalList threadLocalList = this.m_headList; threadLocalList != null; threadLocalList = threadLocalList.m_nextList)
    checked { num += threadLocalList.Count; }
  return num;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Поэтому, &lt;code&gt;Count&lt;/code&gt; намного быстрее метода &lt;code&gt;Any()&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="concurrentdictionary"&gt;ConcurrentDictionary&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;GetEnumerator&lt;/code&gt; не создаёт новой коллекции, а использует ключевое слово &lt;code&gt;yield&lt;/code&gt; для ленивой итерации по коллекции, поэтому &lt;code&gt;Any()&lt;/code&gt; совершает всего одну итерацию:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public IEnumerator&amp;lt;KeyValuePair&amp;lt;TKey, TValue&amp;gt;&amp;gt; GetEnumerator()
{
  foreach (ConcurrentDictionary&amp;lt;TKey, TValue&amp;gt;.Node bucket in this.m_tables.m_buckets)
  {
    ConcurrentDictionary&amp;lt;TKey, TValue&amp;gt;.Node current;
    for (current = Volatile.Read&amp;lt;ConcurrentDictionary&amp;lt;TKey, TValue&amp;gt;.Node&amp;gt;(ref bucket); current != null; current = current.m_next)
      yield return new KeyValuePair&amp;lt;TKey, TValue&amp;gt;(current.m_key, current.m_value);
    current = (ConcurrentDictionary&amp;lt;TKey, TValue&amp;gt;.Node) null;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Свойство &lt;code&gt;Count&lt;/code&gt; блокирует коллекцию и подсчитывает количество элементов:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public int Count
{
  [__DynamicallyInvokable] get
  {
    int locksAcquired = 0;
    try
    {
      this.AcquireAllLocks(ref locksAcquired);
      return this.GetCountInternal();
    }
    finally
    {
      this.ReleaseLocks(0, locksAcquired);
    }
  }
}

private int GetCountInternal()
{
  int num = 0;
  for (int index = 0; index &amp;lt; this.m_tables.m_countPerLock.Length; ++index)
    num += this.m_tables.m_countPerLock[index];
  return num;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;И чем больше элементов в коллекции, тем больше времени требуется на подсчет:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: center;"&gt;Method&lt;/th&gt;
&lt;th style="text-align: center;"&gt;_size&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Scaled&lt;/th&gt;
&lt;th style="text-align: right;"&gt;ScaledSD&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;37.79 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;10&lt;/td&gt;
&lt;td style="text-align: right;"&gt;239.63 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6.34&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;100&lt;/td&gt;
&lt;td style="text-align: right;"&gt;34.46 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;100&lt;/td&gt;
&lt;td style="text-align: right;"&gt;823.62 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;23.90&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;38.22 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;1000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6,264.17 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;163.92&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2.09&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: center;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Any&lt;/td&gt;
&lt;td style="text-align: center;"&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;35.57 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.00&lt;/td&gt;
&lt;td style="text-align: right;"&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;Custom&lt;/td&gt;
&lt;td style="text-align: center;"&gt;10000&lt;/td&gt;
&lt;td style="text-align: right;"&gt;24,819.31 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;697.82&lt;/td&gt;
&lt;td style="text-align: right;"&gt;11.97&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Профайлер показывает, что больше всего времени теряется на блокировках:
&lt;img src="https://blog.rogatnev.net/img/any/dictionary_count.png" alt="Профилирование ConcurrentDictionary.Count"&gt;&lt;/p&gt;
&lt;h2 id="concurrentstack"&gt;ConcurrentStack&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;GetEnumerator&lt;/code&gt; использует ленивое перечисление элементов, поэтому вызов &lt;code&gt;Any()&lt;/code&gt; дешев:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;private IEnumerator&amp;lt;T&amp;gt; GetEnumerator(ConcurrentStack&amp;lt;T&amp;gt;.Node head)
{
  for (ConcurrentStack&amp;lt;T&amp;gt;.Node current = head; current != null; current = current.m_next)
    yield return current.m_value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;В то время как &lt;code&gt;Count&lt;/code&gt; просто подсчитывает количество элементов даже без блокировки:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public int Count
{
  [__DynamicallyInvokable] get
  {
    int num = 0;
    for (ConcurrentStack&amp;lt;T&amp;gt;.Node node = this.m_head; node != null; node = node.m_next)
      ++num;
    return num;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="concurrentqueue"&gt;ConcurrentQueue&lt;/h2&gt;
&lt;p&gt;Реализация &lt;code&gt;GetEnumerator&lt;/code&gt; несколько сложна, но внутри используется ленивое перечисление. &lt;code&gt;Count&lt;/code&gt; использует простые арифметические операции без блокировки. Можем использовать оба способа.&lt;/p&gt;
&lt;p&gt;Как можно заметить, потокобезопасные коллекции имеют более сложную реализацию и отличаются от своих классических коллег.&lt;/p&gt;
&lt;h1 id="section-3"&gt;Универсальный метод для всех коллекций?&lt;/h1&gt;
&lt;p&gt;В классическом фреймворке (.NET 4.7.1) мы должны использовать &lt;code&gt;Count&lt;/code&gt; или &lt;code&gt;Any()&lt;/code&gt; в зависимости от коллекции.&lt;/p&gt;
&lt;p&gt;В новых версиях .NET ситуация изменилась, вы можете ознакомиться с актуальным исследованием в статье &lt;a href="xref:2025-05-06-Any-vs-Count-part-2"&gt;Any() vs Count: часть 2&lt;/a&gt;.&lt;/p&gt;
&lt;h1 id="section-4"&gt;Ссылки&lt;/h1&gt;
&lt;p&gt;&lt;a href="https://github.com/Backs/CountAny"&gt;Исходный код тестов&lt;/a&gt;&lt;/p&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Неизвестный T-SQL</title>
			<link>https://blog.rogatnev.net/posts/ru/2016/09/Unknown-T-SQL.html</link>
			<description>Редко используемые но полезные функции MS SQL Server</description>
			<author>Сергей Рогатнев</author>
			<guid>https://blog.rogatnev.net/posts/ru/2016/09/Unknown-T-SQL.html</guid>
			<pubDate>Tue, 27 Sep 2016 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;Несколько редко используемых, но вполне полезных функций MSSQL. Если вы никогда не слышали о функциях CHOOSE, ROW_NUMBER, RANK, DENSE_RANK, FIRST_VALUE, LAST_VALUE, LAG, LEAD, PERCENTILE_CONT и PERCENTILE_DISC, то ниже вы узнаете, какие полезные вещи можно сделать их помощью. Так же, я покажу несколько примеров использования выражений OVER и PARTITION BY в оконных функциях.&lt;/p&gt;
&lt;!--more--&gt;
&lt;h2 id="section"&gt;Подготовка&lt;/h2&gt;
&lt;p&gt;Создадим две таблицы Employees - информация о сотрудниках, Assessments - информация об изменениях зарплат сотрудников.&lt;/p&gt;
&lt;h3 id="employees"&gt;Employees&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;CREATE TABLE [dbo].[Employees]
(
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [Name] [nvarchar](50) NOT NULL,
    [Age] [int] NOT NULL,
    [TeamName] [nvarchar](50) NOT NULL,
    [Position] [nvarchar](50) NOT NULL,
    [Department] [nvarchar](50) NOT NULL,
    [Salary] [int] NOT NULL,
    [Configuration] [int] NOT NULL,
    CONSTRAINT [PK_Employees] PRIMARY KEY CLUSTERED ([Id] ASC)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="assessments"&gt;Assessments&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;CREATE TABLE [dbo].[Assessments]
(
    [Name] [nvarchar](50) NOT NULL,
    [Year] [int] NOT NULL,
    [Salary] [int] NOT NULL,
    CONSTRAINT [PK_Assessments] PRIMARY KEY CLUSTERED ([Name] ASC, [Year] ASC)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Заполним эти таблицы тестовыми данными:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;INSERT INTO [dbo].[Employees]
(Name, Age, TeamName, Position, Department, Salary, Configuration)
VALUES
('Smith',25,'World','QA','RnD',30000,1),
('Wesson',20,'World','QA','RnD',50000,2),
('Gray',23,'OLAP','QA','RnD',45000,3),
('Black',31,'Flamp','QA','Flamp',60000,1),
('White',33,'World','Dev','RnD',70000,2),
('Constantine',29,'OLAP','Dev','RnD',120000,1),
('Gurken',32,'World','Dev','RnD',110000,1),
('Dunky',26,'Flamp','Dev','Flamp',110000,3)


INSERT INTO dbo.Assessments
(Name, Year, Salary)
SELECT
    e.Name,
    2016,
    e.Salary
FROM dbo.Employees e
UNION ALL
SELECT
    e.Name,
    2015,
    (e.Salary - 1000 * CAST(RAND(CHECKSUM(NEWID()))* 20 AS int))
FROM dbo.Employees e
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="choose"&gt;CHOOSE&lt;/h2&gt;
&lt;p&gt;В таблице Employees есть колонка Configuration, которая может принимать одно из трех значений (1, 2, 3) - конфигурация рабочего места сотрудника (обычный PC, мобильная конфигурация или макбук соответственно). И если для хранения достаточно числового значения, то для отображения необходимо расшифровать его в строку. В классическом варианте можно создать временную таблицу из трех значений и сделать JOIN из нашей таблицы сотрудников:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;CREATE TABLE #Configuration (Id int, Value nvarchar(10))
INSERT INTO #Configuration
VALUES
(1, 'PC'), (2,'Mobile'), (3,'Mac')

SELECT
    e.*,
    c.[Value]
FROM dbo.Employees e
INNER JOIN #Configuration c ON e.Configuration = c.Id
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;В результате получим именно то, что хотели, правда немного многословно: нам пришлось создать дополнительную таблицу и сделать соединение. Более простой вариант - использовать CASE-WHEN:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT
    e.*,
    (CASE e.Configuration
        WHEN 1 THEN 'PC'
        WHEN 2 THEN 'Mobile'
        WHEN 3 THEN 'Mac'
    END)
FROM dbo.Employees e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Но благодаря функции CHOOSE этот код можно еще сократить и сделать более читаемым:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT
    *,
    CHOOSE(e.Configuration, 'PC', 'Mobile','Mac')
FROM dbo.Employees e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Первый аргумент функции - индекс, по которому надо взять значение из оставшегося списка. Если индекс указывает на не существующее значение, то функция привычно вернет NULL.
Все три варианта дают одинаковый результат. Вариант с CHOOSE выглядит предпочтительным для &amp;quot;расшифровки&amp;quot; перечислений, состоящих из небольшого количества значений.&lt;/p&gt;
&lt;h2 id="row_number-rank-dense_rank"&gt;ROW_NUMBER, RANK, DENSE_RANK&lt;/h2&gt;
&lt;p&gt;Все три функции похожи, но наиболее часто используется функция ROW_NUMBER. С нее так же начинается обзор оконных функций, предполагается, что вы уже знакомы с этим понятием. Итак, ROW_NUMBER позволяет нам занумеровать уникальными последовательными значениями все строки выборки или в пределах какой-то группы. Например, давайте составим рейтинг сотрудников по убываю зарплаты в пределах всей компании и в пределах каждой должности:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT
    e.Name,
    e.Salary,
    ROW_NUMBER() OVER(ORDER BY e.Salary DESC) AS EmployeeId,
    e.Position,
    ROW_NUMBER() OVER(PARTITION BY e.Position ORDER BY e.Salary DESC) AS PositionId
FROM dbo.Employees e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Результат:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://1.bp.blogspot.com/-jsCqi_N2a84/V-fczdFuoMI/AAAAAAABJQQ/r4Lq4UruVr444xOefQIJz6BprMDl5DF-ACLcB/s1600/row_number.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;Итак, мы действительно получили рейтинг зарплат и даже с разбивкой по должности. Можно заметить, что некоторые сотрудники (Gurken и Dunky) получают одинаковую зарплату, но находятся на разных позициях рейтинга, что выглядит не логично. Для исправления этой ситуации есть функция RANK:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT
    e.Name,
    e.Salary,
    RANK() OVER(ORDER BY e.Salary DESC) AS EmployeeId,
    e.Position,
    RANK() OVER(PARTITION BY e.Position ORDER BY e.Salary DESC) AS PositionId
    FROM dbo.Employees e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Цель этой функции - дать относительный порядок строки в наборе.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://4.bp.blogspot.com/-n-yf9CrYWAc/V-k1PUvrcTI/AAAAAAABJRk/9NhO5pszSl4qanjgr67ifmVEtwdMJYp9ACLcB/s1600/rank.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;Как мы видим, теперь сотрудники с одинаковой зарплатой находятся на одинаковой позиции в рейтинге. При этом, следующий сотрудник (White) находится на четвертой позиции - эта функция учитывает пропуски. Если же нам не нужны пропуски, и следующий сотрудник должен стать третьим, то необходимо использовать функцию DENSE_RANK:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT
    e.Name,
    e.Salary,
    DENSE_RANK() OVER(ORDER BY e.Salary DESC) AS EmployeeId,
    e.Position,
    DENSE_RANK() OVER(PARTITION BY e.Position ORDER BY e.Salary DESC) AS PositionId
FROM dbo.Employees e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://4.bp.blogspot.com/-f2RwJkJB78U/V-k2L_-OMHI/AAAAAAABJRs/TGWylIlx-3wtmRcHsE14LyJ7LOOynjYZQCEw/s1600/dense_rank.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;Теперь наш рейтинг составлен без пропусков, но с учетом одинаковых значений зарплаты.&lt;/p&gt;
&lt;h2 id="first_value-last_value"&gt;FIRST_VALUE, LAST_VALUE&lt;/h2&gt;
&lt;p&gt;Вот нашей компании потребовалось провести интеграционное тестирование, а это значит, что нужно собрать по одному представителю QA из каждой команды. Пусть будет любой QA из команды, без каких-то условий. Тогда эту задачу можно решить таким способом:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT
    e.TeamName,
    MAX(e.Name) AS QAName
FROM dbo.Employees e
WHERE e.Position = 'QA'
GROUP BY e.TeamName
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Получим вот такой результат:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://2.bp.blogspot.com/-cy7V-XmgGLw/V-k6dJ4UHFI/AAAAAAABJR4/NbrlKP9l_7UHF-5ip0UJm6cdOebjUtkGACEw/s1600/max.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;Вместо функции MAX можно взять MIN, что не сильно изменит ситуацию.
Подумав, что тестирование - это дело ответственное, решили выбрать самого старого по возрасту тестировщика из каждой команды. Это можно сделать таким способом:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT
    e.TeamName,
    (
        SELECT TOP 1 
            e2.Name 
        FROM dbo.Employees e2 
        WHERE e2.TeamName = e.TeamName AND e2.Position = 'QA' 
        ORDER BY e2.Age DESC
    ) AS QAName
FROM dbo.Employees e
GROUP BY e.TeamName
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://3.bp.blogspot.com/-ek1PGqQ1zK0/V-p57AC7fGI/AAAAAAABJTQ/-Pv7S4GxyzM6973RPaVTJakpCeb0UNWOQCLcB/s1600/subquery.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;Как мы видим, Smith старше, чем Wesson, поэтому и попал в нашу итоговую выборку. Но появился один подзапрос, который кажется лишним. Как нам поможет решить эту проблему функция FIRST_VALUE? Эта функция возвращает первую строку сортированного набора. А значит, можем из всех наших сотрудников оставить только QA, поделить по командам, отсортировать по убыванию возраста и выбрать первого:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT
    e.TeamName,
    FIRST_VALUE(e.Name) OVER (PARTITION BY e.TeamName ORDER BY e.Age DESC) AS QAName
FROM [dbo].[Employees] AS e
WHERE e.Position = 'QA'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;В результате получаем:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://1.bp.blogspot.com/-URuCmvyMf7o/V-p7kbbPGhI/AAAAAAABJTY/twpUpz4CPjM515FNIXjmp1fiaV8nI8mDgCLcB/s1600/first_value.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;Почему получили две одинаковые строки (World, Smith)? На первый взгляд это кажется не логичным. Но если посмотреть на запрос без функции FIRST_VALUE, то мы увидим, что выбрали только всех QA (по условию WHERE), а их у нас как раз таки 4, и для каждого выбрали команду и самого старого QA в команде. Сделали немного задом наперед. Для нужного результата нам нужно использовать DISTINCT. Согласен, не самое подходящее использование функции, но все же. Функция LAST_VALUE работает аналогично, но возвращает последний результат набора.&lt;/p&gt;
&lt;h2 id="ntile"&gt;NTILE&lt;/h2&gt;
&lt;p&gt;В связи с реформами в компании, сотрудников из трех существующих команд необходимо распределить по четырем новым командам. Не вопрос! Для этого есть функция NTILE, которая в качестве параметра принимает количество групп, на которые необходимо разделить исходный набор. Каждая строка получит номер группы, к которой она принадлежит:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT
    e.Name,
    e.Position,
    e.TeamName,
    NTILE(4) OVER(ORDER BY e.Id) AS NewTeamId
FROM dbo.Employees e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://4.bp.blogspot.com/-pgDcPFtlk88/V-qBkk6HcrI/AAAAAAABJT4/f1TmbC_zgggM1EscwH0mJ_zeiiNZW5lpgCLcB/s1600/ntile2.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;Каждый сотрудник получил NewTeamId - номер новой команды, в которую попадает сотрудник. Только вот в командах 1 и 2 собрались одни QA, а в командах 3 и 4 - только разработчики. Давайте распределим сначала сотрудников по их должности, для этого добавим выражение PARTITION BY:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT
    e.Name,
    e.Position,
    e.TeamName,
    NTILE(4) OVER(PARTITION BY e.Position ORDER BY e.Id) AS NewTeamId
FROM dbo.Employees e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://4.bp.blogspot.com/-G46qIaUgfR8/V-qAhrfojHI/AAAAAAABJTw/fvJPuStaTrY_cg58fLj3LTGLclKTm7EwwCEw/s1600/ntile.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;Теперь мы сначала распределили всех разработчиков по командам, а потом всех QA, и получили, что в каждой команде есть разработчик и тестировщик, что как минимум лучше, чем прошлый вариант. Если учесть человеческий момент, и собрать в новых командах людей, которые работали в старых командах вместе, то необходимо изменить способ упорядочивания для функции NTILE:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT
    e.Name,
    e.Position,
    e.TeamName,
    NTILE(4) OVER(PARTITION BY e.Position ORDER BY e.TeamName) AS NewTeamId
FROM dbo.Employees e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://4.bp.blogspot.com/-8rrYWzXodso/V-qC2WUV-zI/AAAAAAABJUE/vBRLn1YZeVgz5c6pmjKm6el3xWm2tBcxACLcB/s1600/ntile3.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;Теперь видим, что в новых командах работают знакомые люди.&lt;/p&gt;
&lt;h2 id="lag-lead"&gt;LAG, LEAD&lt;/h2&gt;
&lt;p&gt;Недавно прошла аттестация и сотрудникам повысили зарплату. В таблице Assessments есть информация о зарплатах сотрудников по каждому году, руководству для анализа необходимо узнать сумму, на которую каждому сотруднику сделали повышение. Обычно это решается соединением таблицы на саму себя с использованием смещения, в нашем случае - по году:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT
    a.Name,
    a.[Year],
    a.Salary,
    a2.Salary AS Salary2015,
    a.Salary - a2.Salary AS Plus
FROM dbo.Assessments a
INNER JOIN dbo.Assessments a2 ON a.Name = a2.Name AND a.Year = a2.Year + 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ожидаемый результат:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://3.bp.blogspot.com/-jVq0NGKr2uk/V-qMfi6AeTI/AAAAAAABJUU/79Fc6kX5Q9kXJ4gMiaXHDBx5E4dRPywpgCLcB/s1600/join.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;Меняя выражение Year + 1 мы можем посмотреть повышение за последние 1, 2 и т.д. лет. При этом, если мы хотим одним запросом узнать повышение за год и за 5 лет, то нам придется сделать 2 соединения соответственно. Функция LAG позволяет получить значение предыдущей строки, LEAD - следующей. При этом, как параметр можно указать, на сколько строк нужно &amp;quot;вернуться&amp;quot; назад или вперед:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT 
    a.Name,
    a.[Year],
    a.Salary,
    LAG(a.Salary, 1, NULL) OVER (PARTITION BY a.Name ORDER BY a.[Year]) AS Salary2015,
    a.Salary - LAG(a.Salary, 1, 0) OVER (PARTITION BY a.Name ORDER BY a.[Year]) AS Plus
FROM dbo.Assessments a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://3.bp.blogspot.com/-0BSeClAcon4/V-qOFMpC1jI/AAAAAAABJUg/DR7hy8stp0s-HgqRQUpHOxzejyseOe_nQCLcB/s1600/lag.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;И так, мы указали в функции LAG, что нужно &amp;quot;вернуться&amp;quot; на 1 строку назад в отсортированном наборе. Мы разделили наш набор по сотрудникам и отсортировали по году. Естественно, для первой строки не будет предыдущего набора (а для последней - следующего), поэтому третий параметр функции как раз определяет это отсутствующее значение. Мы получили необходимый результат и еще строки за 2015 год. Мы не можем их отфильтровать в этом же запросе, т.к. это повлияет на результат выполнения функции LAG (для строк только из 2016 года нет предыдущего значения года, очевидно), поэтому обернем это еще в один запрос и отфильтруем:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT
    *
FROM
(
    SELECT 
        a.Name,
        a.[Year],
        a.Salary,
        LAG(a.Salary, 1, NULL) OVER (PARTITION BY a.Name ORDER BY a.[Year]) AS Salary2015,
        a.Salary - LAG(a.Salary, 1, 0) OVER (PARTITION BY a.Name ORDER BY a.[Year]) AS Plus
    FROM dbo.Assessments a
) AS t
WHERE t.[Year] = 2016
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://2.bp.blogspot.com/-MW5GJXnyqKw/V-qQ_Q7p31I/AAAAAAABJUs/ffSLGB0mLu8n6CZJ44GJiEn-FOvdutklgCLcB/s1600/lag2.png%22" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;И мы получили аналогичный результат. Здесь преимущество функции LAG над соединением в том, что мы можем легко сравнивать текущее значение со значением 1, 2, 3 или 5 лет назад просто поменяв одно значение параметра, при этом, можем все это вычислить одним запросом, который будет исполнен за один проход по таблице без соединений. Функция LEAD работает аналогично, но выбирает следующие строки.&lt;/p&gt;
&lt;h2 id="percentile_cont-percentile_disc"&gt;PERCENTILE_CONT, PERCENTILE_DISC&lt;/h2&gt;
&lt;p&gt;Напоследок, пара аналитических функций для подсчета процентилей и медиан в частности. Процентиль довольно часто используется для анализа каких-либо параметров, поэтому логично, что эти функции присутствуют в MSSQL. Давайте посчитаем медиану зарплат в нашей компании (напомню, что медиана - такое число, что ровно половина выборки имеет значение меньше этого набора, а другая половина - больше):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT
    e.Name,
    e.Salary,
    PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY e.Salary) OVER () AS Median,
    e.Position,
    PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY e.Salary) OVER (PARTITION BY e.Position) AS MedianInPosition,
    e.Department,
    PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY e.Salary) OVER (PARTITION BY e.Department) AS MedianInDepartment,
    e.TeamName,
    PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY e.Salary) OVER (PARTITION BY e.TeamName) AS MedianInTeam
FROM dbo.Employees e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://1.bp.blogspot.com/-iRtmGOb8_Jw/V-qTjNAvTHI/AAAAAAABJU4/wvt948vbKBImplE8Vqf2V9lkMwu6lplkwCLcB/s1600/percentile_cont.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;Итак, одним запросом мы узнали, что наш мистер Black получает зарплату на 5000 меньше медианы по компании, но на 12500 больше чем медиана среди QA! Делая различные разделения в выражении OVER мы можем получить показатели по определенному подмножеству, или оставить это выражение пустым, что бы получить результаты по всему набору. В чем отличия функций PERCENTILE_CONT и PERCENTILE_DISC? PERCENTILE_CONT возвращает результат из непрерывного множества значений. Заметьте, что значение медианы 65000 не является чей-либо зарплатой в компании - это статистическое значение. Если же мы хотим использовать как медиану (или процентиль, в общем случае) значение из нашего набора, то необходимо использовать функцию PERCENTILE_DISC:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;SELECT
    e.Name,
    e.Salary,
    PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY e.Salary) OVER () AS Median,
    e.Position,
    PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY e.Salary) OVER (PARTITION BY e.Position) AS MedianInPosition,
    e.Department,
    PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY e.Salary) OVER (PARTITION BY e.Department) AS MedianInDepartment,
    e.TeamName,
    PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY e.Salary) OVER (PARTITION BY e.TeamName) AS MedianInTeam
FROM dbo.Employees e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://2.bp.blogspot.com/-RcPfqRvYD1Y/V-qVDmAxKdI/AAAAAAABJVE/JkKWZpz9rg0pyxgOjizO3do8HVQYsS00gCLcB/s1600/percentile_disc.png" alt="" /&gt;&lt;/p&gt;
&lt;p&gt;Теперь в качестве результата используется одно из значений исходного набора.&lt;/p&gt;
&lt;h2 id="section-1"&gt;Ссылки&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://msdn.microsoft.com/en-us/library/hh213019.aspx"&gt;CHOOSE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://msdn.microsoft.com/en-us/library/ms186734.aspx"&gt;ROW_NUMBER&lt;/a&gt;, &lt;a href="https://msdn.microsoft.com/en-us/library/ms176102.aspx"&gt;RANK&lt;/a&gt;, &lt;a href="https://msdn.microsoft.com/en-us/library/ms173825.aspx"&gt;DENSE_RANK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://msdn.microsoft.com/en-us/library/hh213018.aspx"&gt;FIRST_VALUE&lt;/a&gt;, &lt;a href="https://msdn.microsoft.com/en-us/library/hh231517.aspx"&gt;LAST_VALUE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://msdn.microsoft.com/en-us/library/hh231256.aspx"&gt;LAG&lt;/a&gt;, &lt;a href="https://msdn.microsoft.com/en-us/library/hh213125.aspx"&gt;LEAD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://msdn.microsoft.com/en-us/library/hh231473.aspx"&gt;PERCENTILE_CONT&lt;/a&gt;, &lt;a href="https://msdn.microsoft.com/en-us/library/hh231327.aspx"&gt;PERCENTILE_DISC&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
	</channel>
</rss>