.NET 6源中的错误和可疑的代码片段

271 阅读12分钟

DZone>Web Dev Zone>.NET 6资源中的错误和可疑的代码片段

.NET 6资源中的错误和可疑的代码片段

.NET 6是一个备受期待的重要版本。深入了解一下,看看我们能在.NET库的源代码中发现哪些有趣的东西。

Sergey Vasiliev user avatar通过

谢尔盖-瓦西里耶夫

-

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

事实上,这个方法执行了从CompressionOptionCompressionLevel的映射。这里可疑的是,CompressionOption.NormalCompressionOption.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变量),并执行一些其他操作。

问题又来了,在第一和第二种情况下,都缺少插值符号。因此,specStringspec变量的值不会被替换。

现在准备看看承诺的故事吧。

正如我上面提到的,我检查了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

符号"!"提示我们在这里处理的是一个可空的上下文。好的。

  1. 为什么使用 "as "操作符来铸造,而不是直接铸造?如果有信心schemaAttribute不是空的(这就是我对带有"!"的隐式契约的解读),那么*_defaultAttributes[i]确实有XmlSchemaAttribute*类型?好吧,让我们说一个开发者更喜欢这种语法。好吧。
  2. 如果schemaAttribute不是空的,为什么在下面的Debug.Assert中会有对空的检查?
  3. 如果检查是相关的,并且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实例的操作的话题,一些方法/属性使用了特殊的方法。ThrowNullRefIfNotInitializedThrowInvalidOperationIfNotInitialized。这些方法报告对象的未初始化状态。此外,接口方法的显式实现使用ThrowInvalidOperationIfNotInitialized。也许在上面描述的情况下应该使用它。

在这里,我想问问我们的听众:你有什么样的经验来处理可归零引用类型?你喜欢它们吗?或者你不喜欢它们?你在你的项目中使用过可忽略的引用类型吗?哪些方面做得很好?你有什么困难?我很想知道你对可忽略的引用类型的看法。

顺便说一下,我的同事们已经在几篇文章中写了关于可忽略的引用类型的内容:。时间在流逝,但这个问题仍然是有争议的。

结论

最后,我想再一次祝贺.NET 6开发团队的发布。我还想对所有为这个项目作出贡献的人说声谢谢。我相信他们会解决这些不足之处。未来还有很多成就。

我也希望我能够再次提醒你静态分析是如何有利于开发过程的。 最后,根据良好的传统,我邀请你订阅我的Twitter,以免错过任何有趣的东西。

主题。

csharp, 编程, 代码, 代码质量, dotnet, 开放源代码, 静态分析, 静态分析工具, 静态分析器

经Sergey Vasiliev授权发表于DZone。点击这里查看原文。

DZone贡献者所表达的观点是他们自己的。

在DZone上很受欢迎


评论

网络开发 合作伙伴资源