You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何高效处理数百个项目的数千个C#文件以查找常量依赖

简介

我正在解决以下问题:

  • 给定一个包含一个或多个公开可见常量的C#类型X,求解决方案中所有依赖类型X中常量的C#类型有哪些?

由于常量会在编译期内联,检查二进制文件没有意义,我们需要使用Roslyn API检查源代码。
我不打算使用语义分析,因为它的开销非常高。我会先使用正则表达式检查给定文件是否疑似使用了目标常量,再通过语法树验证。该方案并非万无一失,但足够好用且相对更快。

当前处理规模如下:

  • 项目数量:333
  • 文件数量:45280
  • 存储介质:SSD硬盘
我的实现方案

整体流程如下:

  1. 生成相关Microsoft.Build.Evaluation.Project对象流,从中获取C#文件列表
  2. 基于C#文件列表生成C#文件内容流
  3. 对每个C#文件内容匹配指定正则表达式,每个匹配项都通过对应C#文件内容的语法树判断是否确实使用了目标常量。如果确认则上报对应的类型。

我定义了几个小型辅助类型:

ProjectItem

private class ProjectItem
{
    public readonly string AssemblyName;
    public readonly CSharpParseOptions ParseOptions;
    public readonly IEnumerable<string> CSFilePaths;

    public ProjectItem(TypeMap typeMap, string asmName)
    {
        AssemblyName = asmName;
        var asmProps = typeMap.Assemblies[asmName];
        ParseOptions = asmProps.GetParseOptions();
        CSFilePaths = new Project(asmProps.ProjectPath).GetItems("Compile").Select(item => item.GetMetadataValue("FullPath"));
    }

    public IEnumerable<TextItem> YieldTextItems() => CSFilePaths.Select(csFilePath => new TextItem(this, csFilePath, File.ReadAllText(csFilePath)));
}

其中TypeMap是解决方案中所有类型和程序集的注册表,由其他代码预先构建。可以将它视作可以回答特定问题的「预言机」,比如「返回指定程序集的解析选项(或项目路径)」,但它不提供项目使用的C#文件列表,要获取该列表我们需要实例化对应的Microsoft.Build.Evaluation.Project实例,该操作开销很高。

TextItem

private class TextItem
{
    public readonly string AssemblyName;
    public readonly CSharpParseOptions ParseOptions;
    public readonly string CSFilePath;
    public readonly string Text;

    public TextItem(ProjectItem item, string csFilePath, string text)
    {
        AssemblyName = item.AssemblyName;
        ParseOptions = item.ParseOptions;
        CSFilePath = csFilePath;
        Text = text;
    }

    public IEnumerable<TypeDefKey> YieldDependentTypes(TypeMap typeMap, TypeDefKey constTypeDefKey, Regex regex)
    {
        ...
        SyntaxTree syntaxTree = null;
        foreach (Match m in regex.Matches(Text))
        {
            if (syntaxTree == null)
            {
                syntaxTree = CSharpSyntaxTree.ParseText(Text, ParseOptions, CSFilePath);
                ...
            }

            ...
            if (IsTheRegexMatchIndeedCorrespondsToTheGivenConstantType(syntaxTree, ...))
            {
                var typeDefKey = GetTheType(syntaxTree, ...);
                yield return typeDefKey;
            }
        }
    }
}

基于上述类型,我实现了如下简单的TPL Dataflow流水线:

var regex = GetRegex(...);
var dependentAssemblies = GetDependentAssemblies(...);
var dependentTypes = new ConcurrentDictionary<TypeDefKey, object>();

var produceCSFilePaths = new TransformManyBlock<ICollection<string>, ProjectItem>(asmNames => asmNames.Select(asmName => new ProjectItem(typeMap, asmName)));
var produceCSFileText = new TransformManyBlock<ProjectItem, TextItem>(p => p.YieldTextItems());
var produceDependentTypes = new TransformManyBlock<TextItem, TypeDefKey>(t => t.YieldDependentTypes(typeMap, constTypeDefKey, regex));
var getDependentTypes = new ActionBlock<TypeDefKey>(typeDefKey => dependentTypes.TryAdd(typeDefKey, null));

var linkOptions = new DataflowLinkOptions { PropagateCompletion = true };

produceCSFilePaths.LinkTo(produceCSFileText, linkOptions);
produceCSFileText.LinkTo(produceDependentTypes, linkOptions);
produceDependentTypes.LinkTo(getDependentTypes, linkOptions);

produceCSFilePaths.Post(dependentAssemblies);
produceCSFilePaths.Complete();
getDependentTypes.Completion.Wait();
遇到的问题与疑问
  1. 运行速度慢:耗时约50秒,CPU利用率低。虽然存在大量IO操作,但正则匹配、内容解析为语法树的过程仍需要CPU参与。
  2. 不知道如何结合TransformManyBlock使用异步IO。ProjectItem.YieldTextItems函数可以返回IObservable<TextItem>或IAsyncEnumerable<TextItem>,但TransformManyBlock无法识别这两类返回值。我刚接触TPL Dataflow,不清楚如何绕过该限制,因此目前使用的是阻塞式的File.ReadAllText而非File.ReadAllTextAsync。
  3. 我认为当前流水线(通过默认TaskScheduler)使用的是ThreadPool线程,它是否应该使用真正的独立线程?比如通过Task.Factory.StartNew(..., TaskCreationOptions.LongRunning);创建的线程?它目前使用的是「合适」的线程吗?如果不是该如何修复?我看到有建议实现自定义TaskScheduler,但我找不到示例,现有实现似乎依赖内部逻辑,不知道该如何处理。
  4. 我尝试过提高ProjectItem和TextItem生成环节的MaxDegreeOfParallelism,因为这两个环节以IO为主,比最后检查C#文件内容的环节慢得多,但并没有带来明显的性能提升。我的认知是流水线中越慢的环节需要越高的并行度,但另一方面我不知道SSD读取可以支持多少并行度,完全不知道该如何profiling该流程。

内容的提问来源于stack exchange,提问作者mark

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 07:00:03