Skip to content

故障排除

系统化的故障排除指南,快速解决常见问题

📋 文档信息

难度级别: 中级
预计阅读时间: 20 分钟
前置知识:

  • 源生成器基础
  • 项目配置基础
  • 编译过程理解

🎯 学习目标

学完本章节后,你将能够:

  • 识别和解决常见错误
  • 使用系统化的排查方法
  • 快速查找诊断命令
  • 参考 FAQ 解决问题
  • 使用调试清单

🐛 常见错误详解

错误 1: 生成器不执行

症状

  • 编译成功,但没有生成代码
  • 没有任何错误或警告

原因

  • 生成器项目配置不正确
  • 没有正确引用生成器
  • 特性未找到

解决方案

xml
<!-- 检查项目引用配置 -->
<ItemGroup>
  <ProjectReference Include="..\MyGenerator\MyGenerator.csproj"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>

<!-- 检查生成器项目配置 -->
<PropertyGroup>
  <TargetFramework>netstandard2.0</TargetFramework>
  <IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>

错误 2: 生成的代码不可见

症状

  • 生成器执行了
  • 但生成的代码在 IDE 中不可见

原因

  • 没有使用 partial 类
  • 命名空间不匹配
  • 文件名冲突

解决方案

确保类声明为 partial:

csharp
public partial class MyClass  // 必须是 partial
{
}

检查命名空间匹配:

csharp
// 生成的代码
namespace MyNamespace  // 必须与原始类的命名空间匹配
{
    public partial class MyClass
    {
        // 生成的成员
    }
}

错误 3: 命名冲突

症状

error CS0101: The namespace 'MyNamespace' already contains a definition for 'MyClass'

原因

  • 生成的类名与现有类名冲突
  • 多个生成器生成了相同的类

解决方案

csharp
// 方案 1: 使用唯一的文件名
context.RegisterSourceOutput(classes, (spc, classInfo) =>
{
    var fileName = $"{classInfo.Name}_{GetType().Name}.g.cs";
    spc.AddSource(fileName, code);
});

// 方案 2: 使用嵌套类
private string GenerateCode(ClassInfo classInfo)
{
    return $@"
namespace {classInfo.Namespace}
{{
    public partial class {classInfo.Name}
    {{
        // 生成的代码放在嵌套类中
        public static class Generated
        {{
            // ...
        }}
    }}
}}";
}

// 方案 3: 使用不同的命名空间
private string GenerateCode(ClassInfo classInfo)
{
    return $@"
namespace {classInfo.Namespace}.Generated
{{
    public class {classInfo.Name}Extensions
    {{
        // ...
    }}
}}";
}

错误 4: 循环依赖

症状

error CS0146: Circular base class dependency

原因

  • 生成的代码创建了循环继承
  • 生成的代码引用了尚未生成的类型

解决方案

csharp
// ❌ 错误:创建循环依赖
public partial class A : B { }
public partial class B : A { }

// ✅ 正确:使用接口打破循环
public interface IA { }
public interface IB { }

public partial class A : IA
{
    public IB BProperty { get; set; }
}

public partial class B : IB
{
    public IA AProperty { get; set; }
}

错误 5: 生成的代码语法错误

症状

error CS1002: ; expected
error CS1513: } expected

原因

  • 生成的代码有语法错误
  • 字符串插值或转义问题

调试方法

csharp
context.RegisterSourceOutput(classes, (spc, classInfo) =>
{
    var code = GenerateCode(classInfo);
    
    #if DEBUG
    // 验证生成的代码是否可以解析
    var syntaxTree = CSharpSyntaxTree.ParseText(code);
    var diagnostics = syntaxTree.GetDiagnostics();
    
    if (diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error))
    {
        // 输出语法错误
        foreach (var diagnostic in diagnostics)
        {
            System.Diagnostics.Debug.WriteLine($"Syntax Error: {diagnostic}");
        }
        
        // 输出生成的代码
        System.Diagnostics.Debug.WriteLine("Generated Code:");
        System.Diagnostics.Debug.WriteLine(code);
    }
    #endif
    
    spc.AddSource($"{classInfo.Name}.g.cs", code);
});

常见语法错误

csharp
// ❌ 错误:字符串中的引号未转义
var code = $"public string Name => "{classInfo.Name}";";

// ✅ 正确:转义引号
var code = $"public string Name => \"{classInfo.Name}\";";

// ❌ 错误:多行字符串格式不正确
var code = $"public class {classInfo.Name}
{
}";

// ✅ 正确:使用 @"" 或正确的换行
var code = $@"
public class {classInfo.Name}
{{
}}";

错误 6: 特性未找到

症状

  • 生成器不执行
  • 没有生成任何代码

原因

  • 特性类不在正确的位置
  • 特性命名空间不匹配
  • 特性未标记为 public

解决方案

csharp
// 确保特性是 public 的
[AttributeUsage(AttributeTargets.Class)]
public class GenerateAttribute : Attribute
{
}

// 在生成器中使用完全限定名称
var classes = context.SyntaxProvider
    .ForAttributeWithMetadataName(
        "MyNamespace.GenerateAttribute", // 使用完全限定名称
        predicate: (node, _) => node is ClassDeclarationSyntax,
        transform: (ctx, _) => GetClassInfo(ctx));

// 或者只使用类名(不包含 Attribute 后缀)
var classes = context.SyntaxProvider
    .ForAttributeWithMetadataName(
        "Generate", // 不包含 Attribute 后缀
        predicate: (node, _) => node is ClassDeclarationSyntax,
        transform: (ctx, _) => GetClassInfo(ctx));

错误 7: 内存泄漏

症状

  • 编译时内存使用持续增长
  • IDE 变慢或崩溃

原因

  • 生成器中保存了对 Compilation 或 SemanticModel 的引用
  • 缓存没有正确清理

解决方案

csharp
// ❌ 错误:保存 Compilation 引用
[Generator]
public class LeakyGenerator : IIncrementalGenerator
{
    private Compilation _compilation; // 内存泄漏!
    
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // ...
    }
}

// ✅ 正确:不保存引用
[Generator]
public class CorrectGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // 只在管道中使用 Compilation
        var classes = context.SyntaxProvider
            .ForAttributeWithMetadataName(
                "GenerateAttribute",
                predicate: (node, _) => node is ClassDeclarationSyntax,
                transform: (ctx, _) =>
                {
                    // 在这里使用 ctx.SemanticModel
                    // 不要保存引用
                    return GetClassInfo(ctx);
                });
    }
}

🔧 故障排除指南

系统化排查步骤

步骤 1: 验证生成器配置

xml
<!-- 检查项目引用配置 -->
<ItemGroup>
  <ProjectReference Include="..\MyGenerator\MyGenerator.csproj"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>

<!-- 检查生成器项目配置 -->
<PropertyGroup>
  <TargetFramework>netstandard2.0</TargetFramework>
  <IsRoslynComponent>true</IsRoslynComponent>
  <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

步骤 2: 检查编译输出

powershell
# 使用详细输出编译
dotnet build -v detailed > build.log

# 搜索生成器相关信息
Select-String -Path build.log -Pattern "Generator"

步骤 3: 验证生成的文件

xml
<!-- 启用生成文件输出 -->
<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
powershell
# 查看生成的文件
Get-ChildItem -Path obj\Debug\net8.0\generated -Recurse -File

步骤 4: 使用诊断工具

csharp
// 添加诊断输出
context.RegisterSourceOutput(classes, (spc, classInfo) =>
{
    // 报告处理信息
    var diagnostic = Diagnostic.Create(
        new DiagnosticDescriptor(
            "SG9999",
            "Generator Info",
            "Processing: {0}",
            "Generator",
            DiagnosticSeverity.Info,
            isEnabledByDefault: true),
        Location.None,
        classInfo.Name);
    
    spc.ReportDiagnostic(diagnostic);
    
    // 生成代码...
});

常见问题快速参考

问题可能原因解决方案
生成器不执行项目引用配置错误检查 OutputItemType="Analyzer"
生成器不执行特性未找到使用完全限定名称
代码未生成predicate 过滤太严格检查过滤条件
编译错误生成的代码有语法错误启用 EmitCompilerGeneratedFiles 检查
命名冲突文件名重复使用唯一的文件名
性能问题使用传统生成器迁移到增量生成器
性能问题过度语义分析在 predicate 中过滤
内存泄漏保存 Compilation 引用不要保存引用
IDE 崩溃生成器抛出异常使用 try-catch 和诊断
调试困难无法附加调试器使用 Debugger.Launch()

诊断命令速查

powershell
# 清理并重新编译
dotnet clean
dotnet build

# 详细编译输出
dotnet build -v detailed

# 查看生成的文件
Get-ChildItem obj\Debug\net8.0\generated -Recurse

# 查看编译器版本
dotnet --version

# 查看 SDK 信息
dotnet --info

# 恢复 NuGet 包
dotnet restore

# 清理 NuGet 缓存
dotnet nuget locals all --clear

❓ 调试和诊断 FAQ

1. 如何知道生成器是否被调用?

答: 有多种方法:

方法 1: 使用日志

csharp
public void Initialize(IncrementalGeneratorInitializationContext context)
{
    File.WriteAllText(
        Path.Combine(Path.GetTempPath(), "generator-called.txt"),
        $"Generator called at {DateTime.Now}");
    
    // 生成器逻辑...
}

方法 2: 使用诊断

csharp
context.RegisterSourceOutput(classes, (spc, classInfo) =>
{
    spc.ReportDiagnostic(Diagnostic.Create(
        new DiagnosticDescriptor(
            "SG0001",
            "Generator Called",
            "Generator is processing class: {0}",
            "Generator",
            DiagnosticSeverity.Info,
            isEnabledByDefault: true),
        Location.None,
        classInfo.Name));
});

方法 3: 查看生成的文件

xml
<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

2. 为什么断点不会命中?

答: 可能的原因:

  1. 生成器未被调用

    • 检查项目引用配置
    • 验证特性是否正确
  2. 调试器未附加

    • 使用 Debugger.Launch() 强制附加
    • 确保在 Debug 配置下编译
  3. 代码已优化

    • 在生成器项目中禁用优化:
    xml
    <PropertyGroup>
      <Optimize>false</Optimize>
    </PropertyGroup>

3. 如何调试增量生成器的缓存问题?

答: 使用管道可视化:

csharp
public void Initialize(IncrementalGeneratorInitializationContext context)
{
    var classes = context.SyntaxProvider
        .ForAttributeWithMetadataName(
            "GenerateAttribute",
            predicate: (node, _) => node is ClassDeclarationSyntax,
            transform: (ctx, _) => GetClassInfo(ctx))
        .WithTrackingName("ClassInfo"); // 添加跟踪名称
    
    var filtered = classes
        .Where(x => x != null)
        .WithTrackingName("FilteredClasses"); // 添加跟踪名称
    
    context.RegisterSourceOutput(filtered, GenerateCode);
}

然后使用 dotnet build -v detailed 查看管道执行情况。


4. 如何测试生成器在不同场景下的行为?

答: 使用参数化测试:

csharp
[Theory]
[InlineData("public partial class MyClass { }", true)]
[InlineData("public class MyClass { }", false)] // 不是 partial
[InlineData("internal partial class MyClass { }", false)] // 不是 public
public void Generator_HandlesVariousClassDeclarations(string source, bool shouldGenerate)
{
    var compilation = CreateCompilation(source);
    var generator = new MyGenerator();
    
    var driver = CSharpGeneratorDriver.Create(generator);
    driver = driver.RunGeneratorsAndUpdateCompilation(
        compilation,
        out var outputCompilation,
        out var diagnostics);
    
    var runResult = driver.GetRunResult();
    
    if (shouldGenerate)
    {
        Assert.NotEmpty(runResult.GeneratedTrees);
    }
    else
    {
        Assert.Empty(runResult.GeneratedTrees);
    }
}

5. 如何处理生成器中的异常?

答: 使用 try-catch 和诊断报告:

csharp
context.RegisterSourceOutput(classes, (spc, classInfo) =>
{
    try
    {
        var code = GenerateCode(classInfo);
        spc.AddSource($"{classInfo.Name}.g.cs", code);
    }
    catch (Exception ex)
    {
        // 报告错误诊断
        var diagnostic = Diagnostic.Create(
            new DiagnosticDescriptor(
                "SG0001",
                "Generator Error",
                "Error generating code for {0}: {1}",
                "Generator",
                DiagnosticSeverity.Error,
                isEnabledByDefault: true),
            Location.None,
            classInfo.Name,
            ex.Message);
        
        spc.ReportDiagnostic(diagnostic);
        
        // 不要重新抛出异常
        // throw; // ❌ 这会中断整个编译
    }
});

📝 调试清单

使用这个清单来系统地排查生成器问题:

基础检查

  • [ ] 生成器项目目标框架是 netstandard2.0
  • [ ] 类标记了 [Generator] 特性
  • [ ] 实现了 ISourceGenerator 或 IIncrementalGenerator 接口
  • [ ] 项目引用配置正确(OutputItemType="Analyzer")
  • [ ] 清理并重新构建项目

代码检查

  • [ ] Initialize 方法没有抛出异常
  • [ ] Execute 方法没有抛出异常
  • [ ] 生成的代码语法正确
  • [ ] 文件名唯一且有效
  • [ ] 使用了正确的编码(UTF-8)

性能检查

  • [ ] 使用增量生成器(如果可能)
  • [ ] 避免重复计算
  • [ ] 使用缓存
  • [ ] 避免不必要的语义分析
  • [ ] 测量和优化热点代码

调试工具

  • [ ] 启用详细构建输出
  • [ ] 使用 Debugger.Launch()
  • [ ] 添加日志输出
  • [ ] 使用断点和跟踪点
  • [ ] 查看生成的文件

🔗 相关资源

💡 关键要点

  1. 系统化排查 使用清单和步骤
  2. 检查配置 验证项目引用和生成器配置
  3. 查看输出 检查编译输出和生成的文件
  4. 使用诊断 报告详细的诊断信息
  5. 参考 FAQ 查看常见问题的解决方案
  6. 不要抛出异常 使用诊断报告错误
  7. 持续监控 性能和错误

最后更新: 2025-01-21

基于 MIT 许可发布