Le paramètre out doit-il être initialisé avant de revenir de la méthode?

0800_OutParamsCs_ru / image1.png







Tous ceux qui ont écrit en C # ont sûrement rencontré l'utilisation de paramètres out. Il semble que tout est extrêmement simple et compréhensible avec eux. Mais est-ce vraiment le cas? Pour commencer, je suggère de commencer par un puzzle d'auto-test.







Permettez-moi de vous rappeler que les paramètres out doivent être initialisés par la méthode appelée avant de la quitter.







Maintenant, regardez le morceau de code suivant et voyez s'il se compile.







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





MyStruct est une sorte de type valeur :







public struct MyStruct
{ .... }
      
      





Si vous avez répondu avec confiance `` oui '' ou `` non '', je vous invite à continuer la lecture, car tout n'est pas si clair ...







Fond



Commençons par un peu de contexte. Comment en sommes-nous arrivés à connaître les paramètres?







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.







0800_OutParamsCs_ru / image2.png







, , . :)







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







0800_OutParamsCs_ru / image3.png







, . 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, , .







0800_OutParamsCs_ru / image4.png







MyStruct CancellationToken, strictDiagnostics 1 ( ), compatDiagnostics .







0800_OutParamsCs_ru / image5.png







, . ? :







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







, , — . , CancellationToken MyStruct? out- MyStruct compat , — CancellationToken — ?







, .







0800_OutParamsCs_ru / image7.png







, . . :)







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-.







0800_OutParamsCs_ru / image8.png







, 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.







0800_OutParamsCs_ru / image9.png







. , , 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'. , (fieldnull) : " "? , " ", " ". — " ", " ".







. , , '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 (.. CancellationTokenm_source).







, case- .







field.IsVirtualTupleFieldfalse. 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 : _dev12CompilerCompatibilityfalse, , — false. Compat : — true, — true.







, . :)







compat , CancellationSourcem_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, . . :)







, : Sergey Vasiliev. Should We Initialize an Out Parameter Before a Method Returns?.








All Articles