Using the '?.' Operator in foreach: guard against NullReferenceException that doesn't work

0832_foreach_ConditionalAccess_ru / image1.png

Do you like the '?.' Operator? Who doesn't love? Many people like these laconic null checks. However, today we will talk about the case when the operator '?.' only creates the illusion of security. It's about using it in a foreach loop.







I propose to start with a small self-test problem. Take a look at the following code:







void ForeachTest(IEnumerable<String> collection)
{
  // #1
  foreach (var item in collection.NotNullItems())
    Console.WriteLine(item);

  // #2
  foreach (var item in collection?.NotNullItems())
    Console.WriteLine(item);
}
      
      





, , collection β€” null? , ?. . ? .







, . , .







, . C#, "The foreach statement".







0832_foreach_ConditionalAccess_ru / image2.png







, , "expression". β€” β€” , . " ", "expression" . .







'?.' foreach



, ?.







.







var b = a?.Foo();
      
      





:







  • a == null, b == null;
  • a != null, b == a.Foo().


foreach. , .







void Foo1(IEnumerable<String> collection)
{
  foreach (var item in collection)
    Console.WriteLine(item);
}
      
      





IL , C#, foreach. :







void Foo2(IEnumerable<String> collection)
{
  var enumerator = collection.GetEnumerator();
  try
  {
    while (enumerator.MoveNext())
    {
      var item = enumerator.Current;
      Console.WriteLine(item);
    }
  }
  finally
  {
    if (enumerator != null)
    {
      enumerator.Dispose();
    }
  }
}
      
      





. foreach . , , , for. . , foreach .







β€” collection.GetEnumerator(). ( ) , GetEnumerator. , NullReferenceException.







, ?. foreach:







static void Foo3(Wrapper wrapper)
{
  foreach (var item in wrapper?.Strings)
    Console.WriteLine(item);
}
      
      





:







static void Foo4(Wrapper wrapper)
{
  IEnumerable<String> strings;
  if (wrapper == null)
  {
    strings = null;
  }
  else
  {
    strings = wrapper.Strings;
  }

  var enumerator = strings.GetEnumerator();
  try
  {
    while (enumerator.MoveNext())
    {
      var item = enumerator.Current;
      Console.WriteLine(item);
    }
  }
  finally
  {
    if (enumerator != null)
    {
      enumerator.Dispose();
    }
  }
}
      
      





, , GetEnumerator (strings.GetEnumerator). , strings null, wrapper β€” null. , , , ?. ( ). string.GetEnumerator() NullReferenceException.







?. foreach , .







?



β€” , . . , , foreach null. . , .







void Test1(IEnumerable<String> collection, 
          Func<String, bool> predicate)
{
  foreach (var item in collection?.Where(predicate))
    Console.WriteLine(item);
}
      
      





β€” .







void Test2(IEnumerable<String> collection, 
          Func<String, bool> predicate)
{
  var query = collection?.Where(predicate);
  foreach (var item in query)
    Console.WriteLine(item);
}
      
      





.







void Test3(IEnumerable<String> collection, 
          Func<String, bool> predicate,
          bool flag)
{
  var query = collection != null ? collection.Where(predicate) : null;
  foreach (var item in query)
    Console.WriteLine(item);
}
      
      





PVS-Studio: V3080 Possible null dereference. Consider inspecting 'query'.







.







IEnumerable<String> GetPotentialNull(IEnumerable<String> collection,
                                     Func<String, bool> predicate,
                                     bool flag)
{
  return collection != null ? collection.Where(predicate) : null;
}

void Test4(IEnumerable<String> collection, 
          Func<String, bool> predicate,
          bool flag)
{
  foreach (var item in GetPotentialNull(collection, predicate, flag))
    Console.WriteLine(item);
}
      
      





PVS-Studio: V3080 Possible null dereference of method return value. Consider inspecting: GetPotentialNull(...).







Test3 Test4 , Test1 Test2 β€” ? , :







  • , ?.;
  • , - null, .


, :







  • ;
  • ( / , / ..);
  • , .


?



2 : V3105 V3153.







. PVS-Studio 7.13, .







V3105 , ?., foreach.







void Test(IEnumerable<String> collection, 
          Func<String, bool> predicate)
{
  var query = collection?.Where(predicate);
  foreach (var item in query)
    Console.WriteLine(item);
}
      
      





PVS-Studio: V3105 The 'query' variable was used after it was assigned through null-conditional operator. NullReferenceException is possible.







V3153 , ?. foreach.







void Test(IEnumerable<String> collection, 
          Func<String, bool> predicate)
{
  foreach (var item in collection?.Where(predicate))
    Console.WriteLine(item);
}
      
      





PVS-Studio: V3153 Enumerating the result of null-conditional access operator can lead to NullReferenceException. Consider inspecting: collection?.Where(predicate).









β€” . , , open source . V3105 V3153 !







. , . , .







RavenDB



private void HandleInternalReplication(DatabaseRecord newRecord, 
                                       List<IDisposable> instancesToDispose)
{
  var newInternalDestinations =
        newRecord.Topology?.GetDestinations(_server.NodeTag,
                                            Database.Name,
                                            newRecord.DeletionInProgress,
                                            _clusterTopology,
                                            _server.Engine.CurrentState);
  var internalConnections 
        = DatabaseTopology.FindChanges(_internalDestinations, 
                                       newInternalDestinations);

  if (internalConnections.RemovedDestiantions.Count > 0)
  {
    var removed = internalConnections.RemovedDestiantions
                                     .Select(r => new InternalReplication
      {
        NodeTag = _clusterTopology.TryGetNodeTagByUrl(r).NodeTag,
        Url = r,
        Database = Database.Name
      });

    DropOutgoingConnections(removed, instancesToDispose);
  }
  if (internalConnections.AddedDestinations.Count > 0)
  {
    var added = internalConnections.AddedDestinations
                                   .Select(r => new InternalReplication
    {
      NodeTag = _clusterTopology.TryGetNodeTagByUrl(r).NodeTag,
      Url = r,
      Database = Database.Name
    });
    StartOutgoingConnections(added.ToList());
  }
  _internalDestinations.Clear();
  foreach (var item in newInternalDestinations)
  {
    _internalDestinations.Add(item);
  }
}
      
      





. , . , , , . ;)







, .







private void HandleInternalReplication(DatabaseRecord newRecord, 
                                       List<IDisposable> instancesToDispose)
{
  var newInternalDestinations = newRecord.Topology?.GetDestinations(....);
  ....
  foreach (var item in newInternalDestinations)
    ....
}
      
      





newInternalDestinations ?.. newRecord.Topology β€” null, newInternalDestinations null. foreach NullReferenceException.







PVS-Studio: V3105 The 'newInternalDestinations' variable was used after it was assigned through null-conditional operator. NullReferenceException is possible. ReplicationLoader.cs 828







, β€” newInternalDestinations β€” DatabaseTopology.FindChanges, null ( newDestinations):







internal static 
(HashSet<string> AddedDestinations, HashSet<string> RemovedDestiantions)
FindChanges(IEnumerable<ReplicationNode> oldDestinations, 
            List<ReplicationNode> newDestinations)
{
  ....
  if (newDestinations != null)
  {
    newList.AddRange(newDestinations.Select(s => s.Url));
  }
  ....
}
      
      





MSBuild



public void LogTelemetry(string eventName, 
                         IDictionary<string, string> properties)
{
  string message 
           = $"Received telemetry event '{eventName}'{Environment.NewLine}";

  foreach (string key in properties?.Keys)
  {
    message += $"  Property '{key}' = '{properties[key]}'{Environment.NewLine}";
  }
  ....
}
      
      





PVS-Studio: V3153 Enumerating the result of null-conditional access operator can lead to NullReferenceException. Consider inspecting: properties?.Keys. MockEngine.cs 159







?. foreach. , , . - , . ;)







Nethermind



.







public NLogLogger(....)
{
  ....

  foreach (FileTarget target in global::NLog.LogManager
                                            .Configuration
                                           ?.AllTargets
                                            .OfType<FileTarget>())
  {
    ....
  }
  ....
}
      
      





PVS-Studio: V3153 Enumerating the result of null-conditional access operator can lead to NullReferenceException. NLogLogger.cs 50







"" NullReferenceException ?. foreach. , Configuration null. , "".







Roslyn



private ImmutableArray<char>
GetExcludedCommitCharacters(ImmutableArray<RoslynCompletionItem> roslynItems)
{
  var hashSet = new HashSet<char>();
  foreach (var roslynItem in roslynItems)
  {
    foreach (var rule in roslynItem.Rules?.FilterCharacterRules)
    {
      if (rule.Kind == CharacterSetModificationKind.Add)
      {
        foreach (var c in rule.Characters)
        {
          hashSet.Add(c);
        }
      }
    }
  }

  return hashSet.ToImmutableArray();
}
      
      





PVS-Studio: V3153 Enumerating the result of null-conditional access operator can lead to NullReferenceException. CompletionSource.cs 482







, ? , PVS-Studio .







PVS-Studio



, - , . :)







0832_foreach_ConditionalAccess_ru / image3.png







PVS-Studio PVS-Studio. :









, V3153 V3105, , . , ?. foreach, ( ). , . , , . ;)







, :







public override void
VisitAnonymousObjectCreationExpression(
  AnonymousObjectCreationExpressionSyntax node)
{
  foreach (var initializer in node?.Initializers)
    initializer?.Expression?.Accept(this);
}
      
      





, ?. β€” , . , ( Crysis), β€” .







'?.' foreach ?



, . . , ??.







NullReferenceException:







static void Test(IEnumerable<String> collection,
                 Func<String, bool> predicate)
{
  foreach (var item in collection?.Where(predicate))
    Console.WriteLine(item);
}
      
      





:







static void Test(IEnumerable<String> collection,
                 Func<String, bool> predicate)
{
  foreach (var item in    collection?.Where(predicate) 
                       ?? Enumerable.Empty<String>())
  {
    Console.WriteLine(item);
  }
}
      
      





?. null, ?? Enumerable.Empty<String>() β€” , . , , , null.







static void Test(IEnumerable<String> collection,
                 Func<String, bool> predicate)
{
  if (collection != null)
  {
    foreach (var item in collection.Where(predicate))
      Console.WriteLine(item);
  }
}
      
      





, , , , .









, .







void ForeachTest(IEnumerable<String> collection)
{
  // #1
  foreach (var item in collection.NotNullItems())
    Console.WriteLine(item);

  // #2
  foreach (var item in collection?.NotNullItems())
    Console.WriteLine(item);
}
      
      





, #2 NullReferenceException. #1? , , NullReferenceException β€” collection.NotNullItems(). ! , NotNullItems β€” , :







public static IEnumerable<T>
NotNullItems<T>(this IEnumerable<T> collection) where T : class
{
  if (collection == null)
    return Enumerable.Empty<T>();

  return collection.Where(item => item != null);
}
      
      





, , collection null. Enumerable.Empty<T>(), foreach . #1 , collection β€” null.







, . collection β€” null, NotNullItems . , , collection β€” null. , , : GetEnumerator() .







, collection.NotNullItems() NullReferenceException, ' ' β€” collection?.NotNullItems() β€” .









:







  • ?. foreach β€” ;
  • .


, , .







PVS-Studio 7.13. , - ?. ? .







, , Twitter-.







, : Sergey Vasiliev. The '?.' Operator in foreach Will Not Protect From NullReferenceException.








All Articles