目录
传递给TaskCompletionSource的参数调用了错误的构造函数
不要在P/Invokes的字符串参数上使用OutAttribute
介绍
这是编写代码的激动人心的时刻!特别是对于.NET开发人员而言,该平台将变得越来越智能。现在,默认情况下,我们现在在.NET SDK中包含丰富的诊断信息和代码建议。在需要安装NuGet软件包或其他独立工具之前,请先进行更多代码分析。现在,您将自动在新的.NET 5 SDK中获得它们。
过去,我们一直不愿意向C#添加新警告。这是因为从技术上来说,对于将警告设置为错误的用户而言,添加新警告是一项重大的源变更。但是,多年来,我们遇到了很多情况,我们也确实想警告人们某些错误,从常见的编码错误到常见的API滥用模式。
从.NET 5开始,我们将介绍C#编译器中要调用的AnalysisLevel内容,以安全的方式为这些模式引入警告。将所有面向.NET 5的项目的默认分析级别设置为5,这意味着将引入更多警告(以及修复建议)。
让我们讨论一下项目中AnalysisLevel可能的值意味着什么。我们应该注意的第一件事:除非覆盖默认值,否则AnalysisLevel将根据目标框架进行设置:
目标框架 | 默认为 AnalysisLevel |
net5.0 | 5 |
netcoreapp3.1 或更低 | 4 |
netstandard2.1 或更低 | 4 |
.NET Framework 4.8 或更低 | 4 |
但是,数字0-3呢?这是每个分析级别值含义的更详细说明。
AnalysisLevel | 对C#编译器的影响 | 高级平台API分析 |
5 | 获取新的编译器语言分析(详细信息如下) | 是 |
4 | 与之前版本中向c#编译器传递-warn:4相同 | 没有 |
3 | 与之前版本中向c#编译器传递-warn:3相同 | 没有 |
2 | 与之前版本中向c#编译器传递-warn:2相同 | 没有 |
1个 | 与之前版本中向c#编译器传递-warn:1相同 | 没有 |
0 | 与之前版本中向c#编译器传递-warn:0相同,关闭所有发出的警告 | 没有 |
由于AnalysisLevel与项目的目标框架相关,除非您更改代码目标,否则您将永远不会更改默认分析级别。不过,您可以手动设置分析级别。例如,即使我们以.NET Core App 3.1或.NET Standard(因此AnalysisLevel默认为4)为目标,您仍然可以选择更高级别。
这是执行此操作的示例:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<!-- get more advanced warnings for this project -->
<AnalysisLevel>5</AnalysisLevel>
</PropertyGroup>
</Project>
如果您始终希望处于受支持的最高分析级别,则可以在项目文件中指定latest:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<!-- be automatically updated to the newest stable level -->
<AnalysisLevel>latest</AnalysisLevel>
</PropertyGroup>
</Project>
如果您非常喜欢 冒险,并且想尝试实验性的编译器和平台分析,则可以指定preview获取最新的、前沿的代码诊断程序。
请注意,当您使用latest 或 preview时,计算机之间的分析结果可能会有所不同,具体取决于可用的SDK 和它提供的最高分析级别。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<!-- be opted into experimental code correctness warnings -->
<AnalysisLevel>preview</AnalysisLevel>
</PropertyGroup>
</Project>
最后,我们none的意思是 “我不想看到任何新的警告。” 在这种模式下,您将不会获得任何高级API分析或新的编译器警告。如果您需要更新框架但还没有准备好接受新的警告,则这很有用。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5</TargetFramework>
<!-- I am just fine thanks -->
<AnalysisLevel>none</AnalysisLevel>
</PropertyGroup>
</Project>
您还可以通过“代码分析”属性页在Visual Studio中配置项目的分析级别。只需从解决方案资源管理器导航到项目属性页面。然后转到“代码分析”选项卡。
将来,我们将为每个.NET版本添加新的分析级别。我们的目标是确保一个给定的分析水平始终表示相同的一组分析默认值(规则和他们的严重性)。如果我们想默认启用现有规则,则将在下一个分析级别中执行此操作,而不是更改现有级别。这样可以确保给定的项目/源始终产生相同的警告,而不管SDK是多么新(当然,除非项目使用preview 或 latest)。
由于所有.NET 5项目都将选择“分析级别5” ,因此让我们看一下将提供的一些新警告和建议。
分析级别5中出现的所有新警告和错误
到.NET 5发行时, 粗体字的级别将达到5级。其余是新警告,今天将在.NET 5 Preview 8和Visual Studio 2019 16.8 Preview 2中提供!
ID | 类别 | 严重程度 | 描述 |
互通性 | 警告 | 当代码不适用于所有平台时发出警告 | |
互通性 | 警告 | 不要在字符串参数上为P/Invokes使用OutAttribute | |
性能 | 警告 | 适当时使用AsSpan而不是基于范围的索引器作为字符串 | |
可靠性 | 警告 | 不要与值类型一起使用ReferenceEquals | |
可靠性 | 警告 | 不要在循环中使用stackalloc | |
可靠性 | 警告 | 不要为从MemoryManager派生的类型定义终结器 | |
用法 | 警告 | 重新抛出以保留堆栈详细信息 | |
用法 | 警告 | 参数传递给TaskCompletionSource调用错误的构造函数 | |
正确性 | 警告 | 跟踪程序集中结构的明确分配 | |
正确性 | 错误 | 不允许对非引用类型进行锁定 | |
正确性 | 错误 | 不允许as或is使用静态类型 | |
用法 | 警告 | 当表达式始终为假或真时发出警告 |
常见错误警告
第一组新警告旨在发现潜在的错误,通常在较大的代码库中。无需额外的编译器分析,这些内容很容易引入。
当表达式始终为真或假时发出警告
这个新警告非常普遍。考虑以下代码:
public void M(DateTime dateTime)
{
if (dateTime == null) // warning CS8073
{
return;
}
}
DateTime是一个struct并且struct不能是null。从.NET 5开始,我们将使用CS8073来警告这种情况。警告消息是:
警告CS8073:表达式类型的结果始终为“false”,因为类型“DateTime”的值永远不会等于类型“DateTime?”的“null”
似乎很明显,此代码在做什么是孤立的,没有必要,但请注意,这种检查可能会在具有10个要验证参数的方法中进行。要解决此问题,您可以删除代码(由于其始终为false,因此无论如何都不会做任何事情),或将其类型更改为DateTime?,如果null是该参数的预期值。
public void M(DateTime? dateTime) // We accept a null DateTime
{
if (dateTime == null) // No Warnings
{
return;
}
}
不允许as或is在静态类型
下一个是一个不错的小改进:
static class Fiz
{
}
class P
{
bool M(object o)
{
return o is Fiz; // CS7023
}
}
因为Fiz是静态类,所以像这样的实例对象o将永远无法成为该类型的实例。我们将收到以下警告:
警告CS7023“is”或“as”运算符的第二个操作数可能不是静态类型“Fiz”
解决此问题的方法是重构我们的代码(也许我们实际上是在开始时检查错误的类型),或者使类Fiz成为非静态的:
class Fiz
{
}
class P
{
bool M(object o)
{
return o is Fiz; // no error
}
}
不允许对非引用类型进行锁定
锁定非引用类型(如 int)不会执行任何操作,因为它们是按值传递的,因此它们的不同版本存在于每个堆栈帧中。在过去,对于像lock(5)这样简单的情况,我们会警告你对非引用类型的锁定,但是直到最近,我们才会警告你像下面这样的开放泛型。
public class P
{
public static void GetValue<TKey>(TKey key)
{
lock (key) // CS0185
{
}
}
static void Main()
{
GetValue(1);
}
}
这是一个错误,因为传入int(在此不受限制的泛型下是允许的)实际上不会正确锁定。我们将看到此错误:
错误CS0185'TKey'不是锁语句要求的引用类型
为了解决这个问题,我们需要指出该GetValue 方法仅应提供引用类型。我们可以使用泛型类型约束where TKey : class来做到这一点。
public class P
{
public static void GetValue<TKey>(TKey key) where TKey : class
{
lock (key) // no error
{
}
}
}
重新抛出以保留堆栈详细信息
我们都是优秀的开发人员,所以我们的代码永远不会抛出异常,对吗?好,即使是最好的开发人员也需要处理.NET中的异常,而新程序员遇到的常见陷阱之一是:
try
{
throw new Exception();
}
catch (Exception ex)
{
// probably logging some info here...
// rethrow now that we are done
throw ex; // CA2200
}
在学校里,我得知如果有人向我扔球而我接住了球,我就必须把球扔回去!这样的隐喻使很多人相信throw ex是重新抛出该异常的正确方法。可悲的是,这将改变原始异常中的堆栈。现在,您将收到警告,说明这种情况正在发生。看起来像这样:
警告CA2200重新抛出捕获的异常更改堆栈信息
在几乎所有情况下,此处正确的做法是仅使用throw关键字而不提及我们捕获的异常的变量。
try
{
throw new Exception();
}
catch (Exception ex)
{
// probably logging some info here...
// rethrow now that we are done
throw;
}
我们还提供了代码修复程序,可以轻松地一次修复您的文档,项目或解决方案中的所有这些问题!
不要将ReferenceEquals与值类型一起使用
Equality是.NET中一个棘手的话题。下一个警告试图使意外地通过引用比较一个struct。考虑下面的代码:
int int1 = 1;
int int2 = 1;
Console.WriteLine(object.ReferenceEquals(int1, int2)); // warning CA2013
这将装箱两个int,而ReferenceEquals将总是返回false作为结果。我们将看到以下警告说明:
警告CA2013:请勿将值类型为'int'的参数传递给'ReferenceEquals'。由于值装箱,对“ReferenceEquals”的调用将始终返回“false”。
解决此错误的方法是使用相等运算符== 或object.Equals类似的方法:
int int1 = 1;
int int2 = 1;
Console.WriteLine(int1 == int2); // using the equality operator is fine
Console.WriteLine(object.Equals(int1, int2)); // so is object.Equals
跟踪跨程序集的structs的明确分配
下一个警告是使很多人惊讶地发现这还不是警告:
using System.Collections.Immutable;
class P
{
public void M(out ImmutableArray<int> immutableArray) // CS0177
{
}
}
该规则全部与明确分配有关,这是C#中的一项有用功能,可确保您不会忘记为变量分配值。
警告CS0177:在控制离开当前方法之前,必须将out参数'immutableArray'分配给它
今天CS0177已经针对几种不同情况发布了该报告,但先前显示的情况并未发布。这里的历史是,这是一个错误,可以一直追溯到C#编译器的原始实现。以前,C#编译器在计算确定分配时会忽略从元数据导入的值类型中引用类型的私有字段。这个极其具体的错误意味着类似ImmutableArray这样的类型能够逃脱明确的赋值分析。
现在,编译器将为您正确地指出错误,您可以通过简单地确保始终为其分配值来修复它,如下所示:
using System.Collections.Immutable;
class P
{
public bool M(out ImmutableArray<int> immutableArray) // no warning
{
immutableArray = ImmutableArray<int>.Empty;
}
}
.NET API使用不正确的警告
接下来的示例是有关正确使用.NET库的信息。分析级别可以防止当今对现有.NET API的不当使用,但同时也对.NET库的发展产生影响。如果设计了有用的API但有可能被滥用,则还可以与新API一起添加检测滥用的新警告。
不要为从MemoryManager派生的类型定义终结器
当您想要实现自己的Memory<T>类型时,MemoryManager是一个有用的类。这不是您可能会发现自己做很多事情的事情,但是当您需要它时,您确实需要它。此新警告会触发以下情况:
class DerivedClass <T> : MemoryManager<T>
{
public override bool Dispose(bool disposing)
{
if (disposing)
{
_handle.Dispose();
}
}
~DerivedClass() => Dispose(false); // warning CA2015
}
向此类型添加终结器可能会在垃圾收集器中引入漏洞,我们所有人都希望避免这些漏洞!
警告CA2015 向派生自MemoryManager<T>的类型添加终结器可以允许在Span<T>仍在使用内存时释放内存。
解决方法是删除此终结器,因为它将导致程序中非常细微的错误,难以发现和修复。
class DerivedClass <T> : MemoryManager<T>
{
public override bool Dispose(bool disposing)
{
if (disposing)
{
_handle.Dispose();
}
}
// No warning, since there is no finalizer here
}
传递给TaskCompletionSource的参数调用了错误的构造函数
此警告通知我们,我们仅使用了错误的枚举。
var tcs = new TaskCompletionSource(TaskContinuationOptions.RunContinuationsAsynchronously); // warning CA2247
除非你已经意识到这个问题,否则你可能会在看到它之前盯着它看一会儿。问题在于此构造函数不带TaskContinuationOptions枚举,而是带TaskCreationOptions枚举。发生的事情是,我们正在调用TaskCompletionSource的构造函数,它接受object!考虑到它们的名称有很多相似性,以及它们具有非常相似的值,这个错误很容易造成。
警告CA2247:参数包含TaskContinuationsOptions枚举而不是TaskCreationOptions枚举。
解决方法是传递正确的枚举类型:
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); // no warning
当代码不适用于所有平台时发出警告
这最后一个很傻!在这里,我不会涉及所有复杂内容(期待有关该主题的未来博客文章)。但是,此处发出警告的目的是让您知道,您正在调用的API可能不适用于您要构建的所有目标。
假设我有一个可以在Linux和Windows上运行的应用程序。我有一种方法可用于获取在其下创建日志文件的路径,并且该方法根据其运行位置具有不同的行为。
private static string GetLoggingPath()
{
var appDataDirectory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var loggingDirectory = Path.Combine(appDataDirectory, "Fabrikam", "AssetManagement", "Logging");
// Create the directory and restrict access using Windows
// Access Control Lists (ACLs).
var rules = new DirectorySecurity(); // CA1416
rules.AddAccessRule(
new FileSystemAccessRule(@"fabrikam\log-readers",
FileSystemRights.Read,
AccessControlType.Allow)
);
rules.AddAccessRule(
new FileSystemAccessRule(@"fabrikam\log-writers",
FileSystemRights.FullControl,
AccessControlType.Allow)
);
if (!OperatingSystem.IsWindows())
{
// Just create the directory
Directory.CreateDirectory(loggingDirectory);
}
else
{
Directory.CreateDirectory(loggingDirectory, rules);
}
return loggingDirectory;
}
我正确使用OperatingSystem帮助程序使用OperatingSystem.IsWindows()检查操作系统是否为Windows操作系统,并且仅通过该情况下的规则,但实际上我已经使用了特定于平台的API,这些API在Linux上将不起作用!
警告CA1416:“Linux”上不支持“DirectorySecurity”
解决此问题的正确方法是将我所有平台特定的代码移到else语句中。
private static string GetLoggingPath()
{
var appDataDirectory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var loggingDirectory = Path.Combine(appDataDirectory, "Fabrikam", "AssetManagement", "Logging");
if (!OperatingSystem.IsWindows())
{
// Just create the directory
Directory.CreateDirectory(loggingDirectory);
}
else
{
// Create the directory and restrict access using Windows
// Access Control Lists (ACLs).
var rules = new DirectorySecurity(); // CA1416
rules.AddAccessRule(
new FileSystemAccessRule(@"fabrikam\log-readers",
FileSystemRights.Read,
AccessControlType.Allow)
);
rules.AddAccessRule(
new FileSystemAccessRule(@"fabrikam\log-writers",
FileSystemRights.FullControl,
AccessControlType.Allow)
);
Directory.CreateDirectory(loggingDirectory, rules);
}
return loggingDirectory;
}
低级编码帮助
在编写高性能应用程序时,还有一些警告很有用。这些下一组警告确保您在这些情况下无需牺牲安全性。
不要在P/Invokes的字符串参数上使用OutAttribute
有时您需要与本机代码进行互操作。.NET具有平台调用(P/Invokes)的概念,以使此过程更容易。但是,在与.NET中的本机库之间发送数据方面存在一些陷阱。考虑下面的代码:
[DllImport("MyLibrary")]
private static extern void Goo([Out] string s); // warning CA1417
除非您对编写P/Invokes非常熟悉,否则这里出什么问题并不明显。通常,您会将OutAttribute应用于运行时不知道的类型,以指示应如何整理类型。OutAttribute表示您正在按值传递数据。但是,按值传递字符串是没有意义的,并且可能使运行时崩溃。
警告CA1417请勿对通过值传递的字符串参数“s”使用“OutAttribute”。如果需要将修改后的数据编组回调用者,请使用'out'关键字通过引用传递字符串。
解决方法是将其视为正常的out参数(通过引用传递)。
[DllImport("MyLibrary")]
private static extern void Goo(out string s); // no warning
或者,如果您不需要将字符串编组回调用者,则可以执行以下操作:
[DllImport("MyLibrary")]
private static extern void Goo(string s); // no warning
在适当的时候使用AsSpan而不是基于范围的字符串索引器
这就是确保您不会意外分配字符串。
class Program
{
public void TestMethod(string str)
{
ReadOnlySpan<char> slice = str[1..3]; // CA1831
}
}
在上面清楚的代码中,开发人员的意图是使用C#中基于范围的新索引功能对字符串进行索引。不幸的是,这实际上将分配一个字符串,除非您首先将该字符串转换为一个span。
警告CA1831在“string”上使用“AsSpan”而不是基于“System.Range”的索引器,以避免创建不必要的数据副本
解决方法是在这种情况下仅添加AsSpan调用:
class Program
{
public void TestMethod(string str)
{
ReadOnlySpan<char> slice = str.AsSpan()[1..3]; // no warning
}
}
不要在循环中使用stackalloc
当您要确保正在垃圾回收器上进行的操作很容易时,该stackalloc 关键字非常有用。过去,仅在不安全的代码中允许使用stackalloc,但自C#8起,只要将该变量分配给Span<T> 或 ReadOnlySpan<T>,也可以在unsafe块之外使用。
class C
{
public void TestMethod(string str)
{
int length = 3;
for (int i = 0; i < length; i++)
{
Span<int> numbers = stackalloc int[length]; // CA2014
numbers[i] = i;
}
}
}
在堆栈上分配大量内存可能会导致著名的StackOverflow异常,在该异常中,我们在堆栈上分配的内存超出了允许的范围。循环分配尤其危险。
警告CA2014潜在的堆栈溢出。将stackalloc移出循环。
解决方法是将我们的stackalloc移出循环。
class C
{
public void TestMethod(string str)
{
int length = 3;
Span<int> numbers = stackalloc int[length]; // no warning
for (int i = 0; i < length; i++)
{
numbers[i] = i;
}
}
}
配置分析级别
既然您已经看到了这些警告有多么有用,那么您可能永远不想回到没有正确警告的世界吗?好吧,我知道世界并不总是那样运作。如本文开头所提到的,这些都是源代码突破性的更改,您应该能够按照自己的时间表进行调整。我们现在要引入此功能的部分原因是为了获得两个方面的反馈:
- 如果我们要介绍的一小部分警告过于破坏性
- 如果用于调整警告的机制足以满足您的需求
回到.NET Core 3.1分析级别:
如果您只想回到.NET 5之前的状态(这意味着您在.NET Core 3.1中得到的警告),您需要做的就是将项目文件中的分析级别设置为4。这是一个例子:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<!-- get the exact same warnings you used to -->
<AnalysisLevel>4</AnalysisLevel>
</PropertyGroup>
</Project>
仅关闭一个规则
如果有特定的警告认为您不适用于您的代码库,则可以使用editorconfig文件将其关闭。您可以通过将错误的严重性从错误列表中设置为“无”来执行此操作。
或通过在编辑器中显示警告的灯泡菜单中选择“无”
关闭警告的单个实例
如果您希望警告几乎始终存在并且仅在少数情况下不显示警告,则可以使用灯泡菜单执行以下任一操作:
- 在源中禁止显示。
- 在单独的抑制文件中抑制它。
- 使用属性在源中禁止显示。
总结
我希望这使您对.NET 5中对代码分析的所有改进感到兴奋,并请提供有关此经验的反馈。