编译 API 中级
⏱️ 15-20 分钟 | 📚 中级级别
🎯 学习目标
完成本指南后,你将能够:
- [ ] 管理元数据引用
- [ ] 配置编译选项
- [ ] 处理程序集加载
- [ ] 进行引用解析
- [ ] 诊断编译错误
📖 前置知识
在开始之前,你应该:
- 完成 编译 API 基础
- 理解基本的 Compilation 概念
- 熟悉语法树和语义模型
🔧 常见模式
模式 1:添加元数据引用
用途: 向编译添加外部程序集引用,使代码能够使用外部类型
何时使用: 创建独立的编译实例,或在单元测试中
示例:
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:配置编译选项
用途: 设置编译行为,如输出类型、优化级别、语言版本等
何时使用: 需要控制编译行为时
示例:
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:引用解析和诊断
用途: 检查编译中的引用,诊断缺失的依赖
何时使用: 需要验证引用完整性或调试编译错误时
示例:
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 实例用于代码分析或执行
解决方案:
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 包的程序集引用
解决方案:
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、设置语言版本等
解决方案:
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 对象会影响性能
解决方案:
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:最小化引用数量
问题: 添加过多不必要的引用会增加编译时间
解决方案:
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:忘记添加必要的引用
问题: 编译失败,提示找不到类型
错误示例:
// 缺少 System.Console 引用
var compilation = CSharpCompilation.Create(
"MyAssembly",
new[] { syntaxTree },
new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) });
// 编译包含 Console.WriteLine 的代码会失败正确方法:
var references = new[]
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location)
};
var compilation = CSharpCompilation.Create(
"MyAssembly",
new[] { syntaxTree },
references);为什么: 每个使用的类型都需要其所在程序集的引用
陷阱 2:使用错误的输出类型
问题: 编译选项的输出类型与代码不匹配
错误示例:
// 代码包含 Main 方法,但使用了 DLL 输出类型
var options = new CSharpCompilationOptions(
OutputKind.DynamicallyLinkedLibrary);
var sourceCode = @"
class Program
{
static void Main() { }
}";正确方法:
// 对于包含 Main 方法的代码,使用 ConsoleApplication
var options = new CSharpCompilationOptions(
OutputKind.ConsoleApplication);为什么: 输出类型决定了编译器如何处理入口点
陷阱 3:不检查编译错误
问题: 假设编译总是成功,导致运行时错误
错误示例:
var compilation = CreateCompilation(sourceCode);
// 直接使用,不检查错误
var assembly = EmitAssembly(compilation);正确方法:
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:单元测试中
- 使用最小化的引用集
- 只添加测试所需的引用
示例:
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: 只显示警告
示例:
// 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 在以下步骤中使用:
- 步骤 3:基于特性的生成 - 中级用法
- 步骤 4:增量生成器 - 高级用法