分析器:常见问题
分析器:常见问题
扑克前言
源生成器(增量生成器)由于它特殊的定位,关于它的调试十分困难。在这里分享一些调试它的经验。
另外经常有写类库,然后提供可以生成代码的Attribute给用户的需求,此时需要用到传递引用的知识点。
调试源生成器
源生成器执行时间
源生成器项目和普通的项目不同。
普通的会在你按下运行或调试后才会运行;而源生成器会在两种情况下运行:
重新生成解决方案或该项目时候运行,运行后会生成dll文件。在下一次启动VS的时候,会连着dll一起读取,所以可能会有VS找不到生成的文件导致报错,但可以正常运行的问题,重启VS即可。
在生成项目后第二次及以后打开项目时,每次对代码进行更改都会重新运行源生成器的dll,并实时将生成的代码加入到项目中。所以源生成器的执行效率很大程度关乎用户的编程手感。
以下程序段默认引用命名空间:
1 | using System.Diagnostics; |
启动调试器
在源生成器项目中,直接在Visual Studio用鼠标点击行号左边打的(红色圆形的)断点是没有用的,需要添加一条Debugger.Launch();
,表示启动了调试器。
如果程序运行到这条语句时,会弹出一个窗口,选择调试的程序:
建议选择自己的项目(在此图的第二个)即可。
点击OK后程序会停在Debugger.Launch();
处,此时可以插入Debugger.Break();
或直接鼠标点击插入断点。
如果要启动调试器,Debugger.Launch();
一定要放在源生成器刚开始的位置,而且不要插入多个,尤其不能插在多次执行的程序块内(如循环);若有需要可在其中打断点,否则第二次打开VS会一直弹窗。
生成中关闭调试器
有时运行一半时发现问题无需继续调试时,需要关闭调试器。但简单地终止调试可能无效,因为可能遇到另一个Debugger.Launch();
。
所以我们需要先停止生成,再关闭调试器。
生成→取消
(如果调试器没有关闭)调试→停止调试
关闭Visual Studio前
关闭之前我们应该先把Debugger.Launch();
删除或注释掉,并重新生成项目,以免下次打开VS的时候自动弹出调试器窗口。
类库中分析器的传递引用
从引用项目时的方式就可以看出,生成器项目本身是作为分析器(Analyzer)项目引入的:
1 | <ItemGroup> |
此时类库项目引用了源生成器项目,类库项目又被用户项目引用,那么问题是用户项目可以被源生成器生成代码吗?
答案一般是不能。
如果要实现这种效果,那需要满足两个条件:
类库项目应作为NuGet包被引用,而非项目引用。
类库项目将分析器包含进NuGet包。
综上,我们应该在生成NuGet包的项目内这样写:
1 | <ItemGroup> |
其中XXX.SourceGenerator.dll是源生成器项目的输出文件路径。
在引用该NuGet包之后,除了可以使用生成器外,其他该NuGet项目引用的类型也可以访问,这就是所谓“传递引用”。
其中ReferenceOutputAssembly="false"
是指虽然引用分析器的输出,但不引用他的类型(如class XXXGenerator
)。
注:如果项目没有使用NuGet包的必要,并且可以实现项目引用,又有此类需求;则可以简单地让用户项目按分析器引用源生成器项目即可。
分析器简介
本文分析器主要指以“Analyzer”模式引用的项目或NuGet包,例如源生成器(source generator)、代码分析器(analyzer)、代码修复器(codefixer)等
注:其中源生成器和代码分析器的“血缘关系”更近,可能因为都是.NET使用的分析器,而代码修复器是提供给Visual Studio使用的,所以关系疏远一些
分析器引用别的项目时的问题
根据MSDN和Visual Studio自带的代码示例,所有分析器和源生成器都必须包含以下两个库:
1 | Microsoft.CodeAnalysis.CSharp |
所有代码修复器都必须包含以下这个库:
1 | Microsoft.CodeAnalysis.CSharp.Workspaces |
肯定不止我一个人(bushi)想到,为什么不把一些最简单的类型、或者一些工具库给所有的分析器共享呢?这样引用时就不需要写字符串而是nameof(xxx)
了,多优雅。
可惜由于分析器项目的限制,除了以上说的必须包含的库,其他库都不能很方便地引用。但这也不是说每次写分析器都要手动实现.NET的新特性,Sergio Pedri1大佬实现了Poly#2(PolySharp)库,这个库实现了绝大部分可以实现的特性,并且可以被分析器项目“引用”。因为它是把所有需要的代码生成到你的项目里,而非直接引用,所以绕过了分析器不能引用项目的限制。
引用这个库的方法和其他库一样,在NuGet里下载并添加包引用即可。
如果想要引用其他NuGet包(如System.Text.Json),可以参考我的这篇文章:《分析器/源生成器添加项目依赖的方式》
目标平台问题
分析器项目与普通项目不同,分析器本身的dll不会被用户项目直接引用,而是被编译平台(如.NET SDK或Visual Studio等)引用,从而对代码进行分析。所以分析器本身的dll不会被包含进用户项目生成的应用里(源生成器生成的代码会)
既然分析器的dll是被编译平台引用而非用户项目,那源生成器的目标平台就应该与编译平台有关,而非用户项目。例如,在x64架构的电脑上编译x86的应用程序,此时一般的用户项目目标平台都应该设置为x86,但分析器项目的目标平台却应该与电脑保持一致(x64),因为编译项目时,是电脑上x64的.NET SDK调用了分析器项目。
为了保证项目编译的可移植性,如何判断编译电脑的架构并设置为对应平台呢?此时简单地将目标平台设为AnyCPU即可,AnyCPU正是根据自身电脑架构获取的目标平台。
又如使用GitHub Action时,分析器项目经常会遇到如下错误(CS8034):
1 | CSC : warning CS8034: Unable to load Analyzer assembly D:\a\xxx.SourceGenerator\bin\x86\Debug\netstandard2.0\xxx.SourceGenerator.dll : Unable to load xxx.SourceGenerator [D:\a\xxx.csproj] |
这显然是因为目标平台设置错误,但使用Visual Studio手动编译就没有问题,sln文件里的平台映射(platform map)也确实是AnyCPU啊?这是因为解决方案的平台映射不一定会被所有地方遵循,最稳妥的方案是在csproj文件里指定目标平台。故在分析器项目中加上如下一行(PlatformTarget
)即可:
1 | <Project Sdk="Microsoft.NET.Sdk"> |
不过为了直接运行也不出错,建议平台映射里的AnyCPU也要保留。
IDE兼容性问题
既然分析器是被编译平台调用,那分析器的Roslyn版本也要与编译平台一致。
所有源生成器和代码分析器都会引用以下的库。
1 | Microsoft.CodeAnalysis.CSharp |
而代码修复器则会使用
1 | Microsoft.CodeAnalysis.CSharp.Workspaces |
这两个库的版本都和Roslyn的版本相同(截至本文发布,是4.6.0),而Visual Studio的版本也是相关(VS2022的版本目前是VS 17.6.xxx)。所以如果使用了4.6.0的Roslyn,低于17.6.xxx版本的VS就无法使用了。例如VS2022的版本号是VS 17.xxx,VS2019的版本号是VS 16.xxx。如果编写的分析器需要有较为广泛的兼容性,可能需要降低版本,放弃新特性
分析器的调试方式
分析器在生成(运行)时,默认不会启动调试器,所以需要手动添加:
1 | Debugger.Launch(); |
推荐重新生成时采用Debug模式,Release模式会优化代码,导致有些地方可能看不到需要的变量值。
代码分析器的调试方法和源生成器一样,但代码修复器就有些不同了,添加启动调试器代码并重新生成后,需要先重启VS,这样VS才会重新加载代码生成器(然而添加启动调试器重启后再修改代码修复器代码或者删除启动调试器,都不需要重启VS,只要重新生成一下即可,十分奇怪)。
然后将鼠标放到代码分析器提供的警告上,他会出现修复错误的提示(此处两条相同的提示一条来自于VS本身,一条来自于Resharper,并不是BUG):
点击显示可能的修补程序后,这个框会一闪而过:
然后跳出选择调试器的对话框,注意此处与源生成器或代码分析器不同,此处没有该解决方案本身而是别的打开的解决方案(如果有的话)或者新的VS实例,任选一个即可。我们点开后就可以开心地进行调试了。
目标框架问题
所有分析器项目的目标框架都必须是.NET Standard2.0。据说因为是VS正在由.NET Framework向.NET (Core)迁移,所以使用这个折衷的方式来兼容两方。如果有朝一日VS完全用.NET重写的话,我们就能用上完全版的分析器了。
感谢
感谢Huo Yaoyuan大佬3的无私耐心解答x,学习时参考了吕毅4大佬的博客