﻿# \.NET 7: разбираем ошибки и подозрительные места в исходниках

\.NET 7 зарелизился\. Это хороший повод покопаться в исходниках, чтобы поискать ошибки и странные места\. За комментариями по находкам обратимся к самим разработчикам \.NET — кому знать код, как не им? Погнали\! 

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

Я анализировал релизный код \.NET 7\. Взять его можно на GitHub: [ссылка](https://github.com/dotnet/runtime/tree/v7.0.0)\. 

Перед релизом было 2 выпуска RC \(release candidate\), а поэтому основные баги должны быть устранены\. Но тем даже интереснее, просочилось ли что\-нибудь "в прод"\. 

На каждый подозрительный фрагмент кода я открыл issue на GitHub\. Это помогло понять, какой код ошибочен, какой избыточен, и какие правки вносили разработчики\.

**Issue 1**

Проверка внимательности — что не так с кодом ниже?

```cpp
internal sealed record IncrementalStubGenerationContext(
  StubEnvironment Environment,
  SignatureContext SignatureContext,
  ContainingSyntaxContext ContainingSyntaxContext,
  ContainingSyntax StubMethodSyntaxTemplate,
  MethodSignatureDiagnosticLocations DiagnosticLocation,
  ImmutableArray<AttributeSyntax> ForwardedAttributes,
  LibraryImportData LibraryImportData,
  MarshallingGeneratorFactoryKey<
    (TargetFramework, Version, LibraryImportGeneratorOptions)
  > GeneratorFactoryKey,
  ImmutableArray<Diagnostic> Diagnostics)
{
  public bool Equals(IncrementalStubGenerationContext? other)
  {
    return    other is not null
           && StubEnvironment.AreCompilationSettingsEqual(Environment, 
                                                          other.Environment)
           && SignatureContext.Equals(other.SignatureContext)
           && ContainingSyntaxContext.Equals(other.ContainingSyntaxContext)
           && StubMethodSyntaxTemplate.Equals(other.StubMethodSyntaxTemplate)
           && LibraryImportData.Equals(other.LibraryImportData)
           && DiagnosticLocation.Equals(DiagnosticLocation)
           && ForwardedAttributes.SequenceEqual(other.ForwardedAttributes, 
                (IEqualityComparer<AttributeSyntax>)
                  SyntaxEquivalentComparer.Instance)
          && GeneratorFactoryKey.Equals(other.GeneratorFactoryKey)
          && Diagnostics.SequenceEqual(other.Diagnostics);
    }

    public override int GetHashCode()
    {
      throw new UnreachableException();
    }
}
```

<details>
   <summary>Ответ</summary>

Код проверяет эквивалентность двух объектов: _this_ и _other_\. Однако в одном из выражений допустили ошибку, сравнив свойство _DiagnosticLocation_ с собой же\.

Так неправильно:

```cpp
DiagnosticLocation.Equals(DiagnosticLocation)
```

Так правильно:

```cpp
DiagnosticLocation.Equals(other.DiagnosticLocation)
```


</details>


Эту проблему я нашёл в классе _LibraryImportGenerator_ \([ссылка на GitHub](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.cs#L43)\)\. Чуть позже нашёл ещё 2 записи с такими же ошибками, но уже в других классах:

* класс _JSImportGenerator_, [ссылка на GitHub](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/System.Runtime.InteropServices.JavaScript/gen/JSImportGenerator/JSImportGenerator.cs#L42);
* класс _JSExportGenerator_, [ссылка на GitHub](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/System.Runtime.InteropServices.JavaScript/gen/JSImportGenerator/JSExportGenerator.cs#L37)\. 

Что интересно, в \.NET 7 на эту функциональность есть тест\. Загвоздка в том, что тест тоже ошибочный и поэтому проблему не выявлял\. 

![1015_NET7_Errors_ru/image2.png](https://import.viva64.com/docx/blog/1015_NET7_Errors_ru/image2.png)

В \.NET 8 рассмотренный код сильно переписан, а для \.NET 7 фикс пока не применялся\. Подробности можно почитать в [issue на GitHub](https://github.com/dotnet/runtime/issues/78145)\.  

**Issue 2**

```cpp
internal static void CheckNullable(JSMarshalerType underlyingSig)
{
    MarshalerType underlying = underlyingSig._signatureType.Type;
    if (underlying == MarshalerType.Boolean
        || underlying == MarshalerType.Byte
        || underlying == MarshalerType.Int16
        || underlying == MarshalerType.Int32
        || underlying == MarshalerType.BigInt64
        || underlying == MarshalerType.Int52
        || underlying == MarshalerType.IntPtr
        || underlying == MarshalerType.Double
        || underlying == MarshalerType.Single // <=
        || underlying == MarshalerType.Single // <=
        || underlying == MarshalerType.Char
        || underlying == MarshalerType.DateTime
        || underlying == MarshalerType.DateTimeOffset
        ) return;
    throw new ArgumentException("Bad nullable value type");
}
```

Location: JSMarshalerType\.cs, 387 \([link](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/System.Runtime.InteropServices.JavaScript/src/System/Runtime/InteropServices/JavaScript/JSMarshalerType.cs#L387)\)

Здесь два раза проверили переменную _underlying_ на равенство _MarshalerType\.Single_\. Иногда за подобными одинаковыми проверками скрываются ошибки: условно, должны были проверить переменные _left_ и _right_, но два раза проверили _left_\. Примеры подобных ошибок из Open Source проектов собраны [здесь](https://pvs-studio.ru/ru/blog/examples/v3001/)\. 

Открыл issue на GitHub: [link](https://github.com/dotnet/runtime/issues/78682)\. В рассматриваемом случае из \.NET 7 повезло — проверка просто оказалась избыточной\.

**Issue 3**

```cpp
public static bool TryParse(string text, out MetricSpec spec)
{
  int slashIdx = text.IndexOf(MeterInstrumentSeparator);
  if (slashIdx == -1)
  {
    spec = new MetricSpec(text.Trim(), null);
    return true;
  }
  else
  {
    string meterName = text.Substring(0, slashIdx).Trim();
    string? instrumentName = text.Substring(slashIdx + 1).Trim();
    spec = new MetricSpec(meterName, instrumentName);
    return true;
  }
}
```

Location: MetricsEventSource\.cs, 453 \([link](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MetricsEventSource.cs#L453)\)

Метод _TryParse_ всегда возвращает одно и то же значение – _true_\. Это странно\. Посмотрим, где он используется:

```cpp
private void ParseSpecs(string? metricsSpecs)
{
  ....
  string[] specStrings = ....
  foreach (string specString in specStrings)
  {
    if (!MetricSpec.TryParse(specString, out MetricSpec spec))
    {
      Log.Message($"Failed to parse metric spec: {specString}");
    }
    else
    {
      Log.Message($"Parsed metric: {spec}");
      ....
    }
  }
}
```

Location: MetricsEventSource\.cs, 375 \([link](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MetricsEventSource.cs#L375)\) 

Возвращаемое значение метода _TryParse_ используется как условие оператора _if_\. Если не удалось распарсить строку _specString_, исходное значение должно логироваться\. Иначе логируется полученное представление — _spec_ —, и над ним выполняются ещё какие\-то операции\. 

Проблема в том, что _TryParse_ всегда возвращает _true\._ Значит, then\-ветвь оператора _if_ никогда не выполняется, то есть парсинг всегда считается успешным\.

Issue на GitHub: [link](https://github.com/dotnet/runtime/issues/78625)\.

В результате исправления _TryParse_ превратился в _Parse_, а в вызывающем методе пропал оператор _if_\. В _TryParse_ заодно поменяли _Substring_ на _AsSpan_\.

![1015_NET7_Errors_ru/image3.png](https://import.viva64.com/docx/blog/1015_NET7_Errors_ru/image3.png)

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

```cpp
if (!MetricSpec.TryParse(specString, out MetricSpec spec))
{
  Log.Message("Failed to parse metric spec: {specString}");
}
else
{
  Log.Message("Parsed metric: {spec}");
  ....
}
```

Подробнее про эту и подобную ошибки можно почитать в [статье про проверку \.NET 6](https://pvs-studio.ru/ru/blog/posts/csharp/0903/) \(issue 14\)\. 

**Issue 4**

Раз уж мы затронули тему методов со странными возвращаемыми значениями, рассмотрим ещё один:

```cpp
public virtual bool TryAdd(XmlDictionaryString value, out int key)
{
  ArgumentNullException.ThrowIfNull(value);

  IntArray? keys;

  if (_maps.TryGetValue(value.Dictionary, out keys))
  {
    key = (keys[value.Key] - 1);

    if (key != -1)
    {
      // If the key is already set, then something is wrong
      throw System.Runtime
                  .Serialization
                  .DiagnosticUtility
                  .ExceptionUtility
                  .ThrowHelperError(
      new InvalidOperationException(SR.XmlKeyAlreadyExists));
     }

     key = Add(value.Value);
     keys[value.Key] = (key + 1);
     return true;               // <=
  }

  key = Add(value.Value);
  keys = AddKeys(value.Dictionary, value.Key + 1);
  keys[value.Key] = (key + 1);
  return true;                  // <=
}
```

Location: XmlBinaryWriterSession\.cs, 28 \([link](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/System.Private.DataContractSerialization/src/System/Xml/XmlBinaryWriterSession.cs#L28)\)

Метод или возвращает _true_, или выбрасывает исключение, но никогда не возвращает _false_\. Это уже публичный API, так что и спроса больше\. 

Посмотрим описание на [learn\.microsoft\.com](https://learn.microsoft.com/en-us/dotnet/api/system.xml.xmlbinarywritersession.tryadd?view=net-7.0):

![1015_NET7_Errors_ru/image4.png](https://import.viva64.com/docx/blog/1015_NET7_Errors_ru/image4.png)

Нестыковочка\. Я также открыл issue на GitHub \([ссылка](https://github.com/dotnet/dotnet-api-docs/issues/8656)\), но на момент написания статьи новостей не было\.

**Issue 5**

```cpp
public static Attribute? GetCustomAttribute(ParameterInfo element, 
                                            Type attributeType, 
                                            bool inherit)
{
  // ....
  Attribute[] attrib = GetCustomAttributes(element, attributeType, inherit);

  if (attrib == null || attrib.Length == 0)
    return null;

  if (attrib.Length == 0)
    return null;

  if (attrib.Length == 1)
    return attrib[0];

  throw new AmbiguousMatchException(SR.RFLCT_AmbigCust);
}
```

Location: Attribute\.CoreCLR\.cs, 617 \([link](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/coreclr/System.Private.CoreLib/src/System/Attribute.CoreCLR.cs#L617)\)

В этом фрагменте кода 2 раза проверяют одно и то же выражение — _attrib\.Length \=\= 0_: сначала как правый операнд оператора '\|\|', затем как условное выражение оператора _if_\. 

Иногда это признак ошибки — хотели проверить одно, а проверили другое\. Здесь повезло: вторая проверка оказалась избыточной, и разработчики её убрали\.

Issue на GitHub: [link](https://github.com/dotnet/runtime/issues/78683)\. 

![1015_NET7_Errors_ru/image5.png](https://import.viva64.com/docx/blog/1015_NET7_Errors_ru/image5.png)

**Issue 6**

```cpp
protected virtual XmlSchema? GetSchema()
{
  if (GetType() == typeof(DataTable))
  {
    return null;
  }
  MemoryStream stream = new MemoryStream();

  XmlWriter writer = new XmlTextWriter(stream, null);
  if (writer != null)
  {
    (new XmlTreeGen(SchemaFormat.WebService)).Save(this, writer);
  }
  stream.Position = 0;
  return XmlSchema.Read(new XmlTextReader(stream), null);
}
```

Location: DataTable\.cs, 6678 \([link](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/System.Data.Common/src/System/Data/DataTable.cs#L6678)\)

Создали экземпляр типа _XmlTextWriter_, ссылку на него записали в переменную _writer_ и сразу на следующей строке проверяют её на неравенство _null_\. Проверка всегда будет давать _true_, значит условие здесь лишнее\. 

Нестрашно, но проверку можно убрать\. Так и сделали \([issue на GitHub](https://github.com/dotnet/runtime/issues/78684)\):

![1015_NET7_Errors_ru/image6.png](https://import.viva64.com/docx/blog/1015_NET7_Errors_ru/image6.png)

**Issue 7**

Тоже избыточный код, но чуть менее очевидный\.

```cpp
public int ToFourDigitYear(int year, int twoDigitYearMax)
{
  if (year < 0)
  {
    throw new ArgumentOutOfRangeException(nameof(year), 
                                          SR.ArgumentOutOfRange_NeedPosNum);
  }

  if (year < 100)
  {
    int y = year % 100;
    return (twoDigitYearMax / 100 - (y > twoDigitYearMax % 100 ? 1 : 0)) 
             * 100 + y;
  }
  ....
}
```

Location: GregorianCalendarHelper\.cs, 526 \([link](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/System.Private.CoreLib/src/System/Globalization/GregorianCalendarHelper.cs#L526)\)

Давайте проследим за тем, как уточняется значение переменной _year_ по мере исполнения кода\.

```cpp
ToFourDigitYear(int year, int twoDigitYearMax)
```

_year_ – параметр метода типа _int\._ Значит, его значение находится в диапазоне \[_int\.MinValue_; _int\.MaxValue_\]\. 

При исполнении кода первым делом встречается оператор _if_, в котором выбрасывается исключение:

```cpp
if (year < 0)
{
  throw ....;
}
```

Если исключение не было выброшено, значит значение _year_ лежит в диапазоне \[0; _int\.MaxValue_\]\. 

Дальше ещё один оператор _if_:

```cpp
if (year < 100)
{
  int y = year % 100;
  ....
}
```

Если исполнение кода зашло в then\-ветвь оператора _if_, значит значение _year_ находится в диапазоне \[0; 99\]\. Это уже приводит к интересному результату операции взятия остатка от деления:

```cpp
int y = year % 100;
```

Значение _year_ всегда меньше 100 \(от 0 до 99\)\. Как следствие, результат операции _year % 100_ всегда будет равен левому операнду – _year_\. Получается, что _y_ и _year_ всегда равны\. 

Итого, или код избыточный, или здесь какая\-то ошибка\. Открыл [issue на GitHub](https://github.com/dotnet/runtime/issues/78627): код поправили, убрав переменную _y_\. 

**Issue 8**

```cpp
internal ConfigurationSection
FindImmediateParentSection(ConfigurationSection section)
{
  ....
  SectionRecord sectionRecord = ....
  if (sectionRecord.HasLocationInputs)
  {
    SectionInput input = sectionRecord.LastLocationInput;
    Debug.Assert(input.HasResult, "input.HasResult");
    result = (ConfigurationSection)input.Result;
  }
  else
  {
    if (sectionRecord.HasIndirectLocationInputs)
    {
      Debug.Assert(IsLocationConfig, 
                   "Indirect location inputs exist 
                    only in location config record");
      SectionInput input = sectionRecord.LastIndirectLocationInput;
      Debug.Assert(input != null);
      Debug.Assert(input.HasResult, "input.HasResult");
      result = (ConfigurationSection)input.Result;
    }
    ....
  ....
}
```

Location: MgmtConfigurationRecord\.cs, 341 \([link](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/System.Configuration.ConfigurationManager/src/System/Configuration/MgmtConfigurationRecord.cs#L341)\)

Здесь придётся немного повозиться\. Для начала посмотрим на второй оператор _if_:

```cpp
if (sectionRecord.HasIndirectLocationInputs)
{
  Debug.Assert(IsLocationConfig, 
               "Indirect location inputs exist 
                only in location config record");
  SectionInput input = sectionRecord.LastIndirectLocationInput;
  Debug.Assert(input != null);
  Debug.Assert(input.HasResult, "input.HasResult");
  result = (ConfigurationSection)input.Result;
}
```

В переменную _input_ записывается значение свойства _LastIndirectLocationInput\._ После этого _input_ проверяется в двух ассертах: на _null_ \(_input \!\= null_\) и наличие результата \(_input\.HasResult_\)\. 

Посмотрим на тело свойства _LastIndirectLocationInput, _чтобы понять, какое значение может быть записано в переменную _input_:

```cpp
internal SectionInput LastIndirectLocationInput
  =>   HasIndirectLocationInputs 
     ? IndirectLocationInputs[IndirectLocationInputs.Count - 1] 
     : null;
```

Смотрите, с одной стороны, свойство может вернуть значение _null_\. С другой — видно, что если _HasIndirectLocationInputs_ — _true_, то возвращается _IndirectLocationInputs\[IndirectLocationInputs\.Count \- 1\]_, а не явное значение _null_\. 

Вопрос вот в чём — может ли значение из коллекции _IndirectLocationInputs_ быть равно _null_? Возможно — из этого кода непонятно\. Кстати, тут могли бы помочь nullable\-аннотации, но они включены не во всех проектах \.NET\. 

Возвращаемся в _if_:

```cpp
if (sectionRecord.HasIndirectLocationInputs)
{
  Debug.Assert(IsLocationConfig, 
               "Indirect location inputs exist 
                only in location config record");
  SectionInput input = sectionRecord.LastIndirectLocationInput;
  Debug.Assert(input != null);
  Debug.Assert(input.HasResult, "input.HasResult");
  result = (ConfigurationSection)input.Result;
}
```

Условное выражение — _sectionRecord\.HasIndirectLocationInputs\. _Это то же самое свойство, которое проверяется в _LastIndirectLocationInput_\. Значит, _LastIndirectLocationInput_ точно не возвращает явный _null_\. Однако какое значение будет получено из _IndirectLocationInputs_ и записано в _input_ — непонятно\.

Разработчики сначала проверяют, что _input \!\= null_, и лишь затем смотрят наличие результата — _input\.HasResult_\. Выглядит нормально\. 

Теперь вернёмся в первый оператор _if_:

```cpp
if (sectionRecord.HasLocationInputs)
{
  SectionInput input = sectionRecord.LastLocationInput;
  Debug.Assert(input.HasResult, "input.HasResult");
  result = (ConfigurationSection)input.Result;
}
```

Посмотрим на свойство _LastLocationInput_:

```cpp
internal SectionInput LastLocationInput 
  =>  HasLocationInputs 
    ? LocationInputs[LocationInputs.Count - 1] 
    : null;
```

Оно написано по тому же принципу, что и _LastIndirectLocationInput_\. Как и в предыдущем случае, в зависимости от флага \(_HasLocationInputs_\) возвращается или _null_, или значение из коллекции _LocationInputs_\. 

Возвращаемся к оператору _if_\. Его условное выражение — свойство _HasLocationInputs_, которое проверяется и внутри _LastLocationInput_\. Если зашли в тело _if_, значит _LastLocationInput_ не может вернуть явный _null_\. Может ли значение из коллекции _LocationInputs_ быть _null_? Вопрос открытый\. Если может, то и в _input_ тоже будет записан _null_\. 

Как и в случае с первым рассмотренным _if_, выполняется проверка _input\.HasResult_, а вот проверка _input \!\= null_ на этот раз отсутствует\. 

Ещё раз\. Первый разобранный фрагмент кода: 

```cpp
SectionInput input = sectionRecord.LastIndirectLocationInput;
Debug.Assert(input != null);
Debug.Assert(input.HasResult, "input.HasResult");
result = (ConfigurationSection)input.Result;
```

Второй:

```cpp
SectionInput input = sectionRecord.LastLocationInput;
Debug.Assert(input.HasResult, "input.HasResult");
result = (ConfigurationSection)input.Result;
```

Выглядит так, будто пропустили выражение _Debug\.Assert\(input \!\= null\)_\.

Я открыл [issue на GitHub](https://github.com/dotnet/runtime/issues/78634), где описал это и другие подозрительные места, связанные с _null_\-проверками \(их мы рассмотрим ниже\)\. 

Рассмотренный код исправлять не стали, оставив as is:

![1015_NET7_Errors_ru/image7.png](https://import.viva64.com/docx/blog/1015_NET7_Errors_ru/image7.png)

**Issues c проверкой на null**

В коде я встретил несколько мест, в которых ссылка сначала разыменовывается, а затем проверяется на _null\._ Чтобы не плодить задачи, собрал их все в общем [issue на GitHub](https://github.com/dotnet/runtime/issues/78634)\.

Давайте разберём эти места\. 

**Issue 9**

```cpp
private static RuntimeBinderException BadOperatorTypesError(Expr pOperand1, 
                                                            Expr pOperand2)
{
  // ....
  string strOp = pOperand1.ErrorString;

  Debug.Assert(pOperand1 != null);
  Debug.Assert(pOperand1.Type != null);

  if (pOperand2 != null)
  {
    Debug.Assert(pOperand2.Type != null);
    return ErrorHandling.Error(ErrorCode.ERR_BadBinaryOps,
                               strOp, 
                               pOperand1.Type, 
                               pOperand2.Type);
  }

  return ErrorHandling.Error(ErrorCode.ERR_BadUnaryOp, strOp, pOperand1.Type);
}
```

Location: ExpressionBinder\.cs, 798 \([link](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/ExpressionBinder.cs#L798)\)

Сначала _pOperand1_ разыменовывается \(_pOperand1\.ErrorString_\), а уже на следующей строке проверяется на _null_ в _Debug\.Assert_\. Если _pOperand1_ будет _null_, вместо срабатывания ассерта будет выброшено исключение типа _NullReferenceException_\. 

Код поправили, вынеся проверку _pOperand1_ до использования\. 

Было:

```cpp
string strOp = pOperand1.ErrorString;

Debug.Assert(pOperand1 != null);
Debug.Assert(pOperand1.Type != null);
```

Стало:

```cpp
Debug.Assert(pOperand1 != null);
Debug.Assert(pOperand1.Type != null);

string strOp = pOperand1.ErrorString;
```

**Issue 10**

```cpp
public void Execute()
{
  var count = _callbacks.Count;
  if (count == 0)
  {
    return;
  }

  List<Exception>? exceptions = null;

  if (_callbacks != null)
  {
    for (int i = 0; i < count; i++)
    {
      var callback = _callbacks[i];
      Execute(callback, ref exceptions);
    }
  }

  if (exceptions != null)
  {
    throw new AggregateException(exceptions);
  }
}
```

Location: PipeCompletionCallbacks\.cs, 20 \([link](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/PipeCompletionCallbacks.cs#L20)\)

Переменную _\_callbacks_ сначала используют, а затем проверяют её значение на неравенство _null_:

```cpp
public void Execute()
{
  var count = _callbacks.Count;
  ....
  if (_callbacks != null)
  ....
}
```

На момент написания статьи код исправили, убрав проверку _\_callbacks_ на _null_\. 

Кстати, _\_callbacks_ – это _readonly_ поле, которое инициализируется в конструкторе:

```cpp
internal sealed class PipeCompletionCallbacks
{
  private readonly List<PipeCompletionCallback> _callbacks;
  private readonly Exception? _exception;
  public PipeCompletionCallbacks(List<PipeCompletionCallback> callbacks, 
                                 ExceptionDispatchInfo? edi)
  {
    _callbacks = callbacks;
    _exception = edi?.SourceException;
  }
  ....
}
```

В треде с исправлениями обсуждали, стоит ли добавить в конструктор _Debug\.Assert_ с проверкой _\_callbacks_ на _null_\. В итоге решили не делать\.

![1015_NET7_Errors_ru/image8.png](https://import.viva64.com/docx/blog/1015_NET7_Errors_ru/image8.png)

**Issue 11**

```cpp
private void ValidateAttributes(XmlElement elementNode)
{
  ....
  XmlSchemaAttribute schemaAttribute 
    = (_defaultAttributes[i] as XmlSchemaAttribute)!;
  attrQName = schemaAttribute.QualifiedName;
  Debug.Assert(schemaAttribute != null);
  ....
}
```

Location: DocumentSchemaValidator\.cs, 421 \([link](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/System.Private.Xml/src/System/Xml/Dom/DocumentSchemaValidator.cs#L421)\)

Противоречивый код:

1. Результат работы оператора _as_ записывается в _schemaAttribute_\. Если _\_defaultAttributes\[i\]_ — _null_, или приведение выполнить не удалось, результатом будет _null_\. 
1. null\-forgiving оператор \('\!'\) намекает, что результат приведения не может быть _null_\. Как следствие, _schemaAttribute_ не должен быть равен _null_\. 
1. На следующей строке _schemaAttribute_ разыменовывается\. Ещё строкой ниже проверяется, что она не равна _null_\.

Внимание, вопрос\. Может ли _schemaAttribute_ иметь значение _null_? Из этого кода, конечно, не очень понятно\. 

Исправили код так:

```cpp
....
XmlSchemaAttribute schemaAttribute 
  = (XmlSchemaAttribute)_defaultAttributes[i]!;
attrQName = schemaAttribute.QualifiedName;
....
```

В обсуждении правок предлагали не удалять вызов _Debug\.Assert_, а перенести его строкой выше\. Код выглядел бы так:

```cpp
....
XmlSchemaAttribute schemaAttribute = (XmlSchemaAttribute)_defaultAttributes[i]!;
Debug.Assert(schemaAttribute != null);
attrQName = schemaAttribute.QualifiedName;
....
```

В итоге _Assert_ решили не возвращать:

![1015_NET7_Errors_ru/image9.png](https://import.viva64.com/docx/blog/1015_NET7_Errors_ru/image9.png)

**Issue 12**

Взглянем на конструктор типа _XmlConfigurationElementTextContent_:

```cpp
public XmlConfigurationElementTextContent(string textContent, 
                                          int? linePosition, 
                                          int? lineNumber)
{ .... }
```

Location: XmlConfigurationElementTextContent\.cs, 10 \([link](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementTextContent.cs#L10)\)

Теперь посмотрим, где он используется:

```cpp
public static IDictionary<string, string?> Read(....)
{
  ....
  case XmlNodeType.EndElement:
    ....
    var lineInfo = reader as IXmlLineInfo;
    var lineNumber = lineInfo?.LineNumber;
    var linePosition = lineInfo?.LinePosition;
    parent.TextContent = new XmlConfigurationElementTextContent(string.Empty, 
                                                                lineNumber,
                                                                linePosition);
    ....
    break;
  ....
  case XmlNodeType.Text:
    ....
    var lineInfo = reader as IXmlLineInfo;
    var lineNumber = lineInfo?.LineNumber;
    var linePosition = lineInfo?.LinePosition;

    XmlConfigurationElement parent = currentPath.Peek();

    parent.TextContent = new XmlConfigurationElementTextContent(reader.Value,
                                                                lineNumber, 
                                                                linePosition);
    ....
    break;
  ....
}
```

Locations:

* XmlStreamConfigurationProvider\.cs, 133 \([link](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs#L133)\)
* XmlStreamConfigurationProvider\.cs, 148 \([link](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs#L148)\) 

Заметили подвох в коде? 

Обратите внимание на порядок аргументов и параметров:

* аргументы: \.\.\., _lineNumber_, _linePosition_; 
* параметры: \.\.\., _linePosition_, _lineNumber_\.

Я открыл issue на GitHub \([link](https://github.com/dotnet/runtime/issues/78212)\), код поправили: аргументы поменяли местами и добавили тест\. 

**Issue 13**

Ещё один подозрительный случай: 

```cpp
public virtual bool Nested
{
  get {....}
  set 
  {
    ....
    ForeignKeyConstraint? constraint 
      = ChildTable.Constraints
                  .FindForeignKeyConstraint(ChildKey.ColumnsReference, 
                                            ParentKey.ColumnsReference); 
    ....
  }
}
```

Location: DataRelation\.cs, 486 \([link](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/System.Data.Common/src/System/Data/DataRelation.cs#L486)\)

Посмотрим на метод _FindForeignKeyConstraint_:

```cpp
internal ForeignKeyConstraint? 
FindForeignKeyConstraint(DataColumn[] parentColumns, 
                         DataColumn[] childColumns)
{ .... }
```

Location: ConstraintCollection\.cs, 548 \([link](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/System.Data.Common/src/System/Data/ConstraintCollection.cs#L548)\)

Похоже, порядок аргументов опять неверный:

* параметры: _parent_\.\.\., _child_\.\.\.
* аргументы: _ChildKey_\.\.\., _ParentKey_\.\.\.

Есть ещё один вызов этого метода: там к порядку аргументов нет вопросов\. 

```cpp
ForeignKeyConstraint? foreignKey
  = relation.ChildTable
            .Constraints
            .FindForeignKeyConstraint(relation.ParentColumnsReference,
                                      relation.ChildColumnsReference);
```

Открыл issue на GitHub: [link](https://github.com/dotnet/runtime/issues/78628)\. Увы, на момент написания статьи я не получил комментариев по этому коду\. 

**Issue 14**

Это не все места, где порядок аргументов перепутан — нашёл ещё одно:

```cpp
void RecurseChildren(....)
{
  ....
  string? value 
    =  processValue != null
      ? processValue(new ConfigurationDebugViewContext(
                           child.Key, 
                           child.Path, 
                           valueAndProvider.Value, 
                           valueAndProvider.Provider))
      : valueAndProvider.Value;

  ....
}
```

Location: ConfigurationRootExtensions\.cs, 50 \([link](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/Microsoft.Extensions.Configuration.Abstractions/src/ConfigurationRootExtensions.cs#L50)\)

Посмотрим на конструктор _ConfigurationDebugViewContext_:

```cpp
public ConfigurationDebugViewContext(
  string path, 
  string key, 
  string? value, 
  IConfigurationProvider configurationProvider) 
{ .... }
```

Location: ConfigurationDebugViewContext\.cs, 11 \([link](https://github.com/dotnet/runtime/blob/d099f075e45d2aa6007a22b71b45a08758559f80/src/libraries/Microsoft.Extensions.Configuration.Abstractions/src/ConfigurationDebugViewContext.cs#L11)\)

Последовательность:

* параметры: _path_, _key_, \.\.\.
* аргументы: _child\.Key_, _child\.Path_, \.\.\.

Открыл issue на GitHub: [link](https://github.com/dotnet/runtime/issues/78306)\. Со слов разработчиков, несмотря на ошибку, этот кейс не вызывал проблем\. 

![1015_NET7_Errors_ru/image10.png](https://import.viva64.com/docx/blog/1015_NET7_Errors_ru/image10.png)

Тем не менее, порядок аргументов изменили на правильный\.

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

Код \.NET 7 высокого качества\. Я думаю, этому способствует в том числе налаженный процесс разработки: даты релизов известны, выпуски RC тоже проводятся не зря\.

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

Все фрагменты кода, описанные в статье, я нашёл анализатором PVS\-Studio\. Да, теперь им можно проверять проекты на \.NET 7\. 

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