ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C#预处理指令实战:从条件编译到多框架兼容

C#预处理指令实战:从条件编译到多框架兼容 1. 预处理指令到底是什么先别急着写代码把这个问题想清楚很多C#开发者写了两三年代码可能都没正经用过预处理指令。我第一次接触这东西是在读别人的开源项目看到一堆#if DEBUG、#region第一反应是这玩意儿不是C/C才有的吗C#怎么也有后来自己查文档、翻源码、在实际项目里踩过坑才慢慢明白C#的预处理指令和C/C里的宏处理完全是两码事它其实是一个编译期开关能够根据不同的编译条件决定让哪些代码进入编译产物哪些代码直接被丢弃。用大白话解释你可以把C#编译器想象成一个流水线工人预处理指令就是贴在零件上的标签。工人看到标签写着这个零件不需要就直接把它扔到一边根本不进入下一道工序。注意这是丢弃不是注释掉或者运行时判断跳过。注释掉的代码依然存在于源代码里只是不执行而预处理指令控制的是代码是否存在这一层级被排除的代码连编译都不会编译IL里根本没有它的影子。正因为这个特性预处理指令特别适合用于区分Debug和Release版本的逻辑比如Debug下输出详细日志Release下不输出针对不同平台或框架版本做条件编译比如. NET Framework和.NET CoreWindows和Linux在编译期间注入某些特殊标记配合特性Attribute一起实现反射层面的逻辑控制标记代码区域让Visual Studio的代码折叠功能更清晰#region/#endregion防止重复定义某些常量或代码段#define配合#if使用。很多初学者会问一个问题为什么不用if (isDebug)这种运行时判断答案很简单运行时判断意味着所有分支代码都会被编译进最终程序你只是暂时没执行它而已。但有些代码在特定环境下根本编译不过去比如你在一个不支持某个API的框架版本里调用它写if会直接编译报错写#if就能让这段代码彻底消失编译自然就通过了。另外一个容易被忽略的点是预处理指令是在编译之前处理的但它本身并不属于C#语言的运行时语法。它更接近一种给编译器看的话类似于给编辑器看的注释只不过它带着真实的逻辑控制能力。理解这一点后面看很多奇怪的行为就说得通了。2. 最常用的核心指令逐个拆解#define、#if、#else、#endif是主角2.1 #define和#undef定义和取消编译符号#define的作用是定义一个符号symbol注意它不是定义变量没有值的概念。你可以把它理解成一个开关的标签。比如#define MY_MODE这行代码写在整个文件的顶部放在所有using语句之前。它表示开启MY_MODE这个开关。编译器在后面遇到#if MY_MODE时就会把对应的代码块编译进去。和它对应的是#undef用于取消一个符号的定义#undef MY_MODE实际项目中#define用得不算特别多因为Visual Studio的项目属性里有一个条件编译符号Conditional compilation symbols配置可以在整个项目的层面定义符号不需要在每个文件顶部写。比如你在.csproj文件里加上PropertyGroup Condition$(Configuration) Debug DefineConstantsTRACE;DEBUG;MY_MODE/DefineConstants /PropertyGroup这就相当于在项目的所有文件里都预先定义了TRACE、DEBUG、MY_MODE三个符号。这是Visual Studio新建项目时自动生成的模板逻辑很多人不知道自己的项目默认就带着DEBUG和TRACE符号。这里有一个非常关键的细节#define作用域仅限于当前文件也就是说你在这个.cs文件里定义了MY_MODE切换到另一个.cs文件里是不生效的。但项目级的DefineConstants是全局生效的。所以如果某个符号需要在多个文件里使用就应该放进项目配置里而不是在文件里逐个定义。2.2 #if、#else、#elif、#endif条件编译的主体逻辑这组指令和C#里的if语句在形式上很像但执行时机完全不同。它的格式如下#if DEBUG Console.WriteLine(当前是Debug构建); #elif RELEASE Console.WriteLine(当前是Release构建); #else Console.WriteLine(未知构建配置); #endif编译时编译器先检查是否定义了DEBUG如果是就只编译第一个代码块如果没定义DEBUG但定义了RELEASE就编译第二个两个都没定义就编译#else的块。其余代码块会被直接忽略像是从未存在过一样。这里有一个常见误区很多人以为#elif RELEASE里的RELEASE是Visual Studio自动定义的。实际上新建项目的Release配置下默认只定义了TRACE并没有定义RELEASE。如果你直接写#if RELEASE很可能一块代码都不会编译。想用RELEASE符号需要自己在项目属性里加或者用#if !DEBUG来表示非Debug即Release。我再分享一个我从实际工作中总结出来的经验条件编译的判断条件可以组合使用逻辑运算符比如#if (DEBUG !MY_MODE) || (NET6_0_OR_GREATER) // 满足条件时才编译 #endif注意运算符优先级建议加括号否则很容易出现你预期的逻辑和实际编译结果不一致的情况。我之前就遇到过类似问题写了个#if DEBUG !MY_MODE || SIGNALR结果因为优先级问题SIGNALR被单独划为一组判断导致条件判断范围和预期完全相反。2.3 #region、#endregion、#pragma、#line、#error、#warning除了上面说的条件编译指令还有几个实用但存在感不高的指令#region/#endregion纯粹是给IDE看的用来折叠代码块。它没有任何编译期语义编译产物完全不受影响。这是很多团队用来整理代码结构的基本工具尤其适合把大段的字段声明、属性定义、方法组放到一个个区域里。#pragma warning disable/restore临时关闭编译器警告。例如#pragma warning disable CS0168可以在当前文件位置关掉声明了变量但未使用的警告配合#pragma warning restore CS0168在后续恢复。它比在项目层面全局关掉警告更精确应该在真正需要的地方用而不是无脑加在文件顶部。#line修改编译器报告的行号信息。说实话这个指令我在C#里几乎没用过它更多是从其他语言生成代码时比如代码生成器产出文件用来修正行号映射的。正常手写代码不需要用它。#error和#warning用于在编译期主动抛出错误或警告。比如你在某个模板代码里写了#error 请先配置XXX编译器就会在编译时直接报错中断构建防止代码带着未完成配置发布上线。我用#warning用得相对多经常在待办的临时逻辑处加上它让每次编译时都能收到提醒逼着自己尽快处理。3. 条件编译符号的作用域陷阱文件级、项目级、全局变量这块是整个预处理指令里最容易被忽视的地方值得单独拿出来仔细讲。因为如果弄不清楚作用域你的代码可能在某台机器上编译通过到了另一台机器上就报错而且错误原因非常隐晦。3.1 文件级定义与顺序问题所有预处理指令都必须出现在代码的主要逻辑之前吗不完全是。#define和#undef必须写在文件的顶部且位于任何非注释代码之前。如果你在using System;之后写#define MY_MODE编译器会直接报错CS1032: 无法在预处理器指令之后定义或取消定义预处理器符号这个错误很多初学者都会碰到。注释是允许出现在#define之前的因为注释不参与编译。但一旦出现了实际的C#代码比如using语句、命名空间、类声明之后就不能再有#define/#undef了。另外还要注意#if和#endif必须成对出现且不能嵌套到混乱的程度。理论上支持嵌套比如#if A #if B // A和B都定义时编译 #endif #endif但嵌套层级过深以后代码可读性会急剧下降。我在代码评审时看到过连续嵌套五层#if的情况那种东西无论是调试还是维护都是一种折磨。3.2 项目级定义DefineConstants的优先级刚才提到在.csproj文件里可以通过DefineConstants配置全局符号。但这里有一个优先级问题项目属性里的条件编译符号和文件顶部的#define如果出现同名怎么办答案是文件级#define和项目级符号不是覆盖关系而是或关系。只要任意一层定义了该符号#if就会判断为真。如果你想在某个文件里关掉项目级的符号唯一办法是使用#undef但如前所述#undef必须写在文件顶部、所有代码之前。所以你可以这样#undef DEBUG // 后续代码里 #if DEBUG 将不再成立这在某些特殊场景下确实有用。比如你在项目里同时维护一套公共代码库其中个别文件无论如何都不希望以Debug模式编译就可以在文件顶部用#undef DEBUG明确关掉。3.3 交叉组合多配置矩阵下的符号设计真正复杂的是多目标框架或多平台构建。现在很多库都支持多目标框架比如同时兼容netstandard2.0和net6.0在.csproj里写成TargetFrameworksnetstandard2.0;net6.0/TargetFrameworks。这时候你可能需要根据不同的目标框架启用不同的API调用。C#预处理器指令专门内置了一些框架相关的符号比如框架预定义符号.NET Framework 4.xNETFRAMEWORK,NET48,NET472.NET Core / .NET 5NETCOREAPP,NET6_0,NET7_0,NET8_0.NET StandardNETSTANDARD,NETSTANDARD2_0这些符号在SDK-style项目里是自动定义的你不需要手动加。写多框架库的时候直接用#if NET6_0_OR_GREATER这种判断即可这比判断字符串再调用运行时API要可靠得多。关于这些符号的命名有一个约定俗成的小规律带_OR_GREATER后缀的表示当前框架版本大于等于某个版本。比如NET6_0_OR_GREATER在net7、net8下都会命中。这在写跨版本兼容代码时非常方便可以避免为每个版本写一大段重复分支。3.4 常见的符号未定义排查方法如果你发现某个#if分支没有按预期编译而源代码看起来又没什么问题可以按下面几个步骤排查首先检查项目配置右键项目 → 属性 → 生成 → 条件编译符号确认当前配置Debug/Release下有哪些符号。检查.csproj文件里的DefineConstants看有没有被具体配置覆盖。检查文件顶部有没有#undef把符号取消。查看当前的TargetFramework确认自动符号是否符合预期。如果项目启用了Source Generator或者构建期代码生成还需要确认生成器是否往编译上下文里注入了额外符号。这一步排查听着简单但实际定位起来往往需要翻不少文档。尤其是如果你用了自定义的MSBuild Target符号的注入时机、传递路径都可能出问题。4. 实战案例一用预处理指令做Debug/Release双模式日志与断言理论知识讲得再多不如一个完整的实战例子实在。我先从最经典的场景开始在同一套代码里让Debug构建输出详细的调试日志用断言做运行时检查而Release构建则完全剥离这些内容。4.1 场景设计假设我在写一个上位机通信模块需要和西门子PLC通过OPC UA通信。调试阶段我需要看完整的收发报文例如发送了哪些字节、收到了哪些字节、握手是否成功。上线之后这些日志不仅没意义还会拖慢性能、占满磁盘。最粗暴的方案是写个LogHelper类里面用if (IsDebug)判断。但这样有个问题日志的字符串拼接和格式化代码仍然会被编译进Release版本白白增加了程序集体积严重时还会影响JIT即时编译的优化。更好的方案是结合[Conditional]特性和预处理指令两者配合使用。[Conditional(DEBUG)]是C#提供的一个特性它标记的方法在被调用时只有定义了对应符号调用语句才会被编译。如果当前没有定义DEBUG那么所有调用该方法的代码都会被编译器直接忽略效果类似预处理指令。4.2 代码实现先在项目里定义一个静态日志类using System; using System.Diagnostics; public static class DebugLog { [Conditional(DEBUG)] public static void Write(string message) { Console.WriteLine($[DEBUG] {DateTime.Now:HH:mm:ss.fff} {message}); } [Conditional(DEBUG)] public static void WriteBytes(string title, byte[] data) { var sb new System.Text.StringBuilder(); sb.Append($[{title}] ); foreach (var b in data) { sb.Append(b.ToString(X2)).Append( ); } Console.WriteLine(sb.ToString()); } }调用方代码public class PlcClient { public void Send(byte[] frame) { DebugLog.WriteBytes(SEND, frame); // 实际发送逻辑... } public byte[] Receive() { var buffer new byte[256]; // 实际接收逻辑... DebugLog.WriteBytes(RECV, buffer); return buffer; } }当项目以Debug配置编译时WriteBytes的调用会进入编译产物运行时会输出完整的报文当项目切换为Release时所有DebugLog.Write或DebugLog.WriteBytes的调用点都会被编译器删掉连方法体的代码都不会为每个调用点保留。这里有个容易踩的坑[Conditional]只能用在返回类型为void的方法上不能用在有返回值的方法上。如果你需要返回日志结果或者把日志字符串返回给调用方就需要换一种方案比如结合#if DEBUG定义不同的方法体。下面这个例子就是典型的做法public static string BuildPayload(string cmd, int addr) { #if DEBUG return ${cmd}|{addr}|RAW{string.Join(,, Enumerable.Range(0, addr))}; #else return ${cmd}|{addr}; #endif }这个方法在Debug下构造更详细的协议内容用于本地验证Release下构造精简版本。由于整个#if块在编译期就被确定生产环境里不会残留任何多余字符串拼接逻辑。4.3 自定义断言与团队协作除了日志我们经常还需要在Debug下做严格的参数校验Release下为了性能跳过。这时可以用预处理指令写一个只存在于Debug模式的自定义断言。public static void ValidateFrame(byte[] frame, bool strict) { Debug.Assert(frame ! null, frame 不能为空); Debug.Assert(frame.Length 8, 帧长度至少为8字节); #if DEBUG if (strict) { // 在Debug模式下逐一校验协议字段类型 int type frame[0] 4; int length frame[1]; if (length ! frame.Length - 2) { throw new InvalidOperationException(帧长度字段与实际长度不一致); } } #endif }Debug.Assert本身也受DEBUG符号控制但有些断言逻辑比较复杂或者我们想抛出自定义的异常类型就可以直接用#if DEBUG包起来。发布Release包时这些校验代码完全不参与编译调用方的性能和代码体积都不会受影响。这里顺便说一个团队协作的细节如果你们团队约定用#if DEBUG写调试逻辑一定要在代码评审时规定清晰的使用边界否则很容易出现Debug下跑得好好的发版Release后功能异常的问题。比如忘记把调试模式下的临时跳转逻辑包进#if或者写了一个只有Debug下才会执行的初始化操作Release下漏掉之后数据全乱了。我在做上位机联调时就碰到过Debug模式能读到数据Release模式读不到的情况最后查出是有的初始化代码前面放了#if DEBUGRelease直接被编译器删了等于功能缺失。5. 实战案例二同一个项目兼容多框架、多平台的条件编译策略第二个高频使用场景是多目标框架兼容。你在写类库、SDK、公共组件时经常要面对既要支持老的. NET Framework服务又要跟上.NET 8的局面。这时预处理指令是你唯一的靠谱选择。5.1 多框架符号的自动注入用SDK-style项目创建多目标框架库后打开obj目录下生成的*.GlobalUsings.g.cs或者项目资产文件你会发现编译器已经自动定义了大量符号。比如目标是netstandard2.0;net6.0;net8.0系统会自动注入形如NETSTANDARD2_0、NET6_0、NET8_0、NET6_0_OR_GREATER之类的符号。这一层是IDE和MSBuild自动完成的你不需要手动加但也正因为是自动的很多人不知道这些符号存在反而去写typeof反射判断或者用RuntimeInformation.FrameworkDescription绕了一大圈。5.2 典型兼容写法API差异处理举个例子。你想写一个跨框架的字符串哈希辅助类在. NET Framework里没有System.HashCode类型在. NET 6里可以直接用HashCode.Combine。那么代码可以这么写public static int ComputeHash(string input) { #if NET6_0_OR_GREATER return System.HashCode.Combine(input, StringComparer.Ordinal); #else unchecked { int hash 17; hash hash * 31 input.Length; foreach (char c in input) { hash hash * 31 c; } return hash; } #endif }这样在net6及更高版本下走官方API在旧框架下走自定义算法两套代码互不干扰都能正确编译。再举一个更常见的例子WebClient在. NET 6里标记为已过时TypeScript还有HttpClient。如果你给别人写工具库希望老项目也能用就需要在netstandard2.0下用WebClient在net6下用HttpClient。形式上的写法完全可以用#if切换。5.3 平台相关的处理Windows API调用平台相关代码也是重灾区。比如要调用Windows的注册表或者调用某个只有Windows才有的系统API你在Linux下编译时甚至不应该引用这些类型。虽然现在有运行时判断的方法但编译期裁剪才是最彻底的。public static string GetSystemInfo() { #if WINDOWS // 使用 Windows 专属API return Microsoft.Win32.Registry.LocalMachine.Name; #else return Environment.OSVersion.ToString(); #endif }在SDK-style项目里如果你在.csproj设置了RuntimeIdentifierswin-x64;linux-x64/RuntimeIdentifiers并且启用了UseCurrentRuntimeIdentifiertrue/UseCurrentRuntimeIdentifier系统会自动定义WINDOWS、LINUX等平台符号。否则你需要自己在项目文件里定义。这里有一类很隐蔽的问题不同框架版本的平台符号定义名不完全一样。比如. NET 6引入了WINDOWS符号而. NET Core 3.1时期并没有。你写兼容库时安全做法是用#if NET6_0_OR_GREATER WINDOWS而不是单独判断WINDOWS。否则在旧框架上由于平台符号根本没定义对应代码会被悄悄排除可能连编译报错的机会都没有排查起来很痛苦。5.4 多框架项目里的看不见的编译错误我再分享一个真实踩坑经历。有一个公共组件要同时支持旧框架和新框架我写了一段代码#if NETFRAMEWORK using System.Net; var request HttpWebRequest.Create(url); #else using var client new HttpClient(); #endif项目整体编译能通过但某次我在一个老服务上单独引用这个组件的旧构建版本时突然报找不到System.Net.Http的引用错误。后来发现是老项目本身在web.config里缺少对System.Net.Http程序集的绑定重定向而不是我的代码问题。这提醒我写多框架兼容代码后一定要在实际的消费项目里做一次完整构建验证不能只在组件的测试项目里跑通就认为万事大吉。6. 高级技巧预处理指令与Roslyn源码生成器、MSBuild Target的联动当你做大型框架、脚手架工具或中间件时难免会接触到更底层的构建扩展。预处理指令在这里仍然有一席之地。6.1 通过MSBuild属性动态注入条件编译符号你可以在.csproj里根据不同的编译条件向外暴露自定义符号。比如我想在开启了某个分析器开关时允许代码里用#if STRICT_MODE编写更严格的校验证逻辑。PropertyGroup DefineConstants Condition$(EnableStrictMode) true$(DefineConstants);STRICT_MODE/DefineConstants /PropertyGroup构建命令可以这样传参dotnet build -p:EnableStrictModetrue于是代码里#if STRICT_MODE // 这段代码只在严格模式下编译 if (frame.Length 4096) throw new InvalidDataException(帧过大); #endif这种构建参数即开关的模式在自动化流水线里非常实用。CI持续集成系统可以根据不同环境传入不同参数从而控制同一套代码的不同编译形态。比如在内网测试构建时打开全部调试符号在发布公网包时全部关闭。6.2 与Roslyn Source Generator的配合Roslyn Source Generator源码生成器可以在编译前生成额外的源代码文件。它和预处理指令的关系是生成器本身可以判断当前的编译符号从而决定生成什么样的代码。比如在生成器内部var compileSymbols context.Compilation.SyntaxTrees .SelectMany(tree tree.Options.PreprocessorSymbols) .Distinct() .ToList(); if (compileSymbols.Contains(ENABLE_EXTENDED_API)) { // 生成额外API }这算是一个进阶用法普通业务代码不一定用得上但如果你在写代码生成工具、API框架就能体会到预处理符号在编译期全链路的一致性。它保证MSBuild层注入的符号、源文件里声明的符号、生成器看到的符号、最终编译使用的符号都来自同一个编译上下文不会有信息断层。6.3 和好代码的边界什么时候不该用预处理指令讲了很多用法最后也聊一下它的边界。预处理器指令不是万能的用多了会造成代码可读性严重下降。我把使用边界总结成下面几条希望对你有点参考价值同一份源代码需要针对不同编译目标生成差异极大的实现时用预处理指令合理。只是运行时有条件地选择逻辑分支时不要用预处理指令请用普通if、策略模式、依赖注入。大段大段地用#if包住几十行代码时要考虑是否应该拆出独立文件或独立方法而不是继续堆。预处理器指令无法解决运行时用户开关的问题。它只服务于编译期服务器上一个配置文件里的开关和预处理指令没有任何关系。以我个人的代码习惯一个类里#if的使用次数一般控制在三五次以内。超过这个数量我就会警觉是不是类的职责太杂了是不是应该拆成DebugOnlyBehavior和ReleaseBehavior两个策略类再用工厂方法按编译模式返回不同实例当然这也要结合项目规模来权衡如果只是一个小工具类多几个#if也无妨。7. 容易踩的坑清单编译没报错但行为完全不对我最后整理几个真实的、很隐蔽的坑。这些坑的共同特点是编译阶段完全正常甚至IDE里也没什么红波浪线但程序运行起来行为就是不符合预期。7.1 分支代码块里的变量作用域问题如果你在#if DEBUG块里声明了一个变量在#endif之后再使用直接把编译干挂。这属于语法层面会报错的情况倒是容易发现。但更隐蔽的是你设计了一个变量在Debug下被赋值在Release下从不赋值后面读取时行为分裂。比如string connectionString ; #if DEBUG connectionString servertest;databasedev; #else connectionString serverprod;databaselive; #endif这段代码没问题但如果你把#else分支漏了到了Release下connectionString永远是空字符串程序不报错但连接数据库必然失败。这种问题最恶心的地方在于本地Debug联调一切正常一发布就挂方向很难排查。7.2 预处理器指令对悬空代码的处理C#编译器在遇到未进入编译分支的代码时并不会对其中代码做完整的语法语义分析吗注意这里有个非常重要的细节未进入分支的代码依然会做基本解析但不会做完整的类型绑定和语义分析。所以你在#if FALSE里写了一句明显错误的代码比如调用一个不存在的方法很多情况下编译器不会报错因为这段代码压根不参与编译。但如果这段代码里的语法错误严重到无法解析比如少了右括号、字符串没有闭合那么即使它不进入编译分支编译器也可能报出错误。这一点和C/C的预处理器不太一样。我在实际工作中遇到过好几次在一个永远为假的#if分支里写了半行代码结果编译报错排查了好久才发现是那段死代码的问题。7.3 和#nullable、#pragma的混用顺序C# 8.0引入可空引用类型后很多项目开启了Nullableenable/Nullable。有这么一种情况你在#if分支里使用了可空上下文但不同分支下代码的可空注解状态不一致导致#if DEBUG下没有警告#else下满屏警告。原因在于预处理器指令虽然切换了代码块但#nullable的上下文指令是按行生效的如果你在某个分支中漏写了#nullable restore状态就会泄漏到其他分支。为了解决这个问题我建议在涉及可空上下文的预处理分支里显式写明#nullable disable或#nullable enable不要依赖外部环境的默认值。这虽然会让代码看起来有点啰嗦但能最大程度避免警告漂移和误判。7.4 字符串插值里的意外处理字符串插值$...里的内容不会参与预处理指令解析这个不会有歧义。但是有一种情况你在字符串里写了#if文本比如var s #if DEBUG;看起来像预处理指令但它只是普通字符串编译器不会处理也没有任何影响。反过来如果你的代码里包含#if但前面少了井号或者在注释里写了#if那也不会被解析。容易翻车的是代码格式化工具和代码片段模板。有些工具会自动缩进预处理指令比如把#if加几个空格变成#if这在很多情况下是合法的C#允许预处理指令前有空白。但如果你用了某些老式文本编辑器或者自动对齐插件可能会不小心把一个指令改成普通代码的缩进风格个别版本的老工具甚至会把#region后的文本对齐到代码块缩进导致折叠区域变得混乱看起来像是风格问题实际上是预处理指令和格式工具的交互。7.5 两个项目同时引用同一个文件符号配置不一致这种坑多出现在共享代码文件场景。假设Common.cs被A和B两个项目同时链接A项目定义了USE_A符号B没有。Common.cs里有一段#if USE_A // A逻辑 #else // B逻辑 #endif看起来没问题但如果A的USE_A符号是在某个文件顶部通过#define定义的而Common.cs和那个定义文件不在同一编译单元里那么A项目中USE_A可能压根不存在。因为文件级符号只在本文件内有效跨文件共享必须走项目级DefineConstants。这个我见过多次有同事在Program.cs顶部写#define USE_A然后期待另一个文件里的#if USE_A生效结果当然是静默失败。8. 最后再分享一个我常用的调试技巧让看不见的条件显示出来有时候你确实不确定某个预处理分支是否被编译最简单的验证方式是在代码里临时写一行#error让编译直接失败。比如#if DEBUG // 必须进到这里的逻辑 #error 当前编译包含 DEBUG 分支 #endif如果编译报错说明DEBUG确实定义着如果没有报错说明这个分支没被激活。这个方法比起翻配置、查符号列表来得直接得多。用完记得删除或者改成#warning。另外一个辅助方法是打开生成日志。在Visual Studio里启用详细等级的输出日志或者在命令行执行dotnet build -v diag然后搜索DefineConstants、DefineSymbols这些关键词可以看到编译器实际接收到了哪些符号。这是排查符号注入了但没生效类问题最强的工具。说了这么多C#预处理指令确实是个冷门话题很多书也就是一两页带过。但当你真正做跨平台库、多框架兼容、性能敏感的日志裁剪这类事情时它的价值就会一下体现出来。不要把它想象成C/C那种宏替换的复杂机制它的定位简单直接编译期的开关与筛选。用得好代码能同时服务多个环境用不好排查起来确实折腾。希望这篇整理能让你在写条件编译时少走点弯路。
返回列表