Should the out parameter be initialized before returning from the method?

0800_OutParamsCs_ru / image1.png







Surely everyone who has written in C # has come across the use of out parameters. It seems that everything is extremely simple and understandable with them. But is it really so? For starters, I suggest starting with a self-test puzzle.







Let me remind you that out parameters must be initialized by the called method before exiting it.







, .







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.







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