
Wollten Sie schon immer das Problem der Nullreferenz-Dereferenzierung beseitigen? Wenn ja, ist die Verwendung von nullbaren Referenztypen nicht Ihre Wahl. Ich wundere mich warum? Dies wird heute diskutiert.
Wir haben gewarnt und es ist passiert. Vor ungefähr einem Jahr haben meine Kollegen einen Artikel geschrieben, in dem sie gewarnt haben, dass die Einführung von Nullable Reference-Typen nicht vor Null-Referenz-Dereferenzierungen schützen würde. Wir haben jetzt eine echte Bestätigung unserer Worte, die in den Tiefen von Roslyn gefunden wurden.
Nullable Referenztypen
Die Idee, Nullable Reference- Typen (im Folgenden: NR-Typen) hinzuzufügen, erscheint mir interessant, da das Problem der Dereferenzierung von Null-Referenzen bis heute relevant ist. Die Umsetzung des Schutzes vor Dereferenzierung ist äußerst unzuverlässig. Wie von den Erstellern geplant, darf der Wert null nur die Variablen enthalten, deren Typ mit einem "?" Gekennzeichnet ist. Zum Beispiel eine Variable vom Typ string? sagt, dass es null vom Typ string enthalten kann - im Gegenteil.
Niemand verbietet uns jedoch, null an nicht nullfähige Referenzvariablen zu übergeben.(im Folgenden - NNR) Typen, da sie nicht auf der Ebene des IL-Codes implementiert sind. Der im Compiler integrierte statische Analysator ist für diese Einschränkung verantwortlich. Daher ist diese Innovation eher beratender Natur. Hier ist ein einfaches Beispiel, um zu zeigen, wie es funktioniert:
#nullable enable
object? nullable = null;
object nonNullable = nullable;
var deref = nonNullable.ToString();
Wie wir sehen können, wird der Typ von nonNullable als NNR angegeben, aber wir können dort sicher null übergeben . Natürlich erhalten wir eine Warnung zum Konvertieren von "Konvertieren von Nullliteral oder möglichem Nullwert in einen nicht nullbaren Typ". Dies kann jedoch umgangen werden, indem ein wenig Aggression hinzugefügt wird:
#nullable enable
object? nullable = null;
object nonNullable = nullable!; // <=
var deref = nonNullable.ToString();
Ein Ausrufezeichen und es gibt keine Warnungen. Wenn einer von Ihnen ein Gourmet ist, steht eine andere Option zur Verfügung:
#nullable enable
object nonNullable = null!;
var deref = nonNullable.ToString();
Nun, noch ein Beispiel. Wir erstellen zwei einfache Konsolenprojekte. Im ersten schreiben wir:
namespace NullableTests
{
public static class Tester
{
public static string RetNull() => null;
}
}
Im zweiten schreiben wir:
#nullable enable
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
string? nullOrNotNull = NullableTests.Tester.RetNull();
System.Console.WriteLine(nullOrNotNull.Length);
}
}
}
Bewegen Sie den Mauszeiger über nullOrNotNull und sehen Sie die folgende Meldung:

Uns wird gesagt, dass der String hier nicht null sein kann . Wir verstehen jedoch, dass es hier null sein wird . Wir starten das Projekt und bekommen eine Ausnahme:

Dies sind natürlich nur synthetische Beispiele, mit denen gezeigt werden soll, dass diese Einführung Ihnen keinen Schutz gegen Nullreferenz-Dereferenzierungen garantiert. Wenn Sie dachten, dass Kunststoffe langweilig sind und es überhaupt echte Beispiele gibt, dann bitte ich Sie, sich keine Sorgen zu machen, dann wird dies alles sein.
NR-Typen haben ein anderes Problem - es ist nicht klar, ob sie enthalten sind oder nicht. Zum Beispiel hat die Lösung zwei Projekte. Einer ist mit dieser Syntax gekennzeichnet, der andere nicht. Nachdem Sie ein Projekt mit NR-Typen eingegeben haben, können Sie entscheiden, dass alle markiert werden, sobald eines markiert ist. Dies wird jedoch nicht der Fall sein. Es stellt sich heraus, dass Sie jedes Mal nachsehen müssen, ob der nullfähige Kontext im Projekt oder in der Datei enthalten ist. Andernfalls könnten Sie fälschlicherweise glauben, dass der normale Referenztyp NNR ist.
Wie die Beweise gefunden wurden
Bei der Entwicklung neuer Diagnosen im PVS-Studio-Analysegerät testen wir diese immer anhand realer Projekte. Es hilft in verschiedenen Aspekten. Zum Beispiel:
- siehe "live" bei der Qualität der erhaltenen Warnungen;
- einige der falsch positiven Ergebnisse loswerden;
- Finden Sie interessante Punkte im Code, über die Sie dann sprechen können.
- usw.
Eine der neuen V3156-Diagnosen hat Stellen gefunden, an denen aufgrund des möglichen Nullpunkts Ausnahmen ausgelöst werden können . Der Wortlaut der Diagnoseregel lautet: "Es wird nicht erwartet, dass das Argument der Methode null ist". Das Wesentliche ist, dass die Methode nicht null erwartet. Der Wert kann als Argument an null übergeben werden . Dies kann beispielsweise zu einer Ausnahme oder einer fehlerhaften Ausführung der aufgerufenen Methode führen. Weitere Informationen zu dieser Diagnoseregel finden Sie hier .
Beweise hier
Also kamen wir zum Hauptteil dieses Artikels. Hier sehen Sie echte Codefragmente aus dem Roslyn-Projekt, für die die Diagnose Warnungen ausgegeben hat. Ihre Hauptbedeutung ist, dass entweder der NNR-Typ null übergeben wird oder der Wert des NR-Typs nicht überprüft wird. All dies kann dazu führen, dass eine Ausnahme ausgelöst wird.
Beispiel 1
private static Dictionary<object, SourceLabelSymbol>
BuildLabelsByValue(ImmutableArray<LabelSymbol> labels)
{
....
object key;
var constantValue = label.SwitchCaseLabelConstant;
if ((object)constantValue != null && !constantValue.IsBad)
{
key = KeyForConstant(constantValue);
}
else if (labelKind == SyntaxKind.DefaultSwitchLabel)
{
key = s_defaultKey;
}
else
{
key = label.IdentifierNodeOrToken.AsNode();
}
if (!map.ContainsKey(key)) // <=
{
map.Add(key, label);
}
....
}
V3156 Es wird nicht erwartet, dass das erste Argument der 'ContainsKey'-Methode null ist. Möglicher Nullwert: Schlüssel. SwitchBinder.cs 121 Die
Nachricht gibt an, dass der Schlüssel potenziell null ist . Mal sehen, wo diese Variable einen solchen Wert bekommen kann. Lassen Sie uns zuerst die KeyForConstant- Methode überprüfen :
protected static object KeyForConstant(ConstantValue constantValue)
{
Debug.Assert((object)constantValue != null);
return constantValue.IsNull ? s_nullKey : constantValue.Value;
}
private static readonly object s_nullKey = new object();
Da s_nullKey nicht null ist , wollen wir sehen, was constantValue.Value zurückgibt :
public object? Value
{
get
{
switch (this.Discriminator)
{
case ConstantValueTypeDiscriminator.Bad: return null; // <=
case ConstantValueTypeDiscriminator.Null: return null; // <=
case ConstantValueTypeDiscriminator.SByte: return Boxes.Box(SByteValue);
case ConstantValueTypeDiscriminator.Byte: return Boxes.Box(ByteValue);
case ConstantValueTypeDiscriminator.Int16: return Boxes.Box(Int16Value);
....
default: throw ExceptionUtilities.UnexpectedValue(this.Discriminator);
}
}
}
Es gibt hier zwei Nullliterale, aber in diesem Fall werden wir auf keinen Fall darauf eingehen. Dies ist auf die IsBad- und IsNull- Prüfungen zurückzuführen . Ich möchte Sie jedoch auf den Rückgabetyp dieser Eigenschaft aufmerksam machen. Es ist ein NR-Typ, aber die KeyForConstant- Methode gibt bereits einen NNR-Typ zurück. Es stellt sich heraus, dass die KeyForConstant- Methode im Allgemeinen null zurückgeben kann . Eine andere Quelle, die null zurückgeben kann, ist die AsNode- Methode :
public SyntaxNode? AsNode()
{
if (_token != null)
{
return null;
}
return _nodeOrParent;
}
Bitte achten Sie auch hier auf den Rückgabetyp der Methode - es handelt sich um einen NR-Typ. Es stellt sich heraus, dass wenn wir sagen, dass null von der Methode zurückgegeben werden kann , dies nichts beeinflusst. Es ist interessant, dass der Compiler hier nicht schwört, von NR nach NNR zu konvertieren:

Beispiel 2
private SyntaxNode CopyAnnotationsTo(SyntaxNode sourceTreeRoot,
SyntaxNode destTreeRoot)
{
var nodeOrTokenMap = new Dictionary<SyntaxNodeOrToken,
SyntaxNodeOrToken>();
....
if (sourceTreeNodeOrTokenEnumerator.Current.IsNode)
{
var oldNode = destTreeNodeOrTokenEnumerator.Current.AsNode();
var newNode = sourceTreeNodeOrTokenEnumerator.Current.AsNode()
.CopyAnnotationsTo(oldNode);
nodeOrTokenMap.Add(oldNode, newNode); // <=
}
....
}
V3156 Es wird nicht erwartet, dass das erste Argument der 'Add'-Methode null ist. Möglicher Nullwert: oldNode. SyntaxAnnotationTests.cs 439
Ein weiteres Beispiel mit der oben beschriebenen AsNode- Funktion . Nur dieses Mal wird oldNode vom Typ NR sein. Der obige Schlüssel war vom Typ NNR.
Übrigens kann ich Ihnen nur eine interessante Beobachtung mitteilen. Wie oben beschrieben, testen wir die Diagnose bei der Entwicklung in verschiedenen Projekten. Bei der Überprüfung der positiven Aspekte dieser Regel wurde ein merkwürdiger Moment bemerkt. Etwa 70% aller Warnungen wurden an Methoden der Dictionary- Klasse ausgegeben . Darüber hinaus fielen die meisten von ihnen auf die TryGetValue- Methode... Vielleicht liegt dies daran, dass wir unbewusst keine Ausnahmen von einer Methode erwarten, die das Wort try enthält . Überprüfen Sie Ihren Code auf dieses Muster, um festzustellen, ob Sie etwas Ähnliches finden.
Beispiel 3
private static SymbolTreeInfo TryReadSymbolTreeInfo(
ObjectReader reader,
Checksum checksum,
Func<string, ImmutableArray<Node>,
Task<SpellChecker>> createSpellCheckerTask)
{
....
var typeName = reader.ReadString();
var valueCount = reader.ReadInt32();
for (var j = 0; j < valueCount; j++)
{
var containerName = reader.ReadString();
var name = reader.ReadString();
simpleTypeNameToExtensionMethodMap.Add(typeName, // <=
new ExtensionMethodInfo(containerName, name));
}
....
}
V3156 Das erste Argument der Methode 'Add' wird als Argument an die Methode 'TryGetValue' übergeben und es wird nicht erwartet, dass es null ist. Möglicher Nullwert: typeName. SymbolTreeInfo_Serialization.cs 255
Der Analysator gibt an, dass das Problem im Typnamen liegt . Stellen wir zunächst sicher, dass dieses Argument tatsächlich null ist . Wir schauen uns ReadString an :
public string ReadString() => ReadStringValue();
Schauen Sie sich also ReadStringValue an :
private string ReadStringValue()
{
var kind = (EncodingKind)_reader.ReadByte();
return kind == EncodingKind.Null ? null : ReadStringValue(kind);
}
Großartig, jetzt aktualisieren wir unser Gedächtnis, indem wir uns ansehen, wo unsere Variable übergeben wurde:
simpleTypeNameToExtensionMethodMap.Add(typeName, // <=
new ExtensionMethodInfo(containerName,
name));
Ich denke, es ist Zeit, in die Add- Methode einzusteigen :
public bool Add(K k, V v)
{
ValueSet updated;
if (_dictionary.TryGetValue(k, out ValueSet set)) // <=
{
....
}
....
}
Wenn null als erstes Argument an die Add- Methode übergeben wird, erhalten wir eine ArgumentNullException . Übrigens ist es interessant, dass wenn wir den Mauszeiger in Visual Studio über typeName bewegen , wir sehen, dass sein Typ string ist? ::

In diesem Fall ist der Rückgabetyp der Methode einfach string :

In diesem Fall wird kein Fehler angezeigt , wenn Sie eine Variable vom Typ NNR weiter erstellen und ihr den TypNamen zuweisen .
Lassen Sie uns versuchen, Roslyn fallen zu lassen
Nicht aus Bosheit, sondern zum Spaß schlage ich vor, eines der gezeigten Beispiele zu reproduzieren.

Test 1
Nehmen wir das unter Nummer 3 beschriebene Beispiel:
private static SymbolTreeInfo TryReadSymbolTreeInfo(
ObjectReader reader,
Checksum checksum,
Func<string, ImmutableArray<Node>,
Task<SpellChecker>> createSpellCheckerTask)
{
....
var typeName = reader.ReadString();
var valueCount = reader.ReadInt32();
for (var j = 0; j < valueCount; j++)
{
var containerName = reader.ReadString();
var name = reader.ReadString();
simpleTypeNameToExtensionMethodMap.Add(typeName, // <=
new ExtensionMethodInfo(containerName, name));
}
....
}
Um es zu reproduzieren, müssen Sie die TryReadSymbolTreeInfo- Methode aufrufen , sie ist jedoch privat . Es ist gut, dass die Klasse eine ReadSymbolTreeInfo_ForTestingPurposesOnly- Methode hat , die bereits intern ist :
internal static SymbolTreeInfo ReadSymbolTreeInfo_ForTestingPurposesOnly(
ObjectReader reader,
Checksum checksum)
{
return TryReadSymbolTreeInfo(reader, checksum,
(names, nodes) => Task.FromResult(
new SpellChecker(checksum,
nodes.Select(n => new StringSlice(names,
n.NameSpan)))));
}
Es ist sehr angenehm, dass uns direkt angeboten wird, die TryReadSymbolTreeInfo- Methode zu testen . Erstellen wir daher unsere Klasse nebeneinander und schreiben den folgenden Code:
public class CheckNNR
{
public static void Start()
{
using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream);
writer.Write((byte)170);
writer.Write((byte)9);
writer.Write((byte)0);
writer.Write(0);
writer.Write(0);
writer.Write(1);
writer.Write((byte)0);
writer.Write(1);
writer.Write((byte)0);
writer.Write((byte)0);
stream.Position = 0;
using var reader = ObjectReader.TryGetReader(stream);
var checksum = Checksum.Create("val");
SymbolTreeInfo.ReadSymbolTreeInfo_ForTestingPurposesOnly(reader, checksum);
}
}
Jetzt sammeln wir Roslyn , erstellen eine einfache Konsolenanwendung, verbinden alle erforderlichen DLL-Dateien und schreiben den folgenden Code:
static void Main(string[] args)
{
CheckNNR.Start();
}
Wir starten, erreichen den gewünschten Ort und sehen:

Wechseln Sie als Nächstes zur Add- Methode und rufen Sie die erwartete Ausnahme ab:

Ich möchte Sie daran erinnern, dass die ReadString- Methode einen NNR-Typ zurückgibt , der standardmäßig keine Null enthalten kann . Dieses Beispiel bestätigt erneut die Relevanz der Diagnoseregeln von PVS-Studio für die Suche nach der Dereferenzierung von Nullreferenzen.
Test 2
Nun, da wir bereits begonnen haben, Beispiele zu reproduzieren, warum nicht noch eines reproduzieren? Dieses Beispiel bezieht sich nicht auf NR-Typen. Es wurde jedoch von derselben V3156-Diagnose gefunden, und ich wollte Ihnen davon erzählen. Hier ist der Code:
public SyntaxToken GenerateUniqueName(SemanticModel semanticModel,
SyntaxNode location,
SyntaxNode containerOpt,
string baseName,
CancellationToken cancellationToken)
{
return GenerateUniqueName(semanticModel,
location,
containerOpt,
baseName,
filter: null,
usedNames: null, // <=
cancellationToken);
}
V3156 Das sechste Argument der Methode 'GenerateUniqueName' wird als Argument an die Methode 'Concat' übergeben und es wird nicht erwartet, dass es null ist. Möglicher Nullwert: null. AbstractSemanticFactsService.cs 24
Ich bin ehrlich: Bei dieser Diagnose habe ich nicht wirklich positive Ergebnisse auf der geraden Null erwartet . Schließlich ist es ziemlich seltsam, null an eine Methode zu senden , die aus diesem Grund eine Ausnahme auslöst. Ich habe zwar Orte gesehen, an denen dies gerechtfertigt war (zum Beispiel mit der Expression- Klasse ), aber jetzt geht es nicht darum.
Daher war ich sehr fasziniert, als ich diese Warnung sah. Mal sehen, was in der GenerateUniqueName- Methode passiert .
public SyntaxToken GenerateUniqueName(SemanticModel semanticModel,
SyntaxNode location,
SyntaxNode containerOpt,
string baseName,
Func<ISymbol, bool> filter,
IEnumerable<string> usedNames,
CancellationToken cancellationToken)
{
var container = containerOpt ?? location
.AncestorsAndSelf()
.FirstOrDefault(a => SyntaxFacts.IsExecutableBlock(a)
|| SyntaxFacts.IsMethodBody(a));
var candidates = GetCollidableSymbols(semanticModel,
location,
container,
cancellationToken);
var filteredCandidates = filter != null ? candidates.Where(filter)
: candidates;
return GenerateUniqueName(baseName,
filteredCandidates.Select(s => s.Name)
.Concat(usedNames)); // <=
}
Wir sehen, dass es nur einen Ausgang von der Methode gibt, keine Ausnahmen ausgelöst werden und kein goto . Mit anderen Worten, nichts hindert Sie daran, usedNames an die Concat- Methode zu übergeben und eine ArgumentNullException abzurufen .
Aber das sind alles Worte, lass es uns tun. Suchen Sie dazu, wo Sie diese Methode aufrufen können. Die Methode selbst befindet sich in der AbstractSemanticFactsService- Klasse . Die Klasse ist abstrakt. Nehmen wir der Einfachheit halber die CSharpSemanticFactsService- Klasse , die von ihr erbt. In der Datei dieser Klasse erstellen wir unsere eigene, die die GenerateUniqueName- Methode aufruft . Es sieht aus wie das:
public class DropRoslyn
{
private const string ProgramText =
@"using System;
using System.Collections.Generic;
using System.Text
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(""Hello, World!"");
}
}
}";
public void Drop()
{
var tree = CSharpSyntaxTree.ParseText(ProgramText);
var instance = CSharpSemanticFactsService.Instance;
var compilation = CSharpCompilation
.Create("Hello World")
.AddReferences(MetadataReference
.CreateFromFile(typeof(string)
.Assembly
.Location))
.AddSyntaxTrees(tree);
var semanticModel = compilation.GetSemanticModel(tree);
var syntaxNode1 = tree.GetRoot();
var syntaxNode2 = tree.GetRoot();
var baseName = "baseName";
var cancellationToken = new CancellationToken();
instance.GenerateUniqueName(semanticModel,
syntaxNode1,
syntaxNode2,
baseName,
cancellationToken);
}
}
Jetzt sammeln wir Roslyn, erstellen eine einfache Konsolenanwendung, verbinden alle erforderlichen DLL-Dateien und schreiben den folgenden Code:
class Program
{
static void Main(string[] args)
{
DropRoslyn dropRoslyn = new DropRoslyn();
dropRoslyn.Drop();
}
}
Wir starten die Anwendung und erhalten Folgendes:

Das ist irreführend
Nehmen wir an, wir stimmen dem nullbaren Konzept zu. Es stellt sich heraus, dass wenn wir einen NR-Typ sehen, wir glauben, dass er eine potenzielle Null enthalten kann . Manchmal können Sie jedoch Situationen sehen, in denen der Compiler uns etwas anderes mitteilt. Daher werden hier einige Fälle betrachtet, in denen die Verwendung dieses Konzepts nicht intuitiv ist.
Fall 1
internal override IEnumerable<SyntaxToken>? TryGetActiveTokens(SyntaxNode node)
{
....
var bodyTokens = SyntaxUtilities
.TryGetMethodDeclarationBody(node)
?.DescendantTokens();
if (node.IsKind(SyntaxKind.ConstructorDeclaration,
out ConstructorDeclarationSyntax? ctor))
{
if (ctor.Initializer != null)
{
bodyTokens = ctor.Initializer
.DescendantTokens()
.Concat(bodyTokens); // <=
}
}
return bodyTokens;
}
V3156 Es wird nicht erwartet, dass das erste Argument der 'Concat'-Methode null ist. Möglicher Nullwert: bodyTokens. CSharpEditAndContinueAnalyzer.cs 219 Schauen wir
uns an, warum bodyTokens potenziell null ist, und sehen wir den bedingten Nulloperator :
var bodyTokens = SyntaxUtilities
.TryGetMethodDeclarationBody(node)
?.DescendantTokens(); // <=
Wenn wir in die TryGetMethodDeclarationBody- Methode gehen , werden wir sehen, dass sie null zurückgeben kann . Es ist jedoch relativ groß, so dass ich einen Link dazu hinterlasse , wenn Sie sich selbst davon überzeugen möchten. Mit bodyTokens ist alles klar, aber ich möchte auf das Argument ctor aufmerksam machen :
if (node.IsKind(SyntaxKind.ConstructorDeclaration,
out ConstructorDeclarationSyntax? ctor))
Wie wir sehen können, ist sein Typ auf NR gesetzt. In diesem Fall erfolgt die Dereferenzierung mit der folgenden Zeile:
if (ctor.Initializer != null)
Diese Kombination ist etwas alarmierend. Allerdings könnte man sagen , dass, wahrscheinlich, wenn IsKind kehrt wahr , dann Ctor ist definitiv nicht null . Wie es ist:
public static bool IsKind<TNode>(
[NotNullWhen(returnValue: true)] this SyntaxNode? node, // <=
SyntaxKind kind,
[NotNullWhen(returnValue: true)] out TNode? result) // <=
where TNode : SyntaxNode
{
if (node.IsKind(kind))
{
result = (TNode)node;
return true;
}
result = null;
return false;
}
Hier werden spezielle Attribute verwendet, die angeben, bei welchem Ausgabewert die Parameter nicht null sind . Wir können dies überprüfen, indem wir uns die Logik der IsKind- Methode ansehen . Es stellt sich heraus, dass innerhalb der Bedingung der Typ des Ctors NNR sein muss. Der Compiler versteht dies und sagt, dass der ctor innerhalb der Bedingung nicht null sein wird . Um dies für uns zu verstehen, müssen wir jedoch zur IsKind- Methode gehen und dort das Attribut beachten. Andernfalls sieht es so aus, als würde eine NR-Variable dereferenziert, ohne nach Null zu suchen . Sie können versuchen, Klarheit wie folgt zu schaffen:
if (node.IsKind(SyntaxKind.ConstructorDeclaration,
out ConstructorDeclarationSyntax? ctor))
{
if (ctor!.Initializer != null) // <=
{
....
}
}
Fall 2
public TextSpan GetReferenceEditSpan(InlineRenameLocation location,
string triggerText,
CancellationToken cancellationToken)
{
var searchName = this.RenameSymbol.Name;
if (_isRenamingAttributePrefix)
{
searchName = GetWithoutAttributeSuffix(this.RenameSymbol.Name);
}
var index = triggerText.LastIndexOf(searchName, // <=
StringComparison.Ordinal);
....
}
V3156 Es wird nicht erwartet, dass das erste Argument der 'LastIndexOf'-Methode null ist. Möglicher Nullwert: searchName. AbstractEditorInlineRenameService.SymbolRenameInfo.cs 126
Wir interessieren uns für die Variable searchName . Nach dem Aufrufen der GetWithoutAttributeSuffix- Methode kann null darauf geschrieben werden , aber es ist nicht so einfach. Mal sehen, was darin passiert:
private string GetWithoutAttributeSuffix(string value)
=> value.GetWithoutAttributeSuffix(isCaseSensitive:
_document.GetRequiredLanguageService<ISyntaxFactsService>()
.IsCaseSensitive)!;
Gehen wir tiefer:
internal static string? GetWithoutAttributeSuffix(
this string name,
bool isCaseSensitive)
{
return TryGetWithoutAttributeSuffix(name, isCaseSensitive, out var result)
? result : null;
}
Es stellt sich heraus, dass die TryGetWithoutAttributeSuffix- Methode entweder result oder null zurückgibt . Und die Methode gibt einen NR-Typ zurück. Wenn wir jedoch einen Schritt zurückgehen, stellen wir fest, dass sich der Typ der Methode plötzlich in NNR geändert hat. Dies geschieht aufgrund des versteckten Zeichens "!":
_document.GetRequiredLanguageService<ISyntaxFactsService>()
.IsCaseSensitive)!; // <=
Übrigens ist es in Visual Studio ziemlich schwierig, es zu bemerken:

Durch die Bereitstellung teilt uns der Entwickler mit, dass die Methode niemals null zurückgeben wird . Wenn ich mir die vorherigen Beispiele anschaue und auf die TryGetWithoutAttributeSuffix- Methode eingehe , kann ich mir persönlich nicht sicher sein:
internal static bool TryGetWithoutAttributeSuffix(
this string name,
bool isCaseSensitive,
[NotNullWhen(returnValue: true)] out string? result)
{
if (name.HasAttributeSuffix(isCaseSensitive))
{
result = name.Substring(0, name.Length - AttributeSuffix.Length);
return true;
}
result = null;
return false;
}
Ausgabe
Abschließend möchte ich sagen, dass der Versuch, uns die unnötigen Nullprüfungen zu ersparen, eine großartige Idee ist. NR-Typen sind jedoch eher beratender Natur, da uns niemand strengstens verbietet, Null an einen NNR-Typ zu übergeben. Deshalb bleiben die entsprechenden PVS-Studio-Regeln relevant. Zum Beispiel wie V3080 oder V3156 .
Alles Gute und vielen Dank für Ihre Aufmerksamkeit.

Wenn Sie diesen Artikel einem englischsprachigen Publikum zugänglich machen möchten, verwenden Sie bitte den Übersetzungslink: Nikolay Mironov. Nullable Reference schützt Sie nicht, und hier ist der Beweis .