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。这很可能是复制粘贴错误。
第九名来自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,导致需要第五个参数(实际缺失)
下一个案例出自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。
第七名来自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方法中使用。
同样出自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中的所有位标志,包括Shift、Control和Alt。
因此keys & ~Keys.Modifiers运算不可能产生这些修饰键的值。
前五强案例曾出现在.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
如果ChangeViewMessages为null,整个表达式结果为null而非base.Size。
亚军案例来自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#中较为常见,甚至曾在语言设计会议中讨论过解决方案。
冠军案例来自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#代码中典型错误案例的技术分析。希望这些案例能帮助开发者更好地理解代码中的潜在问题,提升代码质量意识。
共同学习,写下你的评论
评论加载中...
作者其他优质文章