Você gosta do operador '?.' Quem não ama? Muitas pessoas gostam desses lacônicos cheques nulos. No entanto, hoje vamos falar sobre o caso em que a operadora '?.' apenas cria a ilusão de segurança. Trata-se de usá-lo em um loop foreach.
Proponho começar com um pequeno problema de autoteste. Dê uma olhada no seguinte código:
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".

, , "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, .
, :
- ;
- ( / , / ..);
- , .
?
. 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
, - , . :)

PVS-Studio PVS-Studio. :
- . , ;
- , — PVS-Studio;
- BlameNotifier;
- .
, 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 — ;
- .
, , .
, , Twitter-.
, : Sergey Vasiliev. The '?.' Operator in foreach Will Not Protect From NullReferenceException.