﻿# Должен ли out\-параметр быть проинициализирован до возврата из метода?

Наверняка каждый, кто писал на C\#, сталкивался с использованием out\-параметров\. Кажется, что с ними всё предельно просто и понятно\. Но так ли это на самом деле? Для затравки предлагаю начать с задачки для самопроверки\.

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

Напомню, что _out_\-параметры должны быть проинициализированы вызываемым методом до выхода из него\.

А теперь посмотрите на следующий фрагмент кода и ответьте, компилируется ли он\.

```cpp
void CheckYourself(out MyStruct obj)
{
  // Do nothing
}
```

_MyStruct_ \- какой\-то значимый тип:

```cpp
public struct MyStruct
{ .... }
```

Если вы уверенно ответили 'да' или 'нет' \- приглашаю к дальнейшему прочтению, так как всё не так однозначно\.\.\.

## Предыстория

Начнём с небольшой предыстории\. Как мы вообще погрузились в изучение _out_\-параметров?

Всё началось с разработки очередного диагностического правила для [PVS\-Studio](https://pvs-studio.ru/ru/pvs-studio/)\. Идея диагностики заключается в следующем \- один из параметров метода имеет тип _CancellationToken_\. При этом данный параметр в теле метода не используется\. Как следствие, программа может не реагировать \(или реагировать несвоевременно\) на какие\-то действия отмены, например, отмены операции со стороны пользователя\. В ходе просмотра срабатываний одной из первых версий диагностики нашли код примерно следующего вида:

```cpp
void Foo(out CancellationToken ct, ....)
{
  ....
  if (flag)
    ct = someValue;
  else
    ct = otherValue;
  ....
}
```

Очевидно, что это было false positive срабатыванием, поэтому я попросил коллегу добавить в набор модульных тестов ещё один, "с out параметрами"\. Он добавил тестов, в том числе тест такого вида:

```cpp
void TestN(out CancellationToken ct)
{
  Console.WriteLine("....");
}
```

В первую очередь меня интересовали тесты с инициализаций параметров, но я повнимательнее присмотрелся к этому\.\.\. И тут меня осенило\! А как этот код, собственно, компилируется? И компилируется ли вообще? Код компилировался\. Тут я понял, что намечается статья\. :\)

Ради эксперимента решили поменять _CancellationToken_ на какой\-нибудь другой значимый тип\. Например, _TimeSpan_:

```cpp
void TestN(out TimeSpan timeSpan)
{
  Console.WriteLine("....");
}
```

Не компилируется\. Что ж, ожидаемо\. Но почему компилируется пример с _CancellationToken_?

## Модификатор параметра out

Давайте вновь вспомним, что за модификатор параметра такой \- _out_\. Вот основные тезисы, взятые с docs\.microsoft\.com \([out parameter modifier](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/out-parameter-modifier)\):

* The _out_ keyword causes arguments to be passed by reference;
* Variables passed as _out _arguments do not have to be initialized before being passed in a method call\. _**However, the called method is required to assign a value before the method returns\.**_

Особо прошу обратить внимание на выделенное предложение\.

Внимание \- вопрос\. В чём отличие следующих трёх методов, и почему последний компилируется, а первый и второй \- нет?

```cpp
void Method1(out String obj) // compilation error
{ }

void Method2(out TimeSpan obj) // compilation error
{ }

void Method3(out CancellationToken obj) // no compilation error
{ }
```

Пока закономерности не видно\. Может быть есть какие\-то исключения, которые описаны в доках? Для типа _CancellationToken_, например\. Хотя это было бы немного странно \- что в нём такого особенного? В приведённой выше документации я никакой информации по этому поводу не нашёл\. За дополнительными сведениями предлагают обращаться к спецификации языка:_ For more information, see the [C\# Language Specification](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/introduction)\. The language specification is the definitive source for C\# syntax and usage\._

Что ж, посмотрим спецификацию\. Нас интересует раздел "[Output parameters](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/classes#output-parameters)"\. Ничего нового \- всё то же самое: _Every output parameter of a method must be definitely assigned before the method returns\._

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

Что ж, раз официальная документация и спецификация языка ответов нам не дали, придётся немного поковыряться в компиляторе\. :\)

## Погружаемся в Roslyn

Исходники Roslyn можно загрузить со [страницы проекта на GitHub](https://github.com/dotnet/roslyn)\. Для экспериментов я взял ветку _master_\. Работать будем с решением _Compilers\.sln_\. В качестве стартового проекта для экспериментов используем _csc\.csproj_\. Можно даже его запустить на файле с нашими тестами, чтобы убедиться в воспроизводимости проблемы\.

Для экспериментов возьмём следующий код:

```cpp
struct MyStruct
{
  String _field;
}

void CheckYourself(out MyStruct obj)
{
  // Do nothing
}
```

Для проверки, что ошибка на месте, соберём и запустим компилятор на файле, содержащем этот код\. И действительно \- ошибка на месте: _error CS0177: The out parameter 'obj' must be assigned to before control leaves the current method_

Кстати, это сообщение может стать неплохой отправной точкой для погружения в код\. Сам код ошибки \(CS0177\) наверняка формируется динамически, а вот строка формата для сообщения, скорее всего, лежит где\-нибудь в ресурсах\. И это действительно так \- находим ресурс _ERR\_ParamUnassigned_:

```cpp
<data name="ERR_ParamUnassigned" xml:space="preserve">
  <value>The out parameter '{0}' must be assigned to 
         before control leaves the current method</value>
</data>
```

По тому же имени находим код ошибки \- _ERR\_ParamUnassigned \= 177_, а также несколько мест использования в коде\. Нас интересует место, где добавляется ошибка \(метод _DefiniteAssignmentPass\.ReportUnassignedOutParameter_\):

```cpp
protected virtual void ReportUnassignedOutParameter(
  ParameterSymbol parameter, 
  SyntaxNode node, 
  Location location)
{
  ....
  bool reported = false;
  if (parameter.IsThis)
  {
    ....
  }

  if (!reported)
  {
    Debug.Assert(!parameter.IsThis);
    Diagnostics.Add(ErrorCode.ERR_ParamUnassigned, // <=
                    location, 
                    parameter.Name);
  }
}
```

Что ж, очень похоже на интересующее нас место\! Ставим точку останова и убеждаемся, что это \- нужное нам место\. По результатам в _Diagnostics_ будет записано как раз то сообщение, которое мы видели:

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

Что ж, шикарно\. А теперь поменяем _MyStruct_ на _CancellationToken_, иии\.\.\. Мы всё также проходим эту ветку исполнения кода, в которой ошибка записывается в _Diagnostics_\. То есть, она всё ещё на месте\! Вот это поворот\.

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

Немного покопавшись в коде, выходим на метод _DefiniteAssignmentPass\.Analyze_, который инициировал запуск анализа, проверяющего, в том числе, что _out_\-параметры инициализируются\. В нём обнаруживаем, что соответствующий анализ запускается 2 раза:

```cpp
// Run the strongest version of analysis
DiagnosticBag strictDiagnostics = analyze(strictAnalysis: true);
....
// Also run the compat (weaker) version of analysis to see 
   if we get the same diagnostics.
// If any are missing, the extra ones from the strong analysis 
   will be downgraded to a warning.
DiagnosticBag compatDiagnostics = analyze(strictAnalysis: false);
```

А немного ниже находится интересное условие:

```cpp
// If the compat diagnostics did not overflow and we have the same 
   number of diagnostics, we just report the stricter set.
// It is OK if the strict analysis had an overflow here,
   causing the sets to be incomparable: the reported diagnostics will
// include the error reporting that fact.
if (strictDiagnostics.Count == compatDiagnostics.Count)
{
  diagnostics.AddRangeAndFree(strictDiagnostics);
  compatDiagnostics.Free();
  return;
}
```

Ситуация понемногу проясняется\. В результате работы и, strict, и compat анализа, когда мы пытаемся скомпилировать наш код с _MyStruct_, оказывается одинаковое количество диагностик, которые мы в результате и выдадим\.

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

Если же мы меняем в нашем примере _MyStruct_ на _CancellationToken_, _strictDiagnostics_ будет содержать 1 ошибку \(как мы уже видели\), а в _compatDiagnostics_ не будет ничего\. 

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

Как следствие, приведённое выше условие не выполняется и исполнение метода не прерывается\. Куда же девается ошибка компиляции? А она понижается до предупреждения:

```cpp
HashSet<Diagnostic> compatDiagnosticSet 
  = new HashSet<Diagnostic>(compatDiagnostics.AsEnumerable(), 
                            SameDiagnosticComparer.Instance);
compatDiagnostics.Free();
foreach (var diagnostic in strictDiagnostics.AsEnumerable())
{
  // If it is a warning (e.g. WRN_AsyncLacksAwaits), 
     or an error that would be reported by the compatible analysis, 
     just report it.
  if (   diagnostic.Severity != DiagnosticSeverity.Error 
      || compatDiagnosticSet.Contains(diagnostic))
  {
    diagnostics.Add(diagnostic);
    continue;
  }

  // Otherwise downgrade the error to a warning.
  ErrorCode oldCode = (ErrorCode)diagnostic.Code;
  ErrorCode newCode = oldCode switch
  {
#pragma warning disable format
    ErrorCode.ERR_UnassignedThisAutoProperty 
      => ErrorCode.WRN_UnassignedThisAutoProperty,
    ErrorCode.ERR_UnassignedThis             
      => ErrorCode.WRN_UnassignedThis,
    ErrorCode.ERR_ParamUnassigned                   // <=      
      => ErrorCode.WRN_ParamUnassigned,
    ErrorCode.ERR_UseDefViolationProperty    
      => ErrorCode.WRN_UseDefViolationProperty,
    ErrorCode.ERR_UseDefViolationField       
      => ErrorCode.WRN_UseDefViolationField,
    ErrorCode.ERR_UseDefViolationThis        
      => ErrorCode.WRN_UseDefViolationThis,
    ErrorCode.ERR_UseDefViolationOut         
      => ErrorCode.WRN_UseDefViolationOut,
    ErrorCode.ERR_UseDefViolation            
      => ErrorCode.WRN_UseDefViolation,
    _ => oldCode, // rare but possible, e.g. 
                     ErrorCode.ERR_InsufficientStack occurring in 
                     strict mode only due to needing extra frames
#pragma warning restore format
  };

  ....
  var args 
     = diagnostic is DiagnosticWithInfo { 
         Info: { Arguments: var arguments } 
       } 
       ? arguments 
       : diagnostic.Arguments.ToArray();
  diagnostics.Add(newCode, diagnostic.Location, args);
}
```

Что здесь происходит в нашем случае при использовании _CancellationToken_? В цикле происходит обход _strictDiagnostics_ \(напоминаю, что там содержится ошибка про неинициализированный _out_\-параметр\)\. _Then_\-ветвь оператора _if_ не исполняется, так как _diagnostic\.Severity _ имеет значение _DiagnosticSeverity\.Error_, а коллекция _compatDiagnosticSet_ пуста\. А далее происходит маппинг кода ошибки компиляции на новый код \- уже предупреждения, после чего это предупреждение формируется и записывается в результирующую коллекцию\. Таким вот образом ошибка компиляции превратилась в предупреждение\. :\)

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

Выставляем запуск компилятора, указав дополнительный флаг: _csc\.exe %pathToFile% \-w:5_

И видим ожидаемое предупреждение:

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

Теперь мы разобрались, куда пропадает ошибка компиляции, \- она заменяется на низкоприоритетное предупреждение\. Однако у нас до сих пор нет ответа на вопрос, в чём же особенность _CancellationToken_ и его отличие от _MyStruct_? Почему при анализе метода с _out_\-параметром _MyStruct_ compat анализ находит ошибку, а когда тип параметра \- _CancellationToken_ \- ошибка не обнаруживается?

Тут я предлагаю заварить чашечку чая или кофе, так как далее нас ждёт более глубокое погружение\.

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

Надеюсь, вы воспользовались советом и подготовились\. Мы продолжаем\. :\)

Помните метод _ReportUnassignedParameter_, в котором происходила запись ошибки компиляции? Поднимаемся немного выше и смотрим вызывающий метод:

```cpp
protected override void LeaveParameter(ParameterSymbol parameter, 
                                       SyntaxNode syntax, 
                                       Location location)
{
  if (parameter.RefKind != RefKind.None)
  {
    var slot = VariableSlot(parameter);
    if (slot > 0 && !this.State.IsAssigned(slot))
    {
      ReportUnassignedOutParameter(parameter, syntax, location);
    }

    NoteRead(parameter);
  }
}
```

Разница при выполнении этих методов из strict и compat анализа в том, что в первом случае переменная _slot_ имеет значение 1, а во втором \- \-1\. Следовательно, во втором случае не выполняется _then_\-ветвь оператора _if_\. Теперь нужно выяснить, почему во втором случае _slot_ имеет значение \-1\.

Смотрим метод _LocalDataFlowPass\.VariableSlot_:

```cpp
protected int VariableSlot(Symbol symbol, int containingSlot = 0)
{
  containingSlot = DescendThroughTupleRestFields(
                     ref symbol, 
                     containingSlot,                                   
                     forceContainingSlotsToExist: false);

  int slot;
  return 
    (_variableSlot.TryGetValue(new VariableIdentifier(symbol, 
                                                      containingSlot), 
                               out slot)) 
    ? slot 
    : -1;
}
```

В нашем случае _\_variableSlot_ не содержит слота под _out_\-параметр, соответственно, _\_variableSlot\.TryGetValue\(\.\.\.\.\)_ возвращает значение _false_, исполнение кода идёт по alternative\-ветви оператора ?:, и из метода возвращается значение \-1\. Теперь нужно понять, почему _\_variableSlot_ не содержит _out_\-параметра\.

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

Покопавшись, находим метод _LocalDataFlowPass\.GetOrCreateSlot_\. Выглядит он следующим образом:

```cpp
protected virtual int GetOrCreateSlot(
  Symbol symbol, 
  int containingSlot = 0, 
  bool forceSlotEvenIfEmpty = false, 
  bool createIfMissing = true)
{
  Debug.Assert(containingSlot >= 0);
  Debug.Assert(symbol != null);

  if (symbol.Kind == SymbolKind.RangeVariable) return -1;

  containingSlot 
    = DescendThroughTupleRestFields(
        ref symbol, 
        containingSlot,
        forceContainingSlotsToExist: true);

  if (containingSlot < 0)
  {
    // Error case. Diagnostics should already have been produced.
    return -1;
  }

  VariableIdentifier identifier 
    = new VariableIdentifier(symbol, containingSlot);
  int slot;

  // Since analysis may proceed in multiple passes, 
     it is possible the slot is already assigned.
  if (!_variableSlot.TryGetValue(identifier, out slot))
  {
    if (!createIfMissing)
    {
      return -1;
    }

    var variableType = symbol.GetTypeOrReturnType().Type;
    if (!forceSlotEvenIfEmpty && IsEmptyStructType(variableType))
    {
      return -1;
    }

    if (   _maxSlotDepth > 0 
        && GetSlotDepth(containingSlot) >= _maxSlotDepth)
    {
      return -1;
    }

    slot = nextVariableSlot++;
    _variableSlot.Add(identifier, slot);
    if (slot >= variableBySlot.Length)
    {
      Array.Resize(ref this.variableBySlot, slot * 2);
    }

    variableBySlot[slot] = identifier;
  }

  if (IsConditionalState)
  {
    Normalize(ref this.StateWhenTrue);
    Normalize(ref this.StateWhenFalse);
  }
  else
  {
    Normalize(ref this.State);
  }

  return slot;
}
```

Из метода видно, что есть ряд условий, когда метод вернёт значение \-1, а слот не будет добавлен в _\_variableSlot_\. Если же слота под переменную ещё нет, и все проверки проходят успешно, то происходит запись в _\_variableSlot_: _\_variableSlot\.Add\(identifier, slot\)_\. Отлаживаем код и видим, что при выполнении strict анализа все проверки успешно проходят, а вот при compat анализе мы заканчиваем выполнение метода в следующем операторе _if_:

```cpp
var variableType = symbol.GetTypeOrReturnType().Type;
if (!forceSlotEvenIfEmpty && IsEmptyStructType(variableType))
{
  return -1;
}
```

Значение переменной _forceSlotEvenIfEmpty_ в обоих случаях одинаковое \(_false_\), а разница в том, какое значение возвращает метод _IsEmptyStructType_: для strict анализа \- _false_, для compat анализа \- _true_\.

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

Здесь сразу же возникают новые вопросы и желание поэкспериментировать\. То есть получается, что, если тип _out_\-параметра \- "пустая структура" \(позже мы поймём, что это значит\), компилятор считает такой код допустимым и не генерирует ошибку? Убираем в нашем примере из _MyStruct_ поле и компилируем\.

```cpp
struct MyStruct
{  }

void CheckYourself(out MyStruct obj)
{
  // Do nothing
}
```

И этот код успешно компилируется\! Интересно\.\.\. Упоминаний таких особенностей в документации и спецификации я что\-то не помню\. :\)

Но тогда возникает другой вопрос: а как же работает код в случае, когда тип _out_\-параметра \- _CancellationToken_? Ведь это явно не "пустая структура" \- если посмотреть код на referencesource\.microsoft\.com \([ссылка на CancellationToken](https://referencesource.microsoft.com/#mscorlib/system/threading/CancellationToken.cs,36b17ded8b1a228c)\), становится видно, что этот тип содержит и методы, и свойства, и поля\.\.\. Непонятно, копаем дальше\.

Мы остановились на методе _LocalDataFlowPass\.IsEmptyStructType_:

```cpp
protected virtual bool IsEmptyStructType(TypeSymbol type)
{
  return _emptyStructTypeCache.IsEmptyStructType(type);
}
```

Идём глубже \(_EmptyStructTypeCache\.IsEmptyStructType_\):

```cpp
public virtual bool IsEmptyStructType(TypeSymbol type)
{
  return IsEmptyStructType(type, ConsList<NamedTypeSymbol>.Empty);
}
```

И ещё глубже:

```cpp
private bool IsEmptyStructType(
  TypeSymbol type, 
  ConsList<NamedTypeSymbol> typesWithMembersOfThisType)
{
  var nts = type as NamedTypeSymbol;
  if ((object)nts == null || !IsTrackableStructType(nts))
  {
    return false;
  }

  // Consult the cache.
  bool result;
  if (Cache.TryGetValue(nts, out result))
  {
    return result;
  }

  result = CheckStruct(typesWithMembersOfThisType, nts);
  Debug.Assert(!Cache.ContainsKey(nts) || Cache[nts] == result);
  Cache[nts] = result;

  return result;
}
```

Выполнение кода идёт через вызов метода _EmptyStructTypeCache\.CheckStruct_:

```cpp
private bool CheckStruct(
  ConsList<NamedTypeSymbol> typesWithMembersOfThisType, 
  NamedTypeSymbol nts)
{
  .... 
  if (!typesWithMembersOfThisType.ContainsReference(nts))
  {
    ....
    typesWithMembersOfThisType 
      = new ConsList<NamedTypeSymbol>(nts, 
                                      typesWithMembersOfThisType);
    return CheckStructInstanceFields(typesWithMembersOfThisType, nts);
  }

  return true;
}
```

Здесь исполнение заходит в _then_\-ветвь оператора _if_, т\.к\. коллекция _typesWithMembersOfThisType_ пустая \(см\. метод _EmptyStructTypeCache\.IsEmptyStructType_, где она начинает передаваться в качестве аргумента\)\. 

Какая\-то картина уже начинает вырисовываться \- теперь становится понятно, что такое "пустая структура"\. Судя по названиям методов, это такая структура, которая не содержит экземплярных полей\. Но я напоминаю, что в _CancellationToken_ экземплярные поля есть\. Значит, идём ещё глубже, в метод _EmptyStructTypeCache\.CheckStructInstanceFields_\.

```cpp
private bool CheckStructInstanceFields(
  ConsList<NamedTypeSymbol> typesWithMembersOfThisType, 
  NamedTypeSymbol type)
{
  ....
  foreach (var member in type.OriginalDefinition
                             .GetMembersUnordered())
  {
    if (member.IsStatic)
    {
      continue;
    }
    var field = GetActualField(member, type);
    if ((object)field != null)
    {
      var actualFieldType = field.Type;
      if (!IsEmptyStructType(actualFieldType, 
                             typesWithMembersOfThisType))
      {
        return false;
      }
    }
  }

  return true;
}
```

В методе обходятся экземплярные члены, для каждого из которых получается 'actualField'\. Дальше, если удалось получить это значение \(_field_ \- не _null_\) опять выполняется проверка: а является ли тип этого поля "пустой структурой"? Соответственно, если нашли хотя бы одну "не пустую структуру", изначальный тип также считаем "не пустой структурой"\. Если все экземплярные поля \- "пустые структуры", то изначальный тип также считается "пустой структурой"\.

Придётся опуститься ещё немного глубже\. Не беспокойтесь, скоро наше погружение закончится, и мы расставим точки над 'i'\. :\)

Смотрим метод _EmptyStructTypeCache\.GetActualField_:

```cpp
private FieldSymbol GetActualField(Symbol member, NamedTypeSymbol type)
{
  switch (member.Kind)
  {
    case SymbolKind.Field:
      var field = (FieldSymbol)member;
      ....
      if (field.IsVirtualTupleField)
      {
        return null;
      }

      return (field.IsFixedSizeBuffer || 
              ShouldIgnoreStructField(field, field.Type)) 
            ? null 
            : field.AsMember(type);

      case SymbolKind.Event:
        var eventSymbol = (EventSymbol)member;
        return (!eventSymbol.HasAssociatedField || 
               ShouldIgnoreStructField(eventSymbol, eventSymbol.Type)) 
             ? null 
             : eventSymbol.AssociatedField.AsMember(type);
  }

  return null;
}
```

Соответственно, для типа _CancellationToken_ нас интересует _case_\-ветвь _SymbolKind\.Field_\. В неё мы можем попасть только при анализе члена _m\_source_ этого типа \(т\.к\. тип _CancellationToken_ содержит только одно экземплярное поле \- _m\_source_\)\.

Рассмотрим, как происходят вычисления в этой _case_\-ветви в нашем случае\.

_field\.IsVirtualTupleField_ \- _false_\. Переходим к условному оператору и разберём условное выражение _field\.IsFixedSizeBuffer \|\| ShouldIgnoreStructField\(field, field\.Type\)_\. _field\.IsFixedSizeBuffer_ \- не наш случай\. Значение, ожидаемо, _false_\. А вот значение, возвращаемое вызовом метода _ShouldIgnoreStructField\(field, field\.Type\),_ различается для strict и compat анализа \(напоминаю, мы анализируем одно и то же поле одного и того же типа\)\.

Смотрим тело метода _EmptyStructTypeCache\.ShouldIgnoreStructField_:

```cpp
private bool ShouldIgnoreStructField(Symbol member, 
                                     TypeSymbol memberType)
{
  // when we're trying to be compatible with the native compiler, we 
     ignore imported fields (an added module is imported)
     of reference type (but not type parameters, 
     looking through arrays)
     that are inaccessible to our assembly.

  return _dev12CompilerCompatibility &&                             
         ((object)member.ContainingAssembly != _sourceAssembly ||   
          member.ContainingModule.Ordinal != 0) &&                      
         IsIgnorableType(memberType) &&                                 
         !IsAccessibleInAssembly(member, _sourceAssembly);          
}
```

Посмотрим, что отличается для strict и compat анализа\. Хотя, возможно, вы уже догадались самостоятельно\. :\)

Strict анализ: _\_dev12CompilerCompatibility_ \- _false_, следовательно, результат всего выражения \- _false_\. Compat анализ: значения всех подвыражений \- _true_, результат всего выражения \- _true_\. 

А теперь сворачиваем цепочку, поднимаясь с самого конца\. :\)

При compat анализе мы считаем, что должны игнорировать единственное экземплярное поле типа _CancellationSource_ \- _m\_source_\. Таким образом, мы считаем, что _CancellationToken_ \- "пустая структура", следовательно для неё не создаётся слот, и не происходит записи в кэш "пустых структур"\. Так как слот отсутствует, мы не обрабатываем _out_\-параметр и не записываем ошибку компиляции при выполнении compat анализа\. Как результат, strict и compat анализ дают разные результаты, из\-за чего происходит понижение ошибки компиляции до низкоприоритетного предупреждения\.

То есть это не какая\-то особая обработка типа _CancellationToken_ \- есть целый ряд типов, для которых отсутствие инициализации _out_\-параметра не будет приводить к ошибкам компиляции\.

Давайте попробуем посмотреть на практике, для каких типов компиляция будет успешно проходить\. Как обычно, берём наш типичный метод:

```cpp
void CheckYourself(out MyType obj)
{
  // Do nothing
}
```

И пробуем подставлять вместо _MyType_ различные типы\. Мы уже разобрали, что этот код успешно компилируется для _CancellationToken_ и для пустой структуры\. Что ещё?

```cpp
struct MyStruct
{ }

struct MyStruct2
{
  private MyStruct _field;
}
```

Если вместо _MyType_ используем _MyStruct2_, код также успешно компилируется\.

```cpp
public struct MyExternalStruct
{
  private String _field;
}
```

При использовании этого типа код будет успешно компилироваться, если _MyExternalStruct_ объявлен во внешней сборке\. Если в одной сборке с методом _CheckYourself_ \- не скомпилируется\.

При использовании такого типа из внешней сборки код уже не скомпилируется \(поменяли уровень доступа поля _\_field_ с _private_ на _public_\):

```cpp
public struct MyExternalStruct
{
  public String _field;
}
```

При таком изменении типа код тоже не будет компилироваться \(поменяли тип поля со _String_ на _int_\):

```cpp
public struct MyExternalStruct
{
  private int _field;
}
```

В общем, как вы поняли, здесь есть определённый простор для экспериментов\.

## Подытожим

В общем и целом, _out_\-параметры должны быть проинициализированы до того, как вызываемый метод вернёт управление вызывающему\. Однако, как показывает практика, компилятор может внести свои коррективы в это требование, и в некоторых случаях вместо ошибки компиляции будет выдано низкоуровневое предупреждение\. Из\-за чего именно это происходит мы подробно разобрали в предыдущем разделе\.

Что же по поводу типов, для которых можно не инициализировать _out_\-параметры? Например, не обязательна инициализация параметра, если тип \- структура, в которой нет полей\. Или если все поля \- структуры без полей\. Или вот случай с _CancellationToken_: с ним компиляция успешно проходит, так как этот тип находится во внешней библиотеке, единственное поле _m\_source_ ссылочного типа, а само поле недоступно из внешнего кода\. В принципе, несложно придумать и составить ещё своих подобных типов, при использовании которых вы сможете не инициализировать _out_\-параметры и успешно компилировать ваш код\.

Возвращаясь к вопросу из начала статьи:

```cpp
void CheckYourself(out MyStruct obj)
{
  // Do nothing
}
public struct MyStruct
{ .... }
```

Компилируется ли этот код? Как вы уже поняли, ни 'Да', ни 'Нет' не являются правильным ответом\. В зависимости от того, что такое _MyStruct_ \(какие есть поля, где объявлен тип и т\. п\.\), этот код может либо компилироваться, либо не компилироваться\.

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

Такое вот у нас сегодня получилось погружение в исходный код компилятора для того, чтобы ответить на, казалось бы, простой вопрос\. Я думаю, скоро мы повторим этот опыт, так как тема для следующей подобной статьи уже есть\. Так что оставайтесь на связи\. ;\)

Кстати, приглашаю подписаться на [мой аккаунт в Twitter](https://twitter.com/_SergVasiliev_), где я также выкладываю статьи и прочие интересные находки\. Так точно ничего интересного не пропустите\. :\)