WPF:CS0103-当前上下文中不存在名称 “InitializeComponent”

最近开发的项目需要借助 WPF (.NET Core),创建基于 net40net45net472netcoreapp3.1 等多个框架的 WPF 程序,方便为不同的系统部署。

问题

项目的创建都很顺利,当基于 WPF (.NET Framework) 的代码向新项目迁移时,发现无法生成,错误列表出现如下错误提示:

1
2
3
严重性	代码	说明	项目	文件	行	禁止显示状态
错误 CS0103 当前上下文中不存在名称“InitializeComponent” WpfApp1 C:\Users\hd2y\source\repos\WpfApp1\WpfApp1\MainWindow.xaml.cs 25 活动
错误 MC1000 未知的生成错误“Could not find assembly 'mscorlib, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e'. Either explicitly load this assembly using a method such as LoadFromAssemblyPath() or use a MetadataAssemblyResolver that returns a valid assembly.” WpfApp1 C:\Program Files\dotnet\sdk\3.1.300-preview-015048\Sdks\Microsoft.NET.Sdk.WindowsDesktop\targets\Microsoft.WinFX.targets 225

排查

开始排查了项目加载问题、引用程序集、以及部分代码的实现,最终发现在添加 Autofac 的引用时会出现这个问题。

在必应上检索发现了 Autofac 官网的这篇文章:Why are “old versions” of the framework (e.g., System.Core 2.0.5.0) referenced?

但是我的开发环境 .NET Framework 应该不存在问题,至于使用 Autofac 4.x 及以上版本,因为需要支持 .NET Framework 4.0 也是不可能的。

解决

因为暂时没有想到其他解决方案的话,只能换依赖注入工具了。

虽然用习惯了 Autofac,换成 Unity 感觉各种不方便,但是最新版仍然支持 .NET Framework 4.0 不得不说:“真香!”

注:Unity 的配置包 Unity.Configuration 目前不支持 .NET Framework 4.5 以下版本,因为项目对这方面要求不高,所以自定义了。如果依赖配置文件来完成依赖注入,那在 .NET Framework 4.0 下,只能另外想办法了。

Windows XP 提示激活无法启动

MacOS 安装 Windows XP SP3 测试环境 一文我在 MacOS 中使用 Parallels Desktop 安装了 Windows XP 用于测试。

但是最近使用时出现了一些问题,这里记录一下。

提示激活死循环

当成功启动虚拟机后,Windows XP 系统提示:“在您可以登录前,此副本的 Windows 必须被 Microsoft 激活。您现在想激活它吗?”

选择“否”,系统关机。(没有成功关机,卡在壁纸上无响应。)

选择“是”,提示:“Windows已激活,单击‘确定’以退出。”

但是我们点击“确定”后,却闪回“在您可以登录前,此副本的 Windows 必须被 Microsoft 激活。您现在想激活它吗?”

至此,系统启动进入死循环。

解决方案

解决该问题,首先我们需要关机,进入安全模式。

首先我们右键 Dock 里的 Windows 图标,停止运行 Windows XP 虚拟机:

stop running

然后选择配置,进入“硬件” -> “启动顺序”,将“选择启动时的引导设备”勾选起来。

open boot

重新启动我们的 Windows XP 虚拟机,在启动时按“F8”,选择“安全模式”进入。

在安全模式下,打开 cmd,运行一下命令:

1
rundll32.exe syssetup,SetupOobeBnk

运行后重启,即可正常进入系统。

以上命令只是延长试用期,建议直接安装 VOL 版,使用密钥激活。

1
2
3
ed2k://|file|zh-hans_windows_xp_professional_with_service_pack_3_x86_cd_vl_x14-74070.iso|630237184|EC51916C9D9B8B931195EE0D6EE9B40E|/

key MRX3F-47B9T-2487J-KWKMF-RPWBY

安装 IE8

系统内置的为 IE6,可以下载 IE8 的安装包进行安装:下载地址

参考:

使用 Nexus3 搭建私人 NuGet 服务器

一直想搭建一个 NuGet 包管理服务器,毕竟 nuget.org 是公开的,一些公司或个人的包不适合上传。

之前尝试过 NuGet.Server,但是因为其缺少前端的管理工具,所以后面渐渐放弃了。

没想到的是过去几个月的时间,再搜 NuGet,发现了 Nexus 这个宝藏,果断造起来。

使用 docker 运行 nexus3

安装 docker 的部分略过,直接进行安装。

拉取 Nexus 镜像

最新发布的 Nexus 的镜像到本地:

1
docker pull sonatype/nexus3:latest

镜像详细说明:https://hub.docker.com/r/sonatype/nexus3

运行 Nexus

创建文件夹存放 Nexus 持久化数据:

1
mkdir -p /data/nexus

使用 Docker 命令运行 Nexus:

1
docker run -d -p 8081:8081 --name nexus -v /data/nexus:/nexus-data sonatype/nexus3:latest

访问 Nexus

尝试在浏览器中访问 Nexus:http://{ip}:8081

如果无法访问,可以通过以下命令查看容器运行状态:

1
docker ps -a

如果容器的状态为 Exited,那么可以通过以下命令查看日志:

1
docker container logs nexus

查看日志内容,如果开头几行为:

1
2
mkdir: cannot create directory '../sonatype-work/nexus3/log': Permission denied
mkdir: cannot create directory '../sonatype-work/nexus3/tmp': Permission denied

可以发现是我们没有为 Nexus 分配文件夹权限,可以执行以下命令授权:

1
chown -R 200 /data/nexus

然后重新启动我们的 nexus 容器即可:

1
docker start nexus

配置反向代理

建议使用 caddy,具体配置可以参考安装 gitea 的那篇博文:代码仓管理系统 Gitea 搭建

nexus

Nexus 中 NuGet 的简单使用

登录 Nexus

登录默认用户名为:admin。

默认密码可以在文件 /data/nexus/admin.password 查看,登录成功后会提示修改默认密码。

nexus welcome

如上图所示,在登录以后,欢迎页面旁会出现一个 小齿轮 的图标。

创建 HelloNexus 项目

创建一个 .NET Standard 类库项目 用于测试 Nexus 托管 NuGet 包。

内容很简单,增加一个 Nexus.cs 文件,定义一个 Hello 方法:

1
2
3
4
5
6
7
8
9
10
namespace HelloNexus
{
public class Nexus
{
public static string Hello()
{
return "Hello Nexus!";
}
}
}

项目文件 HelloNexus.csproj 做如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net40;net45;</TargetFrameworks>
<Version>1.0.0</Version>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>hd2y</Authors>
<Description>Hello Nexus</Description>
<PackageProjectUrl>https://git.hd2y.net/hd2y/HelloNexus</PackageProjectUrl>
<RepositoryUrl>https://git.hd2y.net/hd2y/HelloNexus</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageTags>Nexus</PackageTags>
<PackageId>$(AssemblyName)</PackageId>
<Title>$(AssemblyName)</Title>
<IsPackable>true</IsPackable>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
</PropertyGroup>

<ItemGroup>
<None Include="..\..\build\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>

</Project>

可以在项目“属性” -> “打包”界面进行配置。

这样,我们生成项目的时候,将会自动进行打包:

package hello nexus

当然,也可以使用 NuGet Package Explorer 工具自行打包:

nuget package explorer

上传 NuGet 包

我们需要登录 Nexus,进入后台管理与设置界面。

Repositories

如下图,进入后台的 Repositories,我们可以看到默认已经配置了 maven 与 nuget:

nexus repositories

而且点击创建仓库,还有很多类型的仓库供我们选择,主流的仓库类型都可以在列表内找到:

nexus create repository

当然这里我们主要还是介绍 NuGet,但是其中的仓库类型,我们还是需要了解一下。

  • nuget.org-proxy:类型是 proxy,表示代理仓库。我们向它请求包(package)的时候,如果本地有,它就从本地提供,如果本地没有,它会从 nuget.org 下载到本地,然后给我提供这个包。

  • nuget-hosted:类型是 hosted,表示托管仓库。我们一般把自己开发的包上传到该仓库中。

  • nuget-group:类型是 group,表示仓库组,它结合了 nuget.org-proxynuget-hosted,能对外提供上述两者中的包。

nuget-hosted 负责包上传,nuget.org-proxy 负责代理从 nuget.org 下载包到 Nexus 缓存中,nuget-group 负责提供包。一般使用 nuget-group 提供的 URL 就可以了,它可以把私服和公共库进行合并。

上传 Package

(一)添加 Nuget Realms

因为 Nexus 认证 Nuget 是通过 Realms 来认证,因此要添加 Nuget Realms。

nexus add nuget realm

(二)上传 NuGet 包

回到网站内容浏览的主视图,在 Upload 选中 nuget-hosted 点击进入,为什么要选中 nuget-hosted 上面已经说明。

nexus upload

选择我们前文打包的 HelloNexus.1.0.0.nupkg 文件,并点击上传。

nexus choose assets

上传成功后,会出现如下图提示:

nexus upload success

(三)检查 NuGet 包

在 Browse 选中 nuget-hosted 点击进入。我们可以查看所有托管的包,可以查看到 HelloNexus.1.0.0.nupkg 已经上传成功。

nexus browse

使用 NuGet 包

获取程序包源地址

在 Nexus 的 Browse 找到 nuget-group 获取到地址。

nexus copy url

配置 NuGet 程序包源

打开 NuGet 包管理器,点击程序包源旁的设置按钮,会弹出如下界面:

nuget add packages source

注意:需要先点击添加,然后选中新增的源进行修改,修改后点击“更新”按钮,不要修改了原来的 nuget.org 源。

查找与安装

选择程序包源为我们新添加的源,然后查找我们上传的程序包 HelloNexus

search hello nexus

选中后,为我们需要的项目勾选安装即可。

批量上传

首先,需要查看 NuGet API Key

nexus-nuget-api-key

下载 nuget.exe,放在 *.nupkg 所在目录下,并添加一个批处理文件 nuget.bat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@echo off
:: init params
set url=https://(这里填写域名)/repository/nuget-hosted/
set deployFile=*.nupkg
echo Searching nupkg file...
rem 启用"延缓环境变量扩充"
SETLOCAL ENABLEDELAYEDEXPANSION
for %%f in (%deployFile%) do (
set name=%%f
echo !name! to deploy to %url%
rem deploy to server
nuget push !name! (这里填写api-key) -source %url%
)

pause

然后直接运行批处理文件,等待上传。

查虫:文章截图中有 Hallo 这个单词出没,是因为前期不小心输入错了,后期发现已经改正,截图就偷懒不再改了。

参考:

C# 基础:float、double 与 decimal

前言

我想看到这个标题的时候,很多小伙伴都有点诧异,这么基础的内容,不就是“浮点数值类型”吗,为什么要写这么基础的入门知识?

当然,我想大家也都知道,想要高精度需要使用 decimal,如果存储的数值比较大要用 double,甚至可能都可以背下来三者的范围:

C# 类型/关键字 大致范围 精度 大小 .NET 类型 类型后缀
float ±1.5 x 10⁻⁴⁵±3.4 x 10³⁸ 大约 6-9 位数字 4 个字节 System.Single fF
double ±5.0 × 10⁻³²⁴±1.7 × 10³⁰⁸ 大约 15-17 位数字 8 个字节 System.Double dD,不带后缀的小数默认为 double
decimal ±1.0 x 10⁻²⁸±7.9228 x 10²⁸ 28-29 16 个字节 System.Decimal mM

二进制浮点型

因为 decimal 又被称之为“货币类型”,所以很多小伙伴在写代码时,只要不是用于财会系统,存储的内容和财富金钱没有关系,通通都使用 double 类型。

毕竟 double 是默认小数类型,写数字的时候还要带个后缀真的是太麻烦了,默认的就好。难道 double 类型提供的十几位的小数不够我们用吗?

那么我们执行以下代码:

1
2
3
4
5
6
7
8
9
10
double num1 = 10000000000000000d + 1d;
Console.WriteLine($"double: 10000000000000000 + 1 = {num1}");
Console.WriteLine($"double: 10000000000000000 == 10000000000000001 is {10000000000000000d == 10000000000000001d}");
double num2 = 0.1d * 0.1d;
Console.WriteLine($"double: 0.1 × 0.1 == 0.01 is {num2 == 0.01}");

// 程序运行输出:
// double: 10000000000000000 + 1 = 10000000000000000
// double: 10000000000000000 == 10000000000000001 is True
// double: 0.1 × 0.1 == 0.01 is False

以上两个结果都不符合我们的预期,但是很多人在写程序的时候忽略了,或者没有预想到会出现这种错误。

主要原因有:

  • 错误的认为数据类型范围内的数字,都可以正确的被存储;
  • 将“精度”理解为该数据类型可以存储的小数位数;
  • 不知道或没有意识到 floatdouble 属于二进制浮点型,其用于表示十进制数字是近似值,不宜进行运算;

十进制浮点型

decimal 类型在其范围和精度内的十进制数完全准确。相反,用二进制数表示十进制数,则可能造成舍入错误。

decimal 被表示成 ±N×10ᵏ。其中 N 是 96 位的正整数,而 -28≤k≤0

而浮点数是 ±N×2ᵏ 的任意数字。其中 N 是用固定数量位数(float 是 24,double 是 53)表示的正整数,k 是 -149~+104(float) 或者 -1075~+970(double)。

所以,我们将之前不符合预期的浮点数运算,修改为使用 decimal 类型:

1
2
3
4
5
6
7
8
9
10
decimal num1 = 10000000000000000m + 1m;
Console.WriteLine($"decimal: 10000000000000000 + 1 = {num1}");
Console.WriteLine($"decimal: 10000000000000000 == 10000000000000001 is {10000000000000000m == 10000000000000001m}");
decimal num2 = 0.1m * 0.1m;
Console.WriteLine($"decimal: 0.1 × 0.1 == 0.01 is {num2 == 0.01m}");

// 程序运行输出:
// decimal: 10000000000000000 + 1 = 10000000000000001
// decimal: 10000000000000000 == 10000000000000001 is False
// decimal: 0.1 × 0.1 == 0.01 is True

将不会再出现“预料之外的运算错误”。

当时我们仍然需要注意几点:

  • decimal 类型是所有数据类型中速度最慢的(一般情况下,在我们业务中这个损耗可以忽略不计);
  • 其数据范围内的数字仍然和其精度相关,不在精度范围内的数字仍然不能正确存储,并且要注意精度不是小数位数;

注意: decimal 在很多文章中不被认为是 浮点数浮点类型,而被称作 十进制数十进制类型,这里为了方便比较,所以统称 floatdoubledecimal 为浮点型。

参考:

C# 基础:构造函数、析构函数、解构函数

因为总感觉对于 C# 的知识掌握的不够透彻,所以最近在 Kindle 上购买了《C# 7.0 本质论》,复习一些基础知识,有了一些新发现。

这里开一个小专题,记录一些书上提到的,在日常编程中可能会比较少用到,但是很有意思的一些内容。

当然构造函数、析构函数,这些都是很熟悉的内容了,但是解构函数还是第一次听说,讲解这个用法的时候,顺便把这两个的提一下,算是凑凑字数吧。_(:з)∠)_

构造函数(Constructor)

构造函数定义了在类实例化过程中发生的事情。

类默认有一个无参数的构造函数,如果我们将该无参数构造函数重写为私有构造函数,该对象将无法直接实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestClass1 { }

public class TestClass2
{
private TestClass2() { }
}

class Program
{
static void Main(string[] args)
{
// 调用默认的无参数构造函数
TestClass1 class1 = new TestClass1();
// 重写了唯一的无参数构造函数,并私有化,下面实例化代码将报错
// TestClass2 class2 = new TestClass2();
}
}

静态构造函数

静态构造函数用于初始化任何静态数据,或执行仅需执行一次的特定操作。将在创建第一个实例或引用任何静态成员之前自动调用静态构造函数。

静态构造函数可以说是实现“单例模式”最偷懒的一种做法,另外如果我们需要在类型构造之前做一些事情的话,可以使用静态构造函数,甚至可以重新加载程序集,具体可以参看
C# 打包引用程序集与动态链接库
这篇文章。

这里通过以下一段代码展示其调用顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class TestClass
{
static TestClass()
{
Console.WriteLine("调用了 TestClass 的静态构造函数");
}
public TestClass()
{
Console.WriteLine("调用了 TestClass 的实例构造函数");
}
public static void TestMethod1()
{
Console.WriteLine("调用了 TestClass 的静态方法 TestMethod1");
}
public void TestMethod2()
{
Console.WriteLine("调用了 TestClass 的实例方法 TestMethod2");
}
}

class Program
{
static void Main(string[] args)
{
TestClass.TestMethod1();
TestClass @class = new TestClass();
@class.TestMethod2();
Console.ReadKey();
}
}

// 程序运行输出:
// 调用了 TestClass 的静态构造函数
// 调用了 TestClass 的静态方法 TestMethod1
// 调用了 TestClass 的实例构造函数
// 调用了 TestClass 的实例方法 TestMethod2

可以看到静态构造函数最先被调用,实际上如果我们代码中出现了 TestClass 这一类型,无论是要调用其静态方法、静态字段、静态属性,还是想要实例化该对象,都将会先触发其静态函数的执行。

构造函数链

构造函数可以像普通的静态与实例方法一样重载,重载时我们可以通过关键字 this 调用当前对象的另一个构造函数,如果想要调用父类的构造函数,可以使用 base 关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class Phone
{
public Phone(string phoneNumber)
{
PhoneNumber = phoneNumber;
Console.WriteLine($"调用 Phone 的构造函数:PhoneNumber={PhoneNumber}");
}
public string PhoneNumber { get; set; }
public void Call(string phoneNumber)
{
Console.WriteLine($"{PhoneNumber}{phoneNumber} 呼出了电话!");
}
}

public class ApplePhone : Phone
{
public ApplePhone(string phoneNumber, string systemVersion, int batteryCapacity) : base(phoneNumber)
{
SystemVersion = systemVersion;
BatteryCapacity = batteryCapacity;
Console.WriteLine($"调用 ApplePhone 的构造函数:PhoneNumber={PhoneNumber}, SystemVersion={SystemVersion}, BatteryCapacity={BatteryCapacity}");
}
public ApplePhone(string phoneNumber) : this(phoneNumber, "未知", -1)
{
Console.WriteLine($"调用 ApplePhone 的构造函数:PhoneNumber={PhoneNumber}");
}
public string SystemVersion { get; set; }
public int BatteryCapacity { get; set; }
}

class Program
{
static void Main(string[] args)
{
TestClass.TestMethod1();
Phone phone = new ApplePhone("13888888888");
phone.Call("119");
Console.ReadKey();
}
}

// 运行程序输出:
// 调用 Phone 的构造函数:PhoneNumber=13888888888
// 调用 ApplePhone 的构造函数:PhoneNumber=13888888888, SystemVersion=未知, BatteryCapacity=-1
// 调用 ApplePhone 的构造函数:PhoneNumber=13888888888
// 13888888888 向 119 呼出了电话!

可以看到,使用构造函数创建实例时,其会先执行链接的构造函数。

析构函数(Destructor)

虽然我们在很多文章中,了解到用于资源释放的这个特殊的函数为“析构函数”,但是实际上在 MSDN 以及 C# 的中文工具书中,其更多被称之为“终结器”。

终结器(也称为 析构函数)用于在垃圾回收器收集类实例时执行任何必要的最终清理操作。

实际上我们是无法控制何时调用析构函数,但是因其调用是在垃圾回收器决定,所以我们可以通过强制的垃圾回收来实现该析构函数的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class Phone
{
public Phone(string phoneNumber)
{
PhoneNumber = phoneNumber;
Console.WriteLine($"调用 Phone 的构造函数:PhoneNumber={PhoneNumber}");
}
public string PhoneNumber { get; set; }
public void Call(string phoneNumber)
{
Console.WriteLine($"{PhoneNumber}{phoneNumber} 呼出了电话!");
}
~Phone()
{
Console.WriteLine($"调用 Phone 的析构函数");
}
}

public class ApplePhone : Phone
{
public ApplePhone(string phoneNumber, string systemVersion, int batteryCapacity) : base(phoneNumber)
{
SystemVersion = systemVersion;
BatteryCapacity = batteryCapacity;
Console.WriteLine($"调用 ApplePhone 的构造函数:PhoneNumber={PhoneNumber}, SystemVersion={SystemVersion}, BatteryCapacity={BatteryCapacity}");
}
public ApplePhone(string phoneNumber) : this(phoneNumber, "未知", -1)
{
Console.WriteLine($"调用 ApplePhone 的构造函数:PhoneNumber={PhoneNumber}");
}
public string SystemVersion { get; set; }
public int BatteryCapacity { get; set; }
~ApplePhone()
{
Console.WriteLine($"调用 ApplePhone 的析构函数");
}
}

class Program
{
static void Main(string[] args)
{
TakeCall();
GC.Collect();
Console.ReadKey();
}

public static void TakeCall()
{
Phone phone = new ApplePhone("13888888888");
phone.Call("119");
}
}

// 运行程序输出:
// 调用 Phone 的构造函数:PhoneNumber=13888888888
// 调用 ApplePhone 的构造函数:PhoneNumber=13888888888, SystemVersion=未知, BatteryCapacity=-1
// 调用 ApplePhone 的构造函数:PhoneNumber=13888888888
// 13888888888 向 119 呼出了电话!
// 调用 ApplePhone 的析构函数
// 调用 Phone 的析构函数

可以注意到,垃圾回收执行后,析构函数的执行顺序为先执行派生类的析构函数。

另外需要注意的是,如果我们直接在 TakeCall 方法中,调用 GC.Collect() 将不会触发该析构函数,因为垃圾回收只会回收那些无法再访问的代码块的对象。

因为这个特点,建议在使用比较稀缺的资源时,通过实现 IDisposable 接口来完成资源的释放,以保证这些资源能够及时的释放,提高程序的性能。

解构函数(Deconstruct)

如果熟悉元组的内容,并了解过“析构元组”的知识,应该对以下内容不会陌生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Program
{
static void Main(string[] args)
{
var (year, _, _, _, _, _) = GetDateTimeInfo("2020年2月11日");
Console.WriteLine($"{year}年是{(DateTime.IsLeapYear(year) ? "闰年" : "平年")}");
Console.ReadKey();
}

public static (int year, int month, int day, int hour, int minute, int second) GetDateTimeInfo(string datetime)
{
int year = 0, month = 1, day = 1, hour = 0, minute = 0, second = 0;
var info = Regex.Split(datetime, "[^0-9]");
if (info.Length > 0 && !string.IsNullOrEmpty(info[0])) year = int.Parse(info[0]);
if (info.Length > 1 && !string.IsNullOrEmpty(info[1])) month = int.Parse(info[1]);
if (info.Length > 2 && !string.IsNullOrEmpty(info[2])) day = int.Parse(info[2]);
if (info.Length > 3 && !string.IsNullOrEmpty(info[3])) hour = int.Parse(info[3]);
if (info.Length > 4 && !string.IsNullOrEmpty(info[4])) minute = int.Parse(info[4]);
if (info.Length > 5 && !string.IsNullOrEmpty(info[5])) second = int.Parse(info[5]);

return (year, month, day, hour, minute, second);
}
}

// 运行程序输出:
// 2020年是闰年

C# 提供内置的元组析构支持,可在单个操作中解包一个元组中的所有项。用于析构元组的常规语法与用于定义元组的语法相似:将要向其分配元素的变量放在赋值语句左侧的括号中。

对于非元组类型的解构,C# 不提供内置支持。但是,用户作为类、结构或接口的创建者,可通过实现一个或多个 Deconstruct 方法来析构该类型的实例。该方法返回 void,且要析构的每个值由方法签名中的 out 参数指示。

将以上代码中的 GetDateTimeInfo 方法,改造成 DateTimeInfo 结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Program
{
static void Main(string[] args)
{
// var (year, _, _, _, _, _) = GetDateTimeInfo("2020年2月11日");
var (year, _, _) = new DateTimeInfo("2020年2月11日");
Console.WriteLine($"{year}年是{(DateTime.IsLeapYear(year) ? "闰年" : "平年")}");
Console.ReadKey();
}
}

public struct DateTimeInfo
{
public DateTimeInfo(string datetime)
{
Year = 0;
Month = 1;
Day = 1;
Hour = 0;
Minute = 0;
Second = 0;
var info = Regex.Split(datetime, "[^0-9]");
if (info.Length > 0 && !string.IsNullOrEmpty(info[0])) Year = int.Parse(info[0]);
if (info.Length > 1 && !string.IsNullOrEmpty(info[1])) Month = int.Parse(info[1]);
if (info.Length > 2 && !string.IsNullOrEmpty(info[2])) Day = int.Parse(info[2]);
if (info.Length > 3 && !string.IsNullOrEmpty(info[3])) Hour = int.Parse(info[3]);
if (info.Length > 4 && !string.IsNullOrEmpty(info[4])) Minute = int.Parse(info[4]);
if (info.Length > 5 && !string.IsNullOrEmpty(info[5])) Second = int.Parse(info[5]);
}
public int Year { get; }
public int Month { get; }
public int Day { get; }
public int Hour { get; }
public int Minute { get; }
public int Second { get; }
public void Deconstruct(out int year, out int month, out int day)
{
(year, month, day, _, _, _) = this;
}
public void Deconstruct(out int year, out int month, out int day, out int hour, out int minute, out int second)
{
year = Year;
month = Month;
day = Day;
hour = Hour;
minute = Minute;
second = Second;
}
}

// 运行程序输出:
// 2020年是闰年

以上涉及到“弃元”的内容,MSDN 上的描述为:析构元组时,通常只需要关注某些元素的值。 从 C# 7.0 开始,便可利用 C# 对弃元的支持,弃元是一种仅能写入的变量,且其值将被忽略 。 在赋值中,通过下划线字符 (_) 指定弃元。 可弃元任意数量的值,且均由单个弃元 _ 表示。

弃元操作,不仅仅在以上析构元组与解构自定义对象时用到,当我们调用的方法存在 out 参数,但是该参数不需要时,我们也可以使用。

例如,我们需要判断一个字符串是否为 decimal 数字,可以调用 C# 自带的 TryParse 方法,这时我们不需要转换成功的结果,就可以舍弃:

1
2
3
4
5
6
string strNum = "2e+8";
bool isNum = decimal.TryParse("2e+8", System.Globalization.NumberStyles.Any, null, out _);
Console.WriteLine($"{strNum} 判断是否为数字的结果:{isNum}");

// 运行程序输出:
// 2e+8 判断是否为数字的结果:True

但需要注意的是,这时我们需要携带 out 关键字,该关键字不能舍弃。

参考:

多线程检测可用的 Web 服务

在日常开发中,各个系统之间进行通信,用的比较多的还是 HTTP 协议,其中 WebService 服务也是我最常用的。

因为正常我们会配置多台 Web 服务器,所以为了确保调用的服务一直是正常的状态,我们需要在服务不可用的时候,自动的将服务切换到正常的 Web 服务器上。

注:内容主要是以 WebService 进行举例,如果使用其他的通信方案,例如 WCF、Web Api 等等,其实也是一样的。

服务连接

因为搭建服务做这个测试相对繁琐,如果对 WebService 服务使用有疑问,可以看我的 SOA 系列文章中的:
SOA —— WebService 知识点总结

而且其实服务可用的检测,除可以 HTTP 请求通过外,其关联的其他服务例如数据库资源等也需要检测,我一般是添加一个 HelloWorld 方法,供客户端建立 SoapClient 以后调用。

这里仅仅使用以下伪代码,来模拟这个过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/// <summary>
/// 服务客户端
/// </summary>
public class SoapClient
{
/// <summary>
/// 初始化客户端(伪)
/// </summary>
/// <param name="name"></param>
public SoapClient(string name)
{
Name = name;
}

/// <summary>
/// 当前客户端名称
/// </summary>
public string Name { get; set; }

private static Random Random = new Random();

/// <summary>
/// 检测服务状态
/// </summary>
/// <returns>服务是否可用</returns>
public bool HelloWorld()
{
// 随机休眠线程 模仿服务资源检测
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}[{Thread.CurrentThread.ManagedThreadId:00}]@[{Name}]:开始连接服务");
Thread.Sleep(Random.Next(500,5000));
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}[{Thread.CurrentThread.ManagedThreadId:00}]@[{Name}]:服务资源检测结束");

// 随机确认客户端连接状态
bool result = Random.NextDouble() > 0.3;
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}[{Thread.CurrentThread.ManagedThreadId:00}]@[{Name}]:服务状态为{result}");
return result;
}
}

运行检测效果:

1
2
SoapClient client = new SoapClient("Test");
client.HelloWorld();

多个服务端

如果只有一个服务端,供我们连接,那其实无论创建多少个客户端其实都没有意义,所以我们这个方案主要是针对存在多个 Web 客户端,针对每个服务端初始化一个客户端进行连接,测试服务状态。

以下的代码,我们会模拟存在 5 台 Web 服务器,需要初始化 5 个客户端,分别测试连接状态,确认客户端建立的连接是否可用。

因为服务器所执行的业务是相同的,只需要一台服务器连接建立成功,即可返回。

不考虑多线程,我们一般可以使用如下代码进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
string[] clientNames = new string[] { "Client1", "Client2", "Client3", "Client4", "Client5" };

SoapClient client = null;
for (int i = 0; i < clientNames.Length; i++)
{
SoapClient testClient = new SoapClient(clientNames[i]);
if (testClient.HelloWorld())
{
client = testClient;
break;
}
}
Console.WriteLine("服务检测结束:连接{0}{1}", client == null ? "失败" : "成功", string.IsNullOrEmpty(client?.Name) ? "" : $"服务名为{client.Name}");

20200129153036

这里存在的问题自然是检测返回可用服务可能会很慢,排在前面的服务如果不可用,将会长时间等待。

多线程建立连接

为解决单线程下,排列在前的不可用服务较多,影响服务连接速度,我们调整代码为多线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
string[] clientNames = new string[] { "Client1", "Client2", "Client3", "Client4", "Client5" };

List<Task<SoapClient>> clients = new List<Task<SoapClient>>();
for (int i = 0; i < clientNames.Length; i++)
{
string name = clientNames[i];
clients.Add(Task.Factory.StartNew(() =>
{
SoapClient client = new SoapClient(name);
bool connResult = client.HelloWorld();
return connResult ? client : null;
}));
}

// 等待所有异步任务执行完成 打印可用的服务
Task.WaitAll(clients.ToArray());
clients.ForEach(client =>
{
if (client.Result != null)
Console.WriteLine($"检测到可用服务:{client.Result.Name}");
});

运行代码得到以下结果:

20200129175355

好像这样已经解决了我们前文提到的问题,通过异步建立服务连接,检测可用的服务。

但是其实以上例子并不是合理的方案,因为如果第一个服务是可用的,理论上我们很快就可以建立连接,返回可用的连接供程序使用。

但是以上多线程的版本,需要等待所有的连接都测试后,才会返回结果,如果后续的连接有错误,检测耗时较长,反而影响了这个过程的执行效率。

多线程建立连接进阶

解决上述多线程连接的问题其实很简单,前文提到 WaitAll 会等待所有的任务执行完成。但是其实我们还有一个 WaitAny 方法可以使用,该方法是任务队列中任意任务执行完成后返回。

那么我们可以将以上代码进行如下调整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
string[] clientNames = new string[] { "Client1", "Client2", "Client3", "Client4", "Client5" };

List<Task<SoapClient>> clients = new List<Task<SoapClient>>();
for (int i = 0; i < clientNames.Length; i++)
{
string name = clientNames[i];
clients.Add(Task.Factory.StartNew(() =>
{
SoapClient client = new SoapClient(name);
bool connResult = client.HelloWorld();
return connResult ? client : null;
}));
}

// 等待任意任务执行完成,获取执行完成的任务返回
int index;
do
{
index = Task.WaitAny(clients.ToArray());
if (clients[index].Result != null)
{
Console.WriteLine($"检测到可用服务:{clients[index].Result.Name}");
break;
}
else
{
Console.WriteLine($"检测到一个不可用的服务。");
clients.RemoveAt(index);
}
} while (clients.Count > 0);

执行效果如下,可以看到我们可以快速的获取到可用的服务以便供客户端使用。

20200129175355

多线程建立连接优化

接下来,我们还需要对以上代码进行优化,主要是在检测到可用服务以后,其他的检测任务就没必要再继续执行了,我们可以使用 Token 取消任务的执行。

但是这里我们就需要对之前用于服务连接的伪代码进行调整,主要是因为 Thread.Sleep() 方法是不可取消的,我们使用 CancellationToken 对执行该方法的代码进行标记,取消时将没有效果。

例如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 开始的示例使用 .NET Framework 4.0 框架
using (CancellationTokenSource source = new CancellationTokenSource())
{
Task task = Task.Factory.StartNew(() =>
{
Console.WriteLine("任务开始执行,需要等待 5 秒钟。");
Thread.Sleep(5000);
Console.WriteLine("任务执行结束。");
}, source.Token);

Thread.Sleep(1000);
source.Cancel();
task.Wait();
}

该段代码仍然会正常执行,并打印“任务执行结束”。

我们需要的效果应该是,当执行 task.Wait() 方法时,如果任务被取消,抛出 TaskCanceledException 异常,控制台无法打印出“任务执行结束”。

所以我们需要将 Thread.Sleep() 方法,调整为可取消的 Task.Delay() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Task.Delay() 为 .NET Framework 4.5 框架的方法
using (CancellationTokenSource source = new CancellationTokenSource())
{
Task task = Task.Run(() =>
{
Console.WriteLine("任务开始执行,需要等待 5 秒钟。");
Task.Delay(5000).Wait(source.Token);
Console.WriteLine("任务执行结束。");
}, source.Token);

Thread.Sleep(1000);
source.Cancel();
try
{
task.Wait();
}
catch (AggregateException ae)
{
foreach (Exception e in ae.InnerExceptions)
{
if (e is TaskCanceledException)
Console.WriteLine("执行的任务被取消!");
else
Console.WriteLine("其他异常:" + e.GetType().Name);
}
}
}

基于此我们需要调整我们 SoapClient 中的 HelloWorld 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// 检测服务状态
/// </summary>
/// <returns>服务是否可用</returns>
public bool HelloWorld(CancellationToken cancellationToken)
{
// 随机休眠线程 模仿服务资源检测
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}[{Thread.CurrentThread.ManagedThreadId:00}]@[{Name}]:开始连接服务");
Task.Delay(Random.Next(500, 5000)).Wait(cancellationToken);
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}[{Thread.CurrentThread.ManagedThreadId:00}]@[{Name}]:服务资源检测结束");

// 随机确认客户端连接状态
bool result = Random.NextDouble() > 0.3;
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}[{Thread.CurrentThread.ManagedThreadId:00}]@[{Name}]:服务状态为{result}");
return result;
}

那么测试连接的代码段则如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
string[] clientNames = new string[] { "Client1", "Client2", "Client3", "Client4", "Client5" };

List<Task<SoapClient>> tasks = new List<Task<SoapClient>>();
using (var cancelTokenSource = new CancellationTokenSource())
{
for (int i = 0; i < clientNames.Length; i++)
{
string name = clientNames[i];
tasks.Add(Task.Factory.StartNew(() =>
{
SoapClient client = new SoapClient(name);
bool connResult = client.HelloWorld(cancelTokenSource.Token);
return connResult ? client : null;
}, cancelTokenSource.Token));
}

// 等待任意任务执行完成,获取执行完成的任务返回
int index;
do
{
index = Task.WaitAny(tasks.ToArray());
if (tasks[index].Result != null)
{
cancelTokenSource.Cancel();
Console.WriteLine($"检测到可用服务:{tasks[index].Result.Name},取消其他测试任务的执行");
break;
}
else
{
Console.WriteLine($"检测到一个不可用的服务。");
tasks[index].Dispose();
tasks.RemoveAt(index);
}
} while (tasks.Count > 0);

// 等待所有任务执行结束
try
{
Task.WaitAll(tasks.ToArray());
}
catch (AggregateException ae)
{
foreach (Exception e in ae.InnerExceptions)
{
if (e is TaskCanceledException)
Console.WriteLine("执行的任务被取消!");
else
Console.WriteLine("其他异常:" + e.GetType().Name);
}
}
}

执行效果如下:

20200130113248

WebService 服务检测方法

基于以上内容的测试,我们可以尝试建立一个 LISWebServiceSoapClient 的 WebService 客户端,用于与服务器进行通信,该方法内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/// <summary>
/// 尝试建立服务连接
/// </summary>
/// <param name="urls">服务地址</param>
/// <returns>是否成功与服务建立连接</returns>
protected virtual LISWebServiceSoapClient TryConnectService(string[] urls)
{
if (urls == null)
throw new ArgumentNullException("服务地址不能为空", nameof(urls));
if (urls.Length == 0)
throw new ArgumentException("服务地址没有内容", nameof(urls));

LISWebServiceSoapClient client = null;
// 异步检查可用连接提高查询速度
List<Task<LISWebServiceSoapClient>> tasks = new List<Task<LISWebServiceSoapClient>>();
using (CancellationTokenSource source = new CancellationTokenSource())
{
for (int i = 0; i < urls.Length; i++)
{
string url = urls[i];
tasks.Add(Task.Factory.StartNew(() =>
{
// 使用配置项初始化服务 并尝试调用测试连接的方法
LISWebServiceSoapClient current = new LISWebServiceSoapClient(new BasicHttpBinding(), new EndpointAddress(url));
current.HelloWorld();
return current;
}, source.Token));
}

// 等待任务返回 设置超时时间为 15s
for (int i = 0; i < tasks.Count; i++)
{
int index = Task.WaitAny(tasks.ToArray(), 15000);

// 超时
if (index == -1) break;

// 获取成功初始化的任务
if (index > -1 && tasks[index].Status == TaskStatus.RanToCompletion)
{
// 有返回就取消其他任务
source.Cancel();
client = tasks[index].Result;
break;
}

// 非成功初始化的从集合中移除
tasks.RemoveAt(index);
}
}

return client;
}

参考:

阿伟死了,GitHub 上那些难以理解的英语缩写

虽然英语很渣,但是还是会偶尔会逛逛 GitHub 一些开源项目的 Issues 与 Pull requests,想着说不定哪天突然开窍就能看懂英语了呢?

这当然是不可能的,(lll¬ω¬) 一般情况下还是浏览器插件划词翻译,但是总有一些类似于中文 ASWL 的简写,让人摸不着头脑,翻译工具也找不到真正的含义。

这里收集一些常见的缩写,搞明白这些缩写真正的含义。

AFAIK

As Far As I Know.

据我所知。

FYI

For Your Information.

仅供参考。

GOTCHA

I’ve Got You.

直译:“抓到你了”。可以引申为“明白你的意思”、 “骗到你了”。当 GOTCHAGot it 时,也有“了解”或“理解”的意思。

IMO (IMHO)

In My (Humble) Opinion.

这是“在我看来”的意思。用邮件进行讨论或向某人提出问题时,经常会收到这种略语用于表达个人意见。

加上 H 的 IMHO 也经常看。humble 是“丑陋,粗糙”的意思。“这是我的愚见”这样接近谦让语的用法。

LGTM

Looks Good To Me.

“我觉得不错”的意思。当审查代码,或您查看资料摘要时,如果没有特殊问题,可以通过的时候使用。

PR

Pull Request.

拉取请求,给其他项目提交代码。

PTAL

Please Take A Look.

请看一看。

SGTM

Sounds Good To Me.

“听起来不错”。和 LGTM 类似,也是通过代码审查的意思。

SSIA

Subject Says It All.

直译:“主题说明一切”。应该是“如题”的意思,我们在论坛中发帖时也经常会使用 RT

TBD

To Be Determined.

“待会儿再决定”,即待定的意思。

根据语境不同,D 还有 Defined、Discussed、Decided,一般是“没搞定”的意思。

TBR

To Be Reviewed.

“有待审查”。提示维护者进行代码审查。

TGIF

Thank God, It’s Friday.

今天是星期五,太好了!

Friday

TIA

Thanks In Advance.

“提前感谢”。在不征得对方同意的情况下,含有“会帮我做的吧”的意思,所以如果用错了场合会让人感觉失礼。

TL;DR.

Too Long. Didn’t Read.

“太长不看”。论坛中用于回复某个过于冗长的帖子,表示:内容实在是太长了,我没有时间或兴趣仔细看。

另外一种流行用法是发帖者用作对过长内容进行简化、提取内容主旨、以方便读者阅读的一种手段。表示:下文太长,这里是摘要。

WFM

Works For Me.

“对我来说有用”。环境能够很好的运行;采纳并感谢用户的提案。

WIP

Work In Progress.

“工作进行中”。一般用于改动很大的 PR,写了一部分先提交,在标题中标明 WIP,表示这个工作还没有完成,方便维护者提前审查部分已经提交的代码。

参考:

C# 打包引用程序集与动态链接库

前言

记得刚接触 C# 开发那会儿,因为经验不足经常犯一些很幼稚的错误,其中发布程序忘记将引用打包就是其中一个。

但是截止到现在,桌面应用程序做的都是一些“玩具”,打包安装感觉使用体验不好,所以现在仍然喜欢使用“绿色免安装”。

虽然免安装了,但是如果引用了一些程序集或者动态链接库,使用起来体验总不太好,特别是拿给别人用,要知道有些电脑小白拷贝可是连快捷方式和可执行文件都分不清。

所以,这里主要总结以下程序集打包、动态链接库的打包方法,而且网上关于程序集打包的文章很常见,但是动态链接库比较少,所以还是值得拿出来分享一下的。

打包引用程序集

这里和网上一样,主要从两方面来说,首先是将程序集作为资源嵌入到主程序中,然后在程序运行时从资源中加载。

另外一种是网上比较常见的,使用 Fody 来管理程序集打包与打包程序集的加载,但本质上两种方案是一样的。

使用 AssemblyResolve 事件打包

了解过反射知识应该都知道 AssemblyLoad 方法可以动态加载程序集,实现一些插件的效果。

首先我们创建一个控制台项目,并通过 NuGet 引用我们常用的 Newtonsoft.Json 程序集,入口函数代码如下:

1
2
3
4
5
6
7
8
9
10
static void Main(string[] args)
{
var obj = new { name = "hd2y", gender = "M", hobby = "coding", website = "https://hd2y.net" };
string json = JsonConvert.SerializeObject(obj);
Console.WriteLine(json);
Console.ReadKey();
}

// 程序运行输入:
// {"name":"hd2y","gender":"M","hobby":"coding","website":"https://hd2y.net"}

这时可以正常运行,是因为我们生成后,我们引用的程序集会自动复制到运行目录下:

20191226154201

这时因为默认情况下,引用属性中“复制本地”被设置为 True,我们可以进行调整:

20191226154301

然后我们重新生成项目,可以发现 Newtonsoft.Json.dll 从我们的运行目录中消失,并且程序无法再正常执行:

20191226154756

这时我们只需要将程序集打包到程序中,并在程序开始运行时,从资源文件中将程序集加载即可。

(一)将程序集打包到主程序中

首先在项目中添加一个 Assets 文件夹,通过查看 Newtonsoft.Json 的引用属性查看程序集所在路径,将程序集拷贝到新建的 Assets 文件夹,并设置 Newtonsoft.Json.dll 的属性,复制到输出路径 设置为 不复制生成操作 修改为 嵌入的资源

20191228145053

这时,我们可以通过反编译工具查看我们嵌入的资源文件:

20191226163411

(二)程序运行时从嵌入资源中加载程序集

然后我们在程序入口添加静态构造函数,注册 AssemblyResolve 事件,使程序集加载失败,我们能正常从嵌入资源中加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Program
{
static Program()
{
// 添加静态构造函数 注册程序集解析失败事件 定义从嵌入资源中加载
AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
{
if (args.Name.StartsWith("Newtonsoft.Json,"))
{
using (Stream stream = typeof(Program).Assembly.GetManifestResourceStream("JohnSun.PackagingTest.ConsoleApp.Assets.Newtonsoft.Json.dll"))
{
byte[] rawAssembly = new byte[stream.Length];
stream.Read(rawAssembly, 0, rawAssembly.Length);
return Assembly.Load(rawAssembly);
}
}
return null;
};
}

static void Main(string[] args)
{
var obj = new { name = "hd2y", gender = "M", hobby = "coding", website = "https://hd2y.net" };
string json = JsonConvert.SerializeObject(obj);
Console.WriteLine(json);
Console.ReadKey();
}
}

这时我们重新运行程序,程序即可正常运行。

使用 Fody.Costura

Fody 为我们提供了一个简单的方式,只需要通过 NuGet 引用 Fody.Costura,然后生成项目就会自动将程序集打包。

我们可以新创建一个项目 JohnSun.PackagingTest.FodyCostura 进行测试,首先使用 NuGet 添加 Newtonsoft.JsonFody.Costura 的引用。

同样我们修改入口,序列化一个对象,并打印到控制台:

1
2
3
4
5
6
7
8
9
10
class Program
{
static void Main(string[] args)
{
var obj = new { name = "hd2y", gender = "M", hobby = "coding", website = "https://hd2y.net" };
string json = JsonConvert.SerializeObject(obj);
Console.WriteLine(json);
Console.ReadKey();
}
}

我们到生成目录可以查看一下生成的文件,可以看到只有一个文件,我们引用的程序集没有复制到该目录,但是生成的项目可以正常运行:

20191226181251

感觉挺神奇的,但是我们同样可以通过反编译,了解具体是怎么实现这个效果的:

20191226181637

然后我们具体的看一下 AssemblyLoader 的代码做什么事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
// Costura.AssemblyLoader
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;

[CompilerGenerated]
internal static class AssemblyLoader
{
private static object nullCacheLock = new object();

private static Dictionary<string, bool> nullCache = new Dictionary<string, bool>();

private static Dictionary<string, string> assemblyNames = new Dictionary<string, string>();

private static Dictionary<string, string> symbolNames = new Dictionary<string, string>();

private static int isAttached;

private static string CultureToString(CultureInfo culture)
{
if (culture == null)
{
return "";
}
return culture.Name;
}

private static Assembly ReadExistingAssembly(AssemblyName name)
{
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies)
{
AssemblyName name2 = assembly.GetName();
if (string.Equals(name2.Name, name.Name, StringComparison.InvariantCultureIgnoreCase) && string.Equals(CultureToString(name2.CultureInfo), CultureToString(name.CultureInfo), StringComparison.InvariantCultureIgnoreCase))
{
return assembly;
}
}
return null;
}

private static void CopyTo(Stream source, Stream destination)
{
byte[] array = new byte[81920];
int count;
while ((count = source.Read(array, 0, array.Length)) != 0)
{
destination.Write(array, 0, count);
}
}

private static Stream LoadStream(string fullName)
{
Assembly executingAssembly = Assembly.GetExecutingAssembly();
if (fullName.EndsWith(".compressed"))
{
using (Stream stream = executingAssembly.GetManifestResourceStream(fullName))
{
using (DeflateStream source = new DeflateStream(stream, CompressionMode.Decompress))
{
MemoryStream memoryStream = new MemoryStream();
CopyTo(source, memoryStream);
memoryStream.Position = 0L;
return memoryStream;
}
}
}
return executingAssembly.GetManifestResourceStream(fullName);
}

private static Stream LoadStream(Dictionary<string, string> resourceNames, string name)
{
if (resourceNames.TryGetValue(name, out string value))
{
return LoadStream(value);
}
return null;
}

private static byte[] ReadStream(Stream stream)
{
byte[] array = new byte[stream.Length];
stream.Read(array, 0, array.Length);
return array;
}

private static Assembly ReadFromEmbeddedResources(Dictionary<string, string> assemblyNames, Dictionary<string, string> symbolNames, AssemblyName requestedAssemblyName)
{
string text = requestedAssemblyName.Name.ToLowerInvariant();
if (requestedAssemblyName.CultureInfo != null && !string.IsNullOrEmpty(requestedAssemblyName.CultureInfo.Name))
{
text = requestedAssemblyName.CultureInfo.Name + "." + text;
}
byte[] rawAssembly;
using (Stream stream = LoadStream(assemblyNames, text))
{
if (stream == null)
{
return null;
}
rawAssembly = ReadStream(stream);
}
using (Stream stream2 = LoadStream(symbolNames, text))
{
if (stream2 != null)
{
byte[] rawSymbolStore = ReadStream(stream2);
return Assembly.Load(rawAssembly, rawSymbolStore);
}
}
return Assembly.Load(rawAssembly);
}

public static Assembly ResolveAssembly(object sender, ResolveEventArgs e)
{
lock (nullCacheLock)
{
if (nullCache.ContainsKey(e.Name))
{
return null;
}
}
AssemblyName assemblyName = new AssemblyName(e.Name);
Assembly assembly = ReadExistingAssembly(assemblyName);
if (assembly != null)
{
return assembly;
}
assembly = ReadFromEmbeddedResources(assemblyNames, symbolNames, assemblyName);
if (assembly == null)
{
lock (nullCacheLock)
{
nullCache[e.Name] = true;
}
if ((assemblyName.Flags & AssemblyNameFlags.Retargetable) != 0)
{
assembly = Assembly.Load(assemblyName);
}
}
return assembly;
}

static AssemblyLoader()
{
assemblyNames.Add("costura", "costura.costura.dll.compressed");
assemblyNames.Add("newtonsoft.json", "costura.newtonsoft.json.dll.compressed");
}

public static void Attach()
{
if (Interlocked.Exchange(ref isAttached, 1) != 1)
{
AppDomain.CurrentDomain.AssemblyResolve += delegate(object sender, ResolveEventArgs e)
{
lock (nullCacheLock)
{
if (nullCache.ContainsKey(e.Name))
{
return null;
}
}
AssemblyName assemblyName = new AssemblyName(e.Name);
Assembly assembly = ReadExistingAssembly(assemblyName);
if (assembly != null)
{
return assembly;
}
assembly = ReadFromEmbeddedResources(assemblyNames, symbolNames, assemblyName);
if (assembly == null)
{
lock (nullCacheLock)
{
nullCache[e.Name] = true;
}
if ((assemblyName.Flags & AssemblyNameFlags.Retargetable) != 0)
{
assembly = Assembly.Load(assemblyName);
}
}
return assembly;
};
}
}
}

实际上也是通过 AssemblyResolve 实现了程序集的加载,但是这个过程变成了编译时自动执行。

如果想要手动的指定在什么时候加载资源中的程序集,我们可以添加一个 FodyWeavers.xml 文件到项目中:

1
2
<?xml version="1.0" encoding="utf-8"?>
<Weavers></Weavers>

这时我们只需要生成,xml 文件的内容会发生变化:

1
2
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"></Weavers>

因为自动引入了 xsd 文件,我们编辑会 xml 时会有提示,我们可以修改以指定不在模块初始化时加载程序集:

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Costura LoadAtModuleInit="false" />
</Weavers>

这时我们生成项目会报错:

1
Fody: Costura was not initialized. Make sure LoadAtModuleInit=true or call CosturaUtility.Initialize().	JohnSun.PackagingTest.FodyCostura			

从提示可以知道,我们需要通过调用 CosturaUtility.Initialize() 方法,手动的指定程序集加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Program
{
static Program()
{
CosturaUtility.Initialize();
}
static void Main(string[] args)
{
var obj = new { name = "hd2y", gender = "M", hobby = "coding", website = "https://hd2y.net" };
string json = JsonConvert.SerializeObject(obj);
Console.WriteLine(json);
Console.ReadKey();
}
}

这时我们就可以顺利通过编译并可以正常的运行项目,但是需要注意的是,我们要将对应代码加在静态构造函数内,确保对象初始化前能够顺利将程序集加载。

实际上 Fody 还有很多有趣的项目,可以到 GitHub 上查看:https://github.com/Fody

.NET Core 3.0 的新特性已经支持将程序打包成独立可执行文件:Single-file executables

打包动态链接库

动态链接库我所知道的使用方法有以下几种:

  1. 使用 Regsvr32 命令注册动态链接库;
  2. 直接将动态链接库放在执行程序所在目录;
  3. 使用 kernel32LoadLibrary 方法动态装载指定目录下的动态链接库;

当然平时偷懒更多是使用第二种方案,既然是要介绍将动态链接库打包,这里自然是要使用第三种动态装载的方案。

另外还有一个优势就是,因为 x86 与 x64 的动态链接库,在创建的 32 或 64 位应用程序时需要注意不能引用错误,如果动态的装载,我们就可以根据程序运行环境选择合适的动态链接库。

过去的解决方案是程序属性将生成目标平台修改为 x86,因为一般供我们调用的动态链接库也是 Win32 的。这样如果该程序集我们需要应用于网站,部署 IIS 还需要调整应用程序池,并因此还可能产生其他问题,所以不推荐。

创建 C++ 动态链接库供测试

如果想要调用,自然先要有一个动态链接库供我们测试,这里直接创建 C++ 的动态链接库项目,修改 pch.h 头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// pch.h: 这是预编译标头文件。
// 下方列出的文件仅编译一次,提高了将来生成的生成性能。
// 这还将影响 IntelliSense 性能,包括代码完成和许多代码浏览功能。
// 但是,如果此处列出的文件中的任何一个在生成之间有更新,它们全部都将被重新编译。
// 请勿在此处添加要频繁更新的文件,这将使得性能优势无效。

#ifndef PCH_H
#define PCH_H

// 添加要在此处预编译的标头
#include "framework.h"

#endif //PCH_H

//定义宏
#ifdef IMPORT_DLL
#else
#define IMPORT_DLL extern "C" _declspec(dllimport) //指的是允许将其给外部调用
#endif

IMPORT_DLL double Addition(double a, double b);
IMPORT_DLL double Subtraction(double a, double b);
IMPORT_DLL double Multiplication(double a, double b);
IMPORT_DLL double Division(double a, double b);

增加对应的源文件 SimpleMath.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "pch.h"

double Addition(double a, double b)
{
return a + b;
}

double Subtraction(double a, double b)
{
return a - b;
}

double Multiplication(double a, double b)
{
return a * b;
}

double Division(double a, double b)
{
return a / b;
}

直接编译生成即可,但是需要注意的是,C++ 的项目生成出的动态链接库的位置与 C# 程序集的路径不太一样,我这里位置是在解决方案目录内,可以通过项目属性查看与配置。

20191228092431

20191228092658

常规应用方案

查看 C++ 动态链接库项目属性可以注意到平台为 活动(Win32),所以我们可以将该动态链接库拷贝到我们的运行目录下(这里是调试目录),但是项目无法正常运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Program
{
static void Main(string[] args)
{
Console.WriteLine($"开始测试:{(Environment.Is64BitProcess ? "64" : "32")} 位进程");
int a = 100, b = 20;
Console.WriteLine($"{a} + {b} = {SimpleMath.Addition(a, b)}");
Console.WriteLine($"{a} - {b} = {SimpleMath.Subtraction(a, b)}");
Console.WriteLine($"{a} * {b} = {SimpleMath.Multiplication(a, b)}");
Console.WriteLine($"{a} / {b} = {SimpleMath.Division(a, b)}");
Console.ReadKey();
}
}

public class SimpleMath
{

[DllImport("SimpleMath", CallingConvention = CallingConvention.Cdecl)]
public static extern double Addition(double a, double b);
[DllImport("SimpleMath", CallingConvention = CallingConvention.Cdecl)]
public static extern double Subtraction(double a, double b);
[DllImport("SimpleMath", CallingConvention = CallingConvention.Cdecl)]
public static extern double Multiplication(double a, double b);
[DllImport("SimpleMath", CallingConvention = CallingConvention.Cdecl)]
public static extern double Division(double a, double b);
}

20191228094238

如上图所示,因为我的系统是 64 位,所以默认 Debug 是以 x64 运行,这时我们需要修改我们运行控制台的 目标平台 为 x86。

20191228094520

修改完成以后程序即可成功执行:

20191228094624

解决 x64 无法运行问题

默认 64 位进程无法执行的原因是因为我们没有提供 x64 的动态链接库,当然如果我们需要调用的没有提供 x64 平台的动态链接库,那么问题到这里就已经终止了,那就是没有解决方案,可以跳过以下内容。

如果我们第三方或者我们从网上,找到的一些我们需要使用的动态链接库,同时提供了 x86 和 x64 平台,那么问题就很好解决了。

我们通过批生成,将我们添加的 C++ 动态链接库项目生成出满足我们测试的版本(VS 菜单栏 -> 生成 -> 批生成 -> Batch 生成):

20191228145811

如上图所示,我们选择了 Release|x86Release|x64 平台的生成,为了方便区分,我们找到生成目录,将原来的动态链接库名称修改,调整为带有目标平台的名称:

20191228095840

然后我们将动态链接库拷贝到我们的控制台项目中,为了方便测试,将动态链接库文件属性修改:生成操作 调整为 复制到输出目录 调整为 始终复制

接下来我们修改项目 SimpleMath 的代码,让一份代码能够同时兼容 x86 与 x64 平台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class SimpleMath
{
[DllImport("SimpleMath_x86", EntryPoint = "Addition", CallingConvention = CallingConvention.Cdecl)]
public static extern double Addition_x86(double a, double b);
[DllImport("SimpleMath_x86", EntryPoint = "Subtraction", CallingConvention = CallingConvention.Cdecl)]
public static extern double Subtraction_x86(double a, double b);
[DllImport("SimpleMath_x86", EntryPoint = "Multiplication", CallingConvention = CallingConvention.Cdecl)]
public static extern double Multiplication_x86(double a, double b);
[DllImport("SimpleMath_x86", EntryPoint = "Division", CallingConvention = CallingConvention.Cdecl)]
public static extern double Division_x86(double a, double b);
[DllImport("SimpleMath_x64", EntryPoint = "Addition", CallingConvention = CallingConvention.Cdecl)]
public static extern double Addition_x64(double a, double b);
[DllImport("SimpleMath_x64", EntryPoint = "Subtraction", CallingConvention = CallingConvention.Cdecl)]
public static extern double Subtraction_x64(double a, double b);
[DllImport("SimpleMath_x64", EntryPoint = "Multiplication", CallingConvention = CallingConvention.Cdecl)]
public static extern double Multiplication_x64(double a, double b);
[DllImport("SimpleMath_x64", EntryPoint = "Division", CallingConvention = CallingConvention.Cdecl)]
public static extern double Division_x64(double a, double b);

public static double Addition(double a, double b)
{
return Environment.Is64BitProcess
? Addition_x64(a, b)
: Addition_x86(a, b);
}
public static double Subtraction(double a, double b)
{
return Environment.Is64BitProcess
? Subtraction_x64(a, b)
: Subtraction_x86(a, b);
}
public static double Multiplication(double a, double b)
{
return Environment.Is64BitProcess
? Multiplication_x64(a, b)
: Multiplication_x86(a, b);
}
public static double Division(double a, double b)
{
return Environment.Is64BitProcess
? Division_x64(a, b)
: Division_x86(a, b);
}
}

这时再将控制台项目的目标平台修改为 Any CPU,我们的程序就不再受动态链接库的影响,无论分发到 64 位或者 32 位的系统上,都可以成功运行。

打包动态链接库到程序集

可以将流程归纳为以下几步:

  1. 将动态链接库设置为嵌入资源;
  2. 运行程序时判断进程是 32 位或 64 位,读取对应的动态链接库将文件写出到一个指定的路径;
  3. 使用 LoadLibrary 方法动态加载该程序集;
  4. 程序可以自由的调用由动态链接库导出的函数;

首先我们在项目中创建一个 Assets 文件夹,然后将两个动态链接库移动到该文件夹下,并将动态链接库文件的属性设置为 嵌入的资源

然后我们需要从 kernel32 导出我们需要的函数:

1
2
3
4
5
6
7
8
static class Kernel32
{
[DllImport("kernel32", SetLastError = true)]
public static extern bool FreeLibrary(IntPtr hModule);

[DllImport("kernel32", SetLastError = true)]
public static extern IntPtr LoadLibrary(string filename);
}

因我们需要根据执行进程加载动态链接库,那么执行函数时就不再需要判断当前是否位 64 位进程,SimpleMath 重新调整为原来的版本。

但是在程序运行时,我们需要将对应需要的文件动态加载,所以需要增加静态构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class SimpleMath
{
static SimpleMath()
{
// 只支持 Windows 平台
if (Environment.OSVersion.Platform != PlatformID.Win32NT)
throw new PlatformNotSupportedException($"Platform {Enum.GetName(typeof(PlatformID), Environment.OSVersion.Platform)} is not supported");

// 读取资源名以及写出路径
string resourceName = $"JohnSun.PackagingTest.DynamicLinkLibrary.Assets.SimpleMath_{(Environment.Is64BitProcess ? "x64" : "x86")}.dll";
string version = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Version}_{(Environment.Is64BitProcess ? 64 : 32)}";
string directory = Path.Combine(Path.GetTempPath(), nameof(SimpleMath), version);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
string fileName = Path.Combine(directory, "SimpleMath.dll");

// 将需要引用的 dll 文件从资源文件写出
using (Stream stream = typeof(SimpleMath).Assembly.GetManifestResourceStream(resourceName))
{
FileInfo fileInfo = new FileInfo(fileName);
// 文件如果不存在或文件存在但是文件大小与资源中不一致 重新将文件写出
if (!fileInfo.Exists || fileInfo.Length != stream.Length)
{
byte[] binary = new byte[stream.Length];
stream.Read(binary, 0, binary.Length);
File.WriteAllBytes(fileName, binary);
}
}

// 加载写出的文件
Kernel32.LoadLibrary(fileName);
}

[DllImport("SimpleMath", CallingConvention = CallingConvention.Cdecl)]
public static extern double Addition(double a, double b);
[DllImport("SimpleMath", CallingConvention = CallingConvention.Cdecl)]
public static extern double Subtraction(double a, double b);
[DllImport("SimpleMath", CallingConvention = CallingConvention.Cdecl)]
public static extern double Multiplication(double a, double b);
[DllImport("SimpleMath", CallingConvention = CallingConvention.Cdecl)]
public static extern double Division(double a, double b);
}

这时我们重新生成项目并运行,可以看到生成目录下已经不存在引用的动态链接库,但是程序可以正常运行(我们可以通过调试查看动态链接库被写出到什么位置):

20191228142933

注:因为我没有学习过 C++,所以动态链接库打包部分代码学习自 OpenHtmlToPdf 开源项目,能够学习到那么多优秀的解决方案,真的要感谢开源社区的大佬们 o( ̄▽ ̄)ブ。

参考:

MacOS 安装 Windows XP SP3 测试环境

因为需要做一些兼容性的测试,所以干脆装一个虚拟机,但是有些资料整理以下,方便下次安装的时候查找。

虚拟机安装环境

这里使用 Parallels Desktop 安装虚拟环境。

可以打开 Parallels Link 来执行安装。

20191228132736

安装

其实都是简单的下一步下一步……所以过程不赘述。

这里整理一下安装过程中以及安装以后要使用的一些资料和软件:

首先是镜像,我这里使用的是:zh-hans_windows_xp_professional_with_service_pack_3_x86_cd_x14-80404.iso

1
2
3
4
5
文件名:zh-hans_windows_xp_professional_with_service_pack_3_x86_cd_x14-80404.iso
SHA1:69DBF131116760932DCF132ADE111D6B45778098
文件大小:601.04MB
发布时间:2008-05-01
ed2k://|file|zh-hans_windows_xp_professional_with_service_pack_3_x86_cd_x14-80404.iso|630239232|CD0900AFA058ACB6345761969CBCBFF4|/

然后是安装许可密钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
JBJ9Q-JJG2C-DHY68-4K3VR-VX9VD
PJW84-XPMBK-P7GDG-6YBCT-2PK86
DKPHR-9PKW2-KJ6XV-BVKVW-FQ968
TCV4Y-BW62X-QQJPJ-HTXQW-KKMT3
MC4XD-VDY8T-2G9H7-4TM46-69G4M
DCJY7-2QMTB-4RBMQ-TJC9M-RY83T
VTPRF-689V6-8HY4R-XPBGH-6QR9J
HD9R6-2X672-2JJ92-64Y39-9BWPJ
B2FTG-96RH6-72W72-WXKTB-G42R6
PJYB2-QWBC4-TDFWD-WY36Q-H7DRY
BQXJ6-2V339-4M72J-66QMP-M3WDY
MVWCG-T8K4C-82Q4F-7RWPP-2DQ2Q
RRWFH-HYG6J-86XXD-87PWC-DXXWB
MK96F-X47MK-M92HP-F2QDM-CWRDY
W48J6-QJQWY-FB7HM-CB69Y-T8VKW
MPW9P-HY3P8-C6HF6-JBK73-B447Q
PJ9PH-DP2YM-RGV33-R8KFX-9XY4W
W4CY9-KKBT9-RHKF3-C63V3-B2CRD
HDP88-DQP2V-PWDWG-Y4XB6-WXJ98
C3XFT-C98TR-P34TC-TCQKJ-CQPVG
HCGVJ-TFPQJ-HQKPV-9X7HP-T8B3G
WFW8Q-W7RXQ-YHBFH-DDQD8-2XMVY
WKFVB-4BP36-69PM4-C4TM8-VPFM8
JKCPF-2FJHW-4VCK2-27B7Q-9TVVG
C2VKV-BQVXR-GHG7M-6HY6H-QXWQ3
KF9F6-TYWTC-KBXXD-PCXRV-V2T3Q
JTQV9-FG9PX-48GRV-P2H44-C6TG3
QTQMJ-DDD7P-YCGVC-GFBRD-GT2VW
MK6P8-X4PBQ-PKFV9-D277P-CC63B
P224M-2WMK8-B3HMT-6YDP2-DPVKM
HB8KM-WH7VG-H93QD-G3747-DTVMY
K89TK-9X6FR-TK6D7-QXJ79-D8RCJ
QJ769-JYMMM-F97YH-BKCJP-2BQ2T
JTWPD-3CPKB-HXRQ8-DXRWK-7VVXY
XJY7Q-9Y6TT-QP3W8-CF89G-RR9XD
KTXCY-44H4W-K8H4D-DXFGC-K46YB
F268D-F3CHM-R26GV-JRFCW-PR37D

建议安装的 SDK 和软件:

  • Win XP Activator:激活工具
  • 7z:压缩文件管理工具
  • Chrome:安装最后一个支持 XP 的版本
  • .NET Framework 3.5:唯一能向下兼容的 dotnet 版本
  • .NET Framework 4.0:Windows XP 上 dotnet 最后一个支持的版本
  • 补丁 NDP40-KB2468871-v2-x86:使用 Microsoft.Bcl.Async 以使 dotnet40 支持 async/await
  • Notepad++:文本编辑器
  • Fiddler 4:HTTP 抓包工具

下载

百度网盘:https://pan.baidu.com/s/13ITHYapvSaberzPQ1wl75A 密码:cqgc

Logger 工具类分享

前言

稍大一些的系统设计,日志模块我们一般都会选用 log4netNLog,开源并且功能齐全,配置功能很好用。

但是偶尔我们也会开发一些小工具,只是简单的输出一些日志,如果再选择这些开源项目的方案,不免显得有点“沉重”。

后面就针对这简单的使用场景,写了一个“简陋”的工具类 Logger.cs 满足一些简单场景的使用。

源代码

不说废话,直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

namespace JohnSun.Utility
{
/// <summary>
/// 日志输出方式
/// </summary>
[Flags]
public enum LogType
{
None = 0,
File = 1,
Console = 2,
All = 3
}

/// <summary>
/// 日志 严重性
/// </summary>
[Flags]
public enum LogLevel
{
[Description("None")]
None = 0,
[Description("跟踪")]
Trace = 1,
[Description("调试")]
Debug = 2,
[Description("消息")]
Info = 4,
[Description("警告")]
Warn = 8,
[Description("错误")]
Error = 16,
[Description("致命")]
Fatal = 32,
[Description("All")]
All = 63
}

/// <summary>
/// 日志
/// </summary>
public class Logger
{
private static readonly Dictionary<LogLevel, string> _logLevelDescription = new Dictionary<LogLevel, string>();

/// <summary>
/// 日志等级的描述信息
/// </summary>
public static Dictionary<LogLevel, string> LogLevelDescription
{
get
{
if (_logLevelDescription.Count == 0)
{
Array values = Enum.GetValues(typeof(LogLevel));
foreach (object value in values)
{
LogLevel level = (LogLevel)value;
FieldInfo info = typeof(LogLevel).GetField(level.ToString());
object[] attrs = info.GetCustomAttributes(false);
if (attrs.ToList().Find(attr => attr is DescriptionAttribute) is DescriptionAttribute attribute)
{
_logLevelDescription[level] = attribute.Description;
}
else
{
_logLevelDescription[level] = level.ToString();
}
}
}
return _logLevelDescription;
}
}

/// <summary>
/// 设置日志记录等级
/// </summary>
public static LogLevel LogLevel { get; set; } = LogLevel.All;

/// <summary>
/// 日志输出方式
/// </summary>
public static LogType LogType { get; set; } = LogType.File;

private static void LogToConsole(string message, ConsoleColor textColor)
{
Console.OutputEncoding = Encoding.UTF8;
Console.ForegroundColor = textColor;
Console.WriteLine(message);
Console.ResetColor();

Console.OutputEncoding = Encoding.Default;
}

private static void LogToConsole(string message, ConsoleColor textColor, ConsoleColor backColor)
{
Console.OutputEncoding = Encoding.UTF8;
Console.ForegroundColor = textColor;
Console.BackgroundColor = backColor;
Console.Write(message);
Console.ResetColor();
Console.WriteLine();

Console.OutputEncoding = Encoding.Default;
}

static void LogToConsole(LogLevel v, string message)
{
switch (v)
{
case LogLevel.Trace:
LogToConsole(message, ConsoleColor.Black, ConsoleColor.DarkYellow);
break;
case LogLevel.Debug:
LogToConsole(message, ConsoleColor.Cyan);
break;
case LogLevel.Info:
LogToConsole(message, ConsoleColor.Yellow);
break;
case LogLevel.Warn:
LogToConsole(message, ConsoleColor.Red);
break;
case LogLevel.Error:
LogToConsole(message, ConsoleColor.Gray, ConsoleColor.Red);
break;
case LogLevel.Fatal:
LogToConsole(message, ConsoleColor.Yellow, ConsoleColor.Red);
break;
}
}

private static readonly object _sync = new object();

static void LogToFile(LogLevel v, string message)
{
string directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
string path = Path.Combine(directory, DateTime.Now.ToString("yyyy-MM-dd") + ".txt");
lock (_sync)
{
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
File.AppendAllText(path, message, Encoding.UTF8);
}
}

static void Log(LogLevel v, string message, Exception exception = null)
{
if ((v & LogLevel) == 0)
return;
if ((LogType & LogType.Console) != 0)
LogToConsole(v, $"{DateTime.Now.ToString("HH:mm:ss")} {LogLevelDescription[v]}: {message}{(exception == null ? "" : $"\r\n异常信息:{exception.Message} {exception.InnerException?.Message}\r\n堆栈跟踪:{exception.StackTrace}")}");
if ((LogType & LogType.File) != 0)
LogToFile(v, $"{DateTime.Now.ToString("HH:mm:ss")} {LogLevelDescription[v]}: {message}{(exception == null ? "" : $"\r\n异常信息:{exception.Message} {exception.InnerException?.Message}\r\n堆栈跟踪:{exception.StackTrace}")}\r\n");
LogEvent?.Invoke(v, message, exception);
}

/// <summary>
/// 跟踪
/// </summary>
/// <param name="message"></param>
/// <param name="exception"></param>
public static void Trace(string message, Exception exception = null) => Log(LogLevel.Trace, message, exception);

/// <summary>
/// 调试
/// </summary>
/// <param name="message"></param>
/// <param name="exception"></param>
public static void Debug(string message, Exception exception = null) => Log(LogLevel.Debug, message, exception);

/// <summary>
/// 消息
/// </summary>
/// <param name="message"></param>
/// <param name="exception"></param>
public static void Info(string message, Exception exception = null) => Log(LogLevel.Info, message, exception);

/// <summary>
/// 警告
/// </summary>
/// <param name="message"></param>
/// <param name="exception"></param>
public static void Warn(string message, Exception exception = null) => Log(LogLevel.Warn, message, exception);

/// <summary>
/// 错误
/// </summary>
/// <param name="message"></param>
/// <param name="exception"></param>
public static void Error(string message, Exception exception = null) => Log(LogLevel.Error, message, exception);

/// <summary>
/// 致命
/// </summary>
/// <param name="message"></param>
/// <param name="exception"></param>
public static void Fatal(string message, Exception exception = null) => Log(LogLevel.Fatal, message, exception);

/// <summary>
/// 日志记录
/// </summary>
/// <param name="v">日志等级</param>
/// <param name="message">消息</param>
public delegate void LogAction(LogLevel v, string message, Exception exception = null);

/// <summary>
/// 日志记录事件
/// </summary>
public static event LogAction LogEvent;
}
}

默认提供了两种日志的输出方式,可以通过标识枚举 LogType 控制,可以输出到文件或输出到控制台。

默认提供了六种日志严重程度,可以通过标识枚举 LogLevel 控制输出哪些类型的日志。

如果这些不能满足需求,例如有将日志输出到 Windows FormWPF、数据库等的需求,可以通过注册 LogEvent 事件实现。

常规用法

默认提供了写文件与输出到控制台的用法,可以参考以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 设置日志输出方式
Logger.LogType = LogType.Console | LogType.File; // 等用于 LogType.All

// 设置输出日志的类型
Logger.LogLevel = LogLevel.All; // 这里设置所有日志类型都输出

// 常见输出
Logger.Trace($"这是一条{Logger.LogLevelDescription[LogLevel.Trace]}信息!");
Logger.Debug($"这是一条{Logger.LogLevelDescription[LogLevel.Debug]}信息!");
Logger.Info($"这是一条{Logger.LogLevelDescription[LogLevel.Info]}信息!");
Logger.Warn($"这是一条{Logger.LogLevelDescription[LogLevel.Warn]}信息!");
Logger.Error($"这是一条{Logger.LogLevelDescription[LogLevel.Error]}信息!");
Logger.Fatal($"这是一条{Logger.LogLevelDescription[LogLevel.Fatal]}信息!");

// 带异常信息的输出
Exception exception = new Exception("这是一条用于测试的异常信息");
Logger.Trace($"这是一条带有异常信息的{Logger.LogLevelDescription[LogLevel.Trace]}信息!", exception);
Logger.Debug($"这是一条带有异常信息的{Logger.LogLevelDescription[LogLevel.Debug]}信息!", exception);
Logger.Info($"这是一条带有异常信息的{Logger.LogLevelDescription[LogLevel.Info]}信息!", exception);
Logger.Warn($"这是一条带有异常信息的{Logger.LogLevelDescription[LogLevel.Warn]}信息!", exception);
Logger.Error($"这是一条带有异常信息的{Logger.LogLevelDescription[LogLevel.Error]}信息!", exception);
Logger.Fatal($"这是一条带有异常信息的{Logger.LogLevelDescription[LogLevel.Fatal]}信息!", exception);

Console.ReadKey();

输出到文件的效果,会根据日期创建文件夹:

20191225192058

输出到控制台的效果,不同日志类型设置了不同的颜色用于区分:

20191225192128

注册事件

偶尔会开发一些窗体应用程序,需要将日志输出到窗体控件,这里以输出到富文本框 RichTextBox 为例。

WinForm 用法:

创建一个 RichTextBox 控件用于输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 设置日志输出方式 这里不启用写文件和输出到控制台
Logger.LogType = LogType.None;

// 设置输出日志的类型 因为不用默认输出方式 这里可以不用设置
Logger.LogLevel = LogLevel.All;

// 注册日志输出事件 输出到富文本框
Logger.LogEvent += (level, msg, exc) =>
{
this.Invoke(new Action(() =>
{
string text = $"{DateTime.Now.ToString("HH:mm:ss")} {Logger.LogLevelDescription[level]}: {msg}{(string.IsNullOrEmpty(exc?.Message) ? "" : $"\r\n异常信息:{exc.Message}")}{(string.IsNullOrEmpty(exc?.StackTrace) ? "" : $"\r\n堆栈跟踪:{exc.StackTrace}")}\r\n";
switch (level)
{
case LogLevel.Trace:
richTextBox1.SelectionColor = Color.DarkGray;
break;
case LogLevel.Debug:
richTextBox1.SelectionColor = Color.SlateGray;
break;
case LogLevel.Info:
richTextBox1.SelectionColor = Color.Green;
break;
case LogLevel.Warn:
richTextBox1.SelectionColor = Color.OrangeRed;
break;
case LogLevel.Error:
richTextBox1.SelectionColor = Color.Red;
break;
case LogLevel.Fatal:
richTextBox1.SelectionColor = Color.Red;
richTextBox1.SelectionBackColor = Color.Yellow;
break;
default:
return;
}
richTextBox1.AppendText(text);
}));
};

// 常见输出
Logger.Trace($"这是一条{Logger.LogLevelDescription[LogLevel.Trace]}信息!");
Logger.Debug($"这是一条{Logger.LogLevelDescription[LogLevel.Debug]}信息!");
Logger.Info($"这是一条{Logger.LogLevelDescription[LogLevel.Info]}信息!");
Logger.Warn($"这是一条{Logger.LogLevelDescription[LogLevel.Warn]}信息!");
Logger.Error($"这是一条{Logger.LogLevelDescription[LogLevel.Error]}信息!");
Logger.Fatal($"这是一条{Logger.LogLevelDescription[LogLevel.Fatal]}信息!");

// 带异常信息的输出
Exception exception = new Exception("这是一条用于测试的异常信息");
Logger.Trace($"这是一条带有异常信息的{Logger.LogLevelDescription[LogLevel.Trace]}信息!", exception);
Logger.Debug($"这是一条带有异常信息的{Logger.LogLevelDescription[LogLevel.Debug]}信息!", exception);
Logger.Info($"这是一条带有异常信息的{Logger.LogLevelDescription[LogLevel.Info]}信息!", exception);
Logger.Warn($"这是一条带有异常信息的{Logger.LogLevelDescription[LogLevel.Warn]}信息!", exception);
Logger.Error($"这是一条带有异常信息的{Logger.LogLevelDescription[LogLevel.Error]}信息!", exception);
Logger.Fatal($"这是一条带有异常信息的{Logger.LogLevelDescription[LogLevel.Fatal]}信息!", exception);

日志打印效果:

20191225194312

WPF 用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 设置日志输出方式 这里不启用写文件和输出到控制台
Logger.LogType = LogType.None;

// 设置输出日志的类型 因为不用默认输出方式 这里可以不用设置
Logger.LogLevel = LogLevel.All;

// 注册日志输出事件 输出到富文本框
Logger.LogEvent += new Logger.LogAction((level, msg, exc) =>
{
this.Dispatcher.Invoke((Action)delegate
{
Paragraph paragraph = new Paragraph();
Run run = new Run() { Text = $"{DateTime.Now.ToString("HH:mm:ss")} {Logger.LogLevelDescription[level]}: {msg}{(string.IsNullOrEmpty(exc?.Message) ? "" : $"\r\n异常信息:{exc.Message}")}{(string.IsNullOrEmpty(exc?.StackTrace) ? "" : $"\r\n堆栈跟踪:{exc.StackTrace}")}" };

switch (level)
{
case LogLevel.Trace:
run.Foreground = Brushes.DarkGray;
break;
case LogLevel.Debug:
run.Foreground = Brushes.SlateGray;
break;
case LogLevel.Info:
run.Foreground = Brushes.Green;
break;
case LogLevel.Warn:
run.Foreground = Brushes.OrangeRed;
break;
case LogLevel.Error:
run.Foreground = Brushes.Red;
break;
case LogLevel.Fatal:
run.Foreground = Brushes.Red;
run.Background = Brushes.Yellow;
break;
}

paragraph.Inlines.Add(run);

// 这个新日志将会输出到富文本框的顶部
// if (this.richTextBox1.Document.Blocks.FirstBlock == null)
// {
// this.richTextBox1.Document.Blocks.Add(paragraph);
// }
// else
// {
// this.richTextBox1.Document.Blocks.InsertBefore(this.richTextBox1.Document.Blocks.FirstBlock, paragraph);
// }
// this.richTextBox1.ScrollToHome();

this.richTextBox1.Document.Blocks.Add(paragraph);
this.richTextBox1.ScrollToEnd();
});
});

// 常见输出
Logger.Trace($"这是一条{Logger.LogLevelDescription[LogLevel.Trace]}信息!");
Logger.Debug($"这是一条{Logger.LogLevelDescription[LogLevel.Debug]}信息!");
Logger.Info($"这是一条{Logger.LogLevelDescription[LogLevel.Info]}信息!");
Logger.Warn($"这是一条{Logger.LogLevelDescription[LogLevel.Warn]}信息!");
Logger.Error($"这是一条{Logger.LogLevelDescription[LogLevel.Error]}信息!");
Logger.Fatal($"这是一条{Logger.LogLevelDescription[LogLevel.Fatal]}信息!");

// 带异常信息的输出
Exception exception = new Exception("这是一条用于测试的异常信息");
Logger.Trace($"这是一条带有异常信息的{Logger.LogLevelDescription[LogLevel.Trace]}信息!", exception);
Logger.Debug($"这是一条带有异常信息的{Logger.LogLevelDescription[LogLevel.Debug]}信息!", exception);
Logger.Info($"这是一条带有异常信息的{Logger.LogLevelDescription[LogLevel.Info]}信息!", exception);
Logger.Warn($"这是一条带有异常信息的{Logger.LogLevelDescription[LogLevel.Warn]}信息!", exception);
Logger.Error($"这是一条带有异常信息的{Logger.LogLevelDescription[LogLevel.Error]}信息!", exception);
Logger.Fatal($"这是一条带有异常信息的{Logger.LogLevelDescription[LogLevel.Fatal]}信息!", exception);

日志打印效果:

20191225195340

如果想在事件中过滤日志,可以这样配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 设置日志输出方式 这里不启用写文件和输出到控制台
Logger.LogType = LogType.None;

// 使用该属性控制窗体中日志输出 输出 Trace Debug 级别以外的日志
Logger.LogLevel = LogLevel.All ^ (LogLevel.Trace | LogLevel.Debug);

// 注册日志输出事件 输出到富文本框
Logger.LogEvent += new Logger.LogAction((level, msg, exc) =>
{
// 判断是否在允许输出的日志类型内
if ((Logger.LogLevel & level) != level)
return;

this.Dispatcher.Invoke((Action)delegate
{
// 同 WPF 输出 代码省略
});
});

// 调用日志输出 同 WPF 输出 代码省略

日志打印效果,可以看到被排除的跟踪与调试日志已经不再显示:

20191225200543

写入到数据库、发邮件等同样可以通过注册事件实现,因为很简单这里不再举例。