dotnet 职业技术学院

博客

dotnet 职业技术学院

C# 快速释放内存的大数组

lindexi 发布于 2020-03-05

本文告诉大家如何使用 Marshal 做出可以快速释放内存的大数组。 最近在做 3D ,需要不断申请一段大内存数组,然后就释放他,但是 C# 对于大内存不是立刻释放,所以就存在一定的性能问题。 在博客园看到了一位大神使用 Marshal 做出快速申请的大数组,于是我就学他的方法来弄一个。本文告诉大家这个类是如何使用。

C# 不能用于文件名的字符

lindexi 发布于 2020-03-05

在 Windows 有一些字符是不能作为文件名,尝试重命名一个文件,输入/ 就可以看到windows 提示的不能作为文件名的字符

C# ValueTuple 原理

lindexi 发布于 2020-03-05

本文告诉大家一些 ValueTuple 的原理,避免在使用出现和期望不相同的值。ValueTuple 是 C# 7 的语法糖,如果使用的 .net Framework 是 4.7 以前,那么需要使用 Nuget 安装System.ValueTuple

C# TimeSpan 时间计算

lindexi 发布于 2020-03-05

本文告诉大家简单的方法进行时间计算。

C# 判断两条直线距离

lindexi 发布于 2020-03-05

本文告诉大家获得两条一般式直线距离。

C# 强转会不会抛出异常

lindexi 发布于 2020-03-05

最近遇到一个小伙伴问我,从一个很大的数强转,会不会抛出异常。实际上不会出现异常。

C# 获得设备usb信息

lindexi 发布于 2020-03-05

本文告诉大家如何获得设备的usb来进行判断是否有哪些usb和找不到usb可能是什么。

C# 复制列表

lindexi 发布于 2020-03-05

本文:如何复制一个列表

C# 动态加载卸载 DLL

lindexi 发布于 2020-03-05

我最近做的软件,需要检测dll或exe是否混淆,需要反射获得类名,这时发现,C#可以加载DLL,但不能卸载DLL。于是在网上找到一个方法,可以动态加载DLL,不使用时可以卸载。

2020-3-4-T型图介绍

dotnet 职业技术学院 发布于 2020-03-04

在看到编译器相关知识时,发现有一个T型图的概念,于是就记录一下


什么是T型图

T型图是用于描述编译器实现时的一种辅助工具。

我们都知道描述一个语言编译,需要源代码,编译器,和目标代码,三个组成部分。

这三者都有自己的生成语言描述,正好可以使用一个T型图标识。

image-20200304192236519

这里下面的t型图表示使用β语言书写的编译器,将α语言写的源代码编译成为γ语言。

image-20200304195208626

T型图作用

有了T型图我们就可以来描述编译器构建。

比如下图,就是我先使用c语言编写了java编译器一个将java代码转成本地机器码的编译器。

然后使用本地现有的c语言编译器,将之前用C语言编写的Java编译器编译成本地机器码。

这样我们就最终得到了一个本地机器码编写的Java编译器。

image-20200304200536250

再举一个比较火的例子,如果我期望使用Java创建一个Java的编译器,即self-hosting,我该怎么用T型图描述呢?

image-20200304201145527

我先创建了一个Java书写的Java编译器。目标是将Java编译成本机的机器码。

但是计算机不能直接运行这个Java编译器,所以使用一个C语言编写的编译器,将Java编译器的Java代码编译成本地机器码。

然后要运行这个C语言编写的编译器,要先调用本机代码,将C语言编译成本地机器码。

这样一轮下来,我们最终得到的可以在本机运行的Java编译器。


参考文档:

2020-3-3-使用T4模板进行C#代码生成

dotnet 职业技术学院 发布于 2020-03-03

有过前端开发经验的同学一定了解模板文件的重要用户。其实C#也有类似的模板功能(T4模板),不仅可以生成html文件,还可以生成代码。今天就给大家介绍一下。


什么是T4模板

T4模板全称是Text Template Transformation Toolkit,因为四个单词的开头字母都是T,所以称作T4模板。

T4模板是一种支持C#或者VB代码开发的模板格式,已经在Visual Studio,MonoDevelop,Rider这些主流IDE中得到支持。

T4不仅能支持在运行时动态生成Html网页这种常见需求,而且还可以在设计时生成各种语言的代码(不仅仅是C#),xaml,xml等以便于提升开发效率。

使用visualstudio创建一个T4模板

我们在项目上右键选择添加新项,在弹出的界面中搜索T4,可以得到两个结果。分别是文本模板(设计时T4模板)和运行时文本模板(运行时T4模板)。前者可以在开发时期或者编译时期生成,后者只能在运行时调用API生成。这里我们先选择文本模板。

image-20200303192301497

这时我们在项目内就多了一个后缀为tt的模板文件。

image-20200303193021735

我们把下面这段内容粘贴进去。注意如果是第一次使用vs可能会弹出一个提示框,选择确认即可。

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
//这个是生成代码
using System;
namespace ConsoleApp2
{
    public class GeneratedClass
    {
        static void Print()
        {
            Console.WriteLine("黄腾霄好帅!");
        }
    }
}

此时我们会发现多了一个同名的.cs文件,其中的代码就是我们刚刚粘贴的内容。

image-20200303193447359

更重要的是,生成的代码就在这个项目中,可以直接使用。

image-20200303193634749

使用代码辅助生成

光是生成静态文件肯定没啥意思,T4可以使用C#代码来辅助文件的生成。

我们下面使用这段代码填充带模板中。

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
//这个是生成代码
using System;
namespace ConsoleApp2
{
    class GeneratedClass
    {
        public static void Print()
        {
<#
             for (int i = 0; i < 5; i++)
            {
#>
            Console.WriteLine("黄腾霄好帅+<#=i+1#>!");
<#
            }
#>
        }
    }
}

这里所有被<# #>包围的代码是模板编译时会执行的代码。

这里的代码表示将Console.WriteLine("黄腾霄好帅+<#=i+1#>!");在生成文件中输出5次。

其中<#=i+1#>表示将表达式i+1的值转为字符串填充至模板的生成文件中。

结果如下

image-20200303195114635

值得注意的是,这里的i+1输出随着循环进行而更新。这说明所在的模板中的代码块都隶属于同一个上下文。

可以实现变量的传递。

More

至此相信你已经可以使用T4模板完成基本的代码生成功能开发了。当然本文作为入门介绍还有很多细节没有介绍。这里可以在微软的官方文档中找到更加详细的介绍:Writing a T4 Text Template - Visual Studio -Microsoft Docs

当然也可以关注我之后关于T4模板的系列博客。


参考文档:

2020-3-1-什么是ring0-ring3

dotnet 职业技术学院 发布于 2020-03-01

大家可能听说过某个代码需要运行在ring 0的说法。但是ring 0究竟是什么,今天就给大家介绍下。


权限控制

我们都知道计算机中运行着许多的软件,有些软件是和硬件打交道的,比如驱动软件,操作系统软件,有些软件只是运行在操作系统之上的,比如浏览器,文本编辑软件等。

为什么要这样设计?

这是为了控制运行软件的权限,让一些特定的软件才能执行某些“危险”的行为,比如读写特定的内存等。

这里就引申出了我们常见的用户态,内核态的概念。那些涉及到计算机硬件资源的操作,就运行在内核态。

比如我们写一个读写文件的软件,我们在软件中进行字符串的拼接,处理,都是运行在用户态。但是当你想要把这些字符串写入文件时,只能通过系统提供的库函数的调用,在内核态“帮”你完成。

using (var file = new StreamWriter(@"C:\htx.txt"))
{
    file.WriteLine("黄腾霄"+"好瘦");
}

image-20200301162642809

使用这种权限控制的好处在于计算机用户的软件不会危及系统的安全,只有稳定的系统软件才能够操作系统的关键内存等硬件设备。从而最大程度保证系统运行的稳定。

Ring

可以用下面这张图来表示这种权限关系

image-20200301163452718

从内到外依次使用0-3标识,这些环(ring)。越内部的圈代表的权限越大。内圈可以访问,修改外圈的资源;外圈不可以访问,修改内圈的资源。

为什么会有4个ring?因为x86的cpu,在Data segment selector中使用了2个bits来描述权限。

我们最常见的是ring 0(内核态),和ring 3(用户态)。因为例如windows和unix这些常见的操作系统,只提供了两种权限模式,所以并没有完全使用整个ring架构。

所以我们在一般情况下,完全可以使用ring 0 表示内核态,ring 3表示用户态。


参考文档:

2020-2-24-Nginx的Http配置入门

dotnet 职业技术学院 发布于 2020-02-24

Nginx大家想必都听说过,今天简单介绍下Nginx的配置使用。


Nginx和Nginx配置

Nginx是一款高性能的反向代理和负载均衡工具。

而Nginx配置是指导Nginx转发请求的文件。

下面是一个示例配置文件。我们可以看到整个配置文件的结构很简单,即由一些全局指令(例如配置工作进程数等)和一些针对特定功能的指令块(例如http配置)构成。

指令块之中还可以嵌套指令块,以完成一些更加复杂的配置。而且子指令块中的配置如果缺省,可以从父指令块中进行获取,进一步简化了配置文件。

image-20200224193120460

Nginx的Http配置

http配置是最常用的配置。它最主要的几个组成部分是upstream指令配置的负载均衡器,和server指令配置的虚拟服务器。

image-20200224194648370

配置反向代理

server指令最主要的一个工作是配置反向代理,当我们访问指定网址时,Nginx帮我们把请求转发给目标服务器。但是此时对于用户端来说是无感知的。

这里我先使用express在本机3000和3001端口创建了2个Node服务,分别返回其对应的端口号。

image-20200224195223604

image-20200224195239048

现在我期望在访问localhost:8080/3000时,请求能转发至localhost:3000,当我访问localhost:8080/3001时,请求转发至localhost:3001

下面是对应的Nginx配置。这个配置在http指令块中定义了一个虚拟服务器,监听本机的8080端口。

location会匹配请求的uri,如果是以/3000开头,就通过proxy_pass指令将请求转发给127.0.0.1:3000。如果是以/3001开头,就通过proxy_pass指令将请求转发给127.0.0.1:3001

worker_processes 1;

events {
    worker_connections 1024;
}

http {
    server {
        listen 8080;
        server_name localhost;
        location /3000 {
            proxy_pass http://127.0.0.1:3000;

        }

        location /3001 {
            proxy_pass http://127.0.0.1:3001;
        }
        
        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root html;
        }
    }

}

我们看到这里报出来了node的异常,因为Nginx仅把请求转发给对应的服务器,并不会修改uri,所以这里实际请求的是127.0.0.1:3001/3001,而我没有在Node中对此路由进行处理,所以Node返回了404

image-20200224200448738

不过我们可以从vs的输出中看到的确获得了请求。

image-20200224200755716

配置静态文件服务

我们看到还有一个location它没有使用proxy_pass,而是使用了root指令。

这个指令是将对应的请求转发至Nginx服务器中,指定文件目录,这里是html/50x.html文件

image-20200224201357179

可以看到这个文件的确就在Nginx的指定目录下。

image-20200224201629814

image-20200224201710849 因此root指令就可以将静态资源的请求直接定位到文件系统,实现动静分离的需求。

配置负载均衡

Nginx另一个强大的功能是配置负载均衡。

这里我用upstream指令配置了一个叫做backend的负载均衡器。另外将 location\的请求转发至这个负载均衡器。

在backend中,我用server指令定义了2个服务器,分别指向本机的3000端口和3001端口。此外对于3000端口的服务器配置了权重为2(默认为1)。

这样现在对本机8080端口的请求,会按照2:1的比例转发至3000端口和3001端口。

worker_processes 1;

events {
    worker_connections 1024;
}

http {
    upstream backend {
        server 127.0.0.1:3000 weight=2;
        server 127.0.0.1:3001;
    }

    server {
        listen 8080;
        server_name localhost;
        location / {
            proxy_pass http://backend;
        }
        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root html;
        }
    }
}

image-20200224202555690

image-20200224202605722

当然Nginx还可以通过轮询(默认),最小连接优先(least_conn指令),按ip固定服务器(ip_hash指令)等多种方式进行配置。

具体可以参考NGINX Docs - HTTP Load Balancing小节


参考文档:

2020-2-22-Unicode代理对(utf-16)

dotnet 职业技术学院 发布于 2020-02-22

我们都知道Unicode的大部分字符都是都是使用16位编码,即2个字节表示。

这也是为什么正则匹配中,Unicode使用“\uxxxx”进行匹配的原因

为什么说是大部分呢?因为还有一个神奇的区域,叫做Unicode代理对。它们需要使用4个字节来表示一个字符。

这里就给大家做介绍。


问题来源

Unicode的产生是为了处理不同语言之间的编码不兼容问题。

比如如果中文和日文的不同文字使用了同一个编码值进行表示,那么一篇中文的软件/操作系统中创作的文章,到了日文软件/操作系统中显示就会出现乱码。

Unicode期望规定一种通用的文字编码方式能够唯一的表示所有的文字字符。这样只要所在的软件/操作系统支持Unicode,任何文章都能在这些电脑上保持一致的字符显示。

显然,为了支持任意的文字,Unicode需要保证其编码范围要大于所有的文字字符数量。

那么,Unicode使用16位编码能够最多表示 ${2^{16}=65536}$,这对于当前常见的主要语言的字符,数学符号等已经基本够用。

但是值得注意的是,世界上还有很多小语种,古代语种,另外语言也会自行发展,例如近两年发展出来的emoji字符等等。如果他们也加入Unicode,那么16位编码空间就远远不够了。

设计方案

为了支持更多的字符,最简单的方法就是增加编码空间,比如3个字节或者4个字节表示一个字。

但是问题来了,如果统一使用4个字节编码,那就意味着同样一篇文章的内容,4字节编码会比2字节编码的体积,增大一倍。这对于存储和网络传输都是非常大的影响。

而且,由于我们的常用字符大部分只需要2个字节就能表示。所以这些额外的空间在大部分情况下,都是白白“浪费”了。

那么有没有一种方案,能够针对常用字符仅使用2个字节表示,对于小语种等其他语言的字符使用更多字节来表示呢?

“自然增长”方案

我们最容易想到的方案是“自然增长”的方案。即,先使用完低位的空间,当空间不够时,再增加高位空间。

例如最早的ASCII码只用了1个字节,到了编码空间不够时,再增加空间。

但是这种方式并不能解决刚刚提出的空间问题,因为它存在前导字符识别的问题。

举个栗子

image-20200222161927215

这里我创建了一个自定义字符,使用“\u12345678”四个字节表示。

但是实际上计算机解析时,他会把这个字符解析为“\u1234”和“\u5678”两个字符。

因为他不知道什么时候该按2个字节解析,什么时候该按4个字节解析。

image-20200222162034039

代理对方案

Unicode采用了代理对(Surrogate Pair)来解决。

他选择了 D800-DBFF编码范围作为前两个字节(utf-16高半区),DC00-DFFF作为后两个字节(utf-16低半区),组成一个四个字节表示的字符。

当软件解析到Unicode连续4个字节的前两个是utf-16高半区,后两个是utf-16低半区,他就会把它识别为一个字符。如果配对失败,或者顺序颠倒则不显示。

D800-DBFF可表示的编码范围有10位,DC00-DFFF可表示的编码范围也有10位,加起来就是20位(00000-FFFFF),这样就可以表示${2^{20}}$个字符。在可见的未来都不会出现不够使用的情况。

而且代理对区间的编码不能单独映射字符,因此不会产生识别错误。

处理字符映射

我们通过代理对解决了编码问题,但是对于人类阅读来说,“\uD800DC00”的表示方法还是太复杂。

而且和基本的两字节表示的Unicode编码放在一起看,并不连续。

因此Unicode将这20位的编码空间映射到了 10000-10FFFF,这样就能和2个字节 0000-FFFF表示的编码空间在一起连续表示了。

所以\uD800\uDC00=\u10000,这也是我们部分语言调试下对emoji字符的码值显示会出现5个HEX的原因。

Unicode平面

Unicode将 000000-10FFFF划分成了17个大小为FFFF的编码空间,其中000000-00FFFF就是我们最常用的字符,叫做0号平面,由2个字节组成。而其他的16个平面叫做辅助平面,由四个字节的代理对生成。

每个功能可以划分特定使用方式,这样就能实现编码和表意的统一,通过编码范围识别出字符所属的用途。例如第二辅助平面主要放置中日韩语言中一些罕见的字符。

代码识别

最后一个问题是编程语言识别问题,由于存在代理对,许多语言的string.length方法会将代理对中的字符(如emoji)个数识别成2个。这样会造成一些诸如光标定位,字符提取等方面的问题。

对于JavaScript,ES6中有String.fromCodePoint(),codePointAt(),for…of循环等方式处理。

具体可以参见阮一峰的博客字符串的新增方法 - ECMAScript 6入门

String.fromCharCode(0x20BB7)
// "ஷ"

let s = '𠮷a';
s.codePointAt(0) // 134071
s.codePointAt(1) // 57271

s.codePointAt(2) // 97

for (let x of 'a\uD83D\uDC0A') {
  console.log(x);
}
// 'a'
// '\uD83D\uDC0A'

对于C#来说可以使用StringInfo进行处理,详情可以参见我的博客2019-11-10-使用StringInfo正确查找字符个数 - huangtengxiao


参考文档:

.NET 将多个程序集合并成单一程序集的 4+3 种方法

dotnet 职业技术学院 发布于 2020-01-10

编写 .NET 程序的时候,我们经常会在项目的输出目录下发现一大堆的文件。除了我们项目自己生成的程序集之外,还能找到这个项目所依赖的一大堆依赖程序集。有没有什么方法可以把这些依赖和我们的程序集合并到一起呢?

本文介绍四种将程序集和依赖打包合并到一起的方法,每一种方法都有其不同的原理和优缺点。我将介绍这些方法的原理并帮助你决定哪种方法最适合你想要使用的场景。


四种方法

目前我已知的将 .NET 程序集与依赖合并到一起的方法有下面四种:

  1. 使用 .NET Core 3.0 自带的 PublishSingleFile 属性合并依赖
  2. 使用 Fody
  3. 使用 SourceYard 源代码包
  4. 使用 ILMerge(微软所写)或者 ILRepack(基于 Mono.Ceil)
  5. 其他方法

如果你还知道有其他的方法,欢迎评论指出,非常感谢!

上面的第五种方法我也会做一些介绍,要么是因为无法真正完成任务或者适用场景非常有限,要么是其原理我还不理解,因此只进行简单介绍。

使用 .NET Core 3.0 自带的 PublishSingleFile 属性合并依赖

.NET Core 3.0 自 Preview 5 开始,增加了发布成单一 exe 文件的功能。

在你的项目文件中增加下面的两行可以开启此功能:

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp3.0</TargetFramework>
++      <RuntimeIdentifier>win10-x64</RuntimeIdentifier>
++      <PublishSingleFile>true</PublishSingleFile>
      </PropertyGroup>
    
    </Project>

第一行 RuntimeIdentifier 一定需要指定,因为发布的单一文件是特定于架构的。这里,我们指定了 win10-x64,你也可以指定为其他的值。可以使用的值你可以在这篇文章中查询到:

第二行 PublishSingleFile 即开启发布时单一文件的功能。这样,你在发布你的程序的时候可以得到一个单一的可执行程序。发布一个 .NET Core 项目的方法是在命令行中输入:

dotnet publish

当然,如果你没有更改任何你的项目文件(没有增加上面的那两行),那么你在使用发布命令的时候就需要把这两个属性再增加上。因此完整的发布命令是下面这样的:

dotnet publish -r win10-x64 /p:PublishSingleFile=true

这里的 -r 就等同于在项目中指定 RuntimeIdentifier 持续。这里的 /p 是在项目中增加一个属性,而增加的属性名是 PublishSingleFile,增加的属性值是 true

使用 .NET Core 3.0 这种自带的发布单一 exe 的方法会将你的程序的全部文件(包括所有依赖文件,包括非托管程序集,包括各种资源文件)全部打包到一个 exe 中。当运行这个 exe 的时候,会首先将所有这些文件生成到本地计算机中一个临时目录下。只有第一次运行这个 exe 的时候才会生成这个目录和其中的文件,之后的运行是不会再次生成的。

下面说一些 .NET Core 3.0 发布程序集的一点扩展——.NET Core 3.0 中对于发布程序集的三种处理方式可以放在一起使用:

  • 裁剪程序集(Assembly Trimmer)
  • 提前编译(Ahead-of-Time compilation,通过 crossgen)后面马上会说到 Microsoft.DotNet.ILCompiler
  • 单一文件打包(Single File Bundling)本小节

关于 .NET Core 3.0 中发布仅一个 exe 的方法、原理和实践,可以参见林德熙的博客:

.NET Core 在 GitHub 上开源:

使用 Fody

在你的项目中安装一个 NuGet 包 Costura.Fody。一般来说,安装完之后,你编译的时候就会生成仅有一个 exe 的程序集了。

如果你继续留意,可以发现项目中多了一个 Fody 的专属配置文件 FodyWeavers.xml,内容如下:

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

仅仅到此为止你已经足够利用 Fody 完成程序集的合并了。

但是,如果希望对 Fody 进行更精细化的配置,可以阅读叶洪的博客:

Fody 在 GitHub 上开源:

使用 SourceYard 源代码包

SourceYard 源代码包在程序集合并上是另辟蹊径的一种合并方式。它不能帮助你将所有的依赖全部合并,但足以让你在发布一些简单应用的时候不至于引入大量的依赖。

例如,你可以考虑新建一个项目,然后安装下面的 NuGet 包:

安装完成之后,你就可以在你的项目中使用到此 NuGet 包为你带来的获取 MAC 地址的工具类了。

using System;
using lindexi.src;

namespace Walterlv.Demo
{
    internal static class Program
    {
        static void Main()
        {
            var macList = MacAddress.GetActiveMacAddress();
            foreach (var mac in macList)
            {
                Console.WriteLine(mac);
            }
        }
    }
}

编译完你的项目,你会发现你的项目没有携带任何依赖。你安装的 NuGet 包并没有成为你的依赖,反而成为你正在编译的程序集的一部分。

如果你要制作一个像上面那样的源代码包,只需要在你要制作 NuGet 包的项目安装上 dotnetCampus.SourceYard,在你打包成 NuGet 包的时候,就会生成一个普通的 NuGet 包以及一个 *.Source.nupkg 的源代码包。将源代码包上传到 nuget.org 上,其他人便可以安装你制作的源代码包了。

关于如何使用 SourceYard 制作一个源代码包的方法可以阅读林德熙的博客:

关于能够做出源代码包的原理,可以阅读我的博客:

SourceYard 在 GitHub 上开源:

使用 ILMerge 或者 ILRepack 等工具

ILMerge 和 ILRepack 的合并就更加富有技术含量——当然坑也更多。

这两个都是工具,因此,你需要将工具下载下来使用。你有很多种方法下载到工具使用,因此我会推荐不同的人群使用不同的工具。

ILMerge

ILMerge 命令行工具是微软官方出品,下载地址:

其使用方法请参见我的博客:

ILRepack

ILRepack 基于 Mono.Ceil 来进行 IL 合并,其使用方法可以参见我的博客:

ILMerge-GUI 工具(已过时,但适合新手随便玩玩)

你可以在以下网址中找到 ILMerge-GUI 的下载链接:

ILMerge-GUI 工具在 Bitbucket 上开源:

其他方法

使用 Microsoft.DotNet.ILCompiler

可以将 .NET Core 编译为单个无依赖的 Native 程序。

你需要先安装一个预览版的 NuGet 包 Microsoft.DotNet.ILCompiler

关于 Microsoft.DotNet.ILCompiler 的使用,你可以阅读林德熙的博客:

使用 dnSpy

dnSpy 支持添加一个模块到程序集,也可以创建模块,还可以将程序集转换为模块。因此,一个程序集可以包含多个模块的功能就可以被充分利用起来。

添加模块到程序集

使用 Warp

Warp 在 GitHub 上开源:

其使用可以参见林德熙的博客:

各种方法的原理和使用场景比较

原理

使用 .NET Core 3.0 自带的 PublishSingleFile 属性合并依赖,其原理是生成一个启动器容器程序。最终没有对程序进行任何修改,只是单纯的打包而已。

使用 Fody,是将程序集依赖放到了资源里面。当要加载程序集的时候,会直接将资源中的程序集流加载到内存中。

使用 SourceYard 源代码包,是直接将源代码合并到了目标项目里面。

使用 ILMerge / ILRepack,是在 IL 级别对程序集进行了合并。

我们可以通过下面一张图来感受一下后三种原理上的不同。

这是一个分别通过 Fody、SourceYard 和 ILMerge / ILRepack 生成的程序集的反编译图。可以看到,对于 ILRepack / ILMerge 和 SourceYard,反编译后看到的源代码都在目标程序集中,而对于 Fody,依赖仅仅出现在资源中。

原理差别

适用范围

由于其原理不同,所以其适用范围和造成的副作用也不同。

如果你基于 .NET Core 3.0 开发,并且也不在意在目标计算机上生成的临时文件夹,那么可以考虑使用 PublishSingleFile 属性合并依赖。

如果你不在乎启动性能以及内存消耗,那么可以考虑 Fody(这意味着小型程序比较适合采用)。

如果你的程序非常在乎启动性能,那么就需要考虑 SourceYard、ILMerge / ILRepack 了。

对于 ILMerge / ILRepack 和 SourceYard 的比较,可以看下面这张表格:

方案 ILRepack / ILMerge SourceYard
适用于 任意 .NET 程序集 通过 SourceYard 发布的 NuGet 包
WPF ILRepack 支持,ILMerge 不支持 支持
调试(支持) 仅支持一般方法的调试 支持一般程序集支持的所有调试方法
调试(不支持) 不支持异步方法调试,不支持显示局部变量 没有不支持的
隐藏 API internal 的类型和成员可以隐藏 必须是 private 类型和成员才可隐藏

可以发现,如果我们能够充分将我们需要的包通过 SourceYard 发布成 NuGet,那么我们将可以获得比 ILRepack / ILMerge 更好的编写和调试体验。

表格之外还有一些特别需要说明的:

  1. ILRepack 额外支持修改 WPF 编译生成的 Baml 文件,将资源的引用路径修改成新程序集的路径。
  2. SourceYard 的类型需要写成 private 才可以隐藏,但是只有内部类才可以写 private,因此如果特别需要隐藏,请首先写一个内部类。(因此,你可能会发现有一个类型有很多个分部类,每一个分部类中都是一个私有的内部类)

开源社区

最后说一下,以上所说的所有方法全部是开源的,有问题欢迎在社区讨论一起解决: