对Reverse扩展方法的突然修改导致构建错误
下面是我拥有的代码(已简化到最小程度)
using System;
using System.Linq;
namespace Test;
public class TestSomething
{
public byte[] DoAThing(byte[] input)
{
return input.Reverse().ToArray();
}
}
实际的代码会对它做一些有用的处理,这里只是为了展示问题。
上个月一切都正常。今天在我们的git服务器上触发了一个自动化代码扫描,针对那个 Reverse 调用出现编译错误,因为显然 Reverse 现在是void?
错误CS0023: 运算符 '.' 不能应用于类型为 'void'
请注意,代码没有改变。由于公司策略,这个分支被锁定(代码进入生产后,我们会创建一个分支并锁定它,并对已投入生产的代码进行定期扫描)。
本地构建时,它编译通过。Reverse 的签名是 System.Collections.Generic.IEnumerable<byte> System.Collections.Generic.IEnumerable<byte>.Reverse()
因此它是在 IEnumerable<byte> 上的一个扩展方法,返回一个 IEnumerable<byte>,至少在我的机器上是这样。
这个项目在.NET 4.8和 .NET 8下都可以构建,在构建服务器上,在这两种框架下得到相同的错误。
我确实发现如果在顶部的usings部分加入 using static System.Linq.Enumerable;,在构建服务器上就能编译通过,但那是在锁定分支的一个分支上。如果我能通过Pull Request解决这个问题,我当然愿意,但这不被允许。
那么问题是:在我的仓库之外可能发生了什么改变?是否存在另一个命名空间中的扩展方法可以就地执行Reverse,它以某种方式找到了它?在我的仓库之外是否有某些配置强制默认的 using 语句?
解决方案
这似乎与一个在C# 14和 .NET 10中有文档记录的破坏性变更有关,即 C# 14中的第一类Span支持 的变更。
来自 第一类Span类型:破坏性变更:在数组上调用Reverse:
破坏性变更
由于任何改变现有场景转换的提案都会引入一些新的破坏性变更。下面是一些例子:
在数组上调用
Reverse当x 是类型
T[]的一个实例时,对x.Reverse()的调用以前会绑定到IEnumerable<T> Enumerable.Reverse<T>(this IEnumerable<T>),现在绑定到void MemoryExtensions.Reverse<T>(this Span<T>)。不幸的是,这些API不兼容(后者就地执行反转并返回void)。.NET 10通过添加一个针对数组的重载
IEnumerable<T> Reverse<T>(this T[]), 见 https://github.com/dotnet/runtime/issues/107723。
void M(int[] a) { foreach (var x in a.Reverse()) { } // fine previously, an error now (`Reverse` returns `void`) foreach (var x in Enumerable.Reverse(a)) { } // workaround }设计会议:https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#reverse
如果你在使用C# 14对 .NET 10框架库进行构建,缓解措施应该会生效并维持当前行为。但在你的问题中你提到你使用的是 .NET 4.8和 .NET 8。在那些早期版本中第一类span尚未实现,因此同样不应有问题,因为旧的enumerable重载应该会被绑定。
然而,如果你将C# 14语法与较早的框架版本混用,或反过来[1],缓解措施将不会生效,你可能会遇到你所看到的确切编译错误。事实上,在 评论 中你也提到:
我们有一个计划任务每月运行一次,启动一个GitHub Action来执行构建。构建的一部分会让代码通过几个漏洞扫描器(Fortify和 Sonatype)。不过它们是在初始构建之后作为步骤运行,因此不太可能引起问题。哦,另外,.NET SDK是 10.0.103,尽管所讨论的项目是.NET 8和 .NET 4.8。如果重要的话,这只是一个单元测试项目。
鉴于单元测试项目使用的是.NET 10,因此基于.NET 10的扫描工具与主项目为.NET 8的组合可能是造成编译错误的原因。
或者,正如 Ivan Petrov 和 Jon Skeet 所建议,你可能在某些的 .csproj 文件中设置了 [<LangVersion>latest</LangVersion>],例如如下所示:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<LangVersion>latest</LangVersion> <!-- Setting this manually causes the compilation error when built using a .NET 10 tool chain -->
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>Test</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
这样做将在使用.NET 10的工具链构建时产生 error CS0023: Operator '.' cannot be applied to operand of type 'void'。
无论你究竟如何将.NET 8与 C# 14混用,要解决编译错误,应该在所有项目中使用一致的.NET和 C#版本——包括主项目和单元测试项目。也就是说,要么让单元测试项目使用.NET 8,要么把所有项目都切换到.NET 10。完成后,务必不要将早期的.NET版本与“未来”的C#版本混用。
如果你在某些 csproj 文件中设置了 <LangVersion>latest</LangVersion>,应将其移除,微软的文档明确警告不要使用此设置,因为它可能导致机器特定的构建失败:
警告
不要将
LangVersion元素设置为latest。latest设置意味着已安装的编译器使用其最新版本。latest的值可能会因机器而异,从而使构建不稳定。并且,它会启用在当前SDK中可能不包含的运行时或库功能所需的语言特性。
(事实上,他们的警告似乎恰好与您看到的问题吻合。)
[1] C# 14与 .NET 8的组合明确为 不受支持的组合:
C# 14仅在.NET 10及更新版本上受支持。