
هل سبق لك أن أردت التخلص من مشكلة الإحالة المرجعية الفارغة؟ إذا كان الأمر كذلك ، فإن استخدام أنواع مرجع Nullable ليس اختيارك. أتساءل لماذا؟ هذا ما سيتم مناقشته اليوم.
لقد حذرنا وحدث ذلك. منذ حوالي عام ، كتب زملائي مقالًا يحذرون فيه من أن إدخال أنواع مرجعية Nullable لن يحمي من إلغاء الإسناد المرجعي الفارغ. لدينا الآن تأكيد حقيقي لكلماتنا ، والذي وجد في أعماق روزلين.
أنواع مرجعية لاغية
فكرة إضافة قيم الفارغة المرجعي (من الآن فصاعدا - NR) أنواع يبدو مثيرة للاهتمام بالنسبة لي، لأن المشكلة المرتبطة dereferencing المراجع باطلة هي ذات الصلة لهذا اليوم. تنفيذ الحماية ضد إلغاء المرجع غير موثوق به للغاية. كما هو مخطط من قبل المبدعين ، يفترضون أن القيمة null يمكن فقط للمتغيرات التي تم تمييز نوعها بعلامة "؟" على سبيل المثال ، متغير من نوع السلسلة؟ يقول أنه يمكن أن يحتوي على قيمة فارغة ، من نوع سلسلة - على العكس من ذلك.
ومع ذلك ، لا أحد يمنعنا من تمرير متغيرات مرجعية خالية إلى غير قابلة للإلغاء على أي حال.(يشار إليها فيما بعد - NNR) ، لأنها لا تنفذ على مستوى كود IL. المحلل الثابت المدمج في المترجم مسؤول عن هذا القيد. لذلك ، هذا الابتكار هو إلى حد ما استشاري بطبيعته. إليك مثال بسيط لإظهار كيفية عمله:
#nullable enable
object? nullable = null;
object nonNullable = nullable;
var deref = nonNullable.ToString();
وكما نرى، ونوع من nonNullable يتم تحديد كما NNR، لكننا يمكن أن تمر بسلام لاغية هناك . بالطبع ، سوف نتلقى تحذيرًا حول تحويل "تحويل قيمة خالية فارغة أو قيمة خالية محتملة إلى نوع غير قابل للقيمة الفارغة." ومع ذلك ، يمكن التحايل على هذا بإضافة القليل من العدوانية:
#nullable enable
object? nullable = null;
object nonNullable = nullable!; // <=
var deref = nonNullable.ToString();
علامة تعجب واحدة ولا توجد تحذيرات. إذا كان أحدكم ذواقة ، فهناك خيار آخر متاح:
#nullable enable
object nonNullable = null!;
var deref = nonNullable.ToString();
حسنًا ، مثال آخر. لنقم بإنشاء مشروعين بسيطين لوحدة التحكم. في البداية نكتب:
namespace NullableTests
{
public static class Tester
{
public static string RetNull() => null;
}
}
في الثاني نكتب:
#nullable enable
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
string? nullOrNotNull = NullableTests.Tester.RetNull();
System.Console.WriteLine(nullOrNotNull.Length);
}
}
}
مرر مؤشر الماوس فوق nullOrNotNull وشاهد الرسالة التالية:

قيل لنا أن السلسلة لا يمكن أن تكون خالية هنا . ومع ذلك ، فإننا نفهم أنه سيكون فارغًا هنا . نبدأ المشروع ونحصل على استثناء:

بالطبع ، هذه مجرد أمثلة تركيبية ، والغرض منها هو إظهار أن هذه المقدمة لا تضمن لك الحماية من إلغاء الإسناد المرجعي الفارغ. إذا كنت تعتقد أن المواد التركيبية مملة ، وحيث توجد أمثلة حقيقية على الإطلاق ، فأنا أطلب منك ألا تقلق ، فسيكون كل هذا.
أنواع NR لديها مشكلة أخرى - ليس من الواضح ما إذا كانت مدرجة أم لا. على سبيل المثال ، الحل له مشروعان. أحدهما تم ترميزه بهذه الصيغة ، والآخر ليس كذلك. بعد الدخول في مشروع مع أنواع NR ، يمكنك أن تقرر أنه بمجرد وضع علامة على واحد فقط ، يتم وضع علامة على الكل. ومع ذلك ، لن يكون هذا هو الحال. اتضح أنك بحاجة إلى البحث في كل مرة عما إذا كان السياق nullable مضمنًا في المشروع أو الملف. خلاف ذلك ، قد تعتقد عن طريق الخطأ أن نوع المرجع العادي هو NNR.
كيف تم العثور على الدليل
عند تطوير تشخيصات جديدة في محلل PVS-Studio ، فإننا نختبرها دائمًا على أساس مشاريعنا الحقيقية. يساعد في جوانب مختلفة. على سبيل المثال:
- مشاهدة "مباشر" عند جودة التحذيرات المستلمة ؛
- تخلص من بعض الإيجابيات الخاطئة ؛
- العثور على نقاط مثيرة للاهتمام في الكود ، والتي يمكنك التحدث عنها بعد ذلك ؛
- إلخ
وجد أحد التشخيصات الجديدة V3156 أماكن قد يتم فيها طرح استثناءات بسبب احتمال وجود قيمة خالية . صياغة القاعدة التشخيصية هي: "لا يُتوقع أن تكون وسيطة الطريقة فارغة". جوهرها هو أن الطريقة لا تتوقع قيمة خالية ، يمكن تمرير القيمة كوسيطة للصفر . يمكن أن يؤدي هذا ، على سبيل المثال ، إلى استثناء أو تنفيذ غير صحيح للطريقة التي تم استدعاؤها. يمكنك قراءة المزيد حول قاعدة التشخيص هذه هنا .
البراهين هنا
لذلك وصلنا إلى الجزء الرئيسي من هذه المقالة. هنا سترى أجزاء من التعليمات البرمجية الحقيقية من مشروع روزلين ، والتي أصدرت التشخيصات تحذيرات بشأنها. المعنى الرئيسي لها هو أنه إما أن نوع NNR يتم تمريره فارغًا ، أو لا يوجد فحص لقيمة نوع NR. كل هذا يمكن أن يؤدي إلى طرح استثناء.
مثال 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 لا يتوقع أن تكون الوسيطة الأولى للأسلوب "ContainsKey" فارغة. القيمة الفارغة المحتملة: مفتاح. SwitchBinder.cs 121 تشير
الرسالة إلى أن المفتاح خالي محتمل . دعونا نرى أين يمكن لهذا المتغير الحصول على هذه القيمة. دعنا نتحقق من طريقة KeyForConstant أولاً :
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();
منذ s_nullKey هو ليس باطلا ، دعونا نرى ما constantValue.Value العوائد :
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);
}
}
}
هناك نوعان من القيم الحرفية الفارغة هنا ، ولكن في هذه الحالة لن ندخل في أي حالة معهم. يرجع ذلك إلى عمليات التحقق من IsBad و IsNull . ومع ذلك ، أود أن ألفت انتباهكم إلى نوع الإرجاع لهذا العقار. إنه نوع NR ، لكن طريقة KeyForConstant ترجع بالفعل نوع NNR. اتضح ، بشكل عام ، أن طريقة KeyForConstant يمكن أن ترجع قيمة خالية . مصدر آخر يمكنه إرجاع قيمة فارغة هو طريقة AsNode :
public SyntaxNode? AsNode()
{
if (_token != null)
{
return null;
}
return _nodeOrParent;
}
مرة أخرى ، يرجى الانتباه إلى نوع الإرجاع للطريقة - إنه نوع NR. اتضح أنه عندما نقول أنه يمكن إرجاع قيمة null من الطريقة ، فإن هذا لا يؤثر على أي شيء. الشيء المثير للاهتمام هو أن المترجم لا يقسم بالتحويل من NR إلى NNR هنا:

مثال 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 لا يتوقع أن تكون الوسيطة الأولى للأسلوب "إضافة" خالية. القيمة الفارغة المحتملة: العقدة القديمة. SyntaxAnnotationTests.cs 439
مثال آخر مع وظيفة AsNode الموضحة أعلاه. هذه المرة فقط ستكون العقدة القديمة من النوع NR. في حين أن المفتاح أعلاه كان من النوع NNR.
بالمناسبة ، لا يسعني إلا أن أشارككم ملاحظة مثيرة للاهتمام. كما وصفت أعلاه ، عند تطوير التشخيص ، نقوم باختباره في مشاريع مختلفة. عند التحقق من إيجابيات هذه القاعدة ، لوحظت لحظة غريبة. تم إصدار حوالي 70٪ من جميع التحذيرات لأساليب فئة القاموس . علاوة على ذلك ، وقع معظمهم على طريقة TryGetValue... ربما يرجع هذا إلى حقيقة أننا لا نتوقع ، دون وعي ، استثناءات من طريقة تحتوي على كلمة try . لذا تحقق من الكود الخاص بك لهذا النمط لمعرفة ما إذا وجدت شيئًا مشابهًا.
مثال 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 يتم تمرير الوسيطة الأولى للأسلوب "Add" كوسيطة إلى أسلوب "TryGetValue" ولا يُتوقع أن تكون خالية. القيمة الفارغة المحتملة: typeName. SymbolTreeInfo_Serialization.cs 255
يقول المحلل أن المشكلة تكمن في typeName . لنتأكد أولاً من أن هذه الوسيطة هي بالفعل قيمة خالية . نحن ننظر في ReadString :
public string ReadString() => ReadStringValue();
لذلك ، انظر إلى ReadStringValue :
private string ReadStringValue()
{
var kind = (EncodingKind)_reader.ReadByte();
return kind == EncodingKind.Null ? null : ReadStringValue(kind);
}
رائع ، لنقم الآن بتحديث ذاكرتنا من خلال النظر إلى المكان الذي تم فيه تمرير المتغير الخاص بنا:
simpleTypeNameToExtensionMethodMap.Add(typeName, // <=
new ExtensionMethodInfo(containerName,
name));
أعتقد أن الوقت قد حان للدخول في طريقة الإضافة :
public bool Add(K k, V v)
{
ValueSet updated;
if (_dictionary.TryGetValue(k, out ValueSet set)) // <=
{
....
}
....
}
في الواقع ، إذا تم تمرير القيمة null إلى التابع Add باعتباره الوسيط الأول ، فسنحصل على ArgumentNullException . بالمناسبة ، من المثير للاهتمام أنه إذا حركنا المؤشر فوق typeName في Visual Studio ، فسنرى أن نوعه عبارة عن سلسلة؟ :

في هذه الحالة ، يكون نوع الإرجاع للطريقة عبارة عن سلسلة فقط :

في هذه الحالة ، إذا قمت بإنشاء متغير من النوع NNR وقمت بتعيينه typeName ، فلن يتم عرض أي خطأ.
دعنا نحاول إسقاط روزلين
ليس من أجل الحقد ، ولكن من أجل المتعة ، أقترح محاولة إعادة إنتاج أحد الأمثلة المعروضة.

اختبار 1
لنأخذ المثال الموضح تحت الرقم 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));
}
....
}
لإعادة إنتاجه ، تحتاج إلى استدعاء طريقة TryReadSymbolTreeInfo ، لكنها خاصة . من الجيد أن يكون للفصل الذي يحتوي عليه طريقة ReadSymbolTreeInfo_ForTestingPurposesOnly ، وهي طريقة داخلية بالفعل :
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)))));
}
إنه لمن دواعي سرورنا أن يُعرض علينا مباشرة اختبار طريقة TryReadSymbolTreeInfo . لذلك ، دعنا ننشئ فصلنا جنبًا إلى جنب ونكتب الكود التالي:
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);
}
}
الآن نقوم بتجميع Roslyn ، وإنشاء تطبيق وحدة تحكم بسيط ، وربط جميع ملفات dll الضرورية وكتابة الكود التالي:
static void Main(string[] args)
{
CheckNNR.Start();
}
ننطلق ونصل إلى المكان المطلوب ونرى:

بعد ذلك ، انتقل إلى طريقة Add واحصل على الاستثناء المتوقع:

اسمحوا لي أن أذكركم بأن ReadString طريقة إرجاع نوع NNR، والتي، حسب التصميم، لا يمكن أن تحتوي على باطل . يؤكد هذا المثال مرة أخرى أهمية قواعد التشخيص الخاصة بـ PVS-Studio للبحث عن إحالة المراجع الفارغة.
اختبار 2
حسنًا ، نظرًا لأننا بدأنا بالفعل في إعادة إنتاج الأمثلة ، فلماذا لا نعيد إنتاج أمثلة أخرى. لن يرتبط هذا المثال بأنواع NR. ومع ذلك ، تم العثور عليه بواسطة نفس تشخيصات V3156 ، وأردت أن أخبرك عنها. ها هو الكود:
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 تم تمرير الوسيطة السادسة للأسلوب "GenerateUniqueName" كوسيطة إلى الأسلوب "Concat" ولا يُتوقع أن تكون خالية. القيمة الفارغة المحتملة: خالية. AbstractSemanticFactsService.cs 24
سأكون صريحًا: عند إجراء هذه التشخيصات ، لم أكن أتوقع حقًا أي إيجابيات على الخط المستقيم فارغة . بعد كل شيء ، من الغريب إرسال قيمة فارغة إلى طريقة ستؤدي إلى استثناء بسبب هذا. على الرغم من أنني رأيت أماكن تم فيها تبرير ذلك (على سبيل المثال ، مع فئة Expression ) ، لكن الآن لا يتعلق الأمر بذلك.
لذلك ، كنت مفتونًا جدًا عندما رأيت هذا التحذير. دعونا نرى ما يحدث في طريقة GenerateUniqueName .
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)); // <=
}
ونحن نرى أن هناك مخرج واحد فقط من الأسلوب، يتم طرح أية استثناءات، و لا اذهب اليه . بمعنى آخر ، لا شيء يمنعك من تمرير الأسماء المستخدمة إلى طريقة Concat والحصول على ArgumentNullException .
لكن هذه كلها كلمات ، لنفعلها. للقيام بذلك ، انظر حيث يمكنك استدعاء هذه الطريقة. الطريقة نفسها موجودة في فئة AbstractSemanticFactsService . الفئة مجردة ، لذا للراحة ، دعنا نأخذ فئة CSharpSemanticFactsService ، التي ترث منها. في ملف هذا الفصل ، سننشئ ملفًا خاصًا بنا ، والذي سيستدعي طريقة GenerateUniqueName . تبدو هكذا:
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);
}
}
الآن نقوم بتجميع Roslyn ، وإنشاء تطبيق وحدة تحكم بسيط ، وربط جميع ملفات dll الضرورية وكتابة الكود التالي:
class Program
{
static void Main(string[] args)
{
DropRoslyn dropRoslyn = new DropRoslyn();
dropRoslyn.Drop();
}
}
نطلق التطبيق ونحصل على ما يلي:

هذا مضلل
لنفترض أننا نتفق مع مفهوم nullable. اتضح أنه إذا رأينا نوع NR ، فإننا نعتقد أنه يمكن أن يحتوي على قيمة خالية محتملة . ومع ذلك ، في بعض الأحيان يمكنك رؤية المواقف عندما يخبرنا المترجم بخلاف ذلك. لذلك ، سننظر هنا في بعض الحالات التي لا يكون فيها استخدام هذا المفهوم بديهيًا.
حالة 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 لا يُتوقع أن تكون الوسيطة الأولى لطريقة "Concat" خالية. القيمة الفارغة المحتملة: bodyTokens. CSharpEditAndContinueAnalyzer.cs 219 دعنا نلقي
نظرة على سبب احتمال أن يكون bodyTokens فارغًا ونرى العامل الشرطي الفارغ :
var bodyTokens = SyntaxUtilities
.TryGetMethodDeclarationBody(node)
?.DescendantTokens(); // <=
إذا انتقلنا إلى طريقة TryGetMethodDecrophoneBody ، فسنرى أنه يمكن إرجاع قيمة خالية . ومع ذلك ، فهي كبيرة نسبيًا ، لذا أترك رابطًا لها إذا كنت تريد أن ترى بنفسك. مع bodyTokens ، كل شيء واضح ، لكني أريد أن ألفت الانتباه إلى حجة ctor :
if (node.IsKind(SyntaxKind.ConstructorDeclaration,
out ConstructorDeclarationSyntax? ctor))
كما نرى ، تم ضبط نوعه على NR. في هذه الحالة ، يحدث إلغاء الإسناد مع السطر أدناه:
if (ctor.Initializer != null)
هذا المزيج مثير للقلق بعض الشيء. ومع ذلك ، ستقول أنه ، على الأرجح ، إذا عاد IsKind صحيحًا ، فإن ctor بالتأكيد ليس فارغًا . على ما هو عليه:
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;
}
هنا ، يتم استخدام السمات الخاصة التي تشير إلى قيمة المخرجات التي لن تكون فيها المعلمات خالية . يمكننا أن نقتنع بهذا من خلال النظر إلى منطق طريقة IsKind . اتضح أنه داخل الشرط ، يجب أن يكون نوع المُنشئ NNR. يفهم المترجم هذا ويقول أن المُنشئ داخل الشرط لن يكون فارغًا . ومع ذلك ، من أجل فهم هذا بالنسبة لنا ، يجب أن نذهب إلى طريقة IsKind ونلاحظ السمة هناك. بخلاف ذلك ، يبدو أنه تم إلغاء الإشارة إلى متغير NR دون التحقق من وجود قيمة خالية . يمكنك محاولة إضافة بعض الوضوح مثل هذا:
if (node.IsKind(SyntaxKind.ConstructorDeclaration,
out ConstructorDeclarationSyntax? ctor))
{
if (ctor!.Initializer != null) // <=
{
....
}
}
الحالة 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 لا يُتوقع أن تكون الوسيطة الأولى لطريقة "LastIndexOf" خالية. القيمة الفارغة المحتملة: searchName. AbstractEditorInlineRenameService.SymbolRenameInfo.cs 126
ونحن مهتمون في searchName متغير . يمكن كتابة null إليه بعد استدعاء طريقة GetWithoutAttributeSuffix ، لكن الأمر ليس بهذه البساطة. دعونا نرى ما يحدث فيها:
private string GetWithoutAttributeSuffix(string value)
=> value.GetWithoutAttributeSuffix(isCaseSensitive:
_document.GetRequiredLanguageService<ISyntaxFactsService>()
.IsCaseSensitive)!;
دعنا نتعمق أكثر:
internal static string? GetWithoutAttributeSuffix(
this string name,
bool isCaseSensitive)
{
return TryGetWithoutAttributeSuffix(name, isCaseSensitive, out var result)
? result : null;
}
اتضح أن طريقة TryGetWithoutAttributeSuffix سترجع إما نتيجة أو خالية . وتقوم الطريقة بإرجاع نوع NR. ومع ذلك ، بالعودة إلى الوراء ، نلاحظ أن نوع الطريقة تغير فجأة إلى NNR. يحدث هذا بسبب العلامة المخفية "!":
_document.GetRequiredLanguageService<ISyntaxFactsService>()
.IsCaseSensitive)!; // <=
بالمناسبة ، من الصعب جدًا ملاحظة ذلك في Visual Studio:

من خلال توفيره ، يخبرنا المطور أن الطريقة لن تعيد القيمة فارغة أبدًا . على الرغم من النظر إلى الأمثلة السابقة والذهاب إلى طريقة TryGetWithoutAttributeSuffix ، إلا أنني شخصياً لا يمكنني التأكد من ذلك:
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;
}
انتاج |
أخيرًا ، أود أن أقول إن محاولة إنقاذنا من عمليات التحقق الفارغة غير الضرورية فكرة رائعة. ومع ذلك ، فإن أنواع NR هي استشارية إلى حد ما بطبيعتها ، لأنه لا أحد يمنعنا بشدة من تمرير قيمة خالية إلى نوع NNR. هذا هو السبب في أن قواعد PVS-Studio المقابلة تظل ذات صلة. على سبيل المثال ، مثل V3080 أو V3156 .
كل التوفيق وشكرا لاهتمامكم.

إذا كنت ترغب في مشاركة هذه المقالة مع جمهور يتحدث الإنجليزية ، فيرجى استخدام رابط الترجمة: نيكولاي ميرونوف. لن يحميك المرجع Nullable ، وهذا هو الدليل .