Skip to content

编译 API 中级

⏱️ 15-20 分钟 | 📚 中级级别

🎯 学习目标

完成本指南后,你将能够:

  • [ ] 管理元数据引用
  • [ ] 配置编译选项
  • [ ] 处理程序集加载
  • [ ] 进行引用解析
  • [ ] 诊断编译错误

📖 前置知识

在开始之前,你应该:

  • 完成 编译 API 基础
  • 理解基本的 Compilation 概念
  • 熟悉语法树和语义模型

🔧 常见模式

模式 1:添加元数据引用

用途: 向编译添加外部程序集引用,使代码能够使用外部类型

何时使用: 创建独立的编译实例,或在单元测试中

示例:

csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

public class MetadataReferenceExample
{
    public CSharpCompilation CreateCompilationWithReferences(string sourceCode)
    {
        // 解析源代码
        var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
        
        // 创建基本引用列表
        var references = new List<MetadataReference>
        {
            // mscorlib - 包含 System.Object, System.String 等
            MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
            
            // System.Runtime
            MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
            
            // System.Linq
            MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location),
            
            // System.Collections
            MetadataReference.CreateFromFile(
                Assembly.Load("System.Collections").Location)
        };
        
        // 创建编译
        var compilation = CSharpCompilation.Create(
            assemblyName: "MyAssembly",
            syntaxTrees: new[] { syntaxTree },
            references: references,
            options: new CSharpCompilationOptions(
                OutputKind.DynamicallyLinkedLibrary));
        
        return compilation;
    }
    
    // 添加自定义程序集引用
    public CSharpCompilation AddCustomReference(
        CSharpCompilation compilation,
        string assemblyPath)
    {
        var reference = MetadataReference.CreateFromFile(assemblyPath);
        return compilation.AddReferences(reference);
    }
}

关键要点:

  • 使用 MetadataReference.CreateFromFile() 从文件创建引用
  • 使用 typeof(Type).Assembly.Location 获取程序集路径
  • 编译是不可变的,AddReferences() 返回新实例

性能考虑:

  • 重用 MetadataReference 对象,避免重复创建
  • 只添加必要的引用,减少编译时间

模式 2:配置编译选项

用途: 设置编译行为,如输出类型、优化级别、语言版本等

何时使用: 需要控制编译行为时

示例:

csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

public class CompilationOptionsExample
{
    // 创建控制台应用程序编译选项
    public CSharpCompilationOptions CreateConsoleAppOptions()
    {
        return new CSharpCompilationOptions(
            outputKind: OutputKind.ConsoleApplication,
            optimizationLevel: OptimizationLevel.Release,
            allowUnsafe: false,
            platform: Platform.AnyCpu);
    }
    
    // 创建类库编译选项
    public CSharpCompilationOptions CreateLibraryOptions()
    {
        return new CSharpCompilationOptions(
            outputKind: OutputKind.DynamicallyLinkedLibrary,
            optimizationLevel: OptimizationLevel.Debug,
            allowUnsafe: false,
            nullableContextOptions: NullableContextOptions.Enable);
    }
    
    // 创建带有特定设置的编译
    public CSharpCompilation CreateCompilationWithOptions(
        string sourceCode,
        CSharpCompilationOptions options)
    {
        var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
        
        var compilation = CSharpCompilation.Create(
            assemblyName: "MyAssembly",
            syntaxTrees: new[] { syntaxTree },
            options: options);
        
        return compilation;
    }
    
    // 修改现有编译的选项
    public CSharpCompilation UpdateOptions(
        CSharpCompilation compilation,
        OptimizationLevel optimizationLevel)
    {
        var newOptions = compilation.Options
            .WithOptimizationLevel(optimizationLevel);
        
        return compilation.WithOptions(newOptions);
    }
}

关键要点:

  • OutputKind 决定输出类型(控制台、类库等)
  • OptimizationLevel 控制优化级别(Debug/Release)
  • 使用 WithXxx() 方法修改选项

最佳实践:

  • 在 Source Generator 中,通常使用项目的编译选项
  • 单元测试中,使用 DynamicallyLinkedLibrary 输出类型

模式 3:引用解析和诊断

用途: 检查编译中的引用,诊断缺失的依赖

何时使用: 需要验证引用完整性或调试编译错误时

示例:

csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.Collections.Generic;
using System.Linq;

public class ReferenceDiagnostics
{
    // 获取所有引用的程序集名称
    public List<string> GetReferenceNames(Compilation compilation)
    {
        var names = new List<string>();
        
        foreach (var reference in compilation.References)
        {
            var assembly = compilation.GetAssemblyOrModuleSymbol(reference) 
                as IAssemblySymbol;
            
            if (assembly != null)
            {
                names.Add(assembly.Name);
            }
        }
        
        return names;
    }
    
    // 检查是否引用了特定程序集
    public bool HasReference(Compilation compilation, string assemblyName)
    {
        foreach (var reference in compilation.References)
        {
            var assembly = compilation.GetAssemblyOrModuleSymbol(reference) 
                as IAssemblySymbol;
            
            if (assembly != null && assembly.Name == assemblyName)
            {
                return true;
            }
        }
        
        return false;
    }
    
    // 诊断缺失的引用
    public List<string> DiagnoseMissingReferences(CSharpCompilation compilation)
    {
        var missingReferences = new List<string>();
        
        // 获取编译诊断
        var diagnostics = compilation.GetDiagnostics();
        
        // 查找与引用相关的错误
        foreach (var diagnostic in diagnostics)
        {
            // CS0246: 找不到类型或命名空间
            // CS0012: 类型在未引用的程序集中定义
            if (diagnostic.Id == "CS0246" || diagnostic.Id == "CS0012")
            {
                missingReferences.Add(diagnostic.GetMessage());
            }
        }
        
        return missingReferences;
    }
    
    // 验证编译是否成功
    public bool IsCompilationSuccessful(CSharpCompilation compilation)
    {
        var diagnostics = compilation.GetDiagnostics();
        
        // 检查是否有错误
        return !diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error);
    }
}

关键要点:

  • 使用 GetAssemblyOrModuleSymbol() 获取引用的程序集符号
  • 使用 GetDiagnostics() 获取编译错误
  • CS0246 和 CS0012 表示缺失引用

💡 实际场景

场景 1:创建可执行的编译

问题: 需要创建一个完整的、可编译的 Compilation 实例用于代码分析或执行

解决方案:

csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

public class ExecutableCompilationBuilder
{
    public CSharpCompilation CreateExecutableCompilation(string sourceCode)
    {
        // 1. 解析源代码
        var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
        
        // 2. 收集所有必要的引用
        var references = GetStandardReferences();
        
        // 3. 配置编译选项
        var options = new CSharpCompilationOptions(
            outputKind: OutputKind.ConsoleApplication,
            optimizationLevel: OptimizationLevel.Release,
            allowUnsafe: false);
        
        // 4. 创建编译
        var compilation = CSharpCompilation.Create(
            assemblyName: "DynamicAssembly",
            syntaxTrees: new[] { syntaxTree },
            references: references,
            options: options);
        
        // 5. 验证编译
        var diagnostics = compilation.GetDiagnostics();
        var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error);
        
        if (errors.Any())
        {
            foreach (var error in errors)
            {
                Console.WriteLine($"错误: {error.GetMessage()}");
            }
            throw new InvalidOperationException("编译失败");
        }
        
        return compilation;
    }
    
    private IEnumerable<MetadataReference> GetStandardReferences()
    {
        // 添加标准 .NET 引用
        var assemblies = new[]
        {
            typeof(object).Assembly,           // System.Private.CoreLib
            typeof(Console).Assembly,          // System.Console
            typeof(Enumerable).Assembly,       // System.Linq
            Assembly.Load("System.Runtime"),   // System.Runtime
            Assembly.Load("System.Collections") // System.Collections
        };
        
        return assemblies
            .Select(a => MetadataReference.CreateFromFile(a.Location))
            .ToList();
    }
}

说明: 这个示例展示了创建完整编译的完整流程:解析代码、添加引用、配置选项、验证结果。

场景 2:动态添加 NuGet 包引用

问题: 需要在运行时添加来自 NuGet 包的程序集引用

解决方案:

csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

public class NuGetReferenceManager
{
    // NuGet 包缓存路径(Windows)
    private readonly string _nugetCachePath = Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
        ".nuget", "packages");
    
    public CSharpCompilation AddNuGetPackageReference(
        CSharpCompilation compilation,
        string packageName,
        string version)
    {
        // 构建包路径
        var packagePath = Path.Combine(
            _nugetCachePath,
            packageName.ToLower(),
            version);
        
        if (!Directory.Exists(packagePath))
        {
            throw new DirectoryNotFoundException(
                $"NuGet 包未找到: {packageName} {version}");
        }
        
        // 查找 lib 目录
        var libPath = Path.Combine(packagePath, "lib");
        
        if (!Directory.Exists(libPath))
        {
            throw new DirectoryNotFoundException(
                $"包中未找到 lib 目录: {packageName}");
        }
        
        // 查找合适的目标框架
        var targetFramework = FindBestTargetFramework(libPath);
        var dllPath = Path.Combine(libPath, targetFramework);
        
        // 添加所有 DLL 引用
        var references = Directory.GetFiles(dllPath, "*.dll")
            .Select(dll => MetadataReference.CreateFromFile(dll))
            .ToList();
        
        return compilation.AddReferences(references);
    }
    
    private string FindBestTargetFramework(string libPath)
    {
        // 查找可用的目标框架
        var frameworks = Directory.GetDirectories(libPath)
            .Select(Path.GetFileName)
            .ToList();
        
        // 优先选择 .NET Standard 或 .NET Core
        var preferred = new[] { "netstandard2.1", "netstandard2.0", "net6.0", "net5.0" };
        
        foreach (var framework in preferred)
        {
            if (frameworks.Contains(framework))
            {
                return framework;
            }
        }
        
        // 返回第一个可用的框架
        return frameworks.FirstOrDefault() 
            ?? throw new InvalidOperationException("未找到可用的目标框架");
    }
}

说明: 这个示例展示了如何从 NuGet 包缓存中加载程序集引用,这在需要动态引用第三方库时很有用。

场景 3:编译选项的高级配置

问题: 需要精确控制编译行为,如启用 nullable、设置语言版本等

解决方案:

csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.Collections.Generic;

public class AdvancedCompilationOptions
{
    public CSharpCompilation CreateAdvancedCompilation(string sourceCode)
    {
        var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
        
        // 配置高级编译选项
        var options = new CSharpCompilationOptions(
            outputKind: OutputKind.DynamicallyLinkedLibrary)
            
            // 启用 nullable 引用类型
            .WithNullableContextOptions(NullableContextOptions.Enable)
            
            // 设置优化级别
            .WithOptimizationLevel(OptimizationLevel.Release)
            
            // 允许不安全代码
            .WithAllowUnsafe(false)
            
            // 设置平台
            .WithPlatform(Platform.AnyCpu)
            
            // 设置溢出检查
            .WithOverflowChecks(true)
            
            // 设置确定性编译
            .WithDeterministic(true)
            
            // 设置并发构建
            .WithConcurrentBuild(true);
        
        var compilation = CSharpCompilation.Create(
            assemblyName: "AdvancedAssembly",
            syntaxTrees: new[] { syntaxTree },
            options: options);
        
        return compilation;
    }
    
    // 为不同环境创建编译选项
    public CSharpCompilationOptions CreateOptionsForEnvironment(
        string environment)
    {
        return environment.ToLower() switch
        {
            "development" => new CSharpCompilationOptions(
                OutputKind.DynamicallyLinkedLibrary)
                .WithOptimizationLevel(OptimizationLevel.Debug)
                .WithOverflowChecks(true),
            
            "production" => new CSharpCompilationOptions(
                OutputKind.DynamicallyLinkedLibrary)
                .WithOptimizationLevel(OptimizationLevel.Release)
                .WithOverflowChecks(false)
                .WithDeterministic(true),
            
            _ => throw new System.ArgumentException(
                $"未知环境: {environment}")
        };
    }
}

说明: 展示了如何使用 WithXxx() 方法链式配置编译选项,以及如何为不同环境创建不同的配置。

🚀 性能优化

优化技巧 1:重用 MetadataReference

问题: 重复创建 MetadataReference 对象会影响性能

解决方案:

csharp
using Microsoft.CodeAnalysis;
using System;
using System.Collections.Generic;
using System.Linq;

public class ReferenceCache
{
    // 缓存常用的引用
    private static readonly Dictionary<string, MetadataReference> _cache = new();
    
    public static MetadataReference GetOrCreateReference(string assemblyPath)
    {
        if (!_cache.TryGetValue(assemblyPath, out var reference))
        {
            reference = MetadataReference.CreateFromFile(assemblyPath);
            _cache[assemblyPath] = reference;
        }
        
        return reference;
    }
    
    // 获取标准引用(已缓存)
    public static IEnumerable<MetadataReference> GetStandardReferences()
    {
        return new[]
        {
            GetOrCreateReference(typeof(object).Assembly.Location),
            GetOrCreateReference(typeof(Console).Assembly.Location),
            GetOrCreateReference(typeof(Enumerable).Assembly.Location)
        };
    }
}

性能提升: 避免重复的文件 I/O 和对象创建,可提升 50% 以上的性能

优化技巧 2:最小化引用数量

问题: 添加过多不必要的引用会增加编译时间

解决方案:

csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.Collections.Generic;
using System.Linq;

public class MinimalReferences
{
    // 只添加必要的引用
    public CSharpCompilation CreateMinimalCompilation(string sourceCode)
    {
        var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
        
        // 分析代码,确定需要的引用
        var requiredReferences = AnalyzeRequiredReferences(sourceCode);
        
        var compilation = CSharpCompilation.Create(
            assemblyName: "MinimalAssembly",
            syntaxTrees: new[] { syntaxTree },
            references: requiredReferences);
        
        return compilation;
    }
    
    private IEnumerable<MetadataReference> AnalyzeRequiredReferences(
        string sourceCode)
    {
        var references = new List<MetadataReference>();
        
        // 始终需要的基础引用
        references.Add(MetadataReference.CreateFromFile(
            typeof(object).Assembly.Location));
        
        // 根据代码内容添加引用
        if (sourceCode.Contains("Console"))
        {
            references.Add(MetadataReference.CreateFromFile(
                typeof(Console).Assembly.Location));
        }
        
        if (sourceCode.Contains("Linq") || sourceCode.Contains("Select"))
        {
            references.Add(MetadataReference.CreateFromFile(
                typeof(Enumerable).Assembly.Location));
        }
        
        return references;
    }
}

性能提升: 减少不必要的引用可以显著减少编译时间

⚠️ 常见陷阱

陷阱 1:忘记添加必要的引用

问题: 编译失败,提示找不到类型

错误示例:

csharp
// 缺少 System.Console 引用
var compilation = CSharpCompilation.Create(
    "MyAssembly",
    new[] { syntaxTree },
    new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) });

// 编译包含 Console.WriteLine 的代码会失败

正确方法:

csharp
var references = new[]
{
    MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
    MetadataReference.CreateFromFile(typeof(Console).Assembly.Location)
};

var compilation = CSharpCompilation.Create(
    "MyAssembly",
    new[] { syntaxTree },
    references);

为什么: 每个使用的类型都需要其所在程序集的引用

陷阱 2:使用错误的输出类型

问题: 编译选项的输出类型与代码不匹配

错误示例:

csharp
// 代码包含 Main 方法,但使用了 DLL 输出类型
var options = new CSharpCompilationOptions(
    OutputKind.DynamicallyLinkedLibrary);

var sourceCode = @"
class Program
{
    static void Main() { }
}";

正确方法:

csharp
// 对于包含 Main 方法的代码,使用 ConsoleApplication
var options = new CSharpCompilationOptions(
    OutputKind.ConsoleApplication);

为什么: 输出类型决定了编译器如何处理入口点

陷阱 3:不检查编译错误

问题: 假设编译总是成功,导致运行时错误

错误示例:

csharp
var compilation = CreateCompilation(sourceCode);
// 直接使用,不检查错误
var assembly = EmitAssembly(compilation);

正确方法:

csharp
var compilation = CreateCompilation(sourceCode);

// 检查编译错误
var diagnostics = compilation.GetDiagnostics();
var errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error);

if (errors.Any())
{
    foreach (var error in errors)
    {
        Console.WriteLine($"错误: {error.GetMessage()}");
    }
    throw new InvalidOperationException("编译失败");
}

var assembly = EmitAssembly(compilation);

为什么: 编译可能因各种原因失败,必须检查并处理错误

🔍 深入探讨

主题 1:程序集加载策略

在不同场景下,程序集加载有不同的策略:

场景 A:Source Generator 中

  • 使用 context.Compilation.References 获取项目引用
  • 不需要手动添加引用

场景 B:独立工具中

  • 需要手动添加所有必要的引用
  • 可以从运行时目录或 NuGet 缓存加载

场景 C:单元测试中

  • 使用最小化的引用集
  • 只添加测试所需的引用

示例:

csharp
public class AssemblyLoadingStrategies
{
    // 策略 1: 从 Source Generator 上下文获取
    public void UseGeneratorReferences(GeneratorExecutionContext context)
    {
        var compilation = context.Compilation;
        // 引用已经包含在 compilation 中
    }
    
    // 策略 2: 手动构建完整引用集
    public CSharpCompilation BuildStandaloneCompilation(string code)
    {
        var references = GetAllRuntimeReferences();
        return CSharpCompilation.Create("Standalone", 
            new[] { CSharpSyntaxTree.ParseText(code) },
            references);
    }
    
    // 策略 3: 最小化测试引用
    public CSharpCompilation BuildTestCompilation(string code)
    {
        var references = new[]
        {
            MetadataReference.CreateFromFile(typeof(object).Assembly.Location)
        };
        return CSharpCompilation.Create("Test",
            new[] { CSharpSyntaxTree.ParseText(code) },
            references);
    }
}

主题 2:编译选项的影响

不同的编译选项会影响代码生成和性能:

OptimizationLevel:

  • Debug: 保留调试信息,不优化
  • Release: 优化代码,移除调试信息

NullableContextOptions:

  • Enable: 启用 nullable 引用类型检查
  • Disable: 禁用检查
  • Warnings: 只显示警告

示例:

csharp
// Debug 配置
var debugOptions = new CSharpCompilationOptions(
    OutputKind.DynamicallyLinkedLibrary)
    .WithOptimizationLevel(OptimizationLevel.Debug)
    .WithOverflowChecks(true);

// Release 配置
var releaseOptions = new CSharpCompilationOptions(
    OutputKind.DynamicallyLinkedLibrary)
    .WithOptimizationLevel(OptimizationLevel.Release)
    .WithOverflowChecks(false)
    .WithDeterministic(true);

⏭️ 下一步

🔗 相关资源

📚 在学习路径中使用

此 API 在以下步骤中使用:

💬 另请参阅

基于 MIT 许可发布