DZone>Web Dev Zone>.NET 6资源中的错误和可疑的代码片段
.NET 6资源中的错误和可疑的代码片段
.NET 6是一个备受期待的重要版本。深入了解一下,看看我们能在.NET库的源代码中发现哪些有趣的东西。
-
Jan. 04, 22 - Web Dev Zone -分析
喜欢 (3)
评论
保存
鸣叫
7.26K浏览次数
加入DZone社区,获得完整的会员体验。
.NET 6变成了一个期待已久的重要版本。如果你为.NET写作,你很难错过这样一个事件。我们也不能错过这个平台的新版本,并决定检查一下我们能在.NET库的来源中找到什么有趣的东西。
关于检查的细节
我从GitHub上的.NET 6发布分支中提取了源代码。这篇文章只涉及库中的可疑之处(位于src/libraries中的那些)。我没有分析运行时本身(也许下一次)。
我用PVS-Studio静态分析器检查了代码。正如你从这篇文章中可能猜到的,PVS-Studio 7.16支持对.NET 6的项目进行分析。用于Linux和macOS的PVS-Studio C#分析器现在也可以在.NET 6上工作。
在过去的一年中,PVS-Studio大大扩展了C#分析器的功能。除了对.NET 6平台的支持,我们还增加了Visual Studio 2022的插件和新的安全诊断程序。此外,我们还优化了C#分析器对大型项目的性能。
但你是来这里阅读.NET 6的,不是吗?让我们不要浪费时间了。
可疑的代码片段
杂项
本节包括各种有趣的代码片段,我无法将其归为一个共同的类别。
问题1
让我们从简单的东西开始。
C#
public enum CompressionLevel
{
Optimal,
Fastest,
NoCompression,
SmallestSize
}
internal static void GetZipCompressionMethodFromOpcCompressionOption(
CompressionOption compressionOption,
out CompressionLevel compressionLevel)
{
switch (compressionOption)
{
case CompressionOption.NotCompressed:
{
compressionLevel = CompressionLevel.NoCompression;
}
break;
case CompressionOption.Normal:
{
compressionLevel = CompressionLevel.Optimal; // <=
}
break;
case CompressionOption.Maximum:
{
compressionLevel = CompressionLevel.Optimal; // <=
}
break;
case CompressionOption.Fast:
{
compressionLevel = CompressionLevel.Fastest;
}
break;
case CompressionOption.SuperFast:
{
compressionLevel = CompressionLevel.Fastest;
}
break;
// fall-through is not allowed
default:
{
Debug.Fail("Encountered an invalid CompressionOption enum value");
goto case CompressionOption.NotCompressed;
}
}
}
PVS-Studio警告。V3139两个或多个案例分支执行相同的动作。ZipPackage.cs 402
事实上,这个方法执行了从CompressionOption到CompressionLevel的映射。这里可疑的是,CompressionOption.Normal和CompressionOption.Maximum值被映射到CompressionLevel.Optimal值。
CompressionOption.Maximum 可能应该与CompressionLevel.SmallestSize匹配。
问题2
现在我们来练习一下。让我们拿System.Text.Json.Nodes.JsonObject做实验。如果你愿意,你可以使用.NET 6 SDK的发布版本来重复上述操作。
JsonObject类型有2个构造函数:一个构造函数只接受选项,另一个则同时接受属性和选项。很明显,我们应该从它们那里期待什么样的行为。文档在这里可以找到。
让我们创建两个JsonObject类型的实例并使用每个构造函数。
C#
static void JsonObject_Test()
{
var properties = new Dictionary<String, JsonNode?>();
var options = new JsonNodeOptions()
{
PropertyNameCaseInsensitive = true
};
var jsonObject1 = new JsonObject(options);
var jsonObject2 = new JsonObject(properties, options);
}
现在让我们检查一下我们创建的对象的状态。
jsonObject1的状态是预期的,但jsonObject2的对象状态不是。为什么在*_options字段中写的是空值*?这有点令人困惑。让我们打开源代码,看看这些构造函数。
C#
public sealed partial class JsonObject : JsonNode
{
....
public JsonObject(JsonNodeOptions? options = null) : base(options) { }
public JsonObject(IEnumerable<KeyValuePair<string, JsonNode?>> properties,
JsonNodeOptions? options = null)
{
foreach (KeyValuePair<string, JsonNode?> node in properties)
{
Add(node.Key, node.Value);
}
}
....
}
在第二个构造函数中,options参数被简单地放弃了:它没有被传递到任何地方,也没有以任何方式使用。而在第一个构造函数中,选项被传递给基类构造函数,在那里它们被写入字段。
C#
internal JsonNode(JsonNodeOptions? options = null)
{
_options = options;
}
相应的PVS-Studio警告。V3117构造函数参数 "options "未被使用。JsonObject.cs 35
问题3
如果我们谈及被遗忘的参数,还有一个有趣的片段。
C#
public class ServiceNameCollection : ReadOnlyCollectionBase
{
....
private ServiceNameCollection(IList list, string serviceName)
: this(list, additionalCapacity: 1)
{ .... }
private ServiceNameCollection(IList list, IEnumerable serviceNames)
: this(list, additionalCapacity: GetCountOrOne(serviceNames))
{ .... }
private ServiceNameCollection(IList list, int additionalCapacity)
{
Debug.Assert(list != null);
Debug.Assert(additionalCapacity >= 0);
foreach (string? item in list)
{
InnerList.Add(item);
}
}
....
}
PVS-Studio警告。V3117构造函数参数 "extraCapacity "未被使用。ServiceNameCollection.cs 46
根据代码,最后一个构造函数的additionalCapacity参数在Debug.Assert中被检查,而没有用于其他方面。这看起来很可疑。这特别有趣:其他构造函数为additionalCapacity参数传递一些值。
问题4
这里是对预知能力的测试(哎呀,搅局者警告)。研究一下下面的代码,试着猜测一下是什么触发了分析器。
C#
public override void CheckErrors()
{
throw new XsltException(SR.Xslt_InvalidXPath,
new string[] { Expression },
_baseUri,
_linePosition,
_lineNumber,
null);
}
似乎只是抛出了一个异常。要了解这里出了什么问题,你需要看看XsltException构造函数。
C#
internal XsltException(string res,
string?[] args,
string? sourceUri,
int lineNumber,
int linePosition,
Exception? inner) : base(....)
{ .... }
如果你比较一下参数的顺序,就会明白是什么触发了分析器。看起来是行的位置和行号调换了位置。
参数的顺序。
- _linePosition
- 行号:_lineNumber
参数的顺序。
- 行号
- 线条位置
PVS-Studio警告。V3066传递给 "XsltException "构造函数的参数的顺序可能不正确。"_linePosition "和"_lineNumber"。编译器.cs 1187
问题5
这里有一段足够大的代码。其中一定隐藏着某种错字。你愿意尝试找到它吗?
C#
public Parser(Compilation compilation,
in JsonSourceGenerationContext sourceGenerationContext)
{
_compilation = compilation;
_sourceGenerationContext = sourceGenerationContext;
_metadataLoadContext = new MetadataLoadContextInternal(_compilation);
_ilistOfTType = _metadataLoadContext.Resolve(
SpecialType.System_Collections_Generic_IList_T);
_icollectionOfTType = _metadataLoadContext.Resolve(
SpecialType.System_Collections_Generic_ICollection_T);
_ienumerableOfTType = _metadataLoadContext.Resolve(
SpecialType.System_Collections_Generic_IEnumerable_T);
_ienumerableType = _metadataLoadContext.Resolve(
SpecialType.System_Collections_IEnumerable);
_listOfTType = _metadataLoadContext.Resolve(typeof(List<>));
_dictionaryType = _metadataLoadContext.Resolve(typeof(Dictionary<,>));
_idictionaryOfTKeyTValueType = _metadataLoadContext.Resolve(
typeof(IDictionary<,>));
_ireadonlyDictionaryType = _metadataLoadContext.Resolve(
typeof(IReadOnlyDictionary<,>));
_isetType = _metadataLoadContext.Resolve(typeof(ISet<>));
_stackOfTType = _metadataLoadContext.Resolve(typeof(Stack<>));
_queueOfTType = _metadataLoadContext.Resolve(typeof(Queue<>));
_concurrentStackType = _metadataLoadContext.Resolve(
typeof(ConcurrentStack<>));
_concurrentQueueType = _metadataLoadContext.Resolve(
typeof(ConcurrentQueue<>));
_idictionaryType = _metadataLoadContext.Resolve(typeof(IDictionary));
_ilistType = _metadataLoadContext.Resolve(typeof(IList));
_stackType = _metadataLoadContext.Resolve(typeof(Stack));
_queueType = _metadataLoadContext.Resolve(typeof(Queue));
_keyValuePair = _metadataLoadContext.Resolve(typeof(KeyValuePair<,>));
_booleanType = _metadataLoadContext.Resolve(SpecialType.System_Boolean);
_charType = _metadataLoadContext.Resolve(SpecialType.System_Char);
_dateTimeType = _metadataLoadContext.Resolve(SpecialType.System_DateTime);
_nullableOfTType = _metadataLoadContext.Resolve(
SpecialType.System_Nullable_T);
_objectType = _metadataLoadContext.Resolve(SpecialType.System_Object);
_stringType = _metadataLoadContext.Resolve(SpecialType.System_String);
_dateTimeOffsetType = _metadataLoadContext.Resolve(typeof(DateTimeOffset));
_byteArrayType = _metadataLoadContext.Resolve(
typeof(byte)).MakeArrayType();
_guidType = _metadataLoadContext.Resolve(typeof(Guid));
_uriType = _metadataLoadContext.Resolve(typeof(Uri));
_versionType = _metadataLoadContext.Resolve(typeof(Version));
_jsonArrayType = _metadataLoadContext.Resolve(JsonArrayFullName);
_jsonElementType = _metadataLoadContext.Resolve(JsonElementFullName);
_jsonNodeType = _metadataLoadContext.Resolve(JsonNodeFullName);
_jsonObjectType = _metadataLoadContext.Resolve(JsonObjectFullName);
_jsonValueType = _metadataLoadContext.Resolve(JsonValueFullName);
// Unsupported types.
_typeType = _metadataLoadContext.Resolve(typeof(Type));
_serializationInfoType = _metadataLoadContext.Resolve(
typeof(Runtime.Serialization.SerializationInfo));
_intPtrType = _metadataLoadContext.Resolve(typeof(IntPtr));
_uIntPtrType = _metadataLoadContext.Resolve(typeof(UIntPtr));
_iAsyncEnumerableGenericType = _metadataLoadContext.Resolve(
IAsyncEnumerableFullName);
_dateOnlyType = _metadataLoadContext.Resolve(DateOnlyFullName);
_timeOnlyType = _metadataLoadContext.Resolve(TimeOnlyFullName);
_jsonConverterOfTType = _metadataLoadContext.Resolve(
JsonConverterOfTFullName);
PopulateKnownTypes();
}
好吧,进展如何?或者说根本就没有错别字?
让我们先来看看分析器的警告。V3080方法返回值可能是空引用。考虑检查一下。Resolve(...)。JsonSourceGenerator.Parser.cs 203
Resolve方法可以返回null。这是该方法的签名所指示的,也是PVS-Studio在程序间分析的帮助下检测到返回null值的可能性时警告我们的内容。
C#
public Type? Resolve(Type type)
{
Debug.Assert(!type.IsArray,
"Resolution logic only capable of handling named types.");
return Resolve(type.FullName!);
}
让我们再进一步,看看Resolve的另一个重载。
C#
public Type? Resolve(string fullyQualifiedMetadataName)
{
INamedTypeSymbol? typeSymbol =
_compilation.GetBestTypeByMetadataName(fullyQualifiedMetadataName);
return typeSymbol.AsType(this);
}
请注意,typeSymbol被写成了nullable引用类型。INamedTypeSymbol?。让我们更进一步,去看看AsType方法。
C#
public static Type AsType(this ITypeSymbol typeSymbol,
MetadataLoadContextInternal metadataLoadContext)
{
if (typeSymbol == null)
{
return null;
}
return new TypeWrapper(typeSymbol, metadataLoadContext);
}
正如你所看到的,如果第一个参数是一个空引用,那么从该方法中返回空值。
现在让我们回到解析器类型的构造函数。在这种类型的构造函数中,通常,Resolve方法调用的结果被简单地写入某个字段。然而,PVS-Studio警告说有一个例外。
C#
_byteArrayType = _metadataLoadContext.Resolve(typeof(byte)).MakeArrayType();
这里,MakeArrayType实例方法被调用,以获得Resolve方法调用的结果。因此,如果Resolve返回null,将发生NullReferenceException。
问题6
C#
public abstract partial class Instrument<T> : Instrument where T : struct
{
[ThreadStatic] private KeyValuePair<string, object?>[] ts_tags;
....
}
PVS-Studio警告。V3079"ThreadStatic "属性被应用于非静态的 "ts_tags "字段,将被忽略 Instrument.netfx.cs 20
让我们引用一下文档的内容。*请注意,除了将ThreadStaticAttribute*属性应用于一个字段之外,你还必须将其定义为一个静态字段(在C#中)或一个共享字段(在Visual Basic中)。
正如你从代码中看到的,ts_tags是一个实例字段。因此,用ThreadStatic属性标记这个字段是没有意义的。要么是这样,要么就是有某种黑魔法在这里发生...
问题7
C#
private static JsonSourceGenerationOptionsAttribute?
GetSerializerOptions(AttributeSyntax? attributeSyntax)
{
....
foreach (AttributeArgumentSyntax node in attributeArguments)
{
IEnumerable<SyntaxNode> childNodes = node.ChildNodes();
NameEqualsSyntax? propertyNameNode
= childNodes.First() as NameEqualsSyntax;
Debug.Assert(propertyNameNode != null);
SyntaxNode? propertyValueNode = childNodes.ElementAtOrDefault(1);
string propertyValueStr = propertyValueNode.GetLastToken().ValueText;
....
}
....
}
PVS-Studio警告。V3146"propertyValueNode "有可能被取消引用。childNodes.ElementAtOrDefault "可以返回默认的空值。JsonSourceGenerator.Parser.cs 560
如果childNodes集合包含少于两个元素,调用ElementAtOrDefault返回default(SyntaxNode)值(即null,因为SyntaxNode是一个类)。在这种情况下,下一行会抛出一个NullReferenceException。特别奇怪的是,propertyValueNode是一个可归零的引用类型,但它*(propertyValueNode*)被解除引用而没有检查。
也许这里有一些隐含的契约,在childNodes中总是有一个以上的元素。例如,如果有propertyNameNode,那么也有propertyValueNode。在这种情况下,为了避免不必要的问题,我们可以使用ElementAt方法调用。
问题8
有这样一种结构。 Microsoft.Extensions.FileSystemGlobbing.FilePatternMatch。这个结构覆盖了*Equals(Object)*方法,这似乎符合逻辑。描述该方法的文档。
假设我们有调用这个方法的代码。
C#
static void FPM_Test(Object? obj)
{
FilePatternMatch fpm = new FilePatternMatch();
var eq = fpm.Equals(obj);
}
如果用空值调用FPM_Test,你认为会发生什么?假值会被写到eq变量中吗?嗯,差不多。
如果我们传递的参数是FilePatternMatch以外的类型的实例,也会抛出这个异常。例如...如果我们传递一个某种类型的数组。
你猜到为什么会发生这种情况了吗?重点是,在Equals方法中,参数没有以任何方式检查是否为空值或类型兼容性,而是简单地无条件地被解开。
C#
public override bool Equals(object obj)
{
return Equals((FilePatternMatch) obj);
}
PVS-Studio警告。V3115将 "null "传给 "Equals "方法不应导致 "NullReferenceException"。FilePatternMatch.cs 61
当然,从文档来看,没有人向我们承诺Equals(Object)如果不接受FilePatternMatch 就会返回false , 但这可能是最令人期待的行为。
重复检查
关于重复检查的有趣之处。你可能并不总是明确地知道:这只是多余的代码,还是应该有其他的东西来代替重复的检查之一?无论怎样,让我们看看几个例子。
问题9
C#
internal DeflateManagedStream(Stream stream,
ZipArchiveEntry.CompressionMethodValues method,
long uncompressedSize = -1)
{
if (stream == null)
throw new ArgumentNullException(nameof(stream));
if (!stream.CanRead)
throw new ArgumentException(SR.NotSupported_UnreadableStream,
nameof(stream));
if (!stream.CanRead)
throw new ArgumentException(SR.NotSupported_UnreadableStream,
nameof(stream));
Debug.Assert(method == ZipArchiveEntry.CompressionMethodValues.Deflate64);
_inflater
= new InflaterManaged(
method == ZipArchiveEntry.CompressionMethodValues.Deflate64,
uncompressedSize);
_stream = stream;
_buffer = new byte[DefaultBufferSize];
}
PVS-Studio警告。V3021有两个具有相同条件表达式的 "if "语句。第一个 "if "语句包含方法返回。这意味着第二个 "if "语句是无意义的。DeflateManagedStream.cs 27
在方法的开始,有几个检查。然而--这就是坏运气--其中一个检查(!stream.CanRead)是完全重复的(包括条件和if语句的然后分支)。
问题10
C#
public static object? Deserialize(ReadOnlySpan<char> json,
Type returnType,
JsonSerializerOptions? options = null)
{
// default/null span is treated as empty
if (returnType == null)
{
throw new ArgumentNullException(nameof(returnType));
}
if (returnType == null)
{
throw new ArgumentNullException(nameof(returnType));
}
JsonTypeInfo jsonTypeInfo = GetTypeInfo(options, returnType);
return ReadFromSpan<object?>(json, jsonTypeInfo)!;
}
PVS-Studio警告。V3021有两个 "if "语句有相同的条件表达式。第一个 "if "语句包含方法返回。这意味着第二个 "if "语句是无意义的。JsonSerializer.Read.String.cs 163
是的,类似的情况,但在一个完全不同的地方。在使用之前,有returnType参数检查是否为空。这很好,但他们检查了两次参数。
问题11
C#
private void WriteQualifiedNameElement(....)
{
bool hasDefault = defaultValue != null && defaultValue != DBNull.Value;
if (hasDefault)
{
throw Globals.NotSupported(
"XmlQualifiedName DefaultValue not supported. Fail in WriteValue()");
}
....
if (hasDefault)
{
throw Globals.NotSupported(
"XmlQualifiedName DefaultValue not supported. Fail in WriteValue()");
}
}
PVS-Studio警告。V3021有两个 "if "语句有相同的条件表达式。第一个 "if "语句包含方法返回。这意味着第二个 "if "语句是无意义的。XmlSerializationWriterILGen.cs 102
这里的情况就比较刺激了。如果说前面的重复检查是一个接着一个,那么在这里它们是在方法的不同末端:几乎相隔20行。然而,被检查的hasDefault局部变量在这段时间内并没有变化。因此,要么在第一次检查时抛出异常,要么根本就不抛出。
问题12
C#
internal static bool AutoGenerated(ForeignKeyConstraint fk, bool checkRelation)
{
....
if (fk.ExtendedProperties.Count > 0)
return false;
if (fk.AcceptRejectRule != AcceptRejectRule.None)
return false;
if (fk.DeleteRule != Rule.Cascade) // <=
return false;
if (fk.DeleteRule != Rule.Cascade) // <=
return false;
if (fk.RelatedColumnsReference.Length != 1)
return false;
return AutoGenerated(fk.RelatedColumnsReference[0]);
}
PVS-Studio警告。V3022表达式 "fk.DeleteRule != Rule.Cascade "总是假的。xmlsaver.cs 1708
传统上,问题是:是需要检查另一个值,还是只是多余的代码?
缺少的插值
首先,让我们看一下发现的几个警告。然后,我给你讲一个小故事。
第13期
C#
internal void SetLimit(int physicalMemoryLimitPercentage)
{
if (physicalMemoryLimitPercentage == 0)
{
// use defaults
return;
}
_pressureHigh = Math.Max(3, physicalMemoryLimitPercentage);
_pressureLow = Math.Max(1, _pressureHigh - 9);
Dbg.Trace($"MemoryCacheStats",
"PhysicalMemoryMonitor.SetLimit:
_pressureHigh={_pressureHigh}, _pressureLow={_pressureLow}");
}
PVS-Studio警告。V3138字符串字面包含一个潜在的插值表达式。考虑检查一下。_pressureHigh。物理内存监控器(PhysicalMemoryMonitor).cs 110
几乎看起来有人想在这里记录*_pressureHigh和_pressureLow字段。然而,数值的替换是行不通的,因为字符串没有被插值;但是,插值符号在Dbg.Trace*方法的第一个参数上,参数中没有什么可以替换的。 :)
问题14
C#
private void ParseSpecs(string? metricsSpecs)
{
....
string[] specStrings = ....
foreach (string specString in specStrings)
{
if (!MetricSpec.TryParse(specString, out MetricSpec spec))
{
Log.Message("Failed to parse metric spec: {specString}");
}
else
{
Log.Message("Parsed metric: {spec}");
....
}
}
}
PVS-Studio警告。V3138字符串字面包含一个潜在的插值表达式。考虑检查一下: spec.MetricsEventSource.cs 381
人们正试图解析specString字符串。如果不成功,就需要记录源字符串。如果成功了,就记录结果(spec变量),并执行一些其他操作。
问题又来了,在第一和第二种情况下,都缺少插值符号。因此,specString和spec变量的值不会被替换。
现在准备看看承诺的故事吧。
正如我上面提到的,我检查了2019年的.NET核心库。我发现有几个字符串很可能必须被插值,但由于漏掉了"$"符号,它们没有被插值。在那篇文章中,相应的警告被描述为问题10和问题11。
我在GitHub上创建了该错误报告。之后,.NET开发团队修复了文章中描述的一些代码片段;其中包括插值字符串的错误(相应的拉动请求)。
此外,在Roslyn Analyzers问题跟踪器中,还创建了一个新的诊断器,以检测这种情况。
我的同事在这里更详细地描述了整个故事。
让我们回到现在。我知道这一切并记住了它,所以当我再次遇到错过插值的错误时,我非常惊讶。这怎么可能呢?毕竟,已经应该有开箱即用的诊断器来帮助避免这些错误。
我决定查看2019年8月15日的那个诊断器开发问题,结果发现:这个诊断器还没有准备好。这就是插值错误的来源问题的答案。
PVS-Studio从7.03版本(2019年6月25日)起就开始检测这类问题了--利用它吧。;)
有些事情会改变,有些不会
在检查过程中,我遇到了几次对我来说似乎隐约有些熟悉的警告。事实证明,我上次已经描述过它们了。由于它们仍然在代码中,我假定这些不是错误。
例如,下面的代码似乎是抛出ArgumentOutOfRangeException的一种非常不寻常的方式。这就是上次检查中的第30个问题。
C#
private ArrayList? _tables;
private DataTable? GetTable(string tableName, string ns)
{
if (_tables == null)
return _dataSet!.Tables.GetTable(tableName, ns);
if (_tables.Count == 0)
return (DataTable?)_tables[0];
....
}
然而,我对先前已经发现的其他片段有一些疑问:例如,问题25。在循环中,seq集合被绕过,但只有集合的第一个元素*seq[0]*被不断访问。这看起来......很不寻常。
C#
public bool MatchesXmlType(IList<XPathItem> seq, int indexType)
{
XmlQueryType typBase = GetXmlType(indexType);
XmlQueryCardinality card = seq.Count switch
{
0 => XmlQueryCardinality.Zero,
1 => XmlQueryCardinality.One,
_ => XmlQueryCardinality.More,
};
if (!(card <= typBase.Cardinality))
return false;
typBase = typBase.Prime;
for (int i = 0; i < seq.Count; i++)
{
if (!CreateXmlType(seq[0]).IsSubtypeOf(typBase)) // <=
return false;
}
return true;
}
PVS-Studio警告。V3102可疑地通过循环内的常数索引访问 "seq "对象的元素。XmlQueryRuntime.cs 729
这段代码让我有点困惑。它让你感到困惑吗?
或者,让我们来看看第34个问题。
C#
public bool Remove(out int testPosition, out MaskedTextResultHint resultHint)
{
....
if (lastAssignedPos == INVALID_INDEX)
{
....
return true; // nothing to remove.
}
....
return true;
}
PVS-Studio警告。V3009很奇怪,这个方法总是返回一个相同的值 "true"。MaskedTextProvider.cs 1531
这个方法以前总是返回true,现在也是如此。同时,注释中说这个方法也可以返回false。成功时返回true,否则返回false。我们可以在文档中找到同样的故事。
我甚至会在下面的例子中放入一个单独的部分,尽管在之前的文章中也有描述。让我们不仅对代码片段本身进行一些推测,而且对片段中使用的一个特性进行推测:可空参考类型。
关于可忽略的引用类型
总的来说,我还没有弄清楚我是否喜欢可空引用类型。
一方面,可空引用类型有一个巨大的优势。它们使方法的签名更具有信息性。只要看一眼方法,就足以了解它是否可以返回null,某个参数是否可以有null值,等等。
另一方面,所有这些都是建立在信任之上的。没有人禁止你写这样的代码。
C#
static String GetStr()
{
return null!;
}
static void Main(string[] args)
{
String str = GetStr();
Console.WriteLine(str.Length); // NRE, str - null
}
是的,是的,是的,这是合成的代码,但是你可以这样写!你可以这样写。如果这样的代码是在你的公司内部写的,我们会去(相对而言)找GetStr的作者,并进行对话。但是,如果GetStr来自某个库,而你又没有这个库的来源,这样的惊喜就不会很愉快了。
让我们从合成例子回到我们的主要话题,即.NET 6。这里有一些微妙的地方。例如,不同的库被分为不同的解决方案。在翻阅它们的过程中,我反复想:这个项目中是否启用了nullable context?事实上,没有对null进行检查--这到底是预期的还是不预期的?可能,在一个项目的上下文中工作时,这不是一个问题。然而,在对所有项目进行粗略分析的情况下,它就会产生某些困难。
然后它就真的变得有趣了。当迁移到一个nullable上下文时,各种奇怪的事情开始出现了。似乎一个变量不能有空值,同时,又有一个检查。让我们面对现实吧,.NET有几个这样的地方。让我给你看看其中的几个。
C#
private void ValidateAttributes(XmlElement elementNode)
{
....
XmlSchemaAttribute schemaAttribute
= (_defaultAttributes[i] as XmlSchemaAttribute)!;
attrQName = schemaAttribute.QualifiedName;
Debug.Assert(schemaAttribute != null);
....
}
PVS-Studio警告。V3095"schemaAttribute "对象在对null进行验证之前被使用。检查行。438, 439.DocumentSchemaValidator.cs 438
符号"!"提示我们在这里处理的是一个可空的上下文。好的。
- 为什么使用 "as "操作符来铸造,而不是直接铸造?如果有信心schemaAttribute不是空的(这就是我对带有"!"的隐式契约的解读),那么*_defaultAttributes[i]确实有XmlSchemaAttribute*类型?好吧,让我们说一个开发者更喜欢这种语法。好吧。
- 如果schemaAttribute不是空的,为什么在下面的Debug.Assert中会有对空的检查?
- 如果检查是相关的,并且schemaAttribute仍然可以有一个空值(与nullable引用类型的语义相反),那么由于抛出的异常,执行将不会到达Debug.Assert。异常将在访问schemaAttribute.QualifiedName时被抛出。
就我个人而言,在看这样一小段代码的时候,我一下子有很多问题。
下面是一个类似的故事。
C#
public Node DeepClone(int count)
{
....
while (originalCurrent != null)
{
originalNodes.Push(originalCurrent);
newNodes.Push(newCurrent);
newCurrent.Left = originalCurrent.Left?.ShallowClone();
originalCurrent = originalCurrent.Left;
newCurrent = newCurrent.Left!;
}
....
}
一方面,newCurrent.Left可以有一个空值,因为执行*?运算符的结果被写入其中(originalCurrent.Left?.ShallowClone())。另一方面,在最后一行,我们看到注解说newCurrent.Left不是空值*。
现在让我们来看看.NET 6的代码片段,事实上,这也是我开始写这一节的原因。在*ImmutableArray类型中的IStructuralEquatable.Equals(object? other, IEqualityComparer comparer)*实现。
C#
internal readonly T[]? array;
bool IStructuralEquatable.Equals(object? other, IEqualityComparer comparer)
{
var self = this;
Array? otherArray = other as Array;
if (otherArray == null)
{
if (other is IImmutableArray theirs)
{
otherArray = theirs.Array;
if (self.array == null && otherArray == null)
{
return true;
}
else if (self.array == null)
{
return false;
}
}
}
IStructuralEquatable ours = self.array!;
return ours.Equals(otherArray, comparer);
}
如果你在Visual Studio中看最后几行代码,编辑器会很有帮助地告诉你,我们的不是空的。从代码中可以看出--self.array是不可为空的引用变量。
好吧,让我们写下下面的代码。
C#
IStructuralEquatable immutableArr = default(ImmutableArray<String>);
var eq = immutableArr.Equals(null, EqualityComparer<String>.Default);
然后我们运行它进行执行,看到一个NullReferenceException。
呜呼。看来,我们的变量,不是空的,事实上还是变成了空引用。
让我们来看看这是怎么发生的。
- immutableArr对象的数组字段采用默认的空值。
- other有一个空值,所以otherArray也有一个空值。
- 对 other是ImmutableArray 的检查给出了 false。
- 在向我们的写值时,self.array字段是空的。
- 你知道剩下的事情。
在这里你可以有一个反驳,即不可变数组有一个不正确的状态,因为它不是通过特殊的方法/属性创建的,而是通过调用默认操作符创建的。对于这样一个对象,在调用Equals时得到一个NRE还是有点奇怪。
然而,这甚至不是问题的关键。代码、注解和提示都表明我们的不是空。事实上,这个变量确实有空值。对我个人来说,这有点破坏了对可空引用类型的信任。
PVS-Studio发出了一个警告。V3125"ours "对象在被验证为null后被使用。检查行。1144, 1136.ImmutableArray_1.cs 1144
顺便说一下,我在上一篇文章(第53期)中写过这个问题。然而,那时还没有nullable注解。
注意: 回到关于默认状态下对ImmutableArray实例的操作的话题,一些方法/属性使用了特殊的方法。ThrowNullRefIfNotInitialized和ThrowInvalidOperationIfNotInitialized。这些方法报告对象的未初始化状态。此外,接口方法的显式实现使用ThrowInvalidOperationIfNotInitialized。也许在上面描述的情况下应该使用它。
在这里,我想问问我们的听众:你有什么样的经验来处理可归零引用类型?你喜欢它们吗?或者你不喜欢它们?你在你的项目中使用过可忽略的引用类型吗?哪些方面做得很好?你有什么困难?我很想知道你对可忽略的引用类型的看法。
顺便说一下,我的同事们已经在几篇文章中写了关于可忽略的引用类型的内容:一,二。时间在流逝,但这个问题仍然是有争议的。
结论
最后,我想再一次祝贺.NET 6开发团队的发布。我还想对所有为这个项目作出贡献的人说声谢谢。我相信他们会解决这些不足之处。未来还有很多成就。
我也希望我能够再次提醒你静态分析是如何有利于开发过程的。 最后,根据良好的传统,我邀请你订阅我的Twitter,以免错过任何有趣的东西。
主题。
csharp, 编程, 代码, 代码质量, dotnet, 开放源代码, 静态分析, 静态分析工具, 静态分析器
经Sergey Vasiliev授权发表于DZone。点击这里查看原文。
DZone贡献者所表达的观点是他们自己的。
在DZone上很受欢迎
- 高绩效团队遵循的软件工程最佳实践
- 检查云计算合成器 - Apache Airflow
- 调试RAM--第一部分。Java垃圾回收--Java Heap Deep Dive
- 全栈式开发中的 "弯道超车 "问题
评论