>
>
>
Должен ли out-параметр быть проинициали…

Сергей Васильев
Статей: 96

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

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

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

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

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

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

public struct MyStruct
{ .... }

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

Предыстория

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

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

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

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

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

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

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

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

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

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

Давайте вновь вспомним, что за модификатор параметра такой - out. Вот основные тезисы, взятые с docs.microsoft.com (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.

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

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

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. The language specification is the definitive source for C# syntax and usage.

Что ж, посмотрим спецификацию. Нас интересует раздел "Output parameters". Ничего нового - всё то же самое: Every output parameter of a method must be definitely assigned before the method returns.

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

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

Исходники Roslyn можно загрузить со страницы проекта на GitHub. Для экспериментов я взял ветку master. Работать будем с решением Compilers.sln. В качестве стартового проекта для экспериментов используем csc.csproj. Можно даже его запустить на файле с нашими тестами, чтобы убедиться в воспроизводимости проблемы.

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

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:

<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):

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 будет записано как раз то сообщение, которое мы видели:

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

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

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

// 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);

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

// 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, оказывается одинаковое количество диагностик, которые мы в результате и выдадим.

Если же мы меняем в нашем примере MyStruct на CancellationToken, strictDiagnostics будет содержать 1 ошибку (как мы уже видели), а в compatDiagnostics не будет ничего.

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

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

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

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

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

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

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

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:

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-параметра.

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

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:

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

Значение переменной forceSlotEvenIfEmpty в обоих случаях одинаковое (false), а разница в том, какое значение возвращает метод IsEmptyStructType: для strict анализа - false, для compat анализа - true.

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

struct MyStruct
{  }

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

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

Но тогда возникает другой вопрос: а как же работает код в случае, когда тип out-параметра - CancellationToken? Ведь это явно не "пустая структура" - если посмотреть код на referencesource.microsoft.com (ссылка на CancellationToken), становится видно, что этот тип содержит и методы, и свойства, и поля... Непонятно, копаем дальше.

Мы остановились на методе LocalDataFlowPass.IsEmptyStructType:

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

Идём глубже (EmptyStructTypeCache.IsEmptyStructType):

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

И ещё глубже:

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:

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.

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:

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:

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-параметра не будет приводить к ошибкам компиляции.

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

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

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

struct MyStruct
{ }

struct MyStruct2
{
  private MyStruct _field;
}

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

public struct MyExternalStruct
{
  private String _field;
}

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

При использовании такого типа из внешней сборки код уже не скомпилируется (поменяли уровень доступа поля _field с private на public):

public struct MyExternalStruct
{
  public String _field;
}

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

public struct MyExternalStruct
{
  private int _field;
}

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

Подытожим

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

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

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

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

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

Заключение

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

Кстати, приглашаю подписаться на мой аккаунт в Twitter, где я также выкладываю статьи и прочие интересные находки. Так точно ничего интересного не пропустите. :)