ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Visual Studio 项目属性、属性表与配置平台矩阵解析

2026/9/19 1:06:25 拓冰建站 浏览量
Visual Studio 项目属性、属性表与配置平台矩阵解析 刚接手一个 C 项目编译报一堆 LNK2019点开属性页一看附加库目录是空的附加依赖项里却塞了七八条带完整盘符的路径而且只填在 Debug|Win32 一份配置里。切到 Release|x64所有设置瞬间回到出厂状态。这种场面我见得太多了。Visual Studio 里的项目属性配置表面上就是右键项目、点属性、在几百个输入框里找到要填的那一格实际上它背后是一套配置 × 平台矩阵 多层继承链 两套完全不同的工程体系的组合规则。你改的那个格子到底属于谁、会被谁覆盖、最终展开成什么命令行才是决定编译能不能过的关键。这篇内容我想把 Visual Studio 项目属性这件事从根上讲透属性页里那些节点各自管什么、属性继承是按什么顺序合并的、属性表.props为什么值得单独花时间、C# 项目的面板和 csproj 是什么对应关系以及配置改完不生效时该怎么一层层往下查。刚上手 VS 的人可以照着抄作业已经写了几年 C 或 C# 的人大概也能从里面找到几个自己一直在踩的坑。1. 先把配置 × 平台这层账算清楚属性页到底在改哪一份设置打开属性页的第一眼很多人会直接往左树里钻忽略了左上角那两个下拉框。而所有我明明改了怎么没用的问题十有八九都出在这里。1.1 配置和平台两个下拉组合出的矩阵是彼此独立的属性页顶部的配置下拉默认是 Debug 和 Release平台下拉在 C 项目里是 Win32、x64、ARM64C# 项目里叫 Any CPU、x86、x64、ARM64。这两个下拉交叉出来的每一个格子都是一份完全独立的设置集合。也就是说你在 Debug|Win32 里填的附加包含目录不会自动出现在 Debug|x64 里更不会出现在 Release|Win32 里。Visual Studio 只是在你新建项目时把一份默认值复制到了矩阵的每个格子里之后它们就各过各的了。想跨格子统一修改把配置切到所有配置、平台切到所有平台再改这是最省事的做法。但要注意一个反直觉的后果这样做会让所有配置的值完全一致而 Debug 和 Release 恰恰应该在某些地方不一样比如运行库、优化开关、预处理器定义。所以我的习惯是分层处理——路径类、第三方库类的东西用所有配置一次填完编译行为类的开关逐格子调。1.2 配置管理器新建自定义配置时别踩的两个坑生成菜单里有个配置管理器很多人只在新建平台时用过它其实它的作用远不止这个。它能做的事包括新建自定义配置比如 Debug_Static、Release_Unicode 这种带有特定含义的配置、重命名配置、决定某个配置是否参与整个解决方案的生成、批量勾选多个项目的平台对应关系。这里有两个坑值得提前说。第一个是新建配置时忘了勾从父级复制结果新配置里所有路径类设置都是空的全项目找不到头文件。第二个是解决方案配置和项目配置不是一对一映射解决方案里可以有一个叫Release的解决方案配置把项目 A 映射到它的 Release、把项目 B 映射到它的 Debug这是调试混合配置时的常用手段但排查问题时如果只看解决方案配置名就会被误导。排查这类问题的动作很简单在解决方案资源管理器里选中项目按 AltEnter 打开属性页看顶部两个下拉的实际值再对照配置管理器里的映射表。1.3 属性继承链谁的值最终说了算C 项目.vcxproj的属性不是一个来源而是好几层合并出来的。按应用顺序大致是这样层级来源典型位置该不该进版本库1工具集与 SDK 默认值MSBuild 安装目录下的 Microsoft.Cpp.*.props否跟 VS 版本走2项目文件本身项目目录下的 .vcxproj是3项目属性表项目里挂载的 .props是4用户级属性表Microsoft.Cpp.$(Platform).user.props否机器私有5用户宏属性页里通用属性 → 用户宏视情况合并规则是后应用的覆盖先应用的属性表内部的顺序另有讲究第 3 章细说。这意味着同一份设置如果在项目属性页里也填了、在属性表里也填了属性页里的值未必赢——取决于属性表的挂载顺序和那一格是否写了追加语法。还有一个容易被忽略的开关属性页右上角有一句从父级或项目默认设置继承。默认是勾上的一旦取消勾选属性表里的值可能就不再起作用了有些项目模板或者别人改过的工程会把它取消掉排查时值得看一眼。1.4 三类文件落点不同混用是很多麻烦的根源解释清楚这三类文件的区别后面排查会轻松很多.vcxproj / .csproj项目自己的定义属性页里改的大部分值最终都写到这里应该提交到仓库。.props 属性表可以挂在多个项目上的配置片段适合放第三方库路径、公共编译选项也应该提交。.user 相关文件.vcxproj.user、.csproj.user、用户级 .props放个人机器相关的调试参数、本地库路径绝对不能提交否则队友拉下来就得改一遍。我见过最典型的事故是同事把本机 D 盘的 Boost 路径填进了 .vcxproj 里的附加包含目录提交之后全组编译报 C1083而他自己因为路径存在一点事都没有。这类问题的根因不是路径填错而是填错了文件。2. C 项目属性页里高频要动的几个节点逐页过一遍左侧那棵树第一次看确实劝退但真正常改的就是那么几页。这一章按实际动手频率从高到低过一遍每一页都说清楚这个值最终变成什么命令行参数。2.1 常规页输出目录、中间目录和目标名称配置属性 → 常规页里的东西不多但每一个都影响其他所有路径的解释方式。核心是三个目录输出目录可执行文件、DLL、LIB 的落点我一般写成$(SolutionDir)build\bin\$(Platform)\$(Configuration)\。中间目录obj、pdb、预编译头这些中间产物的落点建议写成$(SolutionDir)build\obj\$(ProjectName)\$(Platform)\$(Configuration)\。目标名称 / 目标扩展名除非有特殊需求别改。中间目录这里有一个必须强调的细节一定要把 $(ProjectName) 加进去。如果多个项目共用一个中间目录不同项目里同名但内容不同的 pch.h、同名 .obj 会互相污染症状是清理一下就好了重新生成又失败非常难查。而 MSBuild 的增量判断依赖时间戳和中间文件一旦被污染重编译行为会变得不可预测。页面上还有配置类型应用程序 / 动态库 / 静态库平台工具集和Windows SDK 版本。平台工具集决定用哪一版编译器如果队友用了 v143 而你只装了 v142打开项目时会提示重定向或者报错这时要么装对应的工具集要么统一降级项目设置并在提交说明里写清楚。2.2 C/C 页附加包含目录、预处理器定义、语言标准、运行库这是改动最频繁的一页下面四个子节点的用法差别很大。附加包含目录C/C → 常规对应编译器的/I。填写时一行一个目录推荐$(SolutionDir)third_party\include这种基于宏的相对写法。这里有个经验用$(SolutionDir)而不是$(ProjectDir)可以让第三方库路径在解决方案内的所有项目里统一否则每个项目都要写一串..\..\third_party\include改一次目录结构就全崩。预处理器定义C/C → 预处理器对应/D。最常见的几个Debug 配_DEBUGRelease 配NDEBUGUnicode 项目配UNICODE;_UNICODE两个都要一个是 Windows API 的宏一个是 C 运行库的宏处理 MSVC 报不建议使用 strcpy 等不安全函数时配_CRT_SECURE_NO_WARNINGS。这里建议的做法是在项目里用条件判断区分而不是靠所有配置一次性填死——把 NDEBUG 填进 Debug 配置里会导致 assert 全部失效调试时极其迷惑。C 语言标准C/C → 语言对应/std:c17、/std:c20这类开关。如果项目里有人写了 C17 的结构化绑定、折叠表达式却在属性里留着默认值编译会报一堆语法错误看起来像代码写错了其实是标准没开。运行库C/C → 代码生成 → 运行库对应/MD、/MDd、/MT、/MTd四选一。这一格的规则很硬同一个进程里所有链接在一起的模块必须用同一档运行库。/MT和/MD混用会在链接期报 LNK2038 运行库不匹配而跨 DLL 边界传递 CRT 对象比如在一个 /MT 编译的 DLL 里 new、在 /MD 编译的 exe 里 delete即使链接过了运行期也可能堆损坏这种问题比链接错误难查十倍。2.3 链接器页附加库目录、附加依赖项、子系统与入口点我刚入行时最大的困惑就是库目录和附加依赖项到底有没有区别。答案是区别很明确附加库目录链接器 → 常规对应/LIBPATH告诉链接器去哪儿找 .lib填的是目录比如$(SolutionDir)third_party\lib\$(Platform)\$(Configuration)。附加依赖项链接器 → 输入对应传给链接器的文件名列表比如ws2_32.lib;mylib.lib填的是库名带后缀。把完整路径塞进附加依赖项虽然也能链接成功但换个人、换个盘符就废了。而且链接器命令行里出现全路径时很容易和库目录里的同名库产生优先级混乱所以我反对这种写法——即便它在我机器上是好的。子系统链接器 → 系统要看你写的是main还是WinMain控制台程序用/SUBSYSTEM:CONSOLEWindows 窗口程序用/SUBSYSTEM:WINDOWS。写 WinMain 却留着 CONSOLE会出现一个多余的黑框反过来写 main 却设成 WINDOWS链接器会报找不到入口点。真要用自定义入口时去链接器 → 高级 → 入口点里显式指定别再改代码里的函数名。还有一个调试期很有用的开关链接器 → 优化 → 引用里的/OPT:REF在 Release 下默认开启它会剔除未被引用的函数如果你在做插件式加载、靠手工注册的函数表来调用某些函数可能会发现Release 下插件加载不到函数Debug 下没问题——这时候需要考虑是否要保留引用而不是去怀疑反射机制。2.4 调试页与生成事件把启动参数和构建后动作固化下来调试页里的设置不是给编译器看的是给调试器看的。常用的几项命令默认$(TargetPath)改远程调试或调用宿主程序时要动。命令参数命令行参数比每次手动敲方便得多。工作目录默认是项目目录而很多程序按相对路径读配置文件实际部署时又在输出目录运行这个不一致经常导致调试能跑、双击 exe 就崩。我一般直接设成$(OutDir)。环境用PATH$(SolutionDir)build\bin\$(Platform)\$(Configuration);%PATH%这种写法让调试时能找到同解决方案里刚生成的 DLL省去反复拷 DLL。生成事件有三个预生成、预链接、生成后。最常用的是生成后事件把依赖的 DLL 拷到输出目录xcopy /y /d $(SolutionDir)third_party\bin\$(Platform)\*.dll $(OutDir)注意/d参数只复制更新的文件和路径外面的引号——带空格的路径不加引号是生成事件失败的头号原因。另外在生成中使用这一项如果设成否命令行里出现错误也不会让生成失败看起来好像复制成功了其实一句都没执行这个坑我踩过一次排查了半小时。3. 用属性表把配置从单个项目里抽出来复用一个解决方案里十几个项目每个项目都要填一遍同样的第三方库路径这种重复劳动不仅费时间更麻烦的是改一次要改十几处漏一处就等着编译失败。属性表就是解决这个问题的。3.1 为什么第三方库的路径不该散落在每个项目里先看散落式配置的三个后果。第一是升级库版本时的一致性风险路径散落在十几个 vcxproj 里升级到 1.2 版本时漏改一个项目链接期就会报符号不匹配而错误信息只告诉你无法解析的外部符号不告诉你是哪个项目用了旧版本的库。第二是新人接手成本库路径写成本机绝对路径时新人拉下代码第一件事就是逐个改路径。第三是配置分叉有人在某项目里临时加了/bigobj或者改了警告等级没人知道久了就成了历史包袱。属性表把这些问题收敛成一个文件库路径只在里面写一次所有挂载它的项目都跟着走。升级库版本时改一行全解决方案生效。3.2 建一张自己的属性表完整操作流程第一步把属性管理器窗口调出来。它在菜单栏视图 → 其他窗口 → 属性管理器。这个窗口是属性表的唯一入口很多人不知道它的存在是因为默认布局里根本不显示。第二步在属性管理器里展开项目名 → Debug | Win32这类节点能看到默认挂着几张表。右键项目节点 → 添加新项目属性表起个有意义的名字比如Common.ThirdParty.props保存位置建议放在解决方案目录下的build\props\这样能被相对路径引用。第三步双击新建的属性表开始编辑。这里有个必须提醒的地方编辑属性表时属性页标题栏显示的是属性表文件名而不是项目名。很多人在这一步以为自己改的是项目实际上把改动写进了共享的属性表导致我改了一个项目怎么所有项目都变了。第四步在属性表里填值。下面是一份可以直接参考的最小可用结构?xml version1.0 encodingutf-8? Project ToolsVersion4.0 xmlnshttp://schemas.microsoft.com/developer/msbuild/2003 ImportGroup LabelPropertySheets / PropertyGroup LabelUserMacros ThirdPartyRoot$(SolutionDir)third_party/ThirdPartyRoot /PropertyGroup ItemDefinitionGroup ClCompile AdditionalIncludeDirectories$(ThirdPartyRoot)\include;%(AdditionalIncludeDirectories)/AdditionalIncludeDirectories PreprocessorDefinitions_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)/PreprocessorDefinitions /ClCompile Link AdditionalLibraryDirectories$(ThirdPartyRoot)\lib\$(Platform)\$(Configuration);%(AdditionalLibraryDirectories)/AdditionalLibraryDirectories AdditionalDependenciesmylib.lib;%(AdditionalDependencies)/AdditionalDependencies /Link /ItemDefinitionGroup /Project这份结构里最要命的一行是%(AdditionalIncludeDirectories)。它不是可选项。MSBuild 里同名属性的赋值行为是覆盖只有显式写上%(...)才会把上游已经积累的值拼在后面。漏了它属性表会把项目里原有的包含目录全部冲掉症状是挂了属性表以后原来能编译的项目反而编不过了。3.3 加载顺序、覆盖规则与改了没生效的排查实验多张属性表挂在一起时同一属性被定义多次是常态这时候谁赢答案是顺序决定覆盖关系后合并的会盖住先合并的。属性管理器里可以通过右键菜单里那组上移/下移按钮调序。实践中我遵循一条简单约定通用的往下放、特殊的往下放不对——通用的放上面特例的放下面让特例有机会覆盖通用值。如果你的 VS 版本表现和预期不一致别猜做个五分钟的实验新建两张极简属性表各自给同一个属性设不同值都挂到项目上然后打开链接器 → 命令行或C/C → 命令行看最终展开的结果是哪一版再把其中一张表上移重看一次。这比翻文档快得多也更可靠。另一个常见的改了没生效是取消勾选继承复选框。属性页右上角那句从父级或项目默认设置继承如果被取消属性表里定义的值可能就不会参与合并。排查时先确认它是勾上的。3.4 用户属性表与团队共享属性表要分开放Visual Studio 会在每个平台下挂一张用户级属性表通常叫Microsoft.Cpp.$(Platform).user.props位置在用户配置目录里。这张表的设计意图很明确放个人机器相关的东西比如本地的调试工具路径、私有的库位置。它不进版本库也不该进。团队共享的部分统统放进仓库里的build\props\。这样分工之后规则变成一句话凡是队友也需要的就是共享表凡是我这台机器特有的就是用户表。按这条线分永远不会有人因为提交了本机路径而让全组编不过。4. C# 项目的项目属性是另一套逻辑面板与 csproj 的双向对应如果你写的是 C#前面讲的大部分内容对你没用——C# 项目的属性页结构完全不同而且它和 csproj 之间是双向同步关系理解这一点能省掉很多困惑。4.1 应用程序页、生成页里哪些值最终写进了 csprojC# 项目属性页常见的几页是应用程序、生成、生成事件、调试、包、资源、设置、签名、代码分析。关键对应关系如下属性页上的项csproj 里的元素说明程序集名称AssemblyName决定输出文件名默认命名空间RootNamespace新建文件时的默认 namespace目标框架TargetFramework决定可用的 API 集合输出类型OutputTypeExe / WinExe / Library平台目标PlatformTargetAnyCPU / x86 / x64条件编译符号DefineConstants如 DEBUG;TRACE优化代码Optimize布尔值允许不安全代码AllowUnsafeBlocks布尔值语言版本LangVersion部分版本需要在 csproj 手写在面板上改和在 csproj 里直接改效果完全一样——面板本质上就是个可视化编辑器。区别在于面板只会往 csproj 里写它认识的那几个属性而像Nullable、ImplicitUsings、TreatWarningsAsErrors这类较新的开关在某些版本里只能在 csproj 里手写。所以我的做法是日常改动用面板涉及新特性或需要条件判断时直接编辑 csproj改完双击面板确认 VS 能正常解析如果解析失败属性页会变成灰色或者报错。4.2 调试启动参数为什么会被写进 .csproj.user这是 C# 项目里最容易让人困惑的一点你在调试页填的命令行参数、环境变量、工作目录不会写进 csproj而是写进.csproj.user文件。这个设计的用意是启动参数往往和个人调试习惯相关不应该影响队友。但副作用也很明显——你在本地加了一堆参数让程序跑起来提交代码后同事拉下来发现程序起不来因为参数没跟着走。如果启动参数是团队共享的比如必须指定某个配置文件路径正确做法是把它写进Properties/launchSettings.json这个文件是可以提交的。.NET Core 之后还有一层launchSettings.json里可以定义多个 profile每个 profile 有自己的commandName、commandLineArgs、environmentVariables、applicationUrl。VS 启动时默认用第一个 profile调试工具栏的下拉框可以在多个 profile 之间切换。理解了这一层就不会再出现我改了 csproj 里的参数怎么没用这种困惑了。4.3 条件属性与多目标框架场景下的配置写法C# 的 csproj 是标准 MSBuild 文件支持按条件给属性赋值。下面是 Debug 和 Release 分开定义的标准写法PropertyGroup Condition$(Configuration)Debug DefineConstantsDEBUG;TRACE/DefineConstants Optimizefalse/Optimize /PropertyGroup PropertyGroup Condition$(Configuration)Release DefineConstantsTRACE/DefineConstants Optimizetrue/Optimize /PropertyGroup多目标框架时条件要按$(TargetFramework)写。比如一个库既要支持新版框架又要支持旧框架旧框架下引用的包不一样就可以这样ItemGroup Condition$(TargetFramework)net48 PackageReference IncludeSomeLegacyPackage Version2.1.0 / /ItemGroup这里有一条经验条件属性块尽量写在一起不要散落在文件各处。csproj 手改多了以后最怕的是同一个属性在文件头尾各定义一次后定义的静默覆盖前面的结果面板上显示的值和你以为的完全不是一回事。5. 配置改完不生效的典型故障按顺序排查的完整链路这一章不讲结论讲顺序。因为排查这类问题最大的误区就是看到报错就去搜报错信息而百分之八十的情况下报错信息只描述了结果根因在属性页里。5.1 第一层先确认改对了配置、平台和容器节点拿到改了没用的反馈我第一个动作永远是打开属性页看左上角两个下拉的值然后问一句你是在哪个组合下改的现在生成用的是哪个组合具体检查四件事属性页顶部的配置和平台是否等于当前实际编译的那个组合改动是写在项目属性页里还是写在某个属性表里属性表那一格是不是用了覆盖而不是追加右上角的继承复选框是否被取消。这一层看起来太基础但根据我自己的统计它解决掉的改完没生效问题超过一半。原因很朴素VS 默认只显示 Debug|Win32 一份设置而大家按 F5 时可能跑的是 x64或者切了 Release 却没意识到。5.2 第二层看命令行确认宏展开后的真实值属性页里每一组设置下面都有一个命令行节点C/C → 命令行链接器 → 命令行。这个节点显示的是宏展开之后的最终参数是排查问题的利器。举个实际例子你填了$(SolutionDir)third_party\include但$(SolutionDir)在某些情况下会展开成空字符串——比如直接用 msbuild 编译单个 .vcxproj 文件、或者在没有解决方案的情况下打开项目。空展开之后路径变成\third_party\include编译器会去当前驱动器的根目录找当然找不到。只看属性页是看不出来的命令行里一眼就能发现。同样的道理适用于$(SolutionDir)、$(ProjectDir)、$(OutDir)、$(IntDir)这些宏。它们的定义如下表宏含义使用建议$(SolutionDir)解决方案目录含结尾反斜杠在 sln 内构建时可靠单独编译项目时可能为空$(ProjectDir)项目文件所在目录最稳推荐优先使用$(MSBuildProjectDirectory)当前 MSBuild 项目目录走 MSBuild 命令时的稳妥选择$(OutDir)输出目录调试工作目录、生成后事件常引用$(TargetPath)输出文件全路径调试页命令默认值$(Configuration)当前配置名路径里做 Debug/Release 分离$(Platform)当前平台名路径里做位数分离看完了编译命令行如果问题出在链接期再看链接器命令行。库目录到底展开成什么、附加依赖项里究竟是哪几个 lib一目了然。5.3 第三层链接期报错的几种模式与对应根因到了这一层报错信息本身就能指路了。下面这张表是我这些年攒下来的经验对照报错常见根因优先检查C1083 无法打开包括文件包含目录漏配、路径宏展开为空、改错了配置编译命令行里的 /I 参数LNK1104 无法打开文件 xxx.lib库目录错、附加依赖项里写了带路径的库名链接器命令行里的 /LIBPATHLNK2019 无法解析的外部符号库名没加进附加依赖项、C 接口没加 extern C附加依赖项列表、头文件声明LNK2038 检测到运行库不匹配/MT 与 /MD 混用、Debug 与 Release 库混用C/C → 代码生成 → 运行库LNK1112 模块计算机类型冲突x86 项目链接了 x64 的库平台下拉、库目录里的位数子目录重点说两条。LNK2019 最常见的误判是库没找到实际上库找到了、只是库里没有那个符号。原因通常是名字修饰不匹配C 编译器会对函数名做修饰头文件里声明成extern C而库里是 C 修饰名或者反过来链接器就找不到。这种问题靠改库目录是永远解决不了的要看头文件声明和库的编译方式是否一致。LNK2038 的第二常见形态是_ITERATOR_DEBUG_LEVEL不匹配这是 Debug 版标准库和 Release 版标准库混用的典型信号。症状是一个用 Release 编的静态库被一个 Debug 配置的项目链接了。解决方式不是去改宏定义而是给库目录加$(Configuration)这一层让 Debug 走 Debug 目录、Release 走 Release 目录。5.4 第四层增量构建与缓存造成的假象前三层都排除了才轮到怀疑缓存。要强调的是先做清理解决方案 重新生成再考虑删 .vs 目录。这两步顺序不要反因为删 .vs 目录会丢失本地调试配置和 IntelliSense 缓存代价比清理大得多。我遇到的缓存类问题主要有两类。一类是预编译头pch相关的改了编译选项但 pch 没重新生成导致部分编译单元用了旧选项症状是新旧行为混杂。这类问题清理一次基本能解决。另一类是中间目录被多个项目共用导致的污染症状是清理就好、重新生成又坏遇到这种就要回到 2.1 节检查中间目录里有没有$(ProjectName)。还有一类不算 bug 但很费时间的现象改了头文件的搜索顺序比如新增了一个包含目录排在最前面结果同名的头文件被优先命中了新目录里的那一份产生大量莫名其妙的编译错误。这类问题在命令行里看 /I 的顺序就能确认也是为什么我一直建议附加包含目录要一行一个、顺序有讲究。6. 让项目属性跟着仓库走路径宏、忽略清单与统一的约定最后聊聊工程化层面的东西。配置这件事本身不难难的是让整个团队、以及半年后的自己都能拿到一份一致的配置。6.1 用宏写路径别写绝对路径这条规则我说过很多次但每年仍然能看到新项目在犯。绝对路径的问题不是换机器会失效这么简单而是它会让增量构建和分布式缓存彻底失效——因为构建结果依赖于一个无法复现的路径。判断标准很简单属性页里任何一个填写路径的格子内容里如果出现了盘符C:\、D:\或者用户目录就应该被改掉。正确的写法是把根目录抽成一个用户宏比如在属性表里定义ThirdPartyRoot$(SolutionDir)third_party/ThirdPartyRoot然后在各个路径里引用它。这样升级目录结构时只需要改一行。还有一个进阶技巧值得知道$(SolutionDir)只在通过解决方案构建时有值如果你需要一份在单独编译某个项目时也成立的路径用$(MSBuildThisFileDirectory)——它指向当前这个 props 文件自身所在的目录比$(SolutionDir)更稳。我现在的共享属性表里基本都用它。6.2 哪些文件该提交、哪些该忽略这份清单建议直接抄进 .gitignore应该提交.sln、.vcxproj、.vcxproj.filters、.props、.csproj、Directory.Build.props、packages.lock.json、launchSettings.json应该忽略.vs/、bin/、obj/、*.user、*.suo、*.aps、*.ipch、.csproj.user顺序很重要先确认忽略规则生效再开始改项目属性。反过来做的话本机路径已经提交进仓库了还要再写一次提交去清理麻烦。另外提一句Directory.Build.props放在解决方案根目录下的这个文件会被该目录及子目录下的所有 MSBuild 项目自动导入对 C# 项目是标准做法。较新版本的 Visual Studio 对 C 项目也会导入它如果你的工具链版本比较新用它可以省掉不少挂属性表的手工活如果团队里有人用老版本 VS那就老老实实走属性表这条路别指望这一招。6.3 新人拉下代码后对齐配置的检查清单每次有新人加入或者换机器我都会让他按这个顺序过一遍能挡掉大部分环境问题打开解决方案看是否需要重定向项目按团队约定选择工具集版本。打开属性管理器确认属性表都挂上了没有显示黄色感叹号的缺失项。检查属性页里有没有本机绝对路径搜一下盘符有就说明有人在提交前没清理。从 Debug|x64 开始生成一次再从 Release|x64 生成一次两边都能过说明配置层没分叉。打开属性页的命令行节点扫一眼包含目录和库目录展开结果确认指向的是解决方案内的路径而不是用户目录。我自己在实际操作中体会到的是项目属性配置这件事写起来是点点鼠标想管明白却需要把它当成代码来对待——改动要小、要提交、提交说明里写清楚为什么改改动涉及共享属性表时要通知团队。我见过不止一次因为某个人在属性表里加了一行/wd4996导致整个团队三年都没发现某类潜在问题。配置是最容易被忽略的技术资产但它决定了所有代码能不能被正确地构建出来。