WinForms:错误,福尔摩斯

图片5

我们喜欢在Microsoft项目中搜索错误。 怎么了 很简单:他们的项目通常很容易检查(您可以在PVS-Studio具有便捷插件的Visual Studio环境中工作)并且它们包含的错误很少。 这就是为什么通常的工作算法如下:从MS查找并下载一个开源项目; 检查; 选择有趣的错误; 确保其中很少; 写一篇文章,不要忘记赞扬开发人员。 太好了! 双赢:花了一些时间,老板很高兴在博客中看到新材料,业力很好。 但是这一次“出了点问题”。 让我们看看我们在Windows Forms的源代码中发现了什么,以及我们这次是否应该高度评价Microsoft。

引言

在2018年12月上旬,微软宣布发布.NET Core 3 Preview 1.在不久之前(大约10月中旬),GitHub开始积极披露Windows Forms的来源-用于创建Windows桌面应用程序的.NET Core UI平台。 。 您可以在此处查看提交统计信息。 现在,任何人都可以下载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 { .... } .... } 

ifelse 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); // if the effects member contains valid info if ((cf.dwMask & RichTextBoxConstants.CFM_OFFSET) != 0) { selCharOffset = cf.yOffset; // <= } else { // The selection contains characters of different offsets, // so we just return the offset of the first character. selCharOffset = cf.yOffset; // <= } .... } .... } 

而且这里肯定存在复制粘贴错误。 无论条件如何, 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个这样的片段。 它看起来不再像是一种错误。 但是为什么他们这样做对我来说仍然是个谜。 我没有发现任何自动生成代码的迹象。 在编辑器中看起来像这样:

图片3

我会在列表中给您前十个警告:

  1. V3008为变量连续分配两次值。 也许这是一个错误。 检查行:785,784。ProfessionalColorTable.cs 785
  2. V3008为变量连续分配两次值。 也许这是一个错误。 检查行:787、786。ProfessionalColorTable.cs 787
  3. V3008为变量连续分配两次值。 也许这是一个错误。 检查行:789、788。ProfessionalColorTable.cs 789
  4. V3008为变量连续分配两次值。 也许这是一个错误。 检查行:791、790。ProfessionalColorTable.cs 791
  5. V3008为变量连续分配两次值。 也许这是一个错误。 检查行:797、796。ProfessionalColorTable.cs 797
  6. V3008为变量连续分配两次值。 也许这是一个错误。 检查行:799、798。ProfessionalColorTable.cs 799
  7. V3008为变量连续分配两次值。 也许这是一个错误。 检查行:807、806。ProfessionalColorTable.cs 807
  8. V3008为变量连续分配两次值。 也许这是一个错误。 检查行:815、814。ProfessionalColorTable.cs 815
  9. V3008为变量连续分配两次值。 也许这是一个错误。 检查行:817,816。ProfessionalColorTable.cs 817
  10. 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) // <= { // there was nothing to check return; } for (int j = 0; j < myGridTable.GridColumnStyles.Count; j++) { DataGridColumnStyle gridColumn = myGridTable.GridColumnStyles[j]; } checkHierarchy = false; } } 

返回运算符将永远不会执行。 最有可能的是,外部if块中的myGridTable!= 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方法生成了两个警告。 怎么了 根本不检查cscLeftcscRight值是否为null 。 在未成功转换为OrderedCodeStatementCollection类型后,他们可能会获得此值。 然后,将在最后一个返回表达式中引发异常。 当所有左右检查都通过并且未导致该方法的初步退出时,这种情况是可能的。

要修复代码,您应该在所有位置使用cscLeft / cscRight而不是left / right

PVS-Studio: V3020循环内无条件的“中断”。 SelectionService.cs 421

 void ISelectionService.SetSelectedComponents( ICollection components, SelectionTypes selectionType) { .... // Handle the click case object requestedPrimary = null; int primaryIndex; if (fPrimary && 1 == components.Count) { foreach (object o in components) { requestedPrimary = o; if (o == null) { throw new ArgumentNullException(nameof(components)); } break; } } .... } 

该片段指的是“代码气味”。 这里没有错误。 但是,人们对foreach循环的组织方式提出了疑问。 很清楚为什么需要在这里:因为需要提取作为ICollection传递的集合的元素。 但是,为什么最初设计用于单次迭代的循环(前提是集合组件中存在单个元素),却需要诸如break之类的额外支持? 答案可能如下:“从历史上讲,这已经成为事实。” 该代码看起来很难看。

PVS-Studio: V3022表达式'ocxState!= Null'始终为true。 AxHost.cs 2186

 public State OcxState { .... set { .... if (value == null) { return; } .... ocxState = value; if (ocxState != null) // <= { axState[manualUpdate] = ocxState._GetManualUpdate(); licenseKey = ocxState._GetLicenseKey(); } else { axState[manualUpdate] = false; licenseKey = 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 ,然后针对null进行检查。 您好, 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) { .... // Add the column to our internal array int columnCount = (columnHeaders == null ? 0 : columnHeaders.Length); if (columnCount > 0) { ColumnHeader[] newHeaders = new ColumnHeader[columnCount + 1]; if (columnCount > 0) { System.Array.Copy(columnHeaders, 0, newHeaders, 0, columnCount); } .... } .... } 

一个看似无害的错误。 实际上,执行了不会影响操作逻辑的不必要的检查。 有时甚至在需要再次检查某个视觉组件的状态时也可以完成此操作,例如,获取列表中的条目数。 但是在这种情况下,将对局部变量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(); // <= if (lpFrameInfo != null) { lpFrameInfo.cb = Marshal.SizeOf<NativeMethods.tagOIFI>(); lpFrameInfo.fMDIApp = false; .... } return NativeMethods.S_OK; } 

一个不明显的错误。 是的, lprcClipRect参数实际上是使用新值初始化的,而不以任何方式使用它。 但是,最终会导致什么呢? 我认为在调用代码中的某个地方,通过此参数传递的引用将保持不变,尽管并非如此。 确实,请欣赏此方法中其他变量的处理。 甚至其名称(“ Get”前缀)也暗示将通过传递的参数在方法内部执行一些初始化。 就是这样。 前两个参数( ppFrameppDoc )与out修饰符一起传递,并且它们获得新值。 引用lprcPosRectlpFrameInfo用于访问和初始化类字段。 只有lprcClipRect脱颖而出。 此参数可能需要outref修饰符。

PVS-Studio: V3066传递给“ AdjustCellBorderStyle”方法的参数的可能错误顺序:“ isFirstDisplayedRow”和“ isFirstDisplayedColumn”。 DataGridViewComboBoxCell.cs 1934

 protected override void OnMouseMove(DataGridViewCellMouseEventArgs e) { .... dgvabsEffective = AdjustCellBorderStyle( DataGridView.AdvancedCellBorderStyle, dgvabsPlaceholder, singleVerticalBorderAdded, singleHorizontalBorderAdded, isFirstDisplayedRow, // <= isFirstDisplayedColumn); // <= .... } 

分析器怀疑最后两个参数混淆了。 让我们看一下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。 我付出了更多努力,并编写了一个模拟情况的小型控制台程序。 我用其实际值替换了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_DEFAULTLANG_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); // <= .... } finally { .... } .... } 

查看方法主体中的contentReader变量发生了什么。 使用null初始化后,将在其中一项检查中再次对其进行初始化。 但是一系列的检查并没有以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; } // We have found the relevant entry. See if // the bitmask indicates the value is used. if (((1 << element) & s_objEntries[index].Mask) == 0) { found = false; return null; } found = true; switch (element) { case 0: return s_objEntries[index].Value1; .... default: Debug.Fail("Invalid element obtained from LocateObjectEntry"); 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"; .... } 

一个很难找到的错误的好例子。 初始化类字段时,使用相同的值,尽管代码的作者显然不打算这样做(应归咎于复制粘贴)。 分析器通过比较变量名称和分配的字符串的值来得出此结论。 我只给出了有错误的行,但是您应该在代码编辑器中检查一下它的外观:

图片2

对此类错误的检测证明了静态分析工具的强大功能和无尽的关注范围。

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), // <= NativeMethods.GWL_HWNDPARENT)); .... if (currentForm != null && ....) .... } 

这是经典。 使用currentForm变量无需任何检查。 但是,然后在代码中检查是否为 。 在这种情况下,我建议您在使用引用类型时也要多加注意,并使用静态分析器:)。

还有一个这样的错误:

PVS-Studio: V3095在对null进行验证之前,已使用“ backgroundBrush”对象。 检查行:2331,2334。DataGrid.cs 2331

 public Color BackgroundColor { .... set { .... if (!value.Equals(backgroundBrush.Color)) // <= { if (backgroundBrush != null && BackgroundBrush != DefaultBackgroundBrush) .... } } } 

在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); } .... } 

为了完整性起见-也是一种经典的错误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) { // select initial font back in IntUnsafeNativeMethods.SelectObject(new HandleRef(this, Hdc), new HandleRef(null, hInitialFont)); hCurrentFont = hInitialFont; // <= } selectedFont.Dispose(disposing); selectedFont = null; } } .... } 

让我们看看是什么引起了分析仪的注意,以及为什么它可能指示出一个变量已分配值但在代码中从未使用过的问题。

DeviceContext2.cs文件包含部分类。 DisposeFont方法用于处理图形后释放资源:设备上下文和字体。 为了更好地理解,我给出了整个DisposeFont方法。 注意局部变量hCurrentFont 。 问题在于方法中此变量的声明隐藏了相同名称的类字段。 我发现了DeviceContext类的两个方法,其中使用了名称为hCurrentFont字段

 public IntPtr SelectFont(WindowsFont font) { .... hCurrentFont = font.Hfont; .... } public void ResetFont() { .... hCurrentFont = hInitialFont; } 

查看ResetFont方法。 如果,则最后一行正是DisposeFont方法在子块中执行的操作(这是分析器所指向的)。 在DeviceContext.cs文件的部分类的另一部分中声明了这个具有相同名称的hCurrentFont字段:

 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

干净的代码给大家!

Source: https://habr.com/ru/post/zh-CN462809/


All Articles