我们喜欢在Microsoft项目中寻找错误。 怎么了 很简单:他们的项目通常很容易检查(可以在Visual Studio环境中立即完成工作,为此PVS-Studio拥有便捷的插件),并且几乎没有错误。 因此,通常的工作算法如下:从MS查找并下载一个打开的项目; 检查一下; 选择有趣的错误; 确保数量很少; 写一篇文章,不要忘记赞扬开发人员。 太好了! 双赢:花了一些时间,手册很高兴在博客上看到新材料,业力井然有序。 但是这一次出了点问题。 让我们看看在Windows Forms的源代码中发现了什么有趣的东西,以及这次是否应该赞扬Microsoft。
引言在2018年12月上旬,微软
宣布发布.NET Core 3 Preview 1。 您可以在
此处查看提交统计信息。 现在,任何人都可以下载WinForms源代码以进行审查。
我还下载了源代码,以使用PVS-Studio查找错误。 检查没有造成困难。 必需:Visual Studio 2019,.NET Core 3.0 SDK预览,PVS-Studio。 现在接收到分析仪警告日志。
收到PVS-Studio日志后,我通常以诊断编号的升序对其进行排序(Visual Studio中带有PVS-Studio消息日志的窗口具有用于排序和过滤列表的各种选项)。 这允许处理相同类型的错误组,从而大大简化了源代码的分析。 我在星号上标记了有趣的错误,然后,在分析了整个日志之后,我写出了代码片段并进行了描述。 而且由于通常很少有错误,因此我将它们混合在一起,试图将最有趣的错误放在文章的开头和结尾。 但这一次错误的确多了一些(嗯,很长一段时间都无法保存这种阴谋),我将按照诊断编号的顺序进行处理。
发现了什么? 在1670 cs文件中,对540,000行代码(不包括空代码)发出了833个高和中置信度警告(分别为249和584)。 是的,根据传统,我没有检查测试,也没有考虑低置信度(发出215)的警告。 根据我以前的观察,MS对该项目提出了太多警告。 但并非所有警告都是错误。
对于该项目,误报的数量约为30%。 在大约20%的情况下,由于我不熟悉该代码,所以我无法就这是否是一个错误做出准确的结论。 好吧,我错过的错误中至少有20%可以归因于人为因素:匆忙,疲劳等。 顺便说一句,相反的效果也是可能的:我查看了一些相同类型的触发,其触发次数可能达到70-80,通过偶尔会增加我认为是真正的错误的触发次数。
在任何情况下,如果尚未预先配置分析仪,则30%的警告表示实际错误,这是一个很大的百分比。
因此,我能够检测到的错误数量约为240,这在统计数据的范围内。 我认为,对于MS的项目,我再说一遍,这并不是最出色的结果(尽管每1000行代码只有0.44错误),并且WinForms代码中可能存在更多实际错误。 我建议在文章结尾讨论原因,但现在让我们看看最有趣的错误。
失误PVS-Studio:
V3003已检测到'if(A){...} else if(A){...}'模式的使用。 存在逻辑错误的可能性。 检查行:213、224。ButtonStandardAdapter.cs 213
void PaintWorker(PaintEventArgs e, bool up, CheckState state) { up = up && state == CheckState.Unchecked; .... if (up & IsHighContrastHighlighted()) { .... } else if (up & IsHighContrastHighlighted()) { .... } else { .... } .... }
在
if和
else if块中,检查相同条件。 看起来像是复制粘贴。 这是一个错误吗? 如果您查看
IsHighContrastHighlighted方法的声明,则会有疑问:
protected bool IsHighContrastHighlighted() { return SystemInformation.HighContrast && Application.RenderWithVisualStyles && (Control.Focused || Control.MouseIsOver || (Control.IsDefault && Control.Enabled)); }
该方法可能在连续调用中返回不同的值。 当然,在调用方法中执行的操作看起来很奇怪,但是它具有生命权。 但是,我建议作者看一下这段代码。 以防万一。 但是,这是一个很好的例子,说明了在分析不熟悉的代码时很难得出结论。
PVS-Studio:
V3004'then '语句等效于'else'语句。 RichTextBox.cs 1018
public int SelectionCharOffset { get { int selCharOffset = 0; .... NativeMethods.CHARFORMATA cf = GetCharFormat(true);
在这里肯定会出现复制粘贴错误。 无论条件如何,
selCharOffset变量
将始终获得相同的值。
WinForms代码中还有两个类似的错误:
- V3004'then'语句等效于'else'语句。 SplitContainer.cs 1700
- V3004'then'语句等效于'else'语句。 工具tripProfessionalRenderer.cs 371
PVS-Studio:
V3008连续两次为变量分配值。 也许这是一个错误。 检查行:681、680。ProfessionalColorTable.cs 681
internal void InitSystemColors(ref Dictionary<KnownColors, Color> rgbTable) { .... rgbTable[ProfessionalColorTable.KnownColors.msocbvcrCBBdrOuterDocked] = buttonFace; rgbTable[ProfessionalColorTable.KnownColors.msocbvcrCBBdrOuterDocked] = buttonShadow; .... }
该方法填充
rgbTable字典。 分析器指向一段代码,其中两个不同的值依次写入同一键。 一切都会好起来的,但是这种方法还有16个这样的位置,这不再像一个错误。 但是为什么要这样做,对我来说仍然是个谜。 我没有发现自动生成代码的迹象。 在编辑器中,它看起来像这样:
我将列出前十个操作:
- V3008为变量连续分配两次值。 也许这是一个错误。 检查行:785,784。ProfessionalColorTable.cs 785
- V3008为变量连续分配两次值。 也许这是一个错误。 检查行:787、786。ProfessionalColorTable.cs 787
- V3008为变量连续分配两次值。 也许这是一个错误。 检查行:789、788。ProfessionalColorTable.cs 789
- V3008为变量连续分配两次值。 也许这是一个错误。 检查行:791、790。ProfessionalColorTable.cs 791
- V3008为变量连续分配两次值。 也许这是一个错误。 检查行:797、796。ProfessionalColorTable.cs 797
- V3008为变量连续分配两次值。 也许这是一个错误。 检查行:799、798。ProfessionalColorTable.cs 799
- V3008为变量连续分配两次值。 也许这是一个错误。 检查行:807、806。ProfessionalColorTable.cs 807
- V3008为变量连续分配两次值。 也许这是一个错误。 检查行:815、814。ProfessionalColorTable.cs 815
- V3008为变量连续分配两次值。 也许这是一个错误。 检查行:817,816。ProfessionalColorTable.cs 817
- V3008为变量连续分配两次值。 也许这是一个错误。 检查行:823,822。ProfessionalColorTable.cs 823
PVS-Studio:
V3011遇到两个相反的条件。 第二个条件始终为假。 检查行:5242、5240。DataGrid.cs 5242
private void CheckHierarchyState() { if (checkHierarchy && listManager != null && myGridTable != null) { if (myGridTable == null)
return语句将永远不会执行。 最有可能的是,条件
myGridTable!=外部
if块中的
Null在重构期间稍后添加。 现在检查
myGridTable == null是没有意义的。 为了提高代码的质量,请删除此检查。
PVS-Studio:
V3019可能在使用'as'关键字进行类型转换后将不正确的变量与null进行比较。 检查变量“ left”,“ cscLeft”。 TypeCodeDomSerializer.cs 611
PVS-Studio:
V3019可能在使用'as'关键字进行类型转换后将不正确的变量与null进行比较。 检查变量“ right”,“ cscRight”。 TypeCodeDomSerializer.cs 615
public int Compare(object left, object right) { OrderedCodeStatementCollection cscLeft = left as OrderedCodeStatementCollection; OrderedCodeStatementCollection cscRight = right as OrderedCodeStatementCollection; if (left == null) { return 1; } else if (right == null) { return -1; } else if (right == left) { return 0; } return cscLeft.Order - cscRight.Order;
分析仪立即对
Compare方法发出两个警告。 怎么了 只是不检查
cscLeft和
cscRight的值是否等于
null 。 在不成功地转换为
OrderedCodeStatementCollection类型之后,他们可以获得此值。 然后,将在最后一个
return语句中引发异常。 当所有检查都通过
左和
右并且没有导致该方法的初步退出时,这种情况是可能的。
要修复代码,应在
各处使用
cscLeft /
cscRight而不是
left /
right 。
PVS-Studio:
V3020循环内无条件的“中断”。 SelectionService.cs 421
void ISelectionService.SetSelectedComponents( ICollection components, SelectionTypes selectionType) { ....
该片段是指“带有气味”的代码。 这里没有错误。 但是,对
每个循环的组织方式提出了疑问。 为什么在这里需要它-很明显:由于需要提取作为
ICollection传递的集合的元素。 但是,为什么在最初为单次迭代而设计的循环中(前提是
组件集合中存在单个元素),则需要一个额外的安全网(以
中断的形式)? 答案可能是:“它是历史发展的”。 该代码看起来很难看。
PVS-Studio:
V3022表达式'ocxState!= Null'始终为true。 AxHost.cs 2186
public State OcxState { .... set { .... if (value == null) { return; } .... ocxState = value; if (ocxState != null)
由于逻辑错误,在此片段中形成了“死代码”。
else块中的表达式将永远不会执行。
PVS-Studio:
V3027在针对同一逻辑表达式中的null进行验证之前,在逻辑表达式中使用了变量'e'。 图像编辑器.cs 99
public override object EditValue(....) { .... ImageEditor e = ....; Type myClass = GetType(); if (!myClass.Equals(e.GetType()) && e != null && myClass.IsInstanceOfType(e)) { .... } .... }
首先使用条件中的变量
e ,然后检查
空不等式。 嗨,
NullReferenceException 。
另一个类似的错误:
PVS-Studio:
V3027在对同一逻辑表达式中的null进行验证之前,在逻辑表达式中使用了变量'dropDownItem'。 ToolStripMenuItemDesigner.cs 1351
internal void EnterInSituEdit(ToolStripItem toolItem) { .... ToolStripDropDownItem dropDownItem = toolItem as ToolStripDropDownItem; if (!(dropDownItem.Owner is ToolStripDropDownMenu) && dropDownItem != null && dropDownItem.Bounds.Width < commitedEditorNode.Bounds.Width) { .... } .... }
这种情况与上一个类似,只不过具有变量
dropDownItem 。 我认为这些错误是由于重构期间的疏忽而出现的。 稍后,部分条件
!(DropDownItem.Owner是ToolStripDropDownMenu)可能已添加到代码中。
PVS-Studio:
V3030定期检查。 'columnCount> 0'条件已经在行3900中得到验证。ListView.cs 3903
internal ColumnHeader InsertColumn( int index, ColumnHeader ch, bool refreshSubItems) { ....
一个看似无害的错误。 确实,可以执行额外的检查,但这不会影响工作的逻辑。 有时,当您需要重新检查视觉组件的状态时,他们甚至会这样做,例如,通过获取列表中的条目数。 仅在这种情况下,他们才仔细检查
局部变量
columnCount 。 这是非常可疑的。 他们要么想要检查另一个变量,要么在其中一项检查中使用了错误的条件。
PVS-Studio:
V3061参数“ lprcClipRect”始终在使用前在方法主体中重写。 WebBrowserSiteBase.cs 281
int UnsafeNativeMethods.IOleInPlaceSite.GetWindowContext( out UnsafeNativeMethods.IOleInPlaceFrame ppFrame, out UnsafeNativeMethods.IOleInPlaceUIWindow ppDoc, NativeMethods.COMRECT lprcPosRect, NativeMethods.COMRECT lprcClipRect, NativeMethods.tagOIFI lpFrameInfo) { ppDoc = null; ppFrame = Host.GetParentContainer(); lprcPosRect.left = Host.Bounds.X; lprcPosRect.top = Host.Bounds.Y; .... lprcClipRect = WebBrowserHelper.GetClipRect();
显而易见的错误。 是的,
lprcClipRect参数实际上
是用新值初始化的,没有以任何方式使用它。 但是,这会导致什么呢? 我认为在调用代码的某个地方,通过此参数传递的链接将保持不变,尽管其意图有所不同。 确实,请看看在此方法中使用其他变量。 甚至它的名称(“ Get”前缀)也暗示将通过传递的参数在方法内部进行一些初始化。 就是这样。 前两个参数(
ppFrame和
ppDoc )与
out修饰符一起传递并获取新值。 链接
lprcPosRect和
lpFrameInfo用于访问类的字段并对其进行初始化。 并且只有
lprcClipRect不在常规列表中。 最有可能的是,此参数需要
out或
ref修饰符。
PVS-Studio:
V3066传递给“ AdjustCellBorderStyle”方法的参数的可能错误顺序:“ isFirstDisplayedRow”和“ isFirstDisplayedColumn”。 DataGridViewComboBoxCell.cs 1934
protected override void OnMouseMove(DataGridViewCellMouseEventArgs e) { .... dgvabsEffective = AdjustCellBorderStyle( DataGridView.AdvancedCellBorderStyle, dgvabsPlaceholder, singleVerticalBorderAdded, singleHorizontalBorderAdded, isFirstDisplayedRow,
分析器怀疑最后两个参数混淆了。 让我们看一下
AdjustCellBorderStyle方法的声明:
public virtual DataGridViewAdvancedBorderStyle AdjustCellBorderStyle( DataGridViewAdvancedBorderStyledataGridViewAdvancedBorderStyleInput, DataGridViewAdvancedBorderStyle dataGridViewAdvancedBorderStylePlaceholder, bool singleVerticalBorderAdded, bool singleHorizontalBorderAdded, bool isFirstDisplayedColumn, bool isFirstDisplayedRow) { .... }
看起来像是一个错误。 是的,经常有意地以相反的顺序传递一些参数,例如,以便交换某些变量。 但我认为情况并非如此。 调用或被调用方法中没有任何内容说明这种使用模式。 首先,
布尔类型变量令人困惑。 其次,方法的名称也很常见:没有“交换”或“反向”。 而且,犯这样的错误并不难。 人们通常对行/列对的顺序有不同的认识。 例如,对我而言,仅熟悉“行/列”。 但是对于名为
AdjustCellBorderStyle方法的作者
来说 ,显然,更熟悉的顺序是“列/行”。
PVS-Studio:
V3070初始化“ LOCALE_USER_DEFAULT”变量时使用未初始化的变量“ LANG_USER_DEFAULT”。 NativeMethods.cs 890
internal static class NativeMethods { .... public static readonly int LOCALE_USER_DEFAULT = MAKELCID(LANG_USER_DEFAULT); public static readonly int LANG_USER_DEFAULT = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT); .... }
一个罕见的错误。 类字段的初始化顺序混合在一起。 要计算
LOCALE_USER_DEFAULT字段的值,请使用
LANG_USER_DEFAULT字段,该字段尚未初始化且值为0。顺便说一句,
LANG_USER_DEFAULT变量未在代码中的其他任何地方使用。 我不太懒惰,并编写了一个模拟这种情况的小型控制台程序。 我代替了WinForms代码中使用的某些常量的值,而是替换了它们的实际值:
internal static class NativeMethods { public static readonly int LOCALE_USER_DEFAULT = MAKELCID(LANG_USER_DEFAULT); public static readonly int LANG_USER_DEFAULT = MAKELANGID(0x00, 0x01); public static int MAKELANGID(int primary, int sub) { return ((((ushort)(sub)) << 10) | (ushort)(primary)); } public static int MAKELCID(int lgid) { return MAKELCID(lgid, 0x0); } public static int MAKELCID(int lgid, int sort) { return ((0xFFFF & lgid) | (((0x000f) & sort) << 16)); } } class Program { static void Main() { System.Console.WriteLine(NativeMethods.LOCALE_USER_DEFAULT); } }
启动后,控制台上将显示以下内容:0。现在,我们交换
LOCALE_USER_DEFAULT和
LANG_USER_DEFAULT字段的声明。 程序的结果格式为:1024。我认为这里无可奉告了。
PVS-Studio:
V3080可能取消空引用。 考虑检查“ ces”。 第562章
protected void DeserializeStatement( IDesignerSerializationManager manager, CodeStatement statement) { .... CodeExpressionStatement ces = statement as CodeExpressionStatement; if (ces != null) { .... } else { .... DeserializeExpression(manager, null, ces.Expression);
应该“下降”的代码足够稳定,因为仅当
ces引用为
null时,您才能进入
else分支。
另一个类似的例子:
PVS-Studio:
V3080可能取消空引用。 考虑检查“ comboBox”。 ComboBox.cs 6610
public void ValidateOwnerDrawRegions(ComboBox comboBox, ....) { .... if (comboBox != null) { return; } Rectangle topOwnerDrawArea = new Rectangle(0, 0, comboBox.Width, innerBorder.Top); .... }
矛盾的代码。 显然,他们通过编写
if(comboBox!= Null)而不是
if(comboBox == null)来混合测试。 这样-我们将收到下一个
NullReferenceException 。
我们检查了两个非常明显的错误
V3080 ,您可以在其中直观地跟踪方法中可能使用空引用的情况。 但是
V3080诊断更加智能,可以在方法调用链中查找类似的错误。 不久前,我们显着增强了数据流和过程间分析的机制。 您可以在文章“
C#8.0和静态分析中的可空引用类型 ”中阅读有关此内容的信息。 这是在WinForms中发现的类似错误:
PVS-Studio:
V3080'reader.NameTable '中的方法内部可能存在空取消引用。 考虑检查第一个参数:contentReader。 ResXResourceReader.cs 267
private void EnsureResData() { .... XmlTextReader contentReader = null; try { if (fileContents != null) { contentReader = new XmlTextReader(....); } else if (reader != null) { contentReader = new XmlTextReader(....); } else if (fileName != null || stream != null) { .... contentReader = new XmlTextReader(....); } SetupNameTable(contentReader);
查看方法主体中的变量
contentReader会发生什么。 在使用空引用进行初始化之后,由于检查之一,该链接将被初始化。 但是,一系列检查并不以
else块结尾。 这意味着在极少数情况下(或由于将来的重构),链接仍然可以保持零。 接下来,它将被传递到
SetupNameTable方法,在此方法中无需任何验证即可使用它:
private void SetupNameTable(XmlReader reader) { reader.NameTable.Add(ResXResourceWriter.TypeStr); reader.NameTable.Add(ResXResourceWriter.NameStr); .... }
这可能是不安全的代码。
还有一个错误,分析仪必须经过一系列调用才能确定问题:
PVS-Studio:
V3080可能取消空引用。 考虑检查“布局”。 156,第156章
private static Rectangle GetAnchorDestination( IArrangedElement element, Rectangle displayRect, bool measureOnly) { .... AnchorInfo layout = GetAnchorInfo(element); int left = layout.Left + displayRect.X; .... }
分析器声称可以从
GetAnchorInfo方法获取空引用,该方法将在计算
左值时引发异常。 让我们遍历整个呼叫链,并检查是否是这样:
private static AnchorInfo GetAnchorInfo(IArrangedElement element) { return (AnchorInfo)element.Properties.GetObject(s_layoutInfoProperty); } public object GetObject(int key) => GetObject(key, out _); public object GetObject(int key, out bool found) { short keyIndex = SplitKey(key, out short element); if (!LocateObjectEntry(keyIndex, out int index)) { found = false; return null; }
实际上,在某些情况下,关闭调用链的
GetObject方法将返回
null ,而无需任何其他检查,该方法将传递给调用方法。 可能,
GetAnchorDestination方法应提供这种情况。
在WinForms代码中,有很多这样的错误,
超过70个 。 它们都是相似的,因此在本文中不再赘述。
PVS-Studio:
V3091实证分析。 字符串文字中可能存在错字:“ ShowCheckMargin”。 “ ShowCheckMargin”一词可疑。 PropertyNames.cs 136
internal class PropertyNames { .... public static readonly string ShowImageMargin = "ShowCheckMargin"; ... public static readonly string ShowCheckMargin = "ShowCheckMargin"; .... }
一个不太容易发现的错误的好例子。 初始化类字段时,它们使用相同的值,尽管代码的作者显然并不这么认为(应归咎于复制粘贴)。 分析器通过比较变量名称和分配的字符串的值来得出此结论。 我只给出了错误行,但在代码编辑器中查看了它的外观:
正是对此类错误的检测证明了静态分析工具的强大功能和无限关注。
PVS-Studio:
V3095在对null进行验证之前,已使用'currentForm'对象。 检查行:3386、3404。Application.cs 3386
private void RunMessageLoopInner(int reason, ApplicationContext context) { .... hwndOwner = new HandleRef( null, UnsafeNativeMethods.GetWindowLong( new HandleRef(currentForm, currentForm.Handle),
经典版 使用
currentForm变量无需任何检查。 但是在代码的更深处,它检查
空相等性。 在这种情况下,我建议您在使用引用类型以及使用静态分析器时要格外小心:)。
另一个类似的错误:
PVS-Studio:
V3095在对null进行验证之前,已使用“ backgroundBrush”对象。 检查行:2331,2334。DataGrid.cs 2331
public Color BackgroundColor { .... set { .... if (!value.Equals(backgroundBrush.Color))
在WinForms代码中,我遇到了
60多个此类错误。 我认为,所有这些都非常关键,需要开发人员的注意。 但是在本文中,谈论它们并不是那么有趣,因此,我将只限于上面讨论的两个。
PVS-Studio:
V3125已使用'_propInfo'对象,并在不同的执行分支中针对null对其进行了验证。 检查行:996、982。Binding.cs 996
private void SetPropValue(object value) { .... if (....) { if .... else if (_propInfo != null) .... } else { _propInfo.SetValue(_control, value); } .... }
要完成图片,这也是一种经典的bug
V3125 。 相反的情况。 首先,安全地使用可能为null的引用,检查是否为
null ,但随后他们不再在代码中这样做。
还有另一个类似的错误:
PVS-Studio:
V3125在针对null进行验证之后,使用了“所有者”对象。 检查行:64,60。FlatButtonAppearance.cs 64
public int BorderSize { .... set { .... if (owner != null && owner.ParentInternal != null) { LayoutTransaction.DoLayoutIf(....); } owner.Invalidate();
美人 但这是从外部研究人员的角度来看的。 实际上,除了这两个
V3125之外 ,分析器还在WinForms代码中发现了
50多个类似的模式。 开发人员有工作要做。
最后,我认为这是一个非常有趣的错误。
PVS-Studio:
V3137分配了“ hCurrentFont”变量,但该变量未在功能结束时使用。 DeviceContext2.cs 241
sealed partial class DeviceContext : .... { WindowsFont selectedFont; .... internal void DisposeFont(bool disposing) { if (disposing) { DeviceContexts.RemoveDeviceContext(this); } if (selectedFont != null && selectedFont.Hfont != IntPtr.Zero) { IntPtr hCurrentFont = IntUnsafeNativeMethods.GetCurrentObject( new HandleRef(this, hDC), IntNativeMethods.OBJ_FONT); if (hCurrentFont == selectedFont.Hfont) {
让我们看看分析器警告了什么,以及为什么为变量分配了一个值,但以后在该方法中不使用它的事实可能表明存在问题。
在文件
DeviceContext2.cs中声明了部分类。
DisposeFont方法用于处理图形后释放资源:设备上下文和字体。 为了更好地理解,我提供了整个
DisposeFont方法。 注意局部变量
hCurrentFont 。 问题在于,在方法中声明此变量将隐藏相同名称的类字段。 我发现
DeviceContext类的两个方法
都使用了一个名为
hCurrentFont的
字段 :
public IntPtr SelectFont(WindowsFont font) { .... hCurrentFont = font.Hfont; .... } public void ResetFont() { .... hCurrentFont = hInitialFont; }
看一下
ResetFont方法。 最后一行正是
DisposeFont方法在嵌套
if块中
所做的操作(分析器指向此位置)。 在同一个名称的
hCurrentFont字段是在
DeviceContext.cs文件的部分类的另一部分中
声明的 :
sealed partial class DeviceContext : .... { .... IntPtr hInitialFont; .... IntPtr hCurrentFont;
因此,犯了一个明显的错误。 另一个问题是它的重要性。 现在,由于
DisposeFont方法在标有“选择返回初始字体”注释的部分中
起作用 ,因此
hCurrentFont字段将不会被分配一些初始值。 我认为只有代码作者才能给出确切的结论。
结论因此,这一次我不得不“责骂” MS。 在WinForms中,存在许多错误,需要开发人员密切注意。 也许这是由于MS在.NET Core 3和包括WinForms在内的组件上匆忙工作所致。 我认为WinForms代码仍然“潮湿”,但我希望情况会很快好转。
出现大量错误的第二个原因可能是我们的分析仪更好地寻找了它们:)。
顺便说一下,我的同事Sergey Vasiliev的一篇文章很快就会发表,他在其中搜索并发现了.NET Core库代码中的许多问题。 我希望他的工作也将有助于改善.NET平台的性能,因为我们始终尝试将开发人员的项目分析结果传达给开发人员。
好吧,对于那些想要自己改进产品或进行研究以发现他人项目中的错误的人,我建议
下载并尝试使用 PVS-Studio。
所有干净的代码!

如果您想与讲英语的读者分享这篇文章,请使用以下链接:Sergey Khrenov。
WinForms:错误,福尔摩斯