dotnet 职业技术学院 发布于 2019-07-27
做库的时候,需要一定程度上保持 API 的兼容性
首先打开你的库项目,或者如果你希望从零开始也可以直接新建一个项目。这里为了博客阅读的简单,我创建一个全新的项目来演示。
然后,为主要的库项目安装 NuGet 包:
安装完成之后,你的项目文件(.csproj)可能类似于下面这样:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="2.9.3" />
</ItemGroup>
</Project>
在你的项目内创建两个文件:
这就是两个普通的文本文件。创建纯文本文件的方法是在项目上右键 -> 添加
-> 新建项...
,然后在打开的模板中选择 文本文件
,使用上面指定的名称即可(要创建两个)。
然后,编辑项目文件,我们需要将这两个文件加入到项目中来。
如果你看不到上图中的“编辑项目文件”选项,则需要升级项目文件到 SDK 风格,详见:
然后,将这两个文件添加为 AdditionalFiles
:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="2.9.3" />
</ItemGroup>
+ <ItemGroup>
+ <AdditionalFiles Include="PublicAPI.Shipped.txt" />
+ <AdditionalFiles Include="PublicAPI.Unshipped.txt" />
+ </ItemGroup>
</Project>
如果你把这两个文件放到了其他的路径,那么上面也需要改成对应的路径。
这时,这两个文件内容还是空的。
这个时候,你会看到库中的 public
类、方法、属性等都会发出修改建议,说此符号并不是已声明 API 的一部分。
点击小灯泡,即可将点击所在的 API 加入到 PublicAPI.Unshipped.txt
中。
我将两个 API 都添加之后,PublicAPI.Unshipped.txt
文件中现在是这样的(注意有一个隐式构造函数哦):
Walterlv.PackageDemo.ApiTracking.Class1
Walterlv.PackageDemo.ApiTracking.Class1.Class1() -> void
Walterlv.PackageDemo.ApiTracking.Class1.Foo.get -> string
现在,我们将 Foo 属性改名成 Foo2 属性,于是就会出现编译警告:
RS0016 Symbol ‘Foo2.get’ is not part of the declared API.
RS0017 Symbol ‘Walterlv.PackageDemo.ApiTracking.Class1.Foo.get -> string’ is part of the declared API, but is either not public or could not be found
提示 Foo2
属性不是已声明 API 的一部分,而 Foo
属性虽然是已声明 API 的一部分,但已经找不到了。
这种提示对于保持库的兼容性是非常有帮助的。
在分析器的规则上面右键,可以为某项规则设置严重性。
这时,再编译即会报告编译错误。
项目中也会多一个规则集文件:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
+ <CodeAnalysisRuleSet>Walterlv.PackageDemo.ApiTracking.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="2.9.3" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="PublicAPI.Shipped.txt" />
<AdditionalFiles Include="PublicAPI.Unshipped.txt" />
</ItemGroup>
</Project>
前面我们都是在 PublicAPI.Unshipped.txt
文件中追踪 API。但是如果我们的库需要发布一个版本的时候,我们就需要跟上一个版本比较 API 的差异。
上一个发布版本的 API 就记录在 PublicAPI.Shipped.txt
文件中,这两个文件的差异即是这两个版本的 API 差异。在一个新的版本发布后,就需要将 API 归档到 PublicAPI.Shipped.txt
文件中。
参考资料
dotnet 职业技术学院 发布于 2019-07-27
你是否好奇 Visual Studio 2019 中的 .NET Core SDK 预览版开关是全局生效的,那个全局的配置在哪里呢?
本文将和你一起探索找到这个全局的配置文件。
Process Monitor 是微软极品工具箱的一部分,你可以在此页面下载:
当你一开始打开 Process Monitor 的时候,列表中会立刻刷出大量的进程的操作记录。这么多的记录会让我们找到目标进程操作的文件有些吃力,于是我们需要设置规则。
Process Monitor 的工具栏按钮并不多,而且我们这一次的目标只会用到其中的两个:
在工具栏上点击“设置过滤器”,然后,添加我们感兴趣的两个进程名称:
devenv.exe
MSBuild.exe
前者是 Visual Studio 的进程名,后者是 MSBuild.exe 的进程名。我们使用这两个进程名称分别找到 Visual Studio 2019 是如何设置全局 .NET Core 预览配置的,并且在命令行中运行 MSBuild.exe 来验证确实是这个全局配置。
然后排除除了文件意外的所有事件类型,最终是如下过滤器:
现在,我们打开 Visual Studio 2019,然后停留到下面这个界面中。改变一下 .NET Core SDK 预览版选项的勾选状态。
现在,我们点击一下“确定”,将立即可以在 Process Monitor 中看到一些文件的修改:
上面是在点击“确定”按钮一瞬间 Visual Studio 2019 的所有文件操作。你可以注意到左侧的时间,我的截图中从 45 秒到 48 秒是可能有效的文件读写,再后面已经延迟了 10 秒了,多半是其他的操作。
在这些文件中,可以很明显看到文件分为三类:
sdk.txt
一个不知名的文件,但似乎跟我们的 .NET Core SDK 相关SettingsLogs
一看就是给设置功能用的日志VSApplicationInsights
一看就是数据收集相关通过排除法,我们能得知最关键的文件就是那个 sdk.txt
。去看一看那个文件的内容,发现只有一行:
UsePreviews=True
这基本上可以确认 Visual Studio 2019 设置是否使用 .NET Core SDK 预览版就是在这个文件中。
不过,这带来一个疑惑,就是这个路径特别不像是 .NET Core SDK 的配置路径,倒像是 Visual Studio 自己的设置配置。
于是必须通过其他途径来确认这是否就是真实的全局配置。
现在,我们清除一下 Process Monitor 中的已经记录的数据,然后,我们在命令行中对一个项目敲下 msbuild
命令。
> msbuild
然后在 Process Monitor 里面观察事件。这次发现事件相当多,于是换个方式。
因为我们主要是验证 sdk.txt
文件,但同时希望看看是否还有其他文件。于是我们将 sdk.txt
文件相关的事件高亮。
点击 Filter
-> Highlight...
,然后选择 Path
contains
sdk.txt
时则 Include
。
这时,再看捕获到的事件,可以发现编译期间确实读取了这个文件。
此举虽不能成为此文件是全局配置的铁证,但至少说明这个文件与全局配置非常相关。
另外,继续在记录中翻找,还可以发现与此配置可能相关的两个 dll:
要验证此文件确实是全局配置其实也很简单,自行改一改配置,然后使用 MSBuild.exe 编译试试即可。
现在,将 sdk.txt 文件内容改为:
UsePreviews=False
编译一下使用了 .NET Core 3.0 新特性的项目(我使用了 Microsoft.NET.Sdk.WindowsDesktop,这是 3.0 才有的 SDK)。
编译错误,提示 Microsoft.NET.Sdk.WindowsDesktop 这个 SDK 没有找到。
现在,将 sdk.txt 文件内容改为:
UsePreviews=True
编译相同的项目,发现可以正常编译通过了。
这可以证明,此文件正是决定是否使用预览版的决定性证据。
但值得注意的是,打开 Visual Studio 2019 后,发现其设置界面并没有应用此文件最新的修改,这可以说 Visual Studio 2019 的配置是不止这一处。
通过反编译探索的方式感谢小伙伴 KodamaSakuno (神樹桜乃) 彻夜寻找。
相关的代码在 cli/VSSettings.cs at master · dotnet/cli 中,你可以前往查看。
在 VSSettings
的构造函数中,为字段 _settingsFilePath
赋值,拼接了 sdk.txt
文件的路径。
_settingsFilePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Microsoft",
"VisualStudio",
version.Major + ".0_" + instanceId,
"sdk.txt");
读取时,使用此路径下的 sdk.txt
文件读取了 UsePreviews
变量的值。
private void ReadFromDisk()
{
using (var reader = new StreamReader(_settingsFilePath))
{
string line;
while ((line = reader.ReadLine()) != null)
{
int indexOfEquals = line.IndexOf('=');
if (indexOfEquals < 0 || indexOfEquals == (line.Length - 1))
{
continue;
}
string key = line.Substring(0, indexOfEquals).Trim();
string value = line.Substring(indexOfEquals + 1).Trim();
if (key.Equals("UsePreviews", StringComparison.OrdinalIgnoreCase)
&& bool.TryParse(value, out bool usePreviews))
{
_disallowPrerelease = !usePreviews;
return;
}
}
}
// File does not have UsePreviews entry -> use default
_disallowPrerelease = _disallowPrereleaseByDefault;
}
dotnet 职业技术学院 发布于 2019-07-25
我这里使用 Visual Studio 2019 能好好编译的一个项目,发现在另一个小伙伴那里却编译不通过,是在 NuGet 还原那里报告了错误:
调用的目标发生了异常。Error parsing the nested project section in solution file.
本文介绍如何解决这样的问题。
此问题的原因可能有多种:
Project
和 EndProject
不成对,导致某个项目没有被识别出来Project
部分发现对应的项目Project
和 EndProject
不成对Project
和 EndProject
不成对通常是合并分支时,自动解冲突解错了导致的,例如像下面这样:
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo", "Walterlv.Demo\Walterlv.Demo.csproj", "{DC0B1D44-5DF4-4590-BBFE-072183677A78}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo2", "Walterlv.Demo2\Walterlv.Demo2.csproj", "{98FF9756-B95A-4FDB-9858-5106F486FBF3}"
EndProject
而解决方法,就是补全缺失的 EndProject
行:
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo", "Walterlv.Demo\Walterlv.Demo.csproj", "{DC0B1D44-5DF4-4590-BBFE-072183677A78}"
++ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo2", "Walterlv.Demo2\Walterlv.Demo2.csproj", "{98FF9756-B95A-4FDB-9858-5106F486FBF3}"
EndProject
Project
部分发现对应的项目这是说,如果在 Global
部分通过项目 Id 引用了一些项目,但是这些项目没有在前面 Project
部分定义。例如下面的 sln 片段:
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo2", "Walterlv.Demo2\Walterlv.Demo2.csproj", "{98FF9756-B95A-4FDB-9858-5106F486FBF3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
-- {DC0B1D44-5DF4-4590-BBFE-072183677A78} = {20B61509-640C-492B-8B33-FB472CCF1391}
{98FF9756-B95A-4FDB-9858-5106F486FBF3} = {20B61509-640C-492B-8B33-FB472CCF1391}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F2F1AD1B-207B-4731-ABEB-92882F89B155}
EndGlobalSection
EndGlobal
上面红框标注的项目 Id {DC0B1D44-5DF4-4590-BBFE-072183677A78}
在前面的 Project
部分是没有定义的,于是出现问题。这通常也是合并冲突所致。
解决方法是删掉这个多于的配置,或者在前面加回误删的 Project
节点,如:
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo", "Walterlv.Demo\Walterlv.Demo.csproj", "{DC0B1D44-5DF4-4590-BBFE-072183677A78}"
EndProject
可能是 nuget 识别出来的 MSBuild 版本过旧,也可能是没有安装对应的工作负载。
检查你的项目是否安装了需要的工作负载,比如做 Visual Studio 插件开发需要插件工作负载。可以阅读:
我在另外的博客中写了解决方案中项目类型的内容:
而如果是 nuget 自动识别出来的 MSBuild 版本过旧,则你会同时看到下面的这段提示:
NuGet Version: 5.1.0.6013
MSBuild auto-detection: using msbuild version ‘15.9.21.664’ from ‘C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin’. Use option -MSBuildVersion to force nuget to use a specific version of MSBuild.
Error parsing solution file at C:\walterlv\Walterlv.Demo\Walterlv.Demo.sln: 调用的目标发生了异常。 Error parsing the nested project section in solution file.
于是解决方法是使 NuGet 能够找到正确的 MSBuild.exe 的版本。
我在另一篇博客中有写一些决定 MSBuild.exe 版本的方法:
可以通过设置环境变量的方式来解决自动查找版本错误的问题。
你可以看到本文后面附带了很多的参考资料,但实际上这里的所有资料都没有帮助我解决掉任何问题。这个问题的本质是 nuget 识别到了旧版本的 MSBuild.exe。
参考资料
dotnet 职业技术学院 发布于 2019-07-25
使用 nuget restore
命令还原项目的 NuGet 包的时候,NuGet 会尝试自动检测计算机上已经安装的 MSBuild。不过,如果你同时安装了 Visual Studio 2017 和 Visual Studio 2019,那么 NuGet 有可能找到错误版本的 MSBuild。
本文介绍如何解决自动查找版本错误的问题。
当我们敲下 nuget restore
命令的时候,命令行的第 2 行会输出自动检测到的 MSBuild 版本号,就像下面的输出一样:
NuGet Version: 5.0.2.5988
MSBuild auto-detection: using msbuild version ‘15.9.21.664’ from ‘C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin’. Use option -MSBuildVersion to force nuget to use a specific version of MSBuild.
实际上我计算机上同时安装了 Visual Studio 2017 和 Visual Studio 2019,我有两个不同版本的 MSBuild:
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin
C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin
要让 NuGet 找到正确版本的 MSBuild.exe,我们有三种方法。
实际上前面 nuget restore
命令的输出中就已经可以看出来其中一个解决方法了,即使用 -MSBuildVersion
来指定 MSBuild
的版本号。
虽然命令行输出中推荐使用了 -MSBuildVersion
选项来指定 MSBuild 的版本,但是实际上实现同样功能的有两个不同的选项:
-MSBuildPath
自 NuGet 4.0 开始新增的选项,指定 MSBuild 程序的路径。-MSBuildVersion
当同时指定上面两个选项时,-MSBuildPath
选项优先级高于 -MSBuildVersion
选项。
于是我们的 nuget restore
命令改成这样写:
> nuget restore -MSBuildPath "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin"
输出现在会使用期望的 MSBuild 了:
Using Msbuild from 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin'.
NuGet 的命令行自动查找 MSBuild.exe 时,是通过环境变量中的 PATH
变量来找的。会找到 PATH
中第一个包含 msbuild.exe
文件的路径,将其作为自动查找到的 MSBuild 的路径。
所以,我们的解决方法是,如果找错了,我们就把期望正确的 MSBuild 所在的路径设置到不期望的 MSBuild 路径的前面。就像下图这样,我们把 2019 版本的 MSBuild 设置到了 2017 版本的前面。
以下是 NuGet 项目中自动查找 MSBuild.exe 文件的方法,源代码来自 https://github.com/NuGet/NuGet.Client/blob/2b45154b8568d6cbf1469f414938f0e3e88e3704/src/NuGet.Clients/NuGet.CommandLine/MsBuildUtility.cs#L986。
private static string GetMSBuild()
{
var exeNames = new [] { "msbuild.exe" };
if (RuntimeEnvironmentHelper.IsMono)
{
exeNames = new[] { "msbuild", "xbuild" };
}
// Try to find msbuild or xbuild in $Path.
var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(new[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries);
if (pathDirs?.Length > 0)
{
foreach (var exeName in exeNames)
{
var exePath = pathDirs.Select(dir => Path.Combine(dir, exeName)).FirstOrDefault(File.Exists);
if (exePath != null)
{
return exePath;
}
}
}
return null;
}
我故意在桌面上放了一个老旧的 MSBuild.exe,然后将此路径设置到环境变量 PATH
的前面,出现了编译错误。
参考资料
dotnet 职业技术学院 发布于 2019-07-24
一般情况下我们并不需要关心 Visual Studio 解决方案文件格式(.sln),因为 Visual Studio 对解决方案文件的自动修复能力是非常强的。但是如果遇到自动解冲突错误或者编译不通过了,那么此文件还是需要手工修改的。
本文介绍 Visual Studio 解决方案(.sln)文件的格式。
Visual Studio 的解决方案文件由这三个部分组成:
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28606.126
MinimumVisualStudioVersion = 10.0.40219.1
Project
EndProject
Global
EndGlobal
虽然看起来是三个独立的部分,但其实除了版本号之外,项目信息和全局信息还是有挺多耦合部分的。
比如我们来看一个 sln 文件的例子,是一个最简单的只有一个项目的 sln 文件:
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29102.190
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo", "Walterlv.Demo\Walterlv.Demo.csproj", "{DC0B1D44-5DF4-4590-BBFE-072183677A78}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F2F1AD1B-207B-4731-ABEB-92882F89B155}
EndGlobalSection
EndGlobal
下面我们来一一说明。
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29102.190
MinimumVisualStudioVersion = 10.0.40219.1
记录文件的格式版本是 12.0。使用 Visual Studio 2019 编辑/创建。
这里有一个小技巧,这里的 VisualStudioVersion 版本号设置为 15.0 会使得打开 sln 文件的时候默认使用 Visual Studio 2017,而设置为 16.0 会使得打开 sln 文件的时候默认使用 Visual Studio 2019。
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo", "Walterlv.Demo\Walterlv.Demo.csproj", "{DC0B1D44-5DF4-4590-BBFE-072183677A78}"
EndProject
项目信息至少由两行组成,第一行标记项目信息开始,而最后一行表示信息结束。
其格式为:
Project("{项目类型}") = "项目名称", "项目路径", "项目 Id"
EndProject
你可以在我的另一篇博客中找到项目类型:
但是本文列举几个 .NET/C# 项目中的常见类型:
9A19103F-16F7-4668-BE54-9A1E7A4F7556
SDK 风格的 C# 项目文件FAE04EC0-301F-11D3-BF4B-00C04F79EFBC
传统风格的 C# 项目文件2150E333-8FDC-42A3-9474-1A3956D46DE8
解决方案文件夹关于 SDK 风格的项目文件,可以阅读我的另一篇博客:
项目名称和项目路径不必多说,都知道。对于文件夹而言,项目名称就是文件夹的名称,而项目路径也是文件夹的名称。
项目 Id 是在解决方案创建项目的过程中生成的一个新的 GUID,每个项目都不一样。对于 SDK 风格的 C# 项目文件,csproj 中可以指定项目依赖,而如果没有直接的项目依赖,而只是解决方案编译级别的依赖,那么也可以靠 sln 文件中的项目 Id 来指定项目的依赖关系。另外,也通过项目 Id 来对项目做一些编译上的解决方案级别的配置。
Project
和 EndProject
的内部还可以放 ProjectSection
。
比如对于解决方案文件夹,可以包含解决方案文件:
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B002382D-4C9E-4F08-85E5-F12E2C061F5A}"
ProjectSection(SolutionItems) = preProject
.gitattributes = .gitattributes
.gitignore = .gitignore
README.md = README.md
build\Version.props = build\Version.props
EndProjectSection
EndProject
这个解决方案文件夹中包含了四个文件,其路径分别记录在了 ProjectSection
节点里面。
ProjectSection
还可以记录项目依赖关系(非项目之间的真实依赖,而是解决方案级别的编译依赖):
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo", "Walterlv.Demo\Walterlv.Demo.csproj", "{DC0B1D44-5DF4-4590-BBFE-072183677A78}"
ProjectSection(ProjectDependencies) = postProject
{98FF9756-B95A-4FDB-9858-5106F486FBF3} = {98FF9756-B95A-4FDB-9858-5106F486FBF3}
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo2", "Walterlv.Demo2\Walterlv.Demo2.csproj", "{98FF9756-B95A-4FDB-9858-5106F486FBF3}"
EndProject
在这一段节点里面,我们的 Walterlv.Demo
项目依赖于另外一个 Walterlv.Demo2
项目。依赖是以 项目 Id = 项目 Id
的方式写出来的;如果有多个依赖,那么就写多行。不用吐槽为什么一样还要写两遍,因为这是一个固定的格式,后面我们会介绍一些全局配置里面会有两个不一样的。
关于设置项目依赖关系的方法,除了 sln 文件里面的设置之外,还有通过设置项目依赖属性的方式,详情可以阅读:
一个全局信息的例子如下:
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Release|Any CPU.Build.0 = Release|Any CPU
{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F2F1AD1B-207B-4731-ABEB-92882F89B155}
EndGlobalSection
EndGlobal
在这个全局信息的例子中,为解决方案指定了两个配置(Configuration),Debug
和 Release
,平台都是 Any CPU
。同时也为每个项目指定了单独的配置种类,可供选择,每一行都是 项目的配置 = 解决方案的配置
表示此项目的此种配置在解决方案的某个全局配置之下。
如果我们将这两个项目放到文件夹中,那么我们可以额外看到一个新的全局配置 NestedProjects
字面意思是说 {DC0B1D44-5DF4-4590-BBFE-072183677A78}
和 {98FF9756-B95A-4FDB-9858-5106F486FBF3}
两个项目在 {20B61509-640C-492B-8B33-FB472CCF1391}
项目中嵌套,实际意义代表 Walterlv.Demo
和 Walterlv.Demo2
两个项目在 Folder
文件夹下。
GlobalSection(NestedProjects) = preSolution
{DC0B1D44-5DF4-4590-BBFE-072183677A78} = {20B61509-640C-492B-8B33-FB472CCF1391}
{98FF9756-B95A-4FDB-9858-5106F486FBF3} = {20B61509-640C-492B-8B33-FB472CCF1391}
EndGlobalSection
上图解决方案下的整个解决方案全部内容如下:
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29102.190
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo", "Walterlv.Demo\Walterlv.Demo.csproj", "{DC0B1D44-5DF4-4590-BBFE-072183677A78}"
ProjectSection(ProjectDependencies) = postProject
{98FF9756-B95A-4FDB-9858-5106F486FBF3} = {98FF9756-B95A-4FDB-9858-5106F486FBF3}
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo2", "Walterlv.Demo2\Walterlv.Demo2.csproj", "{98FF9756-B95A-4FDB-9858-5106F486FBF3}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Folder", "Folder", "{20B61509-640C-492B-8B33-FB472CCF1391}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DC0B1D44-5DF4-4590-BBFE-072183677A78}.Release|Any CPU.Build.0 = Release|Any CPU
{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{98FF9756-B95A-4FDB-9858-5106F486FBF3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{DC0B1D44-5DF4-4590-BBFE-072183677A78} = {20B61509-640C-492B-8B33-FB472CCF1391}
{98FF9756-B95A-4FDB-9858-5106F486FBF3} = {20B61509-640C-492B-8B33-FB472CCF1391}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F2F1AD1B-207B-4731-ABEB-92882F89B155}
EndGlobalSection
EndGlobal
dotnet 职业技术学院 发布于 2019-07-24
当 A 项目引用 B 项目,那么使用 Visual Studio 或者 MSBuild 编译 A 项目之前就会确保 B 项目已经编译完毕。通常我们指定这种引用是因为 A 项目确实在运行期间需要 B 项目生成的程序集。
但是,现在 B 项目可能仅仅只是一个工具项目,或者说 A 项目编译之后的程序集并不需要 B,仅仅只是将 B 打到一个包中,那么我们其实需要的仅仅是 B 项目先编译而已。
本文介绍如何影响项目的编译顺序,而不带来项目实际引用。
依然在项目中使用往常习惯的方法设置项目引用:
但是,在项目引用设置完成之后,需要打开项目的项目文件(.csproj)给 ProjectReference
节点加上 ReferenceOutputAssembly
的属性设置,将其值设置为 false
。这表示仅仅是项目引用,而不将项目的任何输出程序集作为此项目的依赖。
<ItemGroup>
<ProjectReference Include="..\Walterlv.Demo.Analyzer\Walterlv.Demo.Analyzer.csproj" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Walterlv.Demo.Build\Walterlv.Demo.Build.csproj" ReferenceOutputAssembly="false" />
</ItemGroup>
上面的 ProjectReference
是 Sdk 风格的 csproj 文件中的项目引用。即便不是 Sdk 风格,也是一样的加这个属性。
当然,你写多行也是可以的:
<ItemGroup>
<ProjectReference Include="..\Walterlv.Demo.Analyzer\Walterlv.Demo.Analyzer.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
<ProjectReference Include="..\Walterlv.Demo.Build\Walterlv.Demo.Build.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
这种做法有两个非常棒的用途:
ReferenceOutputAssembly
来引用项目,最终生成的 NuGet 包中就不会生成对这些项目的依赖。此方法可能会是更加常用的方法,但兼容性不那么好,可能在部分旧版本的 Visual Studio 或者 .NET Core 版本的 dotnet build
命令下不容易工作起来。
在解决方案上右键,然后选择“设置项目依赖”:
然后在弹出的项目依赖对话框中选择一个项目的依赖:
这时,如果看看解决方案文件(.sln)则可以看到多出了 ProjectDependencies
区:
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Walterlv.Demo", "Walterlv.Demo\Walterlv.Demo.csproj", "{DC0B1D44-5DF4-4590-BBFE-072183677A78}"
ProjectSection(ProjectDependencies) = postProject
{98FF9756-B95A-4FDB-9858-5106F486FBF3} = {98FF9756-B95A-4FDB-9858-5106F486FBF3}
EndProjectSection
EndProject
更多关于 sln 文件的理解,可以阅读我的另一篇博客:
使用 ReferenceOutputAssembly
属性设置的方式是将项目的编译顺序指定到项目文件中的,这意味着如果使用命令行单独编译这个项目,也是能获得提前编译目标项目的效果的,而不需要打开对应的解决方案编译才可以获得改变编译顺序的效果。
不过使用 ReferenceOutputAssembly
的一个缺陷是,必须要求目标框架能够匹配。比如 .NET Core 2.1 的项目就不能引用 .NET Core 3.0 或者 .NET Framework 4.8 的项目。
而在解决方案级别设置项目依赖则没有框架上的限制。无论你的项目是什么框架,都可以在编译之前先编译好依赖的项目。只是旧版本的 MSBuild 工具和 dotnet build
不支持 ProjectDependencies
这样的解决方案节点,会导致要么不识别这样的项目依赖(从而实际上并没有影响编译顺序)或者无法完成编译(例如出现 Error parsing the nested project section in solution file. 错误)。
参考资料
dotnet 职业技术学院 发布于 2019-07-24
Visual Studio 可以通过得知项目类型快速地为项目显示相应的图标、对应的功能等等。
本文整理已收集到的一些项目的 GUID,如果你把你的解决方案文件(sln)改坏了,那么可以修复一下。
8BB2217D-0F2D-49D1-97BC-3654ED321F3B
ASP.NET 5603C0E0B-DB56-11DC-BE95-000D561079B0
ASP.NET MVC 1F85E285D-A4E0-4152-9332-AB1D724D3325
ASP.NET MVC 2E53F8FEA-EAE0-44A6-8774-FFD645390401
ASP.NET MVC 3E3E379DF-F4C6-4180-9B81-6769533ABE47
ASP.NET MVC 4349C5851-65DF-11DA-9384-00065B846F21
ASP.NET MVC 5 / Web ApplicationFAE04EC0-301F-11D3-BF4B-00C04F79EFBC
C#9A19103F-16F7-4668-BE54-9A1E7A4F7556
C# (SDK 风格的项目)8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942
C++A9ACE9BB-CECE-4E62-9AA4-C7E7C5BD2124
Database4F174C21-8C12-11D0-8340-0000F80270F8
Database (other project types)3EA9E505-35AC-4774-B492-AD1749C4943A
Deployment Cab06A35CCD-C46D-44D5-987B-CF40FF872267
Deployment Merge Module978C614F-708E-4E1A-B201-565925725DBA
Deployment SetupAB322303-2255-48EF-A496-5904EB18DA55
Deployment Smart Device CabF135691A-BF7E-435D-8960-F99683D2D49C
Distributed SystemBF6F8E12-879D-49E7-ADF0-5503146B24B8
Dynamics 2012 AX C# in AOT82B43B9B-A64C-4715-B499-D71E9CA2BD60
ExtensibilityF2A71F9B-5D33-465A-A702-920D77279786
F#6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705
F# (SDK 风格的项目)95DFC527-4DC1-495E-97D7-E94EE1F7140D
IL projectE6FDF86B-F3D1-11D4-8576-0002A516ECE8
J#262852C6-CD72-467D-83FE-5EEB1973A190
JScript20D4826A-C6FA-45DB-90F4-C717570B9F32
Legacy (2003) Smart Device (C#)CB4CE8C6-1BDB-4DC7-A4D3-65A1999772F8
Legacy (2003) Smart Device (VB.NET)581633EB-B896-402F-8E60-36F3DA191C85
LightSwitch Project8BB0C5E8-0616-4F60-8E55-A43933E57E9C
LightSwitchb69e3092-b931-443c-abe7-7e7b65f2a37f
Micro FrameworkC1CDDADD-2546-481F-9697-4EA41081F2FC
Office/SharePoint App786C830F-07A1-408B-BD7F-6EE04809D6DB
Portable Class Library66A26720-8FB5-11D2-AA7E-00C04F688DDE
Project FoldersD954291E-2A0B-460D-934E-DC6B0785DB48
Shared Project593B0543-81F6-4436-BA1E-4747859CAAE2
SharePoint (C#)EC05E597-79D4-47f3-ADA0-324C4F7C7484
SharePoint (VB.NET)F8810EC1-6754-47FC-A15F-DFABD2E3FA90
SharePoint WorkflowA1591282-1198-4647-A2B1-27E5FF5F6F3B
Silverlight4D628B5B-2FBC-4AA6-8C16-197242AEB884
Smart Device (C#)68B1623D-7FB9-47D8-8664-7ECEA3297D4F
Smart Device (VB.NET)2150E333-8FDC-42A3-9474-1A3956D46DE8
解决方案文件夹3AC096D0-A1C2-E12C-1390-A8335801FDAB
TestA5A43C5B-DE2A-4C0C-9213-0A381AF9435A
Universal Windows Class LibraryF184B08F-C81C-45F6-A57F-5ABD9991F28F
VB.NET778DAE3C-4631-46EA-AA77-85C1314464D9
VB.NET (forces use of SDK project system)C252FEB5-A946-4202-B1D4-9916A0590387
Visual Database Tools54435603-DBB4-11D2-8724-00A0C9A8B90C
Visual Studio 2015 Installer Project ExtensionA860303F-1F3F-4691-B57E-529FC101A107
Visual Studio Tools for Applications (VSTA)BAA0C2D2-18E2-41B9-852F-F413020CAA33
Visual Studio Tools for Office (VSTO)E24C65DC-7377-472B-9ABA-BC803B73C61A
Web Site3D9AD99F-2412-4246-B90B-4EAA41C64699
Windows Communication Foundation (WCF)76F1466A-8B6D-4E39-A767-685A06062A39
Windows Phone 8/8.1 Blank/Hub/Webview AppC089C8C0-30E0-4E22-80C0-CE093F111A43
Windows Phone 8/8.1 App (C#)DB03555F-0C8B-43BE-9FF9-57896B3C5E56
Windows Phone 8/8.1 App (VB.NET)60DC8134-EBA5-43B8-BCC9-BB4BC16C2548
Windows Presentation Foundation (WPF)BC8A1FFA-BEE3-4634-8014-F334798102B3
Windows Store (Metro) Apps & ComponentsD954291E-2A0B-460D-934E-DC6B0785DB48
Windows Store App Universal14822709-B5A1-4724-98CA-57A101D1B079
Workflow (C#)D59BE175-2ED0-4C54-BE3D-CDAA9F3214C8
Workflow (VB.NET)32F31D43-81CC-4C15-9DE6-3FC5453562B6
Workflow FoundationEFBA0AD7-5A72-4C68-AF49-83D382785DCF
Xamarin.Android / Mono for Android6BC8ED88-2882-458C-8E55-DFD12B67127B
Xamarin.iOS / MonoTouchF5B4F3BC-B597-4E2B-B552-EF5D8A32436F
MonoTouch Binding6D335F3A-9D43-41b4-9D22-F6F17C4BE596
XNA (Windows)2DF5C3F4-5A5F-47a9-8E94-23B4456F55E2
XNA (XBox)D399B71A-8929-442a-A9AC-8BEC78BB2433
XNA (Zune)参考资料
dotnet 职业技术学院 发布于 2019-07-22
如果某天改了一点代码但是没有完成,我们可能会在注释里面加上 // TODO
。如果某个版本为了控制影响范围临时使用不太合适的方法解了 Bug,我们可能也会在注释里面加上 // TODO
。但是,对于团队项目来说,一个人写的 TODO
可能过了一段时间就淹没在大量的 TODO
堆里面了。如果能够强制要求所有的 TODO
被跟踪,那么代码里面就比较容易能够控制住 TODO
的影响了。
本文将基于 Roslyn 开发代码分析器,要求所有的 TODO
注释具有可被跟踪的负责人等信息。
如果你对基于 Roslyn 编写分析器和代码修改器不了解,建议先阅读我的一篇入门教程:
我们先准备一些公共的信息:
namespace Walterlv.Demo
{
internal static class DiagnosticIds
{
/// <summary>
/// 标记了待办事项的代码必须被追踪。WAL 是我名字(walterlv)的前三个字母。
/// </summary>
public const string TodoMustBeTracked = "WAL302";
}
}
在后面的代码分析器和修改器中,我们将都使用此公共的字符串常量来作为诊断 Id。
我们先添加分析器(TodoMustBeTrackedAnalyzer
)最基础的代码:
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class TodoMustBeTrackedAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
DiagnosticIds.TodoMustBeTracked,
"任务必须被追踪",
"未完成的任务缺少负责人和完成截止日期:{0}",
"Maintainability",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "未完成的任务必须有对应的负责人和截止日期(// TODO @lvyi 2019-08-01),最好有任务追踪系统(如 JIRA)跟踪。");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
=> context.RegisterSyntaxTreeAction(AnalyzeSingleLineComment);
private void AnalyzeSingleLineComment(SyntaxTreeAnalysisContext context)
{
// 这里将是我们分析器的主要代码。
}
}
接下来我们则是要完善语法分析的部分,我们需要找到单行注释和多行注释。
注释在语法节点中不影响代码含义,这些不影响代码含义的语法部件被称作 Trivia
(闲杂部件)。这跟我前面入门教程部分说的语法节点不同,其 API 会少一些,但也更加简单。
我们从语法树的 DescendantTrivia
方法中可以拿到文档中的所有的 Trivia
然后过滤掉获得其中的注释部分。
比如,我们要分析下面的这个注释:
// TODO 林德熙在这个版本写的逗比代码,下个版本要改掉。
在语法节点中判断注释的袋子性,然后使用正则表达式匹配 TODO
、负责人以及截止日期即可。
private static readonly Regex TodoRegex = new Regex(@"//\s*todo", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex AssigneeRegex = new Regex(@"@\w+", RegexOptions.Compiled);
private static readonly Regex DateRegex = new Regex(@"[\d]{4}\s?[年\-\.]\s?[01]?[\d]\s?[月\-\.]\s?[0123]?[\d]\s?日?", RegexOptions.Compiled);
private void AnalyzeSingleLineComment(SyntaxTreeAnalysisContext context)
{
var root = context.Tree.GetRoot();
foreach (var comment in root.DescendantTrivia()
.Where(x =>
x.IsKind(SyntaxKind.SingleLineCommentTrivia)
|| x.IsKind(SyntaxKind.MultiLineCommentTrivia)))
{
var value = comment.ToString();
var todoMatch = TodoRegex.Match(value);
if (todoMatch.Success)
{
var assigneeMatch = AssigneeRegex.Match(value);
var dateMatch = DateRegex.Match(value);
if (!assigneeMatch.Success || !dateMatch.Success)
{
var diagnostic = Diagnostic.Create(Rule, comment.GetLocation(), value);
context.ReportDiagnostic(diagnostic);
}
}
}
}
将上面的类组装起来运行 Visual Studio 调试即可看到效果。没有负责人和截止日期的 TODO
注释将报告编译错误。
TodoMustBeTrackedAnalyzer
类型的完整代码如下:
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;
using Walterlv.Demo;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Walterlv.Analyzers.Maintainability
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class TodoMustBeTrackedAnalyzer : DiagnosticAnalyzer
{
private static readonly LocalizableString Title = "任务必须被追踪";
private static readonly LocalizableString MessageFormat = "未完成的任务缺少负责人和完成截止日期:{0}";
private static readonly LocalizableString Description = "未完成的任务必须有对应的负责人和截止日期(// TODO @lvyi 2019-08-01),最好有任务追踪系统(如 JIRA)跟踪。";
private static readonly Regex TodoRegex = new Regex(@"//\s*todo", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex AssigneeRegex = new Regex(@"@\w+", RegexOptions.Compiled);
private static readonly Regex DateRegex = new Regex(@"[\d]{4}\s?[年\-\.]\s?[01]?[\d]\s?[月\-\.]\s?[0123]?[\d]\s?日?", RegexOptions.Compiled);
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
DiagnosticIds.TodoMustBeTracked,
Title, MessageFormat,
Categories.Maintainability,
DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.RegisterSyntaxTreeAction(AnalyzeSingleLineComment);
}
private void AnalyzeSingleLineComment(SyntaxTreeAnalysisContext context)
{
var root = context.Tree.GetRoot();
foreach (var comment in root.DescendantTrivia()
.Where(x =>
x.IsKind(SyntaxKind.SingleLineCommentTrivia)
|| x.IsKind(SyntaxKind.MultiLineCommentTrivia)))
{
var value = comment.ToString();
var todoMatch = TodoRegex.Match(value);
if (todoMatch.Success)
{
var assigneeMatch = AssigneeRegex.Match(value);
var dateMatch = DateRegex.Match(value);
if (!assigneeMatch.Success || !dateMatch.Success)
{
var diagnostic = Diagnostic.Create(Rule, comment.GetLocation(), value);
context.ReportDiagnostic(diagnostic);
}
}
}
}
}
}
只是报错的话,开发者看到错误可能会一脸懵逼,因为从未见过注释还会报告编译错误的,不知道怎么改。
于是我们需要编写一个代码修改器以便自动完成注释的修改,添加负责人和截止日期。我这里代码修改器修改后的结果就像下面这样:
生成一个新的注释字符串然后替换即可:
using System;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Walterlv.Demo;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
namespace Walterlv.Analyzers.Maintainability
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(TodoMustBeTrackedCodeFixProvider)), Shared]
public class TodoMustBeTrackedCodeFixProvider : CodeFixProvider
{
private const string Title = "添加任务负责人 / 完成日期 / JIRA Id 追踪";
private static readonly Regex AssigneeRegex = new Regex(@"@\w+", RegexOptions.Compiled);
private static readonly Regex DateRegex = new Regex(@"[\d]{4}\s?[年\-\.]\s?[01]?[\d]\s?[月\-\.]\s?[0123]?[\d]\s?日?", RegexOptions.Compiled);
public sealed override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(DiagnosticIds.TodoMustBeTracked);
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
public sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
{
var diagnostic = context.Diagnostics.First();
context.RegisterCodeFix(CodeAction.Create(
Title,
c => FormatTrackableTodoAsync(context.Document, diagnostic, c),
nameof(TodoMustBeTrackedCodeFixProvider)),
diagnostic);
return Task.CompletedTask;
}
private async Task<Document> FormatTrackableTodoAsync(
Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var oldTrivia = root.FindTrivia(diagnostic.Location.SourceSpan.Start);
var oldComment = oldTrivia.ToString();
if (oldComment.Length > 3)
{
oldComment = oldComment.Substring(2).Trim();
if (oldComment.StartsWith("todo", StringComparison.CurrentCultureIgnoreCase))
{
oldComment = oldComment.Substring(4).Trim();
}
}
var comment = $"// TODO @{Environment.UserName} {DateTime.Now:yyyy年M月d日} {oldComment}";
var newTrivia = SyntaxFactory.ParseTrailingTrivia(comment);
var newRoot = root.ReplaceTrivia(oldTrivia, newTrivia);
return document.WithSyntaxRoot(newRoot);
}
}
}
dotnet 职业技术学院 发布于 2019-07-20
有小伙伴看到我有时写了 #if
有时写了 [Conditional]
问我两个不是一样的吗,何必多此一举。然而实际上两者的编译处理是不同的,因此也有不同的应用场景。
于是我写到这篇文章当中。
我们有时会使用 #if DEBUG
或者 [Conditional("DEBUG")]
来让我们的代码仅在特定的条件下编译。
而这里的 DEBUG
是什么呢?
本文要讨论的是 #if
和 Conditional
的使用,这是在 C# 代码中的使用场景,因此,本文后面都将其称之为 “条件编译符号”。
#if
#if DEBUG
Console.WriteLine("欢迎来 blog.walterlv.com 来做客呀!");
#endif
在这段代码中,#if DEBUG
和 #endif
之间的代码仅在 DEBUG 下会编译,在其他配置下是不会编译的。
Conditional
[Conditional("DEBUG")]
public void Foo()
{
Console.WriteLine("欢迎来 blog.walterlv.com 来做客呀!");
}
而这段代码,是会被编译到目标程序集中的。它影响的,是调用这个方法的代码。调用这个方法的代码,仅在 DEBUG 下会编译,在其他配置下是不会编译的。
因为 #if DEBUG
和 #endif
仅仅影响包含在其内的代码块,因此其仅仅影响写的这点代码所在的项目(或者说程序集)。于是使用 #if
只会影响实现代码。
而 [Conditional("DEBUG")]
影响的是调用它的代码,因此可以设计作为 API 使用——让目标项目(或者程序集)仅在目标项目特定的配置下才会编译。
dotnet 职业技术学院 发布于 2019-07-20
移动程序关联 URL 是常态,桌面应用程序其实也早就支持关联 URL 以便在浏览器中打开。当我们的程序关联了一个 URL 协议之后,开发的网站上就可以通过这个 URL 与程序进行互操作,这很互联网。
对于 Windows 桌面应用来说,关联一个 URL 协议是通过修改注册表来实现的。本文介绍如何为你的应用关联一个 URL 协议。
一个常用的 URL 协议是这样子的:https://walterlv.com。前面的 https
就是协议名称,而 https://
放在一起就是在使用 https
协议。
本文我们将定义一个 walterlv
协议,然后关联到我们本地安装的一个桌面应用程序上,然后使用 walterlv://open?id=1
来打开一个 id 为 1 的逗比。
要在 Windows 系统上注册一个 URL 协议,你只需要两个步骤:
就知道你想不出来名字,于是可以使用命名生成工具:Whitman,其原理可阅读 冷算法:自动生成代码标识符(类名、方法名、变量名) - 吕毅。
然后本文使用协议名称 walterlv
。
你需要在注册表的 HKEY_LOCAL_MACHINE\Software\Classes
或者 HKEY_CURRENT_USER\Software\Classes
添加一些子键:
HKEY_CURRENT_USER\Software\Classes
walterlv
(Default) = 吕毅的特殊链接
URL Protocol = WalterlvProtocol
Shell
Open
Command
(Default) = "C:\Users\lvyi\AppData\Local\Walterlv.Foo\Walterlv.Windows.Association.exe" "%1"
在 Classes
中的那个根键 walterlv
就是我们的协议名称,也就是 walterlv://
的那个前缀。
walterlv
根键 中的 (Default)
属性给出的是链接的名称;如果后面没有设置打开方式(也就是那个 Shell\Open\Command
)的话,那么在 Chrome 里打开就会显示为那个名称(如下图)。
URL Protocol
这个注册表项是必须存在的,但里面的值是什么其实无所谓。这只是表示 walterlv
是一个协议。
接下来 Shell\Open\Command
中的 (Default)
值设置为一个打开此协议用的命令行。其中路径后面的 "%1"
是文件资源管理器传入的参数,其实就是文件的完整路径。我们加上了引号是避免解析命令行的时候把包含空格的路径拆成了多个参数。
在正确填写了注册表的以上内容之后,在 Chrome 里打开此链接将看到以下 URL 打开提示:
关于注册表路径的说明:
HKEY_LOCAL_MACHINE
主键是此计算机上的所有用户共享的注册表键值,而 HKEY_CURRENT_USER
是当前用户使用的注册表键值。而我们在注册表的 HKEY_CLASSES_ROOT
中也可以看到跟 HKEY_LOCAL_MACHINE\Software\Classes
和 HKEY_CURRENT_USER\Software\Classes
中一样的文件关联项,是因为 HKEY_CLASSES_ROOT
是 HKEY_LOCAL_MACHINE\Software\Classes
和 HKEY_CURRENT_USER\Software\Classes
合并之后的一个视图,其中用户键值会覆盖此计算机上的相同键值。
也就是说,如果你试图修改文件关联,那么需要去 HKEY_LOCAL_MACHINE\Software\Classes
和 HKEY_CURRENT_USER\Software\Classes
中,但如果只是去查看文件关联的情况,则只需要去 HKEY_CLASSES_ROOT
中。
写入计算机范围内的注册表项需要管理员权限,而写入用户范围内的注册表项不需要管理员权限;你可以酌情选用。
感谢 人猿 提供的补充信息:
假如初次点击不打开,并且勾选了始终,那么以后这个弹框就没有了,而程序也不会打开,需要做下配置的修改 谷歌浏览器:C:\Users(你的用户名)\AppData\Local\Google\Chrome\User Data\Default\Preferences 火狐浏览器:先关闭浏览器C:\Users(你的用户名)\AppData\Roaming\Mozilla\Firefox\Profiles\4uasyvvi.default 找到handlers.json
dotnet 职业技术学院 发布于 2019-07-13
经常有小伙伴跟我撕到底一些常用同音的词语应该使用哪个的问题。于是我将一些常用的软件界面中用错的文案整理出来,为自己和其他开发者提供我 已经整理的结论 和 可以溯源的资料。
下面列举出来的一些词语,有的我写的是 “推荐”,指两者都是正确的,但更应该使用 “推荐” 中的词语;而有的我写的是 “正确”,指只有这一个才是正确的,而其他写法是错误的。
无论哪一种,都说明了理由和可溯源的资料。
看《现代汉语词典》:
第五、六、七版:
【撤销】chèxiāo [动] 取消:~处分|~职务。也作撤消。 【撤消】chèxiāo 同“撤销”。
第三版:
【撤销】chèxiāo [动] 撤除;取消 |> ~原判决 | ~多余的机构。☞ 不宜写作“撤消”。 【撤消】chèxiāo 现在一般写作“撤销”。
可见,“撤消”已经被淘汰,现全部应该使用“撤销”。
那么实际中大家是如何使用的呢?
Windows 系统和 Office 套件使用的是“撤消”:
撤销:Undo。恢复:Redo。重做:Repeat。
有些软件会出现此错误,估计跟 Office 的使用有关。
在正常情况下,Office 的左上角有一对按钮:“撤消” 和 “重做”。但是,“重做” 的意思真的是 “重复上一步操作”。当你点了 “撤消” 之后,这个 “重做” 按钮会消失,变成 “恢复” 按钮,意思是将刚刚 “撤消” 的操作 “恢复” 回来。
因此,如果只是在 Office 软件里看了一眼就把文案抄过来了,那就会出现 “撤消重做” 这样的误用;实际上应该是 “撤销恢复”。
在 第一批异形词整理表 中对于 “账” 和 “帐” 的用法有一项相关的说明,明确 “账本”(zhàngběn)一词是普通话书面语中推荐的使用词形,而 “帐本” 是 “账本” 异形词。
其对于 “账” 和 “帐” 的解释如下:
“账”是“帐”的分化字。古人常把账目记于布帛上悬挂起来以利保存,故称日用的账目为“帐”。后来为了与帷帐分开,另造形声字“账”,表示与钱财有关。“账”“帐”并存并用后,形成了几十组异形词。《简化字总表》、《现代汉语通用字表》中“账”“帐”均收,可见主张分化。二字分工如下:“账”用于货币和货物出入的记载、债务等,如“账本、报账、借账、还账”等;“帐”专表用布、纱、绸子等制成的遮蔽物,如“蚊帐、帐篷、青纱帐(比喻用法)”等。
从主张分化的目的来看,其更推荐在表示“货币和货物出入的记载、债务”时使用“账”,而在表示“布、纱、绸子等制成的遮蔽物”时使用“帐”。那么软件界面中应该使用哪一个呢?
对于“支付宝”/“京东”/“淘宝”/“微信钱包”/各类银行这些一看就跟钱相关的应用里面,很明显推荐使用“账户”。另外一些如论坛 zhànghào,QQ zhànghào 等没有明前与钱相关的应用,其通常也包含一些虚拟的服务行为记录、以及与其他用户相关的虚拟交易方式(例如论坛币、Q 币),因此也推荐使用“账户”。
然而还有一些与这些虚拟交易也没有关系的,非营利组织的或者完全个人的 zhànghào,应该使用什么呢?这些 zhànghù 通常只做一些密码记录、行为记录、用户个人设置个人偏好存储等。从含义上讲,这些信息与“账”描述中的“货物出入的记载”这一句是相关的,而与“帐”中的“布、纱、绸子等制成的遮蔽物”不相关。因此,即便是这些与钱不直接相关的用户 zhànghù 或者 zhànghào 也更加推荐使用 “账号” 和 “账户”。
那么实际中大家是如何使用的呢?
在我们刚刚参考的维基文库中,其使用的就是 “账号”:
京东/1号店/支付宝的登录页面使用了 “账号”(淘宝使用了“会员名”来规避了这种争议词的使用):
淘宝使用了“会员名”来规避了这种争议词的使用。
QQ/微信/网易中使用的是 “帐号”:
Windows 系统采用了 “帐户” 一词。不过其中文版对此异形词做了很友好的适配,无论你输入哪一个词,最终都可以搜到你想要的 zhànghù:
你以为微软统一使用 “帐户” 吗?实际上可以看看下面这个页面,两个词都有使用。微软一定很纠结。
为什么连句号也要拿出来说呢?
从早期的界面设计中一直延续下来一个约定:
如果某个按钮有后续操作,那么这个按钮的名称后面需要带上省略号 “…”。
注意,这是半个省略号 “…”,而不是三个点 “…”。无论中文还是英文都如此。 正在搜寻资料确认到底是什么。
后续操作指的是“需要提供额外的信息”。例如“保存”直接存成文件,而“另存为”需要提供一个新的文件名。因此“保存”没有省略号而“另存为”则有省略号。
这个约定在微软的 Windows 系统中和苹果的 macOS 系统中原本一直都有执行下去,就像下面这样:
直到后来发现,如果继续执行这项约定,那么整个界面中将充斥着省略号,非常影响美观。
于是后来就只在菜单中保留这项约定,其他常显界面中就去掉了省略号:
可能需要解释一下异形词,来自维基文库:
异形词(variant forms of the same word)
普通话书面语中并存并用的同音(本规范中指声、韵、调完全相同)、同义(本规范中指理性意义、色彩意义和语法意义完全相同)而书写形式不同的词语。
而异体字:
异体字(variant forms of a Chinese character)
与规定的正体字同音、同义而写法不同的字。本规范中专指被《第一批异体字整理表》淘汰的异体字。
对于异形词,其不同的写法需要用在不同的场景中;对于异体字,则需要逐渐淘汰使用。
参考资料
[【撤销】【撤消】 | × 【C】√ - 校对标准:寻找权威依据 - Powered by phpwind](http://www.jiaodui.com/bbs/read.php?tid=9865) |
dotnet 职业技术学院 发布于 2019-07-12
Now we’ll talk about a behavior of WPF VisualBrush
. Maybe it is a bug, but let’s view some details and determine whether it is or not.
Let’s make a XAML layout to reproduce such an issue.
We have a large Grid
which contains an inner Grid
and an inner Border
. The Grid
contains a Rectangle
which is as large as the Grid
itself and a TextBlock
which presents some contents. The Border
only shows its background which a VisualBrush
of the Grid
.
This is the whole XAML file:
<Window x:Class="Walterlv.Demo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Walterlv 的 WindowChrome 示例窗口" Height="450" Width="800"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid x:Name="VisualSource">
<Rectangle x:Name="VisibleOr" Fill="LightCoral" Visibility="Visible" />
<TextBlock FontSize="24" TextAlignment="Center" VerticalAlignment="Center">
<Run Text="I'm walterlv, " />
<LineBreak />
<Run Text="I'm reproducing this Visual bug." />
</TextBlock>
</Grid>
<Border>
<Border.Background>
<VisualBrush Visual="{Binding Source={x:Reference VisualSource}}" />
</Border.Background>
</Border>
</Grid>
</Window>
This is the code-behind. Notice that it changes the visibility of the Rectangle
every 1 second.
using System.Threading.Tasks;
using System.Windows;
namespace Walterlv.Demo
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += OnLoaded;
}
private async void OnLoaded(object sender, RoutedEventArgs e)
{
while (true)
{
await Task.Delay(1000);
VisibleOr.Visibility = Visibility.Collapsed;
await Task.Delay(1000);
VisibleOr.Visibility = Visibility.Visible;
}
}
}
}
We know that the VisualBrush
shows and stretch the whole Visual
so we can predicate only two results:
Rectangle
is visible with Visibility
property Visible
, the Border
background which contains the VisualBrush
will be exactly the same with the Grid
.Rectangle
is invisible with Visibility
property Collapsed
, the Border
background which contains the VisualBrush
will stretch the whole content to the Border
without any area of the Rectangle
.But it is the real result?
The animation picture below shows the result when the Rectangle
is visible as the startup:
The animation picture below shows the result when the Rectangle
is invisible as the startup:
Did you notice that?
Only at the very beginning when the program runs it behaves the same as we predicted. But when we change the visibility of the Rectangle
the layout never changes.
I’ve fired this issue into GitHub and this is the link:
dotnet 职业技术学院 发布于 2019-07-12
WPF 的 VisualBrush
可以帮助我们在一个控件中显示另一个控件的外观。这是非常妙的功能。
但是本文需要说其中的一个 Bug —— 如果使用 VisualBrush 显示另一个控件的外观,那么只会在其显示效果有改变的时候刷新,而不会在目标布局改变的时候刷新布局。
我们现在做一个可以用于验证此问题的布局。
在一个大的 Grid
容器中有一个 Grid
和一个 Border
,这个 Grid
将放一个大面积的 Rectangle
和一个表示内容的 TextBlock
;而那个 Border
将完全以 VisualBrush
的形式呈现,呈现的内容是此 Grid
中的全部内容。
它的完整 XAML 代码如下:
<Window x:Class="Walterlv.Demo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Walterlv 的 WindowChrome 示例窗口" Height="450" Width="800"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid x:Name="VisualSource">
<Rectangle x:Name="VisibleOr" Fill="LightCoral" Visibility="Visible" />
<TextBlock FontSize="24" TextAlignment="Center" VerticalAlignment="Center">
<Run Text="I'm walterlv, " />
<LineBreak />
<Run Text="I'm reproducing this Visual bug." />
</TextBlock>
</Grid>
<Border>
<Border.Background>
<VisualBrush Visual="{Binding Source={x:Reference VisualSource}}" />
</Border.Background>
</Border>
</Grid>
</Window>
其后台 C# 代码如下,包含每隔 1 秒钟切换 Rectangle
可见性的代码。
using System.Threading.Tasks;
using System.Windows;
namespace Walterlv.Demo
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += OnLoaded;
}
private async void OnLoaded(object sender, RoutedEventArgs e)
{
while (true)
{
await Task.Delay(1000);
VisibleOr.Visibility = Visibility.Collapsed;
await Task.Delay(1000);
VisibleOr.Visibility = Visibility.Visible;
}
}
}
}
我们知道,VisualBrush
在默认情况下会将 Visual
中的全部内容拉伸到控件中显示,于是可以预估出两个可能的结果:
Rectangle
可见(Visibility
为 Visible
),那么 Border
中以 VisualBrush
显示的内容将完全和下面重叠(因为大小相同,拉伸后正好重叠)。Rectangle
不可见(Visibility
为 Collapsed
),那么 Border
中以 VisualBrush
显示的内容将仅有文字且拉伸到整个 Border
范围。然而实际运行真的是这样子吗?
下面的动图是 Rectangle
初始状态可见时,窗口运行后的结果:
下面的动图是 Rectangle
初始状态不可见时,窗口运行后的结果:
注意到了吗?
只有初始状态才能正确反应我们之前预估出的结果,而无论后面怎么再改变可见性,布局都不会再刷新了。只是——后面 VisualBrush
的内容始终重叠。这意味着 VisualBrush
中目标 Visual
的范围增大之后不会再缩小了。
这是问题吗?
于是在以下 issue 中跟进此问题:
参见:
dotnet 职业技术学院 发布于 2019-07-12
什么时候该抛出异常,抛出什么异常?什么时候该捕获异常,捕获之后怎么处理异常?你可能已经使用异常一段时间了,但对 .NET/C# 的异常机制依然有一些疑惑。那么,可以阅读本文。
本文适用于已经入门 .NET/C# 开发,已经开始在实践中抛出和捕获异常,但是对 .NET 异常机制的用法以及原则比较模糊的小伙伴。通过阅读本文,小伙伴们可以迅速在项目中使用比较推荐的异常处理原则来处理异常。
我们大多数小伙伴可能更多的使用 Exception
的类型、Message
属性、StackTrace
以及内部异常来定位问题,但其实 Exception
类型还有更多的信息可以用于辅助定位问题。
Message
用来描述异常原因的详细信息
StackTrace
包含用来确定错误位置的堆栈跟踪(当有调试信息如 PDB 时,这里就会包含源代码文件名和源代码行号)InnerException
包含内部异常信息Source
这个属性包含导致错误的应用程序或对象的名称Data
这是一个字典,可以存放基于键值的任意数据,帮助在异常信息中获得更多可以用于调试的数据HelpLink
这是一个 url,这个 url 里可以提供大量用于说明此异常原因的信息如果你自己写一个自定义异常类,那么你可以在自定义的异常类中记录更多的信息。然而大多数情况下我们都考虑使用 .NET 中自带的异常类,因此可以充分利用 Exception
类中的已有属性在特殊情况下报告更详细的利于调试的异常信息。
捕捉异常的基本语法是:
try
{
// 可能引发异常的代码。
}
catch (FileNotFoundException ex)
{
// 处理一种类型的异常。
}
catch (IOException ex)
{
// 处理另一种类的异常。
}
除此之外,还有 when
关键字用于筛选异常:
try
{
// 可能引发异常的代码。
}
catch (FileNotFoundException ex) when (Path.GetExtension(ex.FileName) is ".png")
{
// 处理一种类型的异常,并且此文件扩展名为 .png。
}
catch (FileNotFoundException ex)
{
// 处理一种类型的异常。
}
无论是否有带 when
关键字,都是前面的 catch
块匹配的时候执行匹配的 catch
块而无视后面可能也匹配的 catch
块。
如果 when
块中抛出异常,那么此异常将被忽略,when
中的表达式值视为 false
。有个但是,请看:.NET Framework 的 bug?try-catch-when 中如果 when 语句抛出异常,程序将彻底崩溃 - walterlv。
引发异常使用 throw
关键字。只是注意如果要重新抛出异常,请使用 throw;
语句或者将原有异常作为内部异常。
如果你只是随便在业务上创建一个异常,那么写一个类继承自 Exception
即可:
public class MyCustomException : Exception
{
public string MyCustomProperty { get; }
public MyCustomException(string customProperty) => MyCustomProperty = customProperty;
}
不过,如果你需要写一些比较通用抽象的异常(用于被继承),或者在底层组件代码中写自定义异常,那么就建议考虑写全异常的所有构造函数,并且加上可序列化:
[Serializable]
public class InvalidDepartmentException : Exception
{
public InvalidDepartmentException() : base() { }
public InvalidDepartmentException(string message) : base(message) { }
public InvalidDepartmentException(string message, Exception innerException) : base(message, innerException) { }
// 如果异常需要跨应用程序域、跨进程或者跨计算机抛出,就需要能被序列化。
protected InvalidDepartmentException(SerializationInfo info, StreamingContext context) : base(info, context) { }
}
在创建自定义异常的时候,建议:
Exception
结尾Message
属性的值是一个句子,用于描述异常发生的原因。堆栈跟踪从引发异常的语句开始,到捕获异常的 catch
语句结束。
利用这一点,你可以迅速找到引发异常的那个方法,也能找到是哪个方法中的 catch
捕捉到的这个异常。
我们第一个要了解的异常处理原则是——明确 try
catch
finally
的用途!
try
块中,编写可能会发生异常的代码。
最好的情况是,你只将可能会发生异常的代码放到 try
块中,当然实际应用的时候可能会需要额外放入一些相关代码。但是如果你将多个可能发生异常的代码放到一个 try
块中,那么将来定位问题的时候你就会很抓狂(尤其是多个异常还是一个类别的时候)。
catch
块的作用是用来 “恢复错误” 的,是用来 “恢复错误” 的,是用来 “恢复错误” 的。
如果你在 try
块中先更改了类的状态,随后出了异常,那么最好能将状态改回来——这可以避免这个类型或者应用程序的其他状态出现不一致——这很容易造成应用程序“雪崩”。举一个例子:我们写一个程序有简洁模式和专业模式,在从简洁模式切换到专业模式的时候,我们设置 IsProfessionalMode
为 true
,但随后出现了异常导致没有成功切换为专业模式;然而接下来所有的代码在执行时都判断 IsProfessionalMode
为 true
状态不正确,于是执行了一些非预期的操作,甚至可能用到了很多专业模式中才会初始化的类型实例(然而没有完成初始化),产生大量的额外异常;我们说程序雪崩了,多数功能再也无法正常使用了。
当然如果任务已全部完成,仅仅在对外通知的时候出现了异常,那么这个时候不需要恢复状态,因为实际上已经完成了任务。
你可能会有些担心如果我没有任何手段可以恢复错误怎么办?那这个时候就不要处理异常!——如果不知道如何恢复错误,请不要处理异常!让异常交给更上一层的模块处理,或者交给整个应用程序全局异常处理模块进行统一处理(这个后面会讲到)。
另外,异常不能用于在正常执行过程中更改程序的流程。异常只能用于报告和处理错误条件。
finally
块的作用是清理资源。
虽然 .NET 的垃圾回收机制可以在回收类型实例的时候帮助我们回收托管资源(例如 FileStream
类打开的文件),但那个时机不可控。因此我们需要在 finally
块中确保资源可被回收,这样当重新使用这个文件的时候能够立刻使用而不会被占用。
一段异常处理代码中可能没有 catch
块而有 finally
块,这个时候的重点是清理资源,通常也不知道如何正确处理这个错误。
一段异常处理代码中也可能 try
块留空,而只在 finally
里面写代码,这是为了“线程终止”安全考虑。在 .NET Core 中由于不支持线程终止因此可以不用这么写。详情可以参考:.NET/C# 异常处理:写一个空的 try 块代码,而把重要代码写到 finally 中(Constrained Execution Regions) - walterlv。
什么情况下该引发异常?答案是——这真的是一个异常情况!
于是,我们可能需要知道什么是“异常情况”。
一个可以参考的判断方法是——判断这件事发生的频率:
例如这些情况都应该认为是异常:
null
时但传入了 null
而下面这些情况则不应该认为是异常:
对于这些不应该认为是异常的情况,编写的代码就应该尽可能避免异常。
有两种方法来避免异常:
TryDo
模式来完成,例如字符串转数字中的 TryParse
方法,字典中的 TryGetValue
方法。null
(或默认值),而不是引发异常。极其常见的错误案例可被视为常规控制流。通过在这些情况下返回 NULL(或默认值),可最大程度地减小对应用的性能产生的影响。(后面会专门说 null)而当存在下列一种或多种情况时,应引发异常:
请勿有意从自己的源代码中引发 System.Exception、System.SystemException、System.NullReferenceException 或 System.IndexOutOfRangeException。
在前面 try-catch-finally 小节中,我们提到了 catch
块中应该写哪些代码,那里其实已经说明了哪些情况下应该处理异常,哪些情况下不应该处理异常。一句总结性的话是——如果知道如何从错误中恢复,那么就捕获并处理异常,否则交给更上层的业务去捕获异常;如果所有层都不知道如何处理异常,就交给全局异常处理模块进行处理。
对于 .NET 程序,无论是 .NET Framework 还是 .NET Core,都有下面这三个可以全局处理的异常。这三个都是事件,可以自行监听。
AppDomain.UnhandledException
AppDomain.FirstChanceException
throw
块到 catch
块之间的所有帧,而在第一次机会异常事件中,只是刚刚 throw
出来,还没有被任何 catch
块捕捉,因此在这个事件中堆栈信息永远只会包含一帧(不过可以稍微变通一下在第一次机会异常 FirstChanceException 中获取比较完整的异常堆栈)catch
也会引发,因为它引发在 catch
之前catch
就万事大吉可以无视这个事件了。前面我们说过异常仅在真的是异常的情况才应该引发,因此如果这个事件中引发了异常,通常也真的意味着发生了错误(差别只是我们能否从错误中恢复而已)。如果你经常在正常的操作中发现可以通过此事件监听到第一次机会异常,那么一定是应用程序或框架中的异常设计出了问题(可能把正常应该处理的流程当作了异常,可能内部实现代码错误,可能出现了使用错误),这种情况一定是要改代码修 Bug 的。而一些被认为是异常的情况下收到此事件则是正常的。TaskScheduler.UnobservedTaskException
async
/ await
关键字编写异步代码的时候,如果一直有 await
传递,那么异常始终可以被处理到;但中间有异步任务没有 await
导致异常没有被传递的时候,就会引发此事件。async
/ await
的使用(要么应该修改实现避免异常,要么应该正确处理异常并从中恢复错误)对于 GUI 应用程序,还可以监听 UI 线程上专属的全局异常:
Application.DispatcherUnhandledException
或者 Dispatcher.UnhandledException
Application.ThreadException
关于这些全局异常的处理方式和示例代码,可以参阅博客:
任何情况下都不应该抛出这些异常:
Exception
这可是顶级基类,这都抛出来了,使用者再也无法正确地处理此异常了SystemException
这是各种异常的基类,本身并没有明确的意义ApplicationException
这是各种异常的基类,本身并没有明确的意义NullReferenceException
试图在空引用上执行某些方法,除了告诉实现者出现了意料之外的 null 之外,没有什么其它价值了IndexOutOfRangeException
使用索引的时候超出了边界InvalidCastException
表示试图对某个类型进行强转但类型不匹配StackOverflow
表示栈溢出,这通常说明实现代码的时候写了不正确的显式或隐式的递归OutOfMemoryException
表示托管堆中已无法分出期望的内存空间,或程序已经没有更多内存可用了AccessViolationException
这说明使用非托管内存时发生了错误BadImageFormatException
这说明了加载的 dll 并不是期望中的托管 dllTypeLoadException
表示类型初始化的时候发生了错误FormatException
因为当它抛出来时无法准确描述到底什么错了首先是你自己不应该抛出这样的异常。其次,你如果在运行中捕获到了上面这些异常,那么代码一定是写得有问题。
如果是捕获到了上面 CLR 的异常,那么有两种可能:
而一旦捕获到了上面其他种类的异常,那就找到抛这个异常的人,然后对它一帧狂扁即可。
其他的异常则是可以抛出的,只要你可以准确地表明错误原因。
另外,尽量不要考虑抛出聚合异常 AggregateException
,而是优先使用 ExceptionDispatchInfo
抛出其内部异常。详见:使用 ExceptionDispatchInfo 捕捉并重新抛出异常 - walterlv。
在 该不该引发异常 小节中我们说到一个异常会被引发,是因为某个方法声称的任务没有成功完成(失败),而失败的原因有四种:
简单说来,就是:使用错误,实现错误、环境错误。
使用错误:
ArgumentException
表示参数使用错了ArgumentNullException
表示参数不应该传入 nullArgumentOutOfRangeException
表示参数中的序号超出了范围InvalidEnumArgumentException
表示参数中的枚举值不正确InvalidOperationException
表示当前状态下不允许进行此操作(也就是说存在着允许进行此操作的另一种状态)ObjectDisposedException
表示对象已经 Dispose 过了,不能再使用了NotSupportedException
表示不支持进行此操作(这是在说不要再试图对这种类型的对象调用此方法了,不支持)PlatformNotSupportedException
表示在此平台下不支持(如果程序跨平台的话)NotImplementedException
表示此功能尚在开发中,暂时请勿使用实现错误:
前面由 CLR 抛出的异常代码主要都是实现错误
NullReferenceException
试图在空引用上执行某些方法,除了告诉实现者出现了意料之外的 null 之外,没有什么其它价值了IndexOutOfRangeException
使用索引的时候超出了边界InvalidCastException
表示试图对某个类型进行强转但类型不匹配StackOverflowException
表示栈溢出,这通常说明实现代码的时候写了不正确的显式或隐式的递归OutOfMemoryException
表示托管堆中已无法分出期望的内存空间,或程序已经没有更多内存可用了AccessViolationException
这说明使用非托管内存时发生了错误BadImageFormatException
这说明了加载的 dll 并不是期望中的托管 dllTypeLoadException
表示类型初始化的时候发生了错误环境错误:
IOException
下的各种子类Win32Exception
下的各种子类另外,还剩下一些不应该抛出的异常,例如过于抽象的异常和已经过时的异常,这在前面一小结中有说明。
在平时的开发当中,你可能会遇到这样一些异常,它不像是自己代码中抛出的那些常见的异常,但也不包含我们自己的异常堆栈。
这里介绍一些常见这些异常的原因和解决办法。
当出现此异常时,说明非托管内存中发生了错误。如果要解决问题,需要从非托管代码中着手调查。
这个异常是访问了不允许的内存时引发的。在原因上会类似于托管中的 NullReferenceException
。
参考资料
dotnet 职业技术学院 发布于 2019-07-11
本文介绍不那么常见的 WPF 相关的知识。
大多数时候我们只需要在 XAML 中就可以实现我们想要的各种界面效果。这使得你可能已经不知道如何在 C# 代码中创建同样的内容。
比如在代码中创建 DataTemplate
,主要会使用到 FrameworkElementFactory
类型。
可以参考:
WPF 提供 CompositionCollection
用于将多个列表合并为一个,以便在 WPF 界面的同一个列表中显示多个数据源的数据。
<ListBox Name="WalterlvDemoListBox">
<ListBox.Resources>
<CollectionViewSource x:Key="Items1Source" Source="{Binding Items1}"/>
<CollectionViewSource x:Key="Items2Source" Source="{Binding Items2}"/>
</ListBox.Resources>
<ListBox.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{Binding Source={StaticResource Items1Source}}" />
<CollectionContainer Collection="{Binding Source={StaticResource Items2Source}}" />
<ListBoxItem>Walterlv End Item 1</ListBoxItem>
<ListBoxItem>Walterlv End Item 2</ListBoxItem>
</CompositeCollection>
</ListBox.ItemsSource>
</ListBox>
关于 CompositeCollection
的使用示例可以参考:
神樹桜乃写了一份非 WPF 框架的版本,如果希望在非 WPF 程序中使用,可以参考:
在没有使用 WPF 的时候,如果我们要为一个对象添加属性或者行为,我们可能会使用字典来实现。但字典带来了内存泄漏的问题,要自己处理内存泄漏问题可能会写比较复杂的代码。
然而,WPF 的附加属性可以非常容易地为对象添加属性或者行为,而且也不用担心内存泄漏问题。
例如,我曾经用 WPF 来模拟 UWP 流畅设计(Fluent Design)中的光照效果,使用附加属性来管理此行为则完全不用担心内存泄漏问题:
如果你有一些非 WPF 的对象需要做类似 WPF 那种附加属性,那么可以考虑使用 ConditionalWeakTable
来实现,Key 是那个对象,而 Value 是你需要附加的属性或者行为。
这里的引用关系是 Key 引用着 Value,如果 Key 被回收,那么 Value 也可以被回收。
WPF 默认情况下的触摸是通过 COM 组件 PimcManager
获取到的,在禁用实时触摸后会启用系统的 TOUCH
消息获取到,如果开启了 Pointer 消息那么会使用 POINTER
消息。
我们可以继承自 TouchDevice
来模拟触摸,详见:
在现有的 Windowing API 下,系统中看起来非常接近系统级的窗口样式可能都是用不同技术模拟实现的,只是模拟得很像而已。
如果要将 WPF 模拟得很像 UWP,可以参考我的这两篇博客:
目前 WPF 还不能直接使用 Windows 10 Fluent Design 特效。当然如果你的程序非常小,那么模拟一下也不会伤害太多性能:
然而充分利用 Fluent Design 的高性能,需要上 XAML Islands,详见:
[Using the UWP XAML hosting API in a desktop application - Windows apps | Microsoft Docs](https://docs.microsoft.com/en-us/windows/apps/desktop/modernize/using-the-xaml-hosting-api) |