当前位置: 首页 > 编程日记 > 正文

反编译使用yield关键字的方法

我认为这是一个真命题:“没有用.NET Reflector反编译并阅读过代码的程序员不是专业的.NET程序员”。.NET Reflector强大的地方就在于可以把IL代码反编译成可读性颇高的高级语言代码,并且能够支持相当多的“模式”,根据这些模式它可以在一定程度上把 某些语法糖给还原,甚至可以支持简单的Lambda表达式和LINQ。只可惜,.NET Reflector还是无法做到极致,某些情况下生成的代码还是无法还原到易于理解——yield关键字便是这样一个典型的情况。不过还行,对于不复杂的 逻辑,我们可以通过人肉来“整理”个大概。

简单yield方法编译结果分析

yeild的作用是简化枚举器,也 就 是IEnumerator<T>或IEnumerable<T>的实现。“人肉”反编译的关键在于发现编译器的规律,因此我们先 来观察编译器的处理结果。值得注意的是,我们这里所谈的“分析”,都采用的是微软目前的C# 3.0编译器。从理论上来说,这些结果或是规律,都有可能无法运用在Mono和微软之前或今后的C#编译器上。首先我们准备一段使用yield的代码:

static IEnumerator<int> GetSimpleEnumerator()
{
Console.WriteLine("Creating Enumerator");

yield return 0;
yield return 1;
yield return 2;

Console.WriteLine("Enumerator Created");
}

为 了简化问题,我们在这里采用IEnumerator<T>。自动生成的IEnumerable<T>和 IEnumerator<T>区别不大,您可以自己观察一下,有机会我会单独讨论和分析其中的区别。经过编译之后再使用.NET Reflector进行反编译,得到的结果是:

private static IEnumerator<int> GetSimpleEnumerator()
{
return new <GetSimpleEnumerator>d__0(0);
}

[CompilerGenerated]
private sealed class <GetSimpleEnumerator>d__0 : IEnumerator<int>, ...
{
// Fields
private int <>1__state;
private int <>2__current;

// Methods
[DebuggerHidden]
public <GetSimpleEnumerator>d__0(int <>1__state)
{
this.<>1__state = <>1__state;
}

private bool MoveNext()
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
Console.WriteLine("Creating Enumerator");
this.<>2__current = 0;
this.<>1__state = 1;
return true;

case 1:
this.<>1__state = -1;
this.<>2__current = 1;
this.<>1__state = 2;
return true;

case 2:
this.<>1__state = -1;
this.<>2__current = 2;
this.<>1__state = 3;
return true;

case 3:
this.<>1__state = -1;
Console.WriteLine("Enumerator Created");
break;
}

return false;
}

...
}

以上便是编译器生成的逻辑,它将yield关键字这个语法糖转化为普通的.NET结构(再次强调,这只是微软目前的C# 3.0编译器所产生的结果)。从中我们可以得出一些结论:

  • 原本GetSimpleEnumerator方法中包含yield的逻辑不复存在,取而代之的是一个由编译器自动生成的IEnumerator类的实例。
  • 原本GetSimpleEnumerator方法中包含yield的逻辑,被编译器自动转化为对应IEnumerator类中的MoveNext方法的逻辑。
  • 编译器将包含yield逻辑转化为一个状态机,并使用自动生成的state字段保存当前状态。
  • 每次调用MoveNext方法时,都通过switch语句判断state的值,直接进入特定的逻辑片断,并指定下一个状态。

因 为从yield关键字的作用便是“中断”一个方法的逻辑,使它在下次执行MoveNext方法的时候继续执行。这就意味着自动生成的 MoveNext代码必须通过某一个手段来保留上次调用结束之后的“状态”,并根据这个状态决定下次调用的“入口”——这是个典型的状态机的“思路”。由 此看来,编译器如此实现,其“设计”意图也是比较直观的,相信您理解起来也不会有太大问题。

较为复杂的yield方法

上一个例子非常简单,因为GetSimpleEnumerator的逻辑非常简单(只有“顺序”,而没有“循环”和“选择”)。此外,这个方法也没有使用局部变量及参数,于是我们这里不妨再准备一个相对复杂的方法:

private static IEnumerator<int> GetComplexEnumerator(int[] array)
{
<GetComplexEnumerator>d__2 d__ = new <GetComplexEnumerator>d__2(0);
d__.array = array;
return d__;
}

[CompilerGenerated]
private sealed class <GetComplexEnumerator>d__2 : IEnumerator<int>, ...
{
// Fields
private int <>1__state;
private int <>2__current;
public int <i>5__4;
public int <i>5__6;
public int <sumEven>5__3;
public int <sumOdd>5__5;
public int[] array;

// Methods
[DebuggerHidden]
public <GetComplexEnumerator>d__2(int <>1__state)
{
this.<>1__state = <>1__state;
}

private bool MoveNext()
{
// 第一部分
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
Console.WriteLine("Creating Enumerator");
this.<sumEven>5__3 = 0;
this.<i>5__4 = 0;
goto Label_0094;

case 1:
this.<>1__state = -1;
goto Label_0086;

case 2:
goto Label_00F4;

default:
goto Label_0123;
}

// 第二部分
Label_0086:
this.<i>5__4++;
Label_0094:
if (this.<i>5__4 < this.array.Length)
{
if ((this.array[this.<i>5__4] % 2) == 0)
{
this.<sumEven>5__3 += this.array[this.<i>5__4];
this.<>2__current = this.<sumEven>5__3;
this.<>1__state = 1;
return true;
}
goto Label_0086;
}
this.<sumOdd>5__5 = 0;
this.<i>5__6 = 0;
while (this.<i>5__6 < this.array.Length)
{
if ((this.array[this.<i>5__6] % 2) == 0)
{
goto Label_00FB;
}
this.<sumOdd>5__5 += this.array[this.<i>5__6];
this.<>2__current = this.<sumOdd>5__5;
this.<>1__state = 2;
return true;
Label_00F4:
this.<>1__state = -1;
Label_00FB:
this.<i>5__6++;
}
Console.WriteLine("Enumerator Created.");
Label_0123:
return false;
}

...
}

这 下MoveNext的逻辑便一下子复杂了很多。我认为,这是由于编译器期望生成体积小的代码,于是它使用了goto来进行自由的跳转。其实从理论 上说,把这个方法分为N个阶段之后,便可以让它们完全独立地分开,只不过此时各状态间便会出现许多重复的逻辑。不过,这段代码看似复杂,其实您仔细分析便 会发现,它其实也只是将代码拆成了上下两部分(如代码注释所示):

  • 第一部分:状态机的控制逻辑,即根据当前状态进行跳转。
  • 第二部分:主体逻辑,只不过使用goto代替了普通语句中由for/if组成的逻辑,这么做的目的是为了插入Label,可以让第一部分的代码直接跳转到合适的地方——换句话说,由第一部分跳转到的Label便是yield return出现的地方。

从上面的代码中我们还可以看出方法的“参数”及“局部变量”的转化规则:

  • 参数被转化为IEnumerator类的公开字段,命名方式不变,原本的array参数直接变成array字段。
  • 局部变量被转化为IEnumerator类的公开字段,并运用一定的命名规则改名(主要是为了避免和自动生成的current及state字段产生冲突)。对于局部变量localVar,将被转化为<localVar>X__Y的形式。
  • 其他需要自动生成的字段为<>1__state及<>2__current,它们只是进行辅助逻辑,不再赘述。

至此,我们已经掌握了编译器基本的转化规律,可以将其运用到“人肉反编译”的过程中去。

试验:人肉反编译OrderedEnumerable

事实上,.NET框架中的System.Linq.OrderedEnumerable类便是一个包含yield方法的逻辑,使用.NET Reflector得到的相关代码如下:

internal abstract class OrderedEnumerable<TElement> : IOrderedEnumerable<TElement>, ...
{
internal IEnumerable<TElement> source;

internal abstract EnumerableSorter<TElement> GetEnumerableSorter(EnumerableSorter<TElement> next);

public IEnumerator<TElement> GetEnumerator()
{
<GetEnumerator>d__0<TElement> d__ = new <GetEnumerator>d__0<TElement>(0);
d__.<>4__this = (OrderedEnumerable<TElement>) this;
return d__;
}

[CompilerGenerated]
private sealed class <GetEnumerator>d__0 : IEnumerator<TElement>, ...
{
// Fields
private int <>1__state;
private TElement <>2__current;
public OrderedEnumerable<TElement> <>4__this;
public Buffer<TElement> <buffer>5__1;
public int <i>5__4;
public int[] <map>5__3;
public EnumerableSorter<TElement> <sorter>5__2;

[DebuggerHidden]
public <GetEnumerator>d__0(int <>1__state)
{
this.<>1__state = <>1__state;
}

private bool MoveNext()
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
this.<buffer>5__1 = new Buffer<TElement>(this.<>4__this.source);
if (this.<buffer>5__1.count <= 0)
{
goto Label_00EA;
}
this.<sorter>5__2 = this.<>4__this.GetEnumerableSorter(null);
this.<map>5__3 = this.<sorter>5__2.Sort(this.<buffer>5__1.items, this.<buffer>5__1.count);
this.<sorter>5__2 = null;
this.<i>5__4 = 0;
break;

case 1:
this.<>1__state = -1;
this.<i>5__4++;
break;

default:
goto Label_00EA;
}
if (this.<i>5__4 < this.<buffer>5__1.count)
{
this.<>2__current = this.<buffer>5__1.items[this.<map>5__3[this.<i>5__4]];
this.<>1__state = 1;
return true;
}
Label_00EA:
return false;
}

...
}
}

很自然,我们需要“人肉反编译”的便是OrderedEnumerable类的GetEnumerator方法。首先,为了便于理解代码,我们首先还原各名称。既然我们已经知道了局部变量及current/state的命名规则,因此这个工作其实并不困难:

private bool MoveNext()
{
switch (__state)
{
case 0:
__state = -1;
var buffer = new Buffer<TElement>(this.source);
if (buffer.count <= 0)
{
goto Label_00EA;
}

var sorter = this.GetEnumerableSorter(null);
var map = sorter.Sort(buffer.items, buffer.count);
sorter = null;
var i = 0;
break;

case 1:
__state = -1;
i++;
break;

default:
goto Label_00EA;
}

if (i < buffer.count)
{
__current = buffer.items[map[i]];
__state = 1;
return true;
}

Label_00EA:
return false;
}

值 得注意的是,在上面的方法中,this是由原来的<>4__this字段还原而来,它表示的是OrderedEnumerable类 型(而不是自动生成的IEnumerator类)的实例。此外,其中的局部变量您需要将其理解为“自动在多次MoveNext调用中保持状态的变量”—— 这和C语言中的静态局部变量有些接近。自然,__state和__current变量都是自动生成用于保存状态的变量,我们姑且保留它们。

接下来,我们将要还原state等于0时的逻辑。因为我们知道,它其实是yield方法中“第一个yield return”之前的逻辑:

private IEnumerator<TElement> GetEnumerator()
{
var buffer = new Buffer<TElement>(this.source);
if (buffer.count <= 0) yield break;

var sorter = this.GetEnumerableSorter(null);
var map = sorter.Sort(buffer.items, buffer.count);
// 省略sorter = null(为什么?:P)

var i = 0;
if (i < buffer.count)
{
yield return buffer.items[map[i]];
}

...
}

我 们发现,在buffer.count小于等于0的时候MoveNext直接返回false了,于是在GetEnumerator方法中我们便使用 yield break直接退出。在上面的代码中我们已经还原至第一个yield return,那么当调用下一个MoveNext时(即state为1)逻辑又该如何进行呢?我们再“机械”地还原一下:

private IEnumerator<TElement> GetEnumerator()
{
...

i++;
if (i < buffer.count)
{
yield return buffer.items[map[i]];
}
else
{
yield break;
}

...
}

接着,我们会发现代码会不断重复上面这段逻辑,因此我们可以使用一个“死循环”将其包装起来。至此,GetEnumerator便还原成功了:

private IEnumerator<TElement> GetEnumerator()
{
var buffer = new Buffer<TElement>(this.source);
if (buffer.count <= 0) yield break;

var sorter = this.GetEnumerableSorter(null);
var map = sorter.Sort(buffer.items, buffer.count);

var i = 0;
if (i < buffer.count)
{
yield return buffer.items[map[i]];
}

while (true)
{
i++;
if (i < buffer.count)
{
yield return buffer.items[map[i]];
}
else
{
yield break;
}
}
}

不过,又有多少人会写这样的代码呢?的确,这段代码是我们“机械翻译”的结果。不过经过观察,事实上这段代码可以被修改成如下写法:

private IEnumerator<TElement> GetEnumerator()
{
var buffer = new Buffer<TElement>(this.source);
if (buffer.count <= 0) yield break;

var sorter = this.GetEnumerableSorter(null);
var map = sorter.Sort(buffer.items, buffer.count);

for (var i = 0; i < buffer.count; i++)
{
yield return buffer.items[map[i]];
}
}

至 此就完美了。最后这步转换我们利用了人脑的优越性,这样“看出”一种优雅的模式也并非难事——不过这也并非只能靠“感觉”,因为我在上面谈到,编 译器会尽可能生成紧凑的代码,这意味着它和“源代码”相比不会有太多的重复。但经由我们“机械还原”之后,会发现这样一段代码其实是重复出现的:

if (i < buffer.count)
{
yield return buffer.items[map[i]];
}

于是我们便可以朝着“合并代码片断”的方向去思考,得到最终的结果还是有规律可循的。

总结

如果您关注我最近的文章,并且在看到OrderedEnumerable这个类型之后应该会有所察觉:这篇文章只是我在“分析Array和LINQ排序实现” 过程中的一个插曲。没错,这是LINQ排序实现的一小部分。OrderedEnumerable利用了yield关键字,这样我们使用.NET反编译之后 代码的可读性很差。为此,我便特地研究了一下对yield进行“人肉反编译”的做法。不过在一开始,我原本其实是想仔细分析一下yield相关的“编译规 律”,但是我发现在《C# in Depth》一书中已经对这个话题有了非常详尽的描述,只得作罢。之后我又看了这本书网站上公开的样张,感觉非常不错。

事 实上,自从ASP.NET 2.0开始,我似乎就没有看过任何一本ASP.NET 2.0/3.0或是C# 2.0/3.0/4.0的书了,因为我认为这些书中的所有内容都可以从MSDN文档,互联网(如博客)以及自己使用、分析的过程中了解到。不过现在, 《C# in Depth》似乎让我对此类技术图书的“偏见”有所动摇了——但只此一本而已,估计我还是不会去买这样的书。:)

对了,昨天我向“有关部门”了解到,《C# in Depth》已经由图灵出版社引进,翻译完毕,只等审校和出版了。

转载于:https://www.cnblogs.com/andyhebear/articles/4546944.html

相关文章:

Android studio 启动自学模式

今天在网上看到了编译Android的软件Android studio&#xff0c;出于对Android的学习兴趣&#xff0c;我打算开始新一轮的Android的学习。今天就是在网上&#xff0c;以及图书馆里查找了有关Android studio的书籍&#xff0c;但是由于Android是在13年才开始发布的原因吧&#x…

AutoCAD 2D与3D大师班学习教程 AutoCAD 2D and 3D Masterclass

用实例和解决问题的方法完成从基础到专业的AutoCAD课程。 你会学到什么 AutoCAD课程包含创建计划和模型的命令和不同方法的详细使用。 本课程包括对AutoCAD中使用的所有命令和工具的详细解释。 课程内容是按时间顺序设计的&#xff0c;以了解承担项目的实际方法。 本课程包含两…

威纶通宏开机后使用初始化宏指令_【操作系统】我们按下电脑开机键的背后发生了什么?...

作者&#xff1a;CVNot链接&#xff1a;https://juejin.im/post/5e8844996fb9a03c6675b9d6操作系统是什么&#xff1f;操作系统是用来管理计算机硬件的软件&#xff0c;狭义上实现该定义的为操作系统内核&#xff1b;而更加宽泛的操作系统概念为根据内核对外提供了一些OS服务&a…

Linux常用压缩与解压缩命令

.tar 解包&#xff1a;tar xvf FileName.tar打包&#xff1a;tar cvf FileName.tar DirName&#xff08;注&#xff1a;tar是打包&#xff0c;不是压缩&#xff01;&#xff09;———————————————.gz解压1&#xff1a;gunzip FileName.gz解压2&#xff1a;gzip -d…

【Kubernetes】如何使用Kubeadm部署K8S集群

一 . 准备机器 本次环境采用华为云ECS弹性云服务器部署&#xff08;也可以使用VMware&#xff09; vm01&#xff08;2V4G&#xff09;&#xff1a; Ubuntu_18.04作为K8S master节点 vm02&#xff08;1V1G&#xff09;&#xff1a; Ubuntu_18.04作为K8S node节点 备注: 以下所有…

解决ORA-28000: the account is locked

在oracle中&#xff0c;连续十次尝试登陆不成功&#xff0c;那么此账户将会被锁定&#xff08;lock&#xff09;。当使用被锁定的账户登录时&#xff0c;系统会报错&#xff1a;ORA-28000: the account is locked。查询FAILED_LOGIN_ATTEMPTS参数默认值&#xff0c;这个参数限制…

Android sudio Day01-1

今天我开始学习Android studio的第二天&#xff0c;主题是Android studio的安装。 之前的学校学习都是使用Android开发者工具&#xff08;Android development tools&#xff0c;ADT&#xff09;&#xff0c;而ADT作为一个Android开发工具&#xff0c;它是通过内置于Eclipse的…

学习用C#在Unity中创建一个2D Metroidvania游戏

学习用C#在Unity中创建一个2D Metroidvania游戏 你会学到: 构建2D Unity游戏 用C#编程 玩家统计&#xff0c;水平提升&#xff0c;米尔和远程攻击 敌方人工智能系统 制定级别和级别选择 Learn To Create A 2D Metroidvania Game in Unity With C# MP4 |视频:h264&#xff0c;…

3.27课·········悬浮动态分层导航与隐藏导航

例1:分层导航 <title>分层导航</title> <script src"../JavaScript/jquery-1.4.2.min.js">//引用外部JS代码 </script> <style> #apDiv1 {position: fixed;left: auto;top: auto;bottom: auto;width: 237px;height: auto;z-index: 2;m…

.sh是什么语言_shell的重生历史:从sh到bash

shell 门派之争Linux 中的 shell 有很多类型&#xff0c;其中最常用的几种是&#xff1a;Bourne shell (sh)、C shell (csh) 和 Korn shell (ksh)&#xff0c;它们各有优缺点&#xff0c;用户则萝卜青菜&#xff0c;各有所爱。Bourne shell 出师不利Bash&#xff1a;Bourne aga…

【Docker】容器的几种网络模式

当你使用Docker时&#xff0c;你会发现需要了解很多关于网络的知识。Docker作为目前最火的轻量级容器引擎&#xff0c;因此&#xff0c;我们有必要深入了解Docker的网络知识&#xff0c;以满足更高的网络需求。本文介绍了Docker的4种网络模式。 1、首先我们先简单描述一下容器…

微信推送模板消息的PHP代码整理

本文为本人原创&#xff0c;未经许可&#xff0c;不可转载。 博主长期从事微信开发&#xff0c;微信开发相关问题和业务请联系qq 2580234897 最近做过一个需要推送消息的系统&#xff0c;就研究了一下微信的模板消息的推送。由于认证过的微信号&#xff0c;就用测试号做的&…

Android studio Day01-23

AndroidDay01-2&#xff08;Android studio安装&#xff09; 步骤如下&#xff1a; &#xff08;1&#xff09;下载Android studio下载地址&#xff1a;www.developer.android.com./sdk/installing/studio.html &#xff08;2&#xff09;双击安装&#xff0c;建议在studio安…

Unity 3D学习视觉脚本无需编码即可创建高级游戏

在本课程中&#xff0c;您将学习如何在Unity中使用可视化脚本(以前称为Bolt)以及如何在不编写一行代码的情况下创建自己的高级游戏所需的一切。本课程将教你如何掌握可视化脚本&#xff0c;即使你以前没有任何关于unity或编程的经验。 课程获取&#xff1a;Unity 3D学习视觉脚…

树莓派siri homekit_利用树莓派Zero自制一款Homekit摄像头,看上去挺酷,手痒了吗?...

虽然最近相继有多款兼容homekit商用摄像头上市&#xff0c;如果您也和小编一样&#xff0c;喜欢动手&#xff0c;那么DIY一款Homekit摄像头&#xff0c;然后自己3D打印个外壳支架&#xff0c;是不是很酷&#xff1f;HKCam项目Home 4开发者Matthias提供了一个开源项目&#xff0…

如何释放电脑被限制的20%网速?

很多朋友不管是看电影还是玩游戏&#xff0c;总觉得自己的网速慢&#xff0c;这跟自己所办网络的带宽有一定关系&#xff0c;但我们也要知道&#xff0c;我们的电脑在买来时&#xff0c;默认是限制了20%网速的&#xff0c;如何释放这20%的网速&#xff0c;提高用户体验呢&#…

【Docker】registry部署docker私有镜像仓库

Docker Hub作为Docker默认官方公共镜像仓库&#xff0c;但是如果我们不想使用怎么办&#xff0c;第一我们可以替换默认镜像仓库为我们国内的一些镜像仓库&#xff0c;第二就是如我们自己搭建一个自己的私有镜像仓库&#xff0c;官方也提供docker registry镜像&#xff0c;使得搭…

JAVA 面向对象

1&#xff1b;什么叫面向对象&#xff1a;1&#xff1b;面向对象和面向过程是一种思想2&#xff1b;面向过程&#xff1a;强调的是功能行为3&#xff1b;面向对象&#xff1a;将功能进行封装&#xff0c;强调具备了功能的对象2&#xff1b;面向对象的特征&#xff1a;1&#xf…

Android studio Day02-1

AndroidDay02-1&#xff08;project&#xff09; 新建一个project&#xff0c;并选择一个自己project的存贮的位置 Android studio 2.3.1默认的最小的API为15 第一次使用并建立自己的project&#xff0c;软件进行相应的sdk其他的组建的安装 点击next进入下一个界面&#xff0c…

Unity Pro builder创建模块化仓库建筑学习教程

Unity内部的专业3D编辑工作室 你会学到: 直接在Unity内部学习3D建模 使用专业构建器的专业方法 为您的游戏创建模块化资产 了解如何为您的三维模型设置纹理 三维资产的模块化布局 专业后期制作和轻烤 Unity Pro Builder Warehouse MP4 |视频:h264&#xff0c;1280720 |音频:AA…

C++拾遗(五)语句相关

前缀格式与后缀格式 对于表达式&#xff1a;后缀如 i 表达式的值仍是 i&#xff0c;在遇到下一个顺序点后再将 i 加1。前缀 i 表达式的值就是&#xff08;i1&#xff09;&#xff0c;先计算表达式的值&#xff0c;不需要等待      顺序点。 对于类&#xff1a;前缀函数效…

github里的默认域_GitMAD 一款扫描Github上的敏感信息和数据泄漏工具

GitMAD是一个用于发现Github上的敏感信息和数据泄漏的工具。通过给定关键字或域&#xff0c;GitMAD便会搜索Github上托管的代码&#xff0c;以查找是否存在匹配项。一旦找到了匹配项&#xff0c;GitMAD将克隆存储库并在文件中搜索一系列可配置的正则表达式。然后&#xff0c;Gi…

【Docker】Docker的三大核心组件

镜像&#xff08;Image&#xff09;、容器&#xff08;Container&#xff09;、仓库&#xff08;Repository&#xff09;是我们常说的Docker的三大组件&#xff0c;接下来就让我们一起详细地探索一番吧。 一、镜像&#xff08;Image&#xff09; 什么是Docker镜像&#xff1f;…

很高兴开始博客之旅

来到博客园&#xff01;&#xff0c;开启我的博客之旅&#xff0c;感觉棒棒哒转载于:https://www.cnblogs.com/pbnull/p/4562230.html

Android Studio Day02-2

AndroidDay02-2&#xff08;AVD&#xff09; Android APP编译的过程之中总是会需要进行相应的功能的调试&#xff0c;以及界面的布局设置在不同手机上的效果的展示。相应的Android平台都包含有Android虚拟设备管理器&#xff0c;Android虚拟设备管理器允许用户自己创建自己的虚…

网络增强现实开发简介 Introduction to Web AR development

搭配webXR、mindAR、three.js和tensorflow.js 你会学到: 获得构建不同类型的网络增强现实应用程序的实践经验&#xff0c;包括图像效果、人脸效果和世界效果 获得关于增强现实如何在网络浏览器中工作的基本理解 掌握使用WebXR、mind-ar-js和threejs构建web AR应用程序 学习使用…

umi脚手架搭建的项目_15天零成本搭建静态博客,托管于Github Page

博客地址技术栈概览前台&#xff1a;Umi(路由) Antd(视图) TypeScript(增加项目可维护性以及规范性)后台&#xff1a;Umi(路由) Antd(视图) TypeScript(增加项目可维护性以及规范性) Rematch(数据管理)服务&#xff1a;Egg.js(基于koa的下一代企业级应用框架) MongoDB搭建…

[maven] 使用问题及思考汇总

&#xff08;1&#xff09;Maven坐标 maven坐标可以唯一标识一个项目&#xff0c;包含四个元素 groupId , artifactId, packaging, version。 groupId&#xff1a;一般为团体,公司,项目。如 oceanic-web, oceanic-dal, oceanic-biz 同属一个 groupId。 artifactId&#xff1a;在…

VS调试时提示此项目已经过期

问题出因&#xff1a; 1.先前卸载VS重新安装时不全面 解决办法&#xff1a; 1.VS应安装两个C的组件 2.首先在“生成”-》 “生成解决方案”完成编译&#xff0c;问题解决。

(DBA之路【五】)关于锁的故事

首先很抱歉&#xff1a;这篇文章我其实整合了很多别人的文章&#xff0c;但是因为太多&#xff0c;一开始被没留意出处所以很难声明来源&#xff0c;很抱歉&#xff0c;但是这篇文章只用来作为学习笔记&#xff0c;作为新手&#xff0c;我以后会注意的。&#xff08;一&#xf…