为了账号安全,请及时绑定邮箱和手机立即绑定

2025年开源C#项目中最有趣的10个Bug盘点

标签:
C# 开源

2025年全年,PVS-Studio团队持续对开源C#项目进行代码分析工作。这一年我们发现了大量代码缺陷,从中精选了十个最具代表性的案例。希望这些案例能给您带来技术启发,欢迎阅读!

评选标准解读

入选我们年度榜单的代码需要满足以下几项核心指标:

  • 来源于开源项目;
  • 由PVS-udio静态分析工具检测发现;
  • 代码极有可能存在功能性错误;
  • 代码案例具有分析价值;
  • 每个错误类型具有独特性。

由于我们持续多年编写这类总结,已积累了大量值得研究的代码缺陷案例。您可以通过以下链接查阅往期文章:

现在,让我们一同深入分析2025年C#代码中的典型错误案例!

注:本文的案例筛选和排序基于作者的技术判断。如果您对某些Bug的排名有不同见解,欢迎在评论区交流。

第10名:考眼力

榜单开篇案例曾出现在.NET 9代码分析文章中。虽然感觉.NET 9刚刚发布,但一个多月前.NET 10已正式推出。我们已在专文中探讨了主要变更。

现在回到代码分析:

public static void SetAsIConvertible(this ref ComVariant variant,
                                     IConvertible value)
{
  TypeCode tc = value.GetTypeCode();
  CultureInfo ci = CultureInfo.CurrentCulture;

  switch (tc)
  {
    case TypeCode.Empty: break;
    case TypeCode.Object: 
      variant = ComVariant.CreateRaw(....); break;
    case TypeCode.DBNull: 
      variant = ComVariant.Null; break;
    case TypeCode.Boolean: 
      variant = ComVariant.Create<bool>(....)); break;
    case TypeCode.Char: 
      variant = ComVariant.Create<ushort>(value.ToChar(ci)); break;
    case TypeCode.SByte: 
      variant = ComVariant.Create<sbyte>(value.ToSByte(ci)); break;
    case TypeCode.Byte: 
      variant = ComVariant.Create<byte>(value.ToByte(ci)); break;
    case TypeCode.Int16: 
      variant = ComVariant.Create(value.ToInt16(ci)); break;
    case TypeCode.UInt16: 
      variant = ComVariant.Create(value.ToUInt16(ci)); break;
    case TypeCode.Int32: 
      variant = ComVariant.Create(value.ToInt32(ci)); break;
    case TypeCode.UInt32: 
      variant = ComVariant.Create(value.ToUInt32(ci)); break;
    case TypeCode.Int64: 
      variant = ComVariant.Create(value.ToInt64(ci)); break;
    case TypeCode.UInt64: 
      variant = ComVariant.Create(value.ToInt64(ci)); break; // <= 此处应为ToUInt64
    case TypeCode.Single: 
      variant = ComVariant.Create(value.ToSingle(ci)); break;
    case TypeCode.Double: 
      variant = ComVariant.Create(value.ToDouble(ci)); break;
    case TypeCode.Decimal: 
      variant = ComVariant.Create(value.ToDecimal(ci)); break;
    case TypeCode.DateTime: 
      variant = ComVariant.Create(value.ToDateTime(ci)); break;
    case TypeCode.String: 
      variant = ComVariant.Create(....); break;

    default:
      throw new NotSupportedException();
  }
}

发现问题了吗?错误确实存在!

case TypeCode.Int64: 
  variant = ComVariant.Create(value.ToInt64(ci)); break;
case TypeCode.UInt64: 
  variant = ComVariant.Create(value.ToInt64(ci)); break; // <=

PVS-Studio警告:V3139 两个或多个case分支执行相同操作。DynamicVariantExtensions.cs 68

case TypeCode.UInt64分支中,开发者本应使用ToUInt64()方法,却误用了value.ToInt64。这很可能是复制粘贴错误。

第9名:格式字符串问题

第九名来自Neo和NBitcoin项目分析报告

public override string ToString()
{
  var sb = new StringBuilder();

  sb.AppendFormat("{1:X04} {2,-10}{3}{4}", 
                  Position, 
                  OpCode, 
                  DecodeOperand());

  return sb.ToString();
}

PVS-Studio警告:V3025 [CWE-685] 格式字符串不正确。调用'AppendFormat'时期望的格式项数量不匹配。未使用的格式项:{3}、{4}。未使用的参数:第1个。VMInstruction.cs 105

调用重写的ToString方法会引发异常。sb.AppendFormat调用存在两处错误:

  • 传入参数数量少于格式字符串中的占位符数量
  • 占位符索引从0开始而非1,导致需要第五个参数(实际缺失)
第8名:自引用比较

下一个案例出自Lean交易引擎分析报告

public override int GetHashCode()
{
  unchecked
  {
    var hashCode = Definition.GetHashCode();
    var arr = new int[Legs.Count];
    for (int i = 0; i < Legs.Count; i++)
    {
      arr[i] = Legs[i].GetHashCode();
    }

    Array.Sort(arr);

    for (int i = 0; i < arr.Length; i++)
    {
      hashCode = (hashCode * 397) ^ arr[i];
    }

    return hashCode;
  }
}

public override bool Equals(object obj)
{
    ....

    return Equals((OptionStrategyDefinitionMatch) obj);
}

PVS-Studio警告:V3192 属性'Legs'在'GetHashCode'方法中使用,但未在'Equals'方法中出现。OptionStrategyDefinitionMatch.cs 176

分析器进行过程间分析发现,Equals方法未使用Legs属性,而GetHashCode却依赖该属性。

查看Equals方法实现:

public bool Equals(OptionStrategyDefinitionMatch other)
{
  ....

  var positions = other.Legs
                       .ToDictionary(leg => leg.Position, 
                                     leg => leg.Multiplier);
  foreach (var leg in other.Legs)                                   // <=
  {
    int multiplier;
    if (!positions.TryGetValue(leg.Position, out multiplier))
    {
      return false;
    }

    if (leg.Multiplier != multiplier)
    {
      return false;
    }
  }

  return true;
}

方法遍历other.Legs,但positions字典同样源自other.Legs,导致代码检查集合元素是否存在于同一集合中。

解决方案是将标记处的other.Legs替换为Legs

第7名:Equals实现问题

第七名来自ScottPlot分析报告

public class CoordinateRangeMutable : IEquatable<CoordinateRangeMutable>
{
  ....
  public bool Equals(CoordinateRangeMutable? other)
  {
    if (other is null)
      return false;

    return Equals(Min, other.Min) && Equals(Min, other.Min);  // <=
  }

  public override bool Equals(object? obj)
  {
    if (obj is null)
      return false;

    if (obj is CoordinateRangeMutable other)
      return Equals(other);

    return false;
  }

  public override int GetHashCode()
  {
    return Min.GetHashCode() ^ Max.GetHashCode();             // <=
  }
}

PVS-Studio双重警告:

V3192 属性'Max'在'GetHashCode'方法中使用,但未在'Equals'方法中出现。ScottPlot CoordinateRangeMutable.cs 198

V3001 '&&'运算符左右存在相同的子表达式'Equals(Min, other.Min)'。ScottPlot CoordinateRangeMutable.cs 172

分析器发出双重警告。观察重写的Equals方法,其中包含重复的Equals(Min, other.Min)调用。显然,其中一个操作数应为Equals(Max, other.Max)形式。

因此Max属性确实未在Equals方法中使用。

第6名:位运算问题

同样出自ScottPlot分析报告的本案例:

public static Interactivity.Key GetKey(this Keys keys)
{

  Keys keyCode = keys & ~Keys.Modifiers;                   // <=
  Interactivity.Key key = keyCode switch
  {
    Keys.Alt => Interactivity.StandardKeys.Alt,            // <=
    Keys.Menu => Interactivity.StandardKeys.Alt,
    Keys.Shift => Interactivity.StandardKeys.Shift,        // <=
    Keys.ShiftKey => Interactivity.StandardKeys.Shift,
    Keys.LShiftKey => Interactivity.StandardKeys.Shift,
    Keys.RShiftKey => Interactivity.StandardKeys.Shift,
    Keys.Control => Interactivity.StandardKeys.Control,    // <=
    Keys.ControlKey => Interactivity.StandardKeys.Control,
    Keys.Down => Interactivity.StandardKeys.Down,
    Keys.Up => Interactivity.StandardKeys.Up,
    Keys.Left => Interactivity.StandardKeys.Left,
    Keys.Right => Interactivity.StandardKeys.Right,
    _ => Interactivity.StandardKeys.Unknown,
  };

  ....
}

PVS-Studio警告:V3202 检测到不可达代码。case值超出匹配表达式范围。ScottPlot.WinForms FormsPlotExtensions.cs 106

switch中的多个模式值在当前上下文中不可能匹配。查看相关枚举定义:

[Flags]
[TypeConverter(typeof(KeysConverter))]
[Editor(....)]
public enum Keys
{
  /// <summary>
  ///  从键值提取修饰符的位掩码。
  /// </summary>
  Modifiers = unchecked((int)0xFFFF0000),

  ....
  /// <summary>
  ///  SHIFT修饰键。
  /// </summary>
  Shift = 0x00010000,

  /// <summary>
  ///  CTRL修饰键。
  /// </summary>
  Control = 0x00020000,

  /// <summary>
  ///  ALT修饰键。
  /// </summary>
  Alt = 0x00040000
}

Modifiers包含每个异常枚举元素的位标志。传入switch的值通过keys & ~Keys.Modifiers获得,该表达式会排除Modifiers中的所有位标志,包括ShiftControlAlt

因此keys & ~Keys.Modifiers运算不可能产生这些修饰键的值。

第5名:装箱操作问题

前五强案例曾出现在.NET 9分析报告中:

struct StackValue
{
  ....
  public override bool Equals(object obj)
  {
    if (Object.ReferenceEquals(this, obj))
      return true;

    if (!(obj is StackValue))
      return false;

    var value = (StackValue)obj;
    return    this.Kind == value.Kind 
           && this.Flags == value.Flags 
           && this.Type == value.Type;
  }
}

PVS-Studio警告:V3161 使用'ReferenceEquals'比较值类型变量不正确,因为'this'将发生装箱。ILImporter.StackValue.cs 164

ReferenceEquals方法接收Object类型参数。当传递值类型时会发生装箱操作,堆上创建的引用不会匹配任何其他引用。

由于this作为值类型传递,每次调用Equals都会发生装箱。因此ReferenceEquals检查总是返回false

虽然该问题不影响方法功能,但本意是优化反而增加了性能开销。

第4名:事件取消订阅问题

第四名来自MSBuild分析报告

private static void SubscribeImmutablePathsInitialized()
{
  NotifyOnScopingReadiness?.Invoke();

  FileClassifier.Shared.OnImmutablePathsInitialized -= () =>
    NotifyOnScopingReadiness?.Invoke();
}

PVS-Studio警告:V3084 使用匿名函数取消订阅'OnImmutablePathsInitialized'事件无效,因为每个匿名函数声明都会创建独立的委托实例。CheckScopeClassifier.cs 67

此处取消事件订阅无效,因为每个匿名函数都会创建新的委托实例。实际订阅的是第一个委托实例,但取消的是第二个实例的订阅,操作不会生效。

第3名:运算符优先级问题

前三名案例来自Neo和NBitcoin项目分析报告

public override int Size =>   base.Size
                            + ChangeViewMessages?.Values.GetVarSize() ?? 0
                            + 1 + PrepareRequestMessage?.Size ?? 0
                            + PreparationHash?.Size ?? 0
                            + PreparationMessages?.Values.GetVarSize() ?? 0
                            + CommitMessages?.Values.GetVarSize() ?? 0;

PVS-Studio警告:V3123 [CWE-783] '??'运算符的操作顺序可能不符合预期。其优先级低于左侧其他运算符。RecoveryMessage.cs 35

??运算符优先级低于+运算符,但代码格式暗示开发者期望相反的顺序。以子表达式为例:

base.Size + ChangeViewMessages?.Values.GetVarSize() ?? 0

如果ChangeViewMessagesnull,整个表达式结果为null而非base.Size

第2名:模式匹配问题

亚军案例来自Files文件管理器分析报告

protected void ChangeMode(OmnibarMode? oldMode, OmnibarMode newMode)
{
  ....
  var modeSeparatorWidth = 
    itemCount is not 0 or 1 
      ? _modesHostGrid.Children[1] is FrameworkElement frameworkElement
        ? frameworkElement.ActualWidth
        : 0
      : 0;
  ....
}

PVS-Studio警告:V3207 [CWE-670] 'not 0 or 1'逻辑模式可能不会按预期工作。'not'模式仅匹配'or'模式的第一个表达式。Files.App.Controls Omnibar.cs 149

itemCount is not 0 or 1实际表示itemCount is (not 0) or 1,而非预期的"既不是0也不是1"。这种模式匹配问题在C#中较为常见,甚至曾在语言设计会议中讨论过解决方案。

第1名:LINQ延迟执行问题

冠军案例来自Lean交易引擎分析报告,因其隐蔽性而位居榜首:

public void FutureMarginModel_MarginEntriesValid(string market)
{
  ....
  var lineNumber = 0;
  var errorMessageTemplate = $"Error encountered in file " + 
                             $"{marginFile.Name} on line ";
  var csv = File.ReadLines(marginFile.FullName)
                .Where(x =>    !x.StartsWithInvariant("#") 
                            && !string.IsNullOrWhiteSpace(x))
                .Skip(1)
                .Select(x =>
  {
    lineNumber++;                                                  // <=

    ....
  });

  lineNumber = 0;                                                  // <=
  foreach (var line in csv)
  {
    lineNumber++;                                                  // <=

    ....
  }
}

PVS-Studio警告:V3219 变量'lineNumber'在延迟执行的LINQ方法中被捕获后发生变更。方法执行时不会使用原始值。FutureMarginBuyingPowerModelTests.cs 720

lineNumber变量在LINQ的Select方法中被捕获。由于Select是延迟执行方法,委托代码在迭代结果集合时运行,而非调用Select时。

在迭代过程中,lineNumber在委托内和foreach内各递增一次,导致每次迭代实际增加2。foreach前的重置操作也因延迟执行而无法达到预期效果。

结语

至此,我们完成了2025年C#代码中典型错误案例的技术分析。希望这些案例能帮助开发者更好地理解代码中的潜在问题,提升代码质量意识。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消