﻿# Топ\-10 ошибок, найденных в C\# проектах за 2025 год

В 2025 году команда PVS\-Studio продолжила разбирать код открытых C\#\-проектов\. За год было обнаружено немало дефектов, и из всего этого разнообразия мы выбрали 10 наиболее любопытных\. Надеемся, что этот обзор будет не только интересным, но и полезным — приятного чтения\!

![1328_CSharpTop_2025_ru/image1.png](https://import.viva64.com/docx/blog/1328_CSharpTop_2025_ru/image1.png)

## Как составлялся топ?

Уточню, по каким критериям составлялся топ:

* код из Open Source проекта;
* проблема в коде обнаружена с помощью PVS\-Studio;
* с большой вероятностью код содержит в себе ошибку;
* проблема интересна для рассмотрения;
* ошибки не похожи друг на друга\.

Поскольку подобный топ мы составляем регулярно, накопилась внушительная коллекция интересных ошибок\. Ознакомиться со статьями, написанными в прошлые года, можно здесь:

* [топ\-10 ошибок за 2024 год](https://pvs-studio.ru/ru/blog/posts/csharp/1203/);
* [топ\-10 ошибок за 2023 год](https://pvs-studio.ru/ru/blog/posts/csharp/1094/);
* [топ\-10 ошибок за 2022 год](https://pvs-studio.ru/ru/blog/posts/csharp/1020/);
* [топ\-10 ошибок за 2021 год](https://pvs-studio.ru/ru/blog/posts/csharp/0904/);
* [топ\-10 ошибок за 2020 год](https://pvs-studio.ru/ru/blog/posts/csharp/0787/);
* [топ\-10 ошибок за 2019 год](https://pvs-studio.ru/ru/blog/posts/csharp/0698/)\.

Приступим же к самому интересному — просмотру ошибок\!

P\. S\. Ошибки были отобраны и распределены на основе субъективного мнения автора\. Если вам покажется, что какую\-то ошибку засудили \(или наоборот\), можете смело писать об этом в комментариях :\)

## 10 место\. Попробуй найди

Сегодняшний топ открывает ошибка из [статьи о проверке \.NET 9](https://pvs-studio.ru/ru/blog/posts/csharp/1216/)\. Кажется, что \.NET 9 вышел совсем недавно, однако чуть больше месяца назад ему на замену пришёл \.NET 10\. О наиболее значимых изменениях мы рассказали в [статье](https://pvs-studio.ru/ru/blog/posts/csharp/1308/)\.

Возвращаемся к разбору:

```cpp
public static void SetAsIConvertible(this ref ComVariant variant,
                                     IConvertible value)
{
  TypeCode tc = value.GetTypeCode();
  CultureInfo ci = CultureInfo.CurrentCulture;

  switch (tc)
  {
    case TypeCode.Empty: break;
    case TypeCode.Object: 
      variant = ComVariant.CreateRaw(....); break;
    case TypeCode.DBNull: 
      variant = ComVariant.Null; break;
    case TypeCode.Boolean: 
      variant = ComVariant.Create<bool>(....)); break;
    case TypeCode.Char: 
      variant = ComVariant.Create<ushort>(value.ToChar(ci)); break;
    case TypeCode.SByte: 
      variant = ComVariant.Create<sbyte>(value.ToSByte(ci)); break;
    case TypeCode.Byte: 
      variant = ComVariant.Create<byte>(value.ToByte(ci)); break;
    case TypeCode.Int16: 
      variant = ComVariant.Create(value.ToInt16(ci)); break;
    case TypeCode.UInt16: 
      variant = ComVariant.Create(value.ToUInt16(ci)); break;
    case TypeCode.Int32: 
      variant = ComVariant.Create(value.ToInt32(ci)); break;
    case TypeCode.UInt32: 
      variant = ComVariant.Create(value.ToUInt32(ci)); break;
    case TypeCode.Int64: 
      variant = ComVariant.Create(value.ToInt64(ci)); break;
    case TypeCode.UInt64: 
      variant = ComVariant.Create(value.ToInt64(ci)); break;
    case TypeCode.Single: 
      variant = ComVariant.Create(value.ToSingle(ci)); break;
    case TypeCode.Double: 
      variant = ComVariant.Create(value.ToDouble(ci)); break;
    case TypeCode.Decimal: 
      variant = ComVariant.Create(value.ToDecimal(ci)); break;
    case TypeCode.DateTime: 
      variant = ComVariant.Create(value.ToDateTime(ci)); break;
    case TypeCode.String: 
      variant = ComVariant.Create(....); break;

    default:
      throw new NotSupportedException();
  }
}
```

Видите ошибку? А она есть\!

```cpp
case TypeCode.Int64: 
  variant = ComVariant.Create(value.ToInt64(ci)); break;
case TypeCode.UInt64: 
  variant = ComVariant.Create(value.ToInt64(ci)); break; // <=
```

Предупреждение PVS\-Studio: [V3139](https://pvs-studio.ru/ru/docs/warnings/v3139/) Two or more case\-branches perform the same actions\. DynamicVariantExtensions\.cs 68

Надеюсь, вы размяли свои глаза, и зоркость вас не подвела\. В `case TypeCode.UInt64` вместо выражения `value.ToInt64` нужно использовать метод `ToUInt64`, который существует\. Вероятно, эта ошибка произошла из\-за Copy Paste\.

## 9 место\. Некорректный формат

На девятом месте располагается ошибка из [статьи про проверку проектов Neo и NBitcoin](https://pvs-studio.ru/ru/blog/posts/csharp/1312/):

```cpp
public override string ToString()
{
  var sb = new StringBuilder();

  sb.AppendFormat("{1:X04} {2,-10}{3}{4}", 
                  Position, 
                  OpCode, 
                  DecodeOperand());

  return sb.ToString();
}
```

Предупреждение PVS\-Studio: [V3025](https://pvs-studio.ru/ru/docs/warnings/v3025/) \[CWE\-685\] Incorrect format\. A different number of format items is expected while calling 'AppendFormat' function\. Format items not used: \{3\}, \{4\}\. Arguments not used: 1st\. VMInstruction\.cs 105

Вызов переопределённого метода `ToString` неизбежно приведёт к исключению\. Всё из\-за некорректного вызова `sb.AppendFormat`, в котором допущены сразу две ошибки:

* число аргументов для вставки меньше числа плейсхолдеров в строке формата, что и станет причиной исключения;
* даже если первая проблема будет исправлена \(число аргументов и плейсхолдеров станет равным\), этот вызов всё так же будет вызывать исключение, т\. к\. индексация плейсхолдеров начинается с 0, а не с 1\. Из этого следует, что для плейсхолдера с индексом 4 необходим 5\-й аргумент, который будет отсутствовать\.

## 8 место\. Самокопание

Следующая на очереди ошибка из [статьи про проверку трейдерского движка Lean](https://pvs-studio.ru/ru/blog/posts/csharp/1271/):

```cpp
public override int GetHashCode()
{
  unchecked
  {
    var hashCode = Definition.GetHashCode();
    var arr = new int[Legs.Count];
    for (int i = 0; i < Legs.Count; i++)
    {
      arr[i] = Legs[i].GetHashCode();
    }

    Array.Sort(arr);

    for (int i = 0; i < arr.Length; i++)
    {
      hashCode = (hashCode * 397) ^ arr[i];
    }

    return hashCode;
  }
}

public override bool Equals(object obj)
{
    ....

    return Equals((OptionStrategyDefinitionMatch) obj);
}
```

Предупреждение PVS\-Studio: [V3192](https://pvs-studio.ru/ru/docs/warnings/v3192/) The 'Legs' property is used in the 'GetHashCode' method but is missing from the 'Equals' method\. OptionStrategyDefinitionMatch\.cs 176

Анализатор [межпроцедурно](https://pvs-studio.ru/ru/blog/terms/6785/) проверил метод `Equals`, который вызывает переопределённый метод `Equals`, и обнаружил отсутствие использования свойства `Legs`, хотя это свойство используется в методе `GetHashCode`\.

Внимательно рассмотрим метод `Equals`:

```cpp
public bool Equals(OptionStrategyDefinitionMatch other)
{
  ....

  var positions = other.Legs
                       .ToDictionary(leg => leg.Position, 
                                     leg => leg.Multiplier);
  foreach (var leg in other.Legs)                                   // <=
  {
    int multiplier;
    if (!positions.TryGetValue(leg.Position, out multiplier))
    {
      return false;
    }

    if (leg.Multiplier != multiplier)
    {
      return false;
    }
  }

  return true;
}
```

Заметим, что в нём есть итерация по `other.Legs`\. Каждый элемент этой коллекции мы пытаемся найти в словаре `positions`, но этот словарь получается из `other.Legs`\. Таким образом, проверяется наличие элементов коллекции в этой же коллекции\.

Исправить код можно, заменив в отмеченном месте `other.Legs` на `Legs`\.

## 7 место\. Equals с подвохом

На седьмом месте расположилась ошибка, взятая из [статьи про проверку ScottPlot](https://pvs-studio.ru/ru/blog/posts/csharp/1239/):

```cpp
public class CoordinateRangeMutable : IEquatable<CoordinateRangeMutable>
{
  ....
  public bool Equals(CoordinateRangeMutable? other)
  {
    if (other is null)
      return false;

    return Equals(Min, other.Min) && Equals(Min, other.Min);  // <=
  }

  public override bool Equals(object? obj)
  {
    if (obj is null)
      return false;

    if (obj is CoordinateRangeMutable other)
      return Equals(other);

    return false;
  }

  public override int GetHashCode()
  {
    return Min.GetHashCode() ^ Max.GetHashCode();             // <=
  }
}
```

Предупреждения PVS\-Studio:

[V3192](https://pvs-studio.ru/ru/docs/warnings/v3192/) The 'Max' property is used in the 'GetHashCode' method but is missing from the 'Equals' method\. ScottPlot CoordinateRangeMutable\.cs 198

[V3001](https://pvs-studio.ru/ru/docs/warnings/v3001/) There are identical sub\-expressions 'Equals\(Min, other\.Min\)' to the left and to the right of the '&&' operator\. ScottPlot CoordinateRangeMutable\.cs 172

На этот фрагмент кода анализатор выдал сразу два срабатывания\. Давайте разберёмся, почему так получилось\.

Начнём со срабатывания диагностики [V3192](https://pvs-studio.ru/ru/docs/warnings/v3192/)\. В сообщении анализатора говорится, что свойство `Max` используется в методе `GetHashCode`, но не используется в методе `Equals`\. Посмотрев на переопределённый метод `Equals`, можно заметить, что в его теле вызывается ещё один `Equals`\. В нём мы видим следующую запись: `Equals(Min, other.Min) && Equals(Min, other.Min)`\. А на этот фрагмент указала диагностика [V3001](https://pvs-studio.ru/ru/docs/warnings/v3001/)\.

Очевидно, что один из операндов `&&` должен иметь следующий вид: `Equals(Max, other.Max)`\.

Получается, что анализатор прав, в методе `Equals` действительно не фигурирует `Max`\.

## 6 место\. Битовые фокусы

Завершает первую половину топа дефект, который, как и предыдущий, взят из [статьи про проверку ScottPlot](https://pvs-studio.ru/ru/blog/posts/csharp/1239/): 

```cpp
public static Interactivity.Key GetKey(this Keys keys)
{
  
  Keys keyCode = keys & ~Keys.Modifiers;                   // <=
  Interactivity.Key key = keyCode switch
  {
    Keys.Alt => Interactivity.StandardKeys.Alt,            // <=
    Keys.Menu => Interactivity.StandardKeys.Alt,
    Keys.Shift => Interactivity.StandardKeys.Shift,        // <=
    Keys.ShiftKey => Interactivity.StandardKeys.Shift,
    Keys.LShiftKey => Interactivity.StandardKeys.Shift,
    Keys.RShiftKey => Interactivity.StandardKeys.Shift,
    Keys.Control => Interactivity.StandardKeys.Control,    // <=
    Keys.ControlKey => Interactivity.StandardKeys.Control,
    Keys.Down => Interactivity.StandardKeys.Down,
    Keys.Up => Interactivity.StandardKeys.Up,
    Keys.Left => Interactivity.StandardKeys.Left,
    Keys.Right => Interactivity.StandardKeys.Right,
    _ => Interactivity.StandardKeys.Unknown,
  };

  ....
}
```

Предупреждение PVS\-Studio: [V3202](https://pvs-studio.ru/ru/docs/warnings/v3202/) Unreachable code detected\. The 'case' value is out of range of the match expression\. ScottPlot\.WinForms FormsPlotExtensions\.cs 106

Несколько значений паттернов внутри `switch` невозможны в текущем контексте\. Давайте попробуем разобраться, в чём тут дело\. 

Сперва стоит рассмотреть значения, которые соответствуют "проблемным" элементам перечисления\.

```cpp
[Flags]
[TypeConverter(typeof(KeysConverter))]
[Editor(....)]
public enum Keys
{
  /// <summary>
  ///  The bit mask to extract modifiers from a key value.
  /// </summary>
  Modifiers = unchecked((int)0xFFFF0000),

  ....
  /// <summary>
  ///  The SHIFT modifier key.
  /// </summary>
  Shift = 0x00010000,

  /// <summary>
  ///  The  CTRL modifier key.
  /// </summary>
  Control = 0x00020000,

  /// <summary>
  ///  The ALT modifier key.
  /// </summary>
  Alt = 0x00040000
}
```

Далее переведём их в двоичную систему:

|Имя|Значение \\\(16\\\-ая система\\\)|Бинарное представление \\\(2\\\-ая система\\\)|
|---|---|---|
|Modifiers|0xFFFF0000|1111 1111 1111 1111 0000 0000 0000 0000|
|Shift|0x00010000|0000 0000 0000 0001 0000 0000 0000 0000|
|Control|0x00020000|0000 0000 0000 0010 0000 0000 0000 0000|
|Alt|0x00040000|0000 0000 0000 0100 0000 0000 0000 0000|



Теперь становится понятно, что `Modifiers` включает в себя каждый из "проблемных" членов перечисления\.

Значение, передаваемое в `switch`, получается из выражения `keys & ~Keys.Modifiers`\. Это выражение исключает из `keys` значение `Keys.Modifiers`\. Однако, помимо `Keys.Modifiers`, также будут исключены `Shift`, `Control` и `Alt`, так как `Modifiers` уже включает в себя эти значения \(исходя из того, что для каждого ненулевого бита "проблемных" элементов перечисления у `Modifiers` есть ненулевой бит\)\.

Из всего этого мы можем сделать вывод, что нет такой комбинации битов, которая позволит получить `Shift`, `Control` или `Alt` для операции `keys & ~Keys.Modifiers`\.

Возможно, проблема заключается не в значениях перечисления, а в реализации `switch`\.

## 5 место\. Упаковали

Пятёрку лидеров открывает ошибка из [статьи о проверке \.NET 9](https://pvs-studio.ru/ru/blog/posts/csharp/1216/):

```cpp
struct StackValue
{
  ....
  public override bool Equals(object obj)
  {
    if (Object.ReferenceEquals(this, obj))
      return true;

    if (!(obj is StackValue))
      return false;

    var value = (StackValue)obj;
    return    this.Kind == value.Kind 
           && this.Flags == value.Flags 
           && this.Type == value.Type;
  }
}
```

Предупреждение PVS\-Studio: [V3161](https://pvs-studio.ru/ru/docs/warnings/v3161/)  Comparing value type variables with 'ReferenceEquals' is incorrect because 'this' will be boxed\. ILImporter\.StackValue\.cs 164

Метод `ReferenceEquals` принимает параметр типа `Object`, при передаче значимого типа будет произведена [упаковка](https://pvs-studio.ru/ru/blog/terms/6695/)\. Созданная в куче ссылка не будет равна никакой другой\. 

Так как в качестве первого аргумента передаётся `this` структуры, упаковка будет производиться при каждом вызове метода `Equals`\. Следовательно, проверка с помощью `ReferenceEquals` всегда будет возвращать `false`\.

Стоит сказать, что данная проблема не влияет на логику работы метода\. Однако проверка с помощью метода `ReferenceEquals` была сделана, чтобы не выполнять дальнейшее сравнение, если ссылки равны\. Т\. е\. это своего рода оптимизация\. На самом же деле мы получаем полностью противоположную ситуацию:

* код после проверки будет выполняться всегда;
* при каждом вызове `Equals` будет производиться упаковка\.

Забавно, что данную проблему также ищет анализатор, всторенный в \.NET \(правило [CA2013](https://learn.microsoft.com/en-us/Dotnet/fundamentals/code-analysis/quality-rules/ca2013)\)\. Однако сами разработчики \.NET не смогли её избежать :\)

Возможно, на проекте данное правило было отключено\. К тому же, по умолчанию [CA2013](https://learn.microsoft.com/en-us/Dotnet/fundamentals/code-analysis/quality-rules/ca2013) включено начиная с \.NET 5\.

## 4 место\. Отписка

На четвёртом месте находится ошибка из [статьи про проверку MSBuild](https://pvs-studio.ru/ru/blog/posts/csharp/1222/):

```cpp
private static void SubscribeImmutablePathsInitialized()
{
  NotifyOnScopingReadiness?.Invoke();

  FileClassifier.Shared.OnImmutablePathsInitialized -= () =>
    NotifyOnScopingReadiness?.Invoke();
}
```

Предупреждение PVS\-Studio: [V3084](https://pvs-studio.ru/ru/docs/warnings/v3084/)\. Anonymous function is used to unsubscribe from 'OnImmutablePathsInitialized' event\. No handlers will be unsubscribed, as a separate delegate instance is created for each anonymous function declaration\. CheckScopeClassifier\.cs 67

В этом случае отписка от делегата не будет выполнена, поскольку каждый раз, когда объявляется анонимная функция, создаётся новый экземпляр делегата\. Попытка отписаться от этого экземпляра не приведёт к желаемому результату\. Условно `OnImmutablePathsInitialized` подписан на `делегат 1`, а отписывается от `делегата 2`, что ничего не поменяет\.

## 3 место\. Путаница в приоритетах

Вот мы и добрались до тройки лидеров\. На почётном третьем месте расположилась ошибка из [статьи про проверку проектов Neo и NBitcoin](https://pvs-studio.ru/ru/blog/posts/csharp/1312/):

```cpp
public override int Size =>   base.Size
                            + ChangeViewMessages?.Values.GetVarSize() ?? 0
                            + 1 + PrepareRequestMessage?.Size ?? 0
                            + PreparationHash?.Size ?? 0
                            + PreparationMessages?.Values.GetVarSize() ?? 0
                            + CommitMessages?.Values.GetVarSize() ?? 0;
```

Предупреждение PVS\-Studio: [V3123](https://pvs-studio.ru/ru/docs/warnings/v3123/) \[CWE\-783\] Perhaps the '??' operator works in a different way than it was expected\. Its priority is lower than priority of other operators in its left part\. RecoveryMessage\.cs 35

На этом коде анализатор выдал сразу несколько предупреждений диагностики [V3123](https://pvs-studio.ru/ru/docs/warnings/v3123/), но для компактности я привёл только одно\. Оператор `??` имеет меньший приоритет по сравнению с оператором `+`\. Однако форматирование приведённого выражения указывает на то, что разработчик ожидал обратное\.

Но есть ли в этом случае разница, какая операция будет выполнена первой? Для ответа на этот вопрос рассмотрим пример одного подвыражения сложения, если `ChangeViewMessages` равен `null`:

```cpp
base.Size + ChangeViewMessages?.Values.GetVarSize() ?? 0
```

Неважно, какое значение будет у `base.Size`, результат подвыражения всегда будет 0, ведь в результате сложения `base.Size` c `null` получится всё тот же `null`\.

Если поместить `ChangeViewMessages?.Values.GetVarSize() ?? 0` в скобки, изменив тем самым порядок вычисления операций, то результатом будет `base.Size`\.

## 2 место\. Коварный паттерн

Второе место забирает ошибка из [статьи про проверку файлового менеджера Files](https://pvs-studio.ru/ru/blog/posts/csharp/1295/):

```cpp
protected void ChangeMode(OmnibarMode? oldMode, OmnibarMode newMode)
{
  ....
  var modeSeparatorWidth = 
    itemCount is not 0 or 1 
      ? _modesHostGrid.Children[1] is FrameworkElement frameworkElement
        ? frameworkElement.ActualWidth
        : 0
      : 0;
  ....
}
```

Предупреждение PVS\-Studio: [V3207](https://pvs-studio.ru/ru/docs/warnings/v3207/) \[CWE\-670\] The 'not 0 or 1' logical pattern may not work as expected\. The 'not' pattern is matched only to the first expression from the 'or' pattern\. Files\.App\.Controls Omnibar\.cs 149

В этом коде нас интересует часть `itemCount is not 0 or 1`\. Уже догадались? Этот паттерн избыточен\. Его вторая часть ни на что не влияет\.

Люди привыкли, что когда мы в речи говорим "x это не 0 или 1", то имеем в виду, что x это не 0 и не 1\. Но в C\# приоритет операций другой\. `x is not 0 or 1` на самом деле `x is (not 0) or 1`\. Подобные ошибки могут приводить не просто к избыточности, но и к ошибкам типа `NullReferenceException: list is not null or list.Count == 0`\. По этой проблеме даже была [встреча](https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-11-04.md#not-and-or-patterns), на которой обсуждались возможные пути решения\. Исходя из обсуждения, вероятно, в будущем эта ситуация будет отлавливаться или компилятором, или встроенным статическим анализом как предупреждение\.

## 1 место\. Как работает LINQ?

Победителем становится ошибка из [статьи про проверку трейдерского движка Lean](https://pvs-studio.ru/ru/blog/posts/csharp/1271/)\. Именно эта ошибка располагается на первом месте из\-за её неочевидности\. Не каждый разработчик задумывается о том, какое поведение будет при использовании методов с отложенным выполнением в комбинации с захваченными переменными\. Все подробности дальше:

```cpp
public void FutureMarginModel_MarginEntriesValid(string market)
{
  ....
  var lineNumber = 0;
  var errorMessageTemplate = $"Error encountered in file " + 
                             $"{marginFile.Name} on line ";
  var csv = File.ReadLines(marginFile.FullName)
                .Where(x =>    !x.StartsWithInvariant("#") 
                            && !string.IsNullOrWhiteSpace(x))
                .Skip(1)
                .Select(x =>
  {
    lineNumber++;                                                  // <=

    ....
  });

  lineNumber = 0;                                                  // <=
  foreach (var line in csv)
  {
    lineNumber++;                                                  // <=

    ....
  }
}
```

Предупреждение PVS\-Studio: [V3219](https://pvs-studio.ru/ru/docs/warnings/v3219/) The 'lineNumber' variable was changed after it was captured in a LINQ method with deferred execution\. The original value will not be used when the method is executed\. FutureMarginBuyingPowerModelTests\.cs 720

Переменная `lineNumber`, захватывается и инкрементируется в делегате, который передаётся в LINQ\-метод\. Так как `Select` — метод с отложенным выполнением, код делегата будет выполнен при итерировании по получившейся коллекции, а не в момент вызова `Select`\. 

Во время итерирования по коллекции `csv` переменная `lineNumber` также инкрементируется\. Получается, что на каждой итерации значение `lineNumber` будет увеличиваться на 2 \(первый раз, когда отработает код делегата, второй раз — внутри `foreach`\), что выглядит странно\.

Обратите внимание на присваивание `lineNumber = 0` перед `foreach`\. Скорее всего, ожидалось, что перед циклом значение этой переменной может быть отлично от нуля\. Однако это невозможно, так как переменная инициализируется нулём, а единственное изменение её значения перед `foreach` находится в коде делегата\. Как было сказано ранее, код делегата выполнится при итерировании по коллекции\. Судя по всему, разработчик ожидал выполнения кода делегата перед циклом\.

## Заключение

Вот и всё — мы с вами разобрали самые интересные срабатывания по мнению автора :\)

Надеюсь, подборка оказалась интересной и дала пищу для размышлений по работе с кодом\.

Если вы хотите узнать, встречаются ли подобные проблемы в вашем проекте, самое время проверить его с помощью статического анализатора\. Оставляю [здесь](https://pvs-studio.ru/ru/pvs-studio/try-free/) ссылку для скачивания\.