wenmo8 发布的文章

一:背景

1. 讲故事

为什么说这东西比较坑人呢?是因为最近一个月接到了两个dump,都反应程序卡死无响应,最后分析下来是因为
线程饥饿
导致,那什么原因导致的线程饥饿呢?进一步分析发现罪魁祸首是
MySql.Data
,这就让人无语了,并且反馈都是升级了
MySql.Data
驱动引发,接下来我们简单聊一下。

二: MySql.Data 到底怎么了

1. 祸根溯源

早期版本的
MySql.Data
访问数据库都是以同步的方式进行,比如:
ExecuteReader
而不是
ExecuteReaderAsync
,随着项目的升级改造需要提升MySql.Data的版本, MySql为了向前兼容保留了同步方法,下面引用最新的 MySql.Data 9.1.0 截图和参考代码如下:


// MySql.Data, Version=9.1.0.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d
// MySql.Data.MySqlClient.MySqlConnection
using System.Threading;

public override void Open()
{
	OpenAsync(execAsync: false, CancellationToken.None).GetAwaiter().GetResult();
}


// MySql.Data, Version=9.1.0.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d
// MySql.Data.MySqlClient.MySqlCommand
using System.Data;
using System.Threading;

public new MySqlDataReader ExecuteReader()
{
	return ExecuteReaderAsync(CommandBehavior.Default, execAsync: false, CancellationToken.None).GetAwaiter().GetResult();
}

public override object ExecuteScalar()
{
	return ExecuteScalarAsync(execAsync: false, CancellationToken.None).GetAwaiter().GetResult();
}

仔细看上面这段代码,不觉让人吸了一口凉气,所谓的同步方式竟然是用
异步方法简单包装
而来的,这种异步混用同步的方式很容易导致线程饥饿,即线程池中已无可用线程来唤醒 GetResult() 下的 Event 事件,这个我准备后面用一篇文章详细来聊一下线程饥饿,这里用
C#内功修炼训练营
中的一张图来演示下.NET8 中异步在线程池中的走法。

2. 线程饥饿的现场

问题方法给大家列出来的,接下来用 windbg 看下dump中的故障现场吧。

  1. 某考试系统的故障

看故障现象比较简单,使用
!tp

!tpq
即可,输出如下:


0:000> !tp
Using the Portable thread pool.

CPU utilization:  1%
Workers Total:    268
Workers Running:  268
Workers Idle:     0
Worker Min Limit: 4
Worker Max Limit: 32767

0:000> !sos tpq
global work item queue________________________________
0x000002410E750218 Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT<Microsoft.AspNetCore.Hosting.HostingApplication+Context>
0x000002410E7505A0 Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT<Microsoft.AspNetCore.Hosting.HostingApplication+Context>
0x000002410E750928 Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT<Microsoft.AspNetCore.Hosting.HostingApplication+Context>
...
local per thread work items_____________________________________
0x0000024114903310 System.Runtime.CompilerServices.AsyncTaskMethodBuilder<MySql.Data.MySqlClient.MySqlPool>+AsyncStateMachineBox<MySql.Data.MySqlClient.MySqlPoolManager+<GetPoolAsync>d__23>

从卦中可以看到线程池中目前有268个线程,此时都处于运行状态,并且线程池的全局队列积压了
1000+
的任务没有处理,接下来使用
~*e !clrstack
观察每个线程都在做什么。


0:287> !clrstack
OS Thread Id: 0x39ec (287)
        Child SP               IP Call Site
000000858C5FD1B8 00007ffc95ca04e4 [HelperMethodFrame_1OBJ: 000000858c5fd1b8] System.Threading.Monitor.ObjWait(Int32, System.Object)
000000858C5FD2E0 00007ffc087cccc9 System.Threading.Monitor.Wait(System.Object, Int32) [/_/src/coreclr/System.Private.CoreLib/src/System/Threading/Monitor.CoreCLR.cs @ 156]
000000858C5FD310 00007ffc087cd027 System.Threading.ManualResetEventSlim.Wait(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs @ 561]
000000858C5FD3D0 00007ffc087cc4f2 System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3072]
000000858C5FD440 00007ffc087cc099 System.Threading.Tasks.Task.InternalWaitCore(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3007]
000000858C5FD4C0 00007ffc08796cc6 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task, System.Threading.Tasks.ConfigureAwaitOptions) [/_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs @ 111]
000000858C5FD500 00007ffc086ffbc4 xxxx.UpdateAnswerUrl(System.String, Int32, System.Collections.Generic.Dictionary`2<System.String,System.String>)

发现这些线程都卡在
xxxx.UpdateAnswerUrl
方法上,那到底卡在方法的何处呢?可以用
!U /d 00007ffc086ffbc4
观察方法的反汇编代码,看看这个00007ffc086ffbc4停留在何处?输出如下:

0:000> !U /d 00007ffc086ffbc4
Normal JIT generated code
xxx.UpdateAnswerUrl(System.String, Int32, System.Collections.Generic.Dictionary`2<System.String,System.String>)
...
00007ffc`086ffb79 ff15114bb9fe    call    qword ptr [00007ffc`07294690] (System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[[MySql.Data.MySqlClient.MySqlCommand+<ExecuteScalarAsync>d__117, MySql.Data]](<ExecuteScalarAsync>d__117 ByRef), mdToken: 000000000600646B)
00007ffc`086ffb7f 488b8c2468010000 mov     rcx,qword ptr [rsp+168h]
00007ffc`086ffb87 4885c9          test    rcx,rcx
00007ffc`086ffb8a 0f84890c0000    je      00007ffc`08700819
00007ffc`086ffb90 3809            cmp     byte ptr [rcx],cl
00007ffc`086ffb92 48898c2498010000 mov     qword ptr [rsp+198h],rcx
00007ffc`086ffb9a 488d8c2498010000 lea     rcx,[rsp+198h]
00007ffc`086ffba2 48baf02b5006fc7f0000 mov rdx,7FFC06502BF0h (MT: System.Runtime.CompilerServices.TaskAwaiter`1[[System.Object, System.Private.CoreLib]])
00007ffc`086ffbac ff158e7cdefd    call    qword ptr [00007ffc`064e7840] (System.Runtime.CompilerServices.TaskAwaiter`1[[System.__Canon, System.Private.CoreLib]].GetResult(), mdToken: 00000000060065F0)
00007ffc`086ffbb2 48898424e8000000 mov     qword ptr [rsp+0E8h],rax
00007ffc`086ffbba eb0d            jmp     00007ffc`086ffbc9
00007ffc`086ffbbc 33d2            xor     edx,edx
00007ffc`086ffbbe ff1544d4bffd    call    qword ptr [00007ffc`062fd008] (System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task, System.Threading.Tasks.ConfigureAwaitOptions), mdToken: 00000000060065E4)
>>> 00007ffc`086ffbc4 e960ffffff      jmp     00007ffc`086ffb29

从汇编代码中可以观测它是在获取
ExecuteScalarAsync
方法的 Result 结果,有了这个信息就可以翻源代码了,截图如下:

最终就发现了ExecuteScalar下面的荒唐一幕。。。

  1. 某跟踪埋点系统的故障

埋点系统也是一样的问题,使用
!tp
观察到线程池有 602 个线程都处于运行状态,输出如下:


0:000> !tp
Using the Portable thread pool.

CPU utilization:  11%
Workers Total:    602
Workers Running:  602
Workers Idle:     0
Worker Min Limit: 32
Worker Max Limit: 32767

然后通过
~*e !clrstack
观察发现线程都处于
Open()
方法中,输出如下:


OS Thread Id: 0x1a9d4 (23)
        Child SP               IP Call Site
0000007AD4DBE228 00007ff9feb70b24 [HelperMethodFrame_1OBJ: 0000007ad4dbe228] System.Threading.Monitor.ObjWait(Int32, System.Object)
0000007AD4DBE350 00007ff9b655d55e System.Threading.Monitor.Wait(System.Object, Int32) [/_/src/coreclr/System.Private.CoreLib/src/System/Threading/Monitor.CoreCLR.cs @ 156]
0000007AD4DBE380 00007ff9b656860e System.Threading.ManualResetEventSlim.Wait(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs @ 561]
0000007AD4DBE420 00007ff9b6581729 System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3072]
0000007AD4DBE4A0 00007ff9b6581516 System.Threading.Tasks.Task.InternalWaitCore(Int32, System.Threading.CancellationToken) [/_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @ 3007]
0000007AD4DBE520 00007ff959e9e9f4 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task, System.Threading.Tasks.ConfigureAwaitOptions) [/_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs @ 111]
0000007AD4DBE560 00007ff95752e95b MySql.Data.MySqlClient.MySqlConnection.Open()
...

可恶的是 Open() 方法内部也是用
异步转同步
实现的,真的无语了。

3. 解决方法

要想解决这个问题,大概两种方法吧。

  1. 使用纯异步写法,这也是高版本 MySql.Data 极力推荐的,不然就给你埋坑。。。
  2. 退回到低版本的 MySql.Data,继续使用真正的同步版写法。

三:总结

挺意外的是 MySql.Data 项目在 github:
https://github.com/mysql/mysql-connector-net
上没开 issue 栏。

这就无法让社区开发者介入,真的很奇葩,只能在这里给大家做个预警吧。
图片名称

前言

今天大姚给大家分享一个好消息,GitHub Copilot 可以免费使用了!在此之前若开发者要使用 GitHub Copilot 需要付费订阅,每月订阅费用起步价为 10 美元,而经过验证的学生、教师和开源项目维护者则可以申请免费使用。今天咱们一起来看看在 Visual Studio 使用 GitHub Copilot 有多么的智能,是否真的能够提高.NET开发工作效率。

GitHub Copilot介绍

GitHub Copilot 是一款 AI 编码助手,可帮助你更快、更省力地编写代码,从而将更多精力集中在问题解决和协作上。

GitHub Copilot Free包含哪些功能?

  • 每月 2000 代码补全,帮助开发者快速完成代码编写。
  • 每月 50 个聊天消息请求,支持询问编程问题、解释现有代码和查找错误。
  • 支持在 Visual Studio Code 和 Visual Studio 中进行跨多个文件的编辑。
  • 支持在 Visual Studio Code、Visual Studio、JetBrains IDEs 和 GitHub.com 上的 Copilot 聊天功能。
  • 支持在 Visual Studio Code、Visual Studio、JetBrains IDEs、Vim/Neovim、Xcode 和 Azure Data Studio 中的代码补全。

GitHub Copilot支持语言

支持Python、JavaScript、TypeScript、Ruby、Go、C# 和 C++等多种语言。

安装最新版本的 Visual Studio 2022

GitHub Copilot 的最新体验现已集成到 Visual Studio 中,并将 GitHub Copilot 的代码补全和 GitHub Copilot 聊天功能合并到一个方便的包中。下载最新的 Visual Studio 2022,以发现新增功能和改进项。

Visual Studio 2022 版本 17.10 或更高版本。

激活GitHub Copilot Free

注册或者登录GitHub账号 => Settings => Copilot => 点击 Start using Copilot Free。

Visual Studio中开始免费使用Copilot

添加GitHub账号:

Visual Studio中Copilot的相关设置

Visual Studio中使用Copilot代码补全

Visual Studio中使用Copilot代码生成

下面演示的是 Visual Studio 中 GitHub Copilot 的代码生成功能。如果选择使用代码,则可以选择 Tab 以将其插入代码文件中:

Visual Studio中使用Copilot内联聊天

使用快捷键
Alt+\
,进行内联聊天:

Visual Studio中使用Copilot汇总我的代码

Visual Studio中使用Copilot编写单元测试

Visual Studio中使用Copilot修改我的代码

Visual Studio中使用Copilot聊天窗口提问

更多Visual Studio编程效率提升技巧

参考文章

在当今全球化的软件开发浪潮中,应用的国际化(i18n)与本地化(L10n)显得尤为重要。Avalonia UI 作为一款强大的跨平台 UI 框架,为开发者提供了多种实现国际化的途径。其中,使用传统的 Resx 资源文件进行国际化处理,不仅兼容了原 Winform、WPF、ASP.NET Core 等开发场景下的使用习惯,还借助一些实用工具和特定的开发流程,让国际化的实现变得高效且有条理。

1. 引言:Resx 资源文件与 Avalonia UI 国际化的邂逅

在软件开发领域,国际化是确保应用能够跨越语言和文化边界,触达全球用户的关键。Avalonia UI 以其灵活的架构和丰富的功能,在跨平台应用开发中崭露头角。而 Resx 资源文件,作为一种久经考验的本地化资源管理方式,在 Avalonia UI 中也找到了新的用武之地。通过将两者结合,开发者能够在熟悉的开发模式下,为应用赋予多语言支持的能力,轻松应对不同地区用户的需求。

下图是使用VS扩展
ResXManager
对Resx资源文件进行管理的截图:

2. 详细使用步骤:构建多语言应用的基石

2.1. Resx 资源文件的精心布局

2.1.1. 项目目录规划与基础资源文件创建

开启您的 Avalonia UI 项目之旅,无论是已有的成熟项目还是全新创建的项目,首先在项目中添加一个用于存放国际化资源的目录,这里我们命名为
I18n
(您可根据项目实际情况自定义目录名)。在这个目录下,创建默认的英文语言资源文件
Resource.resx
。这个文件将作为整个国际化资源体系的基础,承载着应用在英文环境下的所有文本资源。

2.1.2. 多语言资源文件的拓展

  • 当英文资源文件就绪后,我们可以进一步拓展其他语言的资源文件。以中文简体、中文繁体和日语为例,它们的文件名需要遵循特定的命名规则:文件名前缀与默认语言资源文件名保持一致,即
    Resource
    ,并添加对应的
    CultureName
    后缀。例如,中文简体对应的资源文件名为
    Resource.zh-CN.resx
    ,中文繁体为
    Resource.zh-Hant.resx
    ,日语则是
    Resource.ja-JP.resx
    。这样的命名方式有助于 Avalonia UI 在运行时准确识别并加载不同语言的资源。
  • 借助强大的
    ResXManager
    工具,我们可以方便地打开这些资源文件进行多语言文本的编辑。在编辑过程中,需要特别注意语言 Key 的命名,它必须满足 C# 变量语法,因为后续的开发流程会依据这些 Key 生成对应的语言 Key 类,确保在代码层面能够精准地引用和操作这些资源。

2.2. NuGet 包的引入:增强国际化功能的得力助手

Install-Package AvaloniaExtensions.Axaml

这个包为我们的项目带来了一系列实用的 API,包括多语言切换功能、便捷的获取 Key 对应翻译字符串的方法,以及在
axaml
前端界面中对语言标记的支持。这些功能将极大地简化我们在国际化开发过程中的代码编写和界面设计工作。

2.3. T4 文件:从资源文件到强类型资源类的桥梁

2.3.1. T4 文件的创建与配置

有了资源文件后,虽然已经能够在一定程度上实现国际化功能,但直接使用字符串 Key 在代码中进行资源引用既容易出错又不够直观。因此,我们引入 T4 文件来根据资源文件生成强类型的资源类。在之前创建的
I18n
目录下,添加一个 T4 文件,例如
Language.tt
(文件名可根据项目需求灵活调整)。

2.3.2. T4 文件的内容解析与生成逻辑

打开
Language.tt
文件,其内容包含了一系列的指令和代码片段。首先,通过
#import
指令引入了多个命名空间,这些命名空间为后续的代码操作提供了必要的功能支持,如处理 XML 数据、文件操作等。


<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="System.Xml.Linq" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Xml.Linq" #>
<#@ import namespace="System.IO" #>
<#@ output extension=".cs" #>
//------------------------------------------------------------------------------  
// <auto-generated>  
//     This code was generated by a tool.  
//     Changes to this file may cause incorrect behavior and will be lost if  
//     the code is regenerated.  
// </auto-generated>  
//------------------------------------------------------------------------------
<#
    const string ResourceFileName = "Resources.resx";
#>

namespace <#=System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint").ToString()#>;

public static class Language
{
<#
    var resourceKeys = XElement.Load(this.Host.ResolvePath(ResourceFileName))
        .Elements("data")
        .Select(item => item.Attribute("name")?.Value)
        .Where(item => item != null);

	var resourceDesignerName = Path.GetFileNameWithoutExtension(ResourceFileName);

    foreach (string resourceKey in resourceKeys)
    {
#>
	public static readonly string <#= resourceKey #> = "<#= resourceKey #>";
<#
    }
#>
}

其中,
ResourceFileName
变量指定了前面创建的默认 Resx 资源文件名,这是 T4 文件生成强类型资源类的依据。在 T4 文件的主体部分,通过
XElement.Load
方法加载指定的资源文件,并使用 LINQ 查询表达式从资源文件的 XML 结构中提取出所有的语言 Key。然后,针对每个提取到的 Key,生成一个对应的公共静态只读字符串字段,字段名与 Key 相同,初始值也为 Key。这样,当 T4 文件执行保存操作(通常通过
Ctrl + S
)时,就会在相同目录下生成一个名为
Language.cs
的 C# 文件,其中包含了强类型的资源类。例如:


//------------------------------------------------------------------------------  
// <auto-generated>  
//     This code was generated by a tool.  
//     Changes to this file may cause incorrect behavior and will be lost if  
//     the code is regenerated.  
// </auto-generated>  
//------------------------------------------------------------------------------

namespace CodeWF.Toolbox.I18n;

public static class Language
{
	public static readonly string AppName = "AppName";
	public static readonly string Home = "Home";
	public static readonly string SearchToolTip = "SearchToolTip";
	public static readonly string Setting = "Setting";
	public static readonly string DesiredAvailabilityNotification = "DesiredAvailabilityNotification";
	public static readonly string AccessToolbox = "AccessToolbox";
	public static readonly string MissingTool = "MissingTool";
	public static readonly string InterfaceStyleSettings = "InterfaceStyleSettings";
	public static readonly string GeneralSettings = "GeneralSettings";
	public static readonly string Theme = "Theme";
	public static readonly string FollowingSystem = "FollowingSystem";
	public static readonly string LightMode = "LightMode";
	public static readonly string DarkMode = "DarkMode";
	public static readonly string LanguageKey = "LanguageKey";
	public static readonly string AutoOpenToolboxAtStartup = "AutoOpenToolboxAtStartup";
	public static readonly string HideTrayIconOnClose = "HideTrayIconOnClose";
	public static readonly string TurnOn = "TurnOn";
	public static readonly string TurnOff = "TurnOff";
	public static readonly string Exit = "Exit";
	public static readonly string SureExit = "SureExit";
	public static readonly string FindInTrayIcon = "FindInTrayIcon";
	public static readonly string ShowMainWindow = "ShowMainWindow";
	public static readonly string DisplayPromptWhenClosing = "DisplayPromptWhenClosing";
	public static readonly string NoMorePrompts = "NoMorePrompts";
	public static readonly string About = "About";
	public static readonly string AboutMessage = "AboutMessage";
}

这个生成的资源类使得我们在代码中能够以强类型的方式引用资源 Key,大大提高了代码的可读性和可维护性。

2.4. 在项目中的具体应用:让多语言功能鲜活起来

2.4.1. 代码中的资源引用

在 C# 代码中,我们可以借助
I18nManager
类来获取指定语言 Key 对应的翻译字符串。例如:

I18nManager.GetString(Language.Setting)

这里的
Language.Setting
就是通过 T4 文件生成的强类型资源类中的字段,通过这种方式,我们能够在代码的任何地方方便地获取并使用多语言资源,确保应用在不同语言环境下的正确显示。

2.4.2. Axaml 界面中的语言绑定


axaml
前端界面中,我们首先需要引入相应的命名空间:

xmlns:i18n="https://codewf.com"
xmlns:language="clr-namespace:CodeWF.Toolbox.I18n"

然后,通过数据绑定的方式将界面元素的文本属性与语言资源关联起来。例如:

<TextBlock Text="{i18n:I18n {x:Static language:Language.AppName}}" />

这样,当应用的语言环境发生变化时,界面元素的文本会自动更新为对应的翻译文本,实现了界面的动态国际化。

  1. 语言切换的实现

实现语言切换功能也非常简单,只需调用
I18nManager.Instance.Culture
属性,并传入目标语言的
CultureInfo
对象即可。例如:

I18nManager.Instance.Culture = new CultureInfo(language);

这里的
language
变量可以是任何有效的语言代码,如
zh-CN

ja-JP
等。当设置了新的语言文化后,整个应用的语言显示会立即更新,为用户提供无缝的多语言切换体验。

3. 总结:Resx 资源文件国际化方案的优劣剖析

通过 Resx 资源文件实现 Avalonia UI 应用的国际化,无疑为传统开发者提供了一条熟悉且便捷的道路。它充分利用了已有的开发经验和工具生态,如 ResXManager 和 T4 文件技术,使得国际化的开发过程能够高效地融入到现有的项目流程中。然而,这种方式也并非完美无缺。其对于普通用户侧的维护来说,可能存在一定的难度。普通用户可能不熟悉 Resx 文件的结构和编辑方式,也难以理解 T4 文件生成代码的逻辑。这就要求在项目的设计和实施过程中,开发者需要充分考虑到后续的维护成本,可能需要为普通用户提供一些简单易用的界面或工具来辅助他们进行国际化资源的更新和管理。但总体而言,对于具有一定技术基础的开发团队和项目来说,Resx 资源文件的国际化方案仍然是一种值得推荐的选择,它在功能、效率和兼容性方面都有着出色的表现。

希望本文能够为广大 Avalonia UI 开发者在国际化实践中提供有益的参考和指导,让您的应用能够在全球舞台上绽放光彩。

彩蛋:下篇文章介绍XML文件实现国际化

.NET 单文件执行程序拆解器 SingleFileExtractor

.NET 现在支持将程序打包为单文件格式,这方便了部署,问题是,我们不能直接看到程序中使用了哪些 DLL,更不能简单地通过查看文件属性的方式,看到这些 DLL 的版本。要是可以像使用 Zip 一样,可以打开这个合成的文件,直接查看内容就好了。

SingleFileExtractor 就是这样的工具。

它可以从单文件的应用程序中将其中包含的内容,包括程序集、配置文件等等,拆解到文件夹中,方便开发人员进行分析。

1. 安装

它的安装也很方便,通过 dotnet 工具的方式进行安装和使用。

dotnet tool install -g sfextract

注意,当前 2024/12/20 使用
dotnet tool install -g sfextract
安装此工具,需要 .NET 6.0

2. 使用

2.1 命令行

sfextract [file] -o|--output [directory]

例如

sfextract Application.exe -o path/to/output/

2.2 列出其中的文件

sfextract Application.exe

3. 在程序中使用

在项目中安装
SingleFileExtractor.Core
NuGet 包,

然后,使用 SingleFileExtractor 提供的 ExecutableReader 类来打开文件。

var reader = new ExecutableReader("application.exe");

3.1 读取启动信息

当你希望知道程序的入口点的时候,可以这样读取启动信息

var reader = new ExecutableReader("application.exe");
var startupInfo = reader.StartupInfo;

3.2 提取全部文件。

var reader = new ExecutableReader("application.exe");

// Validate if executable is a single file executable, and can be extracted
var isSingleFile = reader.IsSingleFile;

if (isSingleFile)
{
    // Extract specific file entry
    await reader.Manifest.Entries[0].ExtractToFileAsync("example.dll");
    // , or create an in-memory stream of a specifc file entry
    var stream = await reader.Manifest.Entries[0].AsStreamAsync()
    
    // Extract all files to a directory
    await reader.ExtractToDirectoryAsync("path/to/output");
}

3.3 提取指定内容到文件

var reader = new ExecutableReader("application.exe");
await reader.Manifest.Entries[0].ExtractToFileAsync("example.dll");

3.4 提取指定内容到流

var reader = new ExecutableReader("application.exe");
var stream = await reader.Manifest.Entries[0].AsStreamAsync()

4. 为什么创造它?

我正在处理的另一个程序需要我从单文件的执行程序中提取内容,另外我还看到有人在询问这个问题,所以,我决定将它开发为一个 dotnet 工具的形式,并使用 NuGet 包的形式发布。

项目地址:
https://github.com/Droppers/SingleFileExtractor

题目:给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。

回文数

是指正序(从左向右)和倒序(从右向左)读都是一样的整数。

例如,121 是回文,而 123 不是。

01
、反转字符串法

此题我第一反应就是直接把整数转为字符串,然后通过字符串Reverse方法,反转字符串,最后再比较整数字符串和反转后字符串是否相等即可得出结果。代码实现如下:

//反转字符串法
public static bool IsPalindrome1(int x)
{
    var strX = x.ToString();
    //反转字符串
    var reversedStr = new string(strX.Reverse().ToArray());
    //比较入参和反转后字符串是否相等
    return strX == reversedStr;
}

02
、反转字符数组法

此方法和反转字符串一脉相通,只是实现选用了不同的方法,此法是把整数字符串转为字符数组,然后再深拷贝一份用于反转的字符数组,通过Array.Reverse方法进行反转,最后通过SequenceEqual比较两个字符数组值是否相等。

其中有以下两个点容易出错:

其一因为字符数组是引用类型必须使用深拷贝,而不能像字符串那样直接赋值,深拷贝有很多种办法ToArray是比较简洁的方式;

其二因为本题要求是比较两个字符数组的值是否相等,因此不能直接使用==比较;

具体实现代码如下:

//反转字符数组法
public static bool IsPalindrome2(int x)
{
    //x转为字符数组
    var strXChars = x.ToString().ToCharArray();
    //深拷贝一份字符数组用于反转
    var reversedChars = strXChars.ToArray();
    //反转字符数组
    Array.Reverse(reversedChars);
    //比较入参和反转后字符串是否相等
    return strXChars.SequenceEqual(reversedChars);
}

03
、双指针法

前面两个方法本质上都是在使用转为字符串的方法处理,我们进阶一下尝试不用转字符串的方式来处理。

虽然不用转字符串的方式,但是我们可以借鉴其思想,比如前面题目处理回文子串思想。我们是否可以像字符串一样,从整数的两端由外向内逐一比较两端的数字,如果都相等则结果为回文数。

在不转成字符串的情况下,怎么拿到整数的首尾数字呢?尾数字可以通过取余的方式获取,那么首数字要怎么获取呢?

比如12345,如果我们想要取到首数字1,那么就可以通过12345/10000=1获取,那么10000要怎么获取呢?我们可以对整数进行循环取整,假设除数div=1,取1次整则div乘10,最终得到10000。

然后我们就可以通过x/div取整获取左边数字,通过x%10取余获取右边数字,然后比较左右是否相等,如果相等则把左右两边已比较过的数字去掉,对剩下的整数继续比较,直至所有数字完成比较。

具体实现代码如下:

//双指针
public static bool IsPalindrome3(int x)
{
    //负数肯定不是回文数
    if (x < 0)
    {
        return false;
    }
    //定义除数变量,用于从前截取数字
    var div = 1;
    //通过循环对x取整,然后在乘10
    //求得可以获取x第1位数字对应的除数
    //比如12345,则除数为10000
    while (x / div >= 10)
    {
        div *= 10;
    }
    while (x > 0)
    {
        //获取左边数字
        var left = x / div;
        //获取右边数字
        var right = x % 10;
        //比较左右数字是否相等
        if (left != right)
        {
            //不相等则直接返回
            return false;
        }
        //去掉左边数字
        x = x % div;
        //去掉右边数字
        x = x / 10;
        //因为去掉两个数字
        //所以除数需除100
        div /= 100;
    }
    //为回文数字
    return true;
}

04
、反转全部数字法

双指针法可能逻辑上比较复杂些,那么我们是否可以像字符串一样把整个整数反转过来呢?

其实反转整个数字其实也很简单,可以借鉴上一个方法中,取整取余的方式,把整个整数反转过来,然后直接比较反转后的整数和原整数是否相等即可。

其中需要注意的是一个32位整数反转后可能会导致溢出,因此反转结果我们需要使用64位整数存储。具体实现代码如下:

//反转全部数字
public static bool IsPalindrome4(int x)
{
    //负数肯定不是回文数
    if (x < 0)
    {
        return false;
    }
    //把入参赋值给临时变量
    int temp = x;
    //反转结果
    long reversed = 0;
    //从后往前循环处理整数中的每一个数字
    while (x != 0)
    {
        //获取x的个位数字
        int digit = temp % 10;
        //移除x的个位数字
        temp = temp / 10;
        //把x的个位数字拼接到反转结果的个位上
        reversed = reversed * 10 + digit;
    }
    //直接比较入参和反转后的整数是否相等
    return x == reversed;
}

05
、反转一半数字

其实并不需要反转完所有数字我们才能知道是否是回文数,只需要反转完一半我们即可以知道是否为回文数。

比如输入 1221,我们可以将数字 “1221” 的后半部分从 “21” 反转为 “12”,并将其与前半部分 “12” 进行比较,因为二者相同,我们得知数字 1221 是回文。

此方法关键点是判断何时结束,以及是回文数的条件是啥。

对于反转一半数字,我们必须考虑到整数的长度是奇数还是偶数,如下图:

由此可以得出当x > revertedNumber时可以结束比较,而满足回文数的条件分奇偶两种情况x == revertedNumber 或 x == revertedNumber / 10。

具体代码实现如下:

//反转一半数字
public static bool IsPalindrome5(int x)
{
    //特殊情况:
    //当 x < 0 时,x 不是回文数。
    //同样地,如果数字的最后一位是 0,为了使该数字为回文,
    //则其第一位数字也应该是 0,只有 0 满足这一属性
    if (x < 0 || (x % 10 == 0 && x != 0))
    {
        return false;
    }
    var revertedNumber = 0;
    while (x > revertedNumber)
    {
        //获取x的个位数字
        var digit = x % 10;
        //把x的个位数字拼接到反转结果的个位上
        revertedNumber = revertedNumber * 10 + digit;
        //移除x的个位数字
        x /= 10;
    }
    //当数字长度为奇数时,例如,当输入为 12321 时,
    //在 while 循环的末尾我们可以得到 x = 12,revertedNumber = 123,
    //因此可得 x == revertedNumber/10
    //而当数字长度为偶数时,则 x == revertedNumber。
    return x == revertedNumber || x == revertedNumber / 10;
}


:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。
https://gitee.com/hugogoos/Planner