零基础写框架:从零设计一个模块化和自动服务注册框架

模块化和自动服务注册

基于 ASP.NET Core 开发的 Web 框架中,最著名的是 ABP,ABP 主要特点之一开发不同项目(程序集)时,在每个项目中创建一个模块类,程序加载每个程序集中,扫描出所有的模块类,然后通过模块类作为入口,初始化程序集。

使用模块化开发程序,好处是不需要关注程序集如何加载配置。开发人员开发程序集时,在模块类中配置如何初始化、如何读取配置,使用者只需要将模块类引入进来即可,由框架自动启动模块类。

Maomi.Core 也提供了模块化开发的能力,同时还包括简单易用的自动服务注册。Maomi.Core 是一个很简洁的包,可以在控制台、Web 项目、WPF 项目中使用,在 WPF 项目中结合 MVVM 可以大量减少代码复杂度,让代码更加清晰明朗。

快速入手

有 Demo1.Api、Demo1.Application 两个项目,每个项目都有一个模块类,模块类需要实现 IModule 接口。

image-20240218083153329

Demo1.Application 项目的 ApplicationModule.cs 文件内容如下:

    public class ApplicationModule : IModule
    {
        // 模块类中可以使用依赖注入
        private readonly IConfiguration _configuration;
        public ApplicationModule(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        public void ConfigureServices(ServiceContext services)
        {
            // 这里可以编写模块初始化代码
        }
    }

如果要将服务注册到容器中,在 class 上加上 [InjectOn] 特性即可。

    public interface IMyService
    {
        int Sum(int a, int b);
    }

    [InjectOn] // 自动注册的标记
    public class MyService : IMyService
    {
        public int Sum(int a, int b)
        {
            return a + b;
        }
    }

上层模块 Demo1.Api 中的 ApiModule.cs 可以通过特性注解引用底层模块。

    [InjectModule<ApplicationModule>]
    public class ApiModule : IModule
    {
        public void ConfigureServices(ServiceContext services)
        {
            // 这里可以编写模块初始化代码
        }
    }

最后,在程序启动时配置模块入口,并进行初始化。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// 注册模块化服务,并设置 ApiModule 为入口
builder.Services.AddModule<ApiModule>();

var app = builder.Build();

模块可以依赖注入

在 ASP.NET Core 配置 Host 时,会自动注入一些框架依赖的服务,如 IConfiguration 等,因此在 .AddModule<ApiModule>() 开始初始化模块服务时,模块获取已经注入的服务。

image-20240218164324287

每个模块都需要实现 IModule 接口,其定义如下:

    /// <summary>
    /// 模块接口
    /// </summary>
    public interface IModule
    {
        /// <summary>
        /// 模块中的依赖注入
        /// </summary>
        /// <param name="context">模块服务上下文</param>
        void ConfigureServices(ServiceContext context);
    }

除了可以直接在模块构造函数注入服务之外,还可以通过 ServiceContext context 获取服务和配置。

    /// <summary>
    /// 模块上下文
    /// </summary>
    public class ServiceContext
    {
        private readonly IServiceCollection _serviceCollection;
        private readonly IConfiguration _configuration;


        internal ServiceContext(IServiceCollection serviceCollection, IConfiguration configuration)
        {
            _serviceCollection = serviceCollection;
            _configuration = configuration;
        }

        /// <summary>
        /// 依赖注入服务
        /// </summary>
        public IServiceCollection Services => _serviceCollection;

        /// <summary>
        /// 配置
        /// </summary>
        public IConfiguration Configuration => _configuration;
    }

模块化

因为模块之间会有依赖关系,为了识别这些依赖关系,Maomi.Core 使用树来表达依赖关系。

Maomi.Core 在启动模块服务时,扫描所有模块类,然后将模块依赖关系存放到模块树中,然后按照左序遍历的算法对模块逐个初始化,也就是先从底层模块开始进行初始化。

循环依赖检测

Maomi.Core 可以识别模块循环依赖

比如,有以下模块和依赖:

[InjectModule<A>()]
[InjectModule<B>()]
class C:IModule

[InjectModule<A>()]
class B:IModule

// 这里出现了循环依赖
[InjectModule<C>()]
class A:IModule

// C 是入口模块
services.AddModule<C>();

因为 C 模块依赖 A、B 模块,所以 A、B 是节点 C 的子节点,而 A、B 的父节点则是 C。当把 A、B、C 三个模块以及依赖关系扫描完毕之后,会得到以下的模块依赖树。

如下图所示,每个模块都做了下标,表示不同的依赖关系,一个模块可以出现多次,C1 -> A0 表示 C 依赖 A。

image-20240218165015839

C0 开始,没有父节点,则不存在循环依赖。

从 A0 开始,A0 -> C0 ,该链路中也没有出现重复的 A 模块。

从 C1 开始,C1 -> A0 -> C0 ,该链路中 C 模块重复出现,则说明出现了循环依赖。

从 C2 开始,C2 -> A1 -> B0 -> C0 ,该链路中 C 模块重复出现,则说明出现了循环依赖。

模块初始化顺序

在生成模块树之后,通过对模块树进行后序遍历即可。

比如,有以下模块以及依赖。

[InjectModule<C>()]
[InjectModule<D>()]
class E:IModule

[InjectModule<A>()]
[InjectModule<B>()]
class C:IModule

[InjectModule<B>()]
class D:IModule
    
[InjectModule<A>()]
class B:IModule
    
class A:IModule

// E 是入口模块
services.AddModule<E>();

生成模块依赖树如图所示:

首先从 E0 开始扫描,因为 E0 下存在子节点 C0、 D0,那么就会先顺着 C0 再次扫描,扫描到 A0 时,因为 A0 下已经没有子节点了,所以会对 A0 对应的模块 A 进行初始化。根据上图模块依赖树进行后序遍历,初始化模块的顺序是(已经被初始化的模块会跳过):

服务自动注册

Maomi.Core 是通过 [InjectOn] 识别要注册该服务到容器中,其定义如下:

    /// <summary>
    /// 依赖注入标记
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
    public class InjectOnAttribute : Attribute
    {
        /// <summary>
        /// 要注入的服务
        /// </summary>
        public Type[]? ServicesType { get; set; }

        /// <summary>
        /// 生命周期
        /// </summary>
        public ServiceLifetime Lifetime { get; set; }

        /// <summary>
        /// 注入模式
        /// </summary>
        public InjectScheme Scheme { get; set; }

        /// <summary>
        /// 是否注入自己
        /// </summary>
        public bool Own { get; set; } = false;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="lifetime"></param>
        /// <param name="scheme"></param>
        public InjectOnAttribute(ServiceLifetime lifetime = ServiceLifetime.Transient, InjectScheme scheme = InjectScheme.OnlyInterfaces)
        {
            Lifetime = lifetime;
            Scheme = scheme;
        }
    }

使用 [InjectOn] 时,默认是注册服务为 Transient 生命周期,且注册所有接口。

    [InjectOn]
    public class MyService : IAService, IBService

等同于:

services.AddTransient<IAService, MyService>();
services.AddTransient<IBService, MyService>();

如果只想注册 IAService,可以将注册模式设置为InjectScheme.Some ,然后自定义注册的类型:

    [InjectOn(
        lifetime: ServiceLifetime.Transient,
        Scheme = InjectScheme.Some,
        ServicesType = new Type[] { typeof(IAService) }
        )]
    public class MyService : IAService, IBService

也可以把自身注册到容器中:

[InjectOn(Own = true)]
public class MyService : IMyService

等同于:

services.AddTransient<IAService, MyService>();
services.AddTransient<MyService>();

如果服务继承了类、接口,只想注册父类,那么可以这样写:

    public class ParentService { }

    [InjectOn(
        Scheme = InjectScheme.OnlyBaseClass
        )]
    public class MyService : ParentService, IDisposable 

等同于:

services.AddTransient<ParentService, MyService>();
services.AddTransient<MyService>();

如果只注册自身,忽略接口等,可以使用:

[InjectOn(ServiceLifetime.Scoped, Scheme = InjectScheme.None, Own = true)]

模块化和自动服务注册的设计和实现

在本小节中,我们将会开始设计一个支持模块化和自动服务注册的小框架,从设计和实现 Maomi.Core 开始,我们在后面的章节中会掌握更多框架技术的设计思路和实现方法,从而掌握从零开始编写一个框架的能力。

项目说明

创建一个名为 Maomi.Core 的类库项目,这个类库中将会包含框架核心抽象和实现代码。

为了减少命名空间长度,便于开发的时候引入需要的命名空间,打开 Maomi.Core.csproj 文件,在 PropertyGroup 属性中,添加一行配置:

<RootNamespace>Maomi</RootNamespace>

配置 <RootNamespace> 属性之后,我们在 Maomi.Core 项目中创建的类型,其命名空间都会以 Maomi. 开头,而不是 Maomi.Core

接着为项目添加两个依赖包,以便实现自动依赖注入和初始化模块时提供配置。

Microsoft.Extensions.DependencyInjection
Microsoft.Extensions.Configuration.Abstractions

模块化设计

当本章的代码编写完毕之后,我们可以这样实现一个模块、初始化模块、引入依赖模块。代码示例如下:

    [InjectModule<ApplicationModule>]
    public class ApiModule : IModule
    {
        private readonly IConfiguration _configuration;
        public ApiModule(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        public void ConfigureServices(ServiceContext context)
        {
            var configuration = context.Configuration;
            context.Services.AddCors();
        }
    }

从这段代码,笔者以从上到下的顺序来解读我们需要实现哪些技术点。

1,模块依赖。

[InjectModule<ApplicationModule>] 表示当前模块需要依赖哪些模块。如果需要依赖多个模块,可以使用多个特性,示例如下:

[InjectModule<DomainModule>]
[InjectModule<ApplicationModule>]

2,模块接口和初始化。

每一个模块都需要实现 IModule 接口,框架识别到类型继承了这个接口后才会把类型当作一个模块类进行处理。IModule 接口很简单,只有 ConfigureServices(ServiceContext context) 一个方法,可以在这个方法中编写初始化模块的代码。ConfigureServices 方法中有一个 ServiceContext 类型的参数, ServiceContext 中包含了 IServiceCollection、IConfiguration ,模块可以从 ServiceContext 中获得当前容器的服务、启动时的配置等。

3,依赖注入

每个模块的构造函数都可以使用依赖注入,可以在模块类中注入需要的服务,开发者可以在模块初始化时,通过这些服务初始化模块。

基于以上三点,我们可以先抽象出特性类、接口等,由于这些类型不包含具体的逻辑,因此从这一部分先下手,实现起来会更简单,可以避免大脑混乱,编写框架时不知道要从哪里先下手。

创建一个 ServiceContext 类,用于在模块间传递服务上下文信息,其代码如下:

    public class ServiceContext
    {
        private readonly IServiceCollection _serviceCollection;
        private readonly IConfiguration _configuration;

        internal ServiceContext(IServiceCollection serviceCollection, IConfiguration configuration)
        {
            _serviceCollection = serviceCollection;
            _configuration = configuration;
        }

        public IServiceCollection Services => _serviceCollection;
        public IConfiguration Configuration => _configuration;
    }

根据实际需求,还可以在 ServiceContext 中添加日志等属性字段。

创建 IModule 接口。

    public interface IModule
    {
        void ConfigureServices(ServiceContext services);
    }

创建 InjectModuleAttribute 特性,用于引入依赖模块。

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
    public class InjectModuleAttribute : Attribute
    {
        // 依赖的模块
        public Type ModuleType { get; private init; }
        public InjectModuleAttribute(Type type)
        {
            ModuleType = type;
        }
    }

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
    public sealed class InjectModuleAttribute<TModule> : InjectModuleAttribute
        where TModule : IModule
    {
        public InjectModuleAttribute() : base(typeof(TModule)){}
    }

泛型特性属于 C# 11 的新语法。

定义两个特性类后,我们可以使用 [InjectModule(typeof(AppModule))] 或 InjectModule<AppModule> 的方式定义依赖模块。

自动服务注册的设计

当完成本章的代码编写后,如果需要注入服务,只需要标记 [InjectOn] 特性即可。

// 简单注册
[InjectOn]
public class MyService : IMyService
// 注注册并设置生命周期为 scope
[InjectOn(ServiceLifetime.Scoped)]
public class MyService : IMyService

// 只注册接口,不注册父类
[InjectOn(InjectScheme.OnlyInterfaces)]
public class MyService : ParentService, IMyService

有时我们会有各种各样的需求,例如 MyService 继承了父类 ParentService 和接口 IMyService,但是只需要注册 ParentService,而不需要注册接口;又或者只需要注册 MyService,而不需要注册 ParentService 、 IMyService

创建 InjectScheme 枚举,定义注册模式:

    public enum InjectScheme
    {
        // 注入父类、接口
        Any,
        
        // 手动选择要注入的服务
        Some,
        
        // 只注入父类
        OnlyBaseClass,
        
        // 只注入实现的接口
        OnlyInterfaces,
        
        // 此服务不会被注入到容器中
        None
    }

定义服务注册特性:

    // 依赖注入标记
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
    public class InjectOnAttribute : Attribute
    {
        // 要注入的服务
        public Type[]? ServicesType { get; set; }
        
        // 生命周期
        public ServiceLifetime Lifetime { get; set; }
        
        // 注入模式
        public InjectScheme Scheme { get; set; }

        // 是否注入自己
        public bool Own { get; set; } = false;
        
        public InjectOnAttribute(ServiceLifetime lifetime = ServiceLifetime.Transient, 
                                 InjectScheme scheme = InjectScheme.OnlyInterfaces)
        {
            Lifetime = lifetime;
            Scheme = scheme;
        }
    }

模块依赖

因为模块之间会有依赖关系,因此为了生成模块树,需要定义一个 ModuleNode 类表示模块节点,一个 ModuleNode 实例标识一个依赖关系

    /// <summary>
    /// 模块节点
    /// </summary>
    internal class ModuleNode
    {
        // 当前模块类型
        public Type ModuleType { get; set; } = null!;

        // 链表,指向父模块节点,用于循环引用检测
        public ModuleNode? ParentModule { get; set; }
        
        // 依赖的其它模块
        public HashSet<ModuleNode>? Childs { get; set; }

        // 通过链表检测是否出现了循环依赖
        public bool ContainsTree(ModuleNode childModule)
        {
            if (childModule.ModuleType == ModuleType) return true;
            if (this.ParentModule == null) return false;
            // 如果当前模块找不到记录,则向上查找
            return this.ParentModule.ContainsTree(childModule);
        }

        public override int GetHashCode()
        {
            return ModuleType.GetHashCode();
        }

        public override bool Equals(object? obj)
        {
            if (obj == null) return false;
            if(obj is ModuleNode module)
            {
                return GetHashCode() == module.GetHashCode();
            }
            return false;
        }
    }

框架在扫描所有程序集之后,通过 ModuleNode 实例将所有模块以及模块依赖组成一颗模块树,通过模块树来判断是否出现了循环依赖。

比如,有以下模块和依赖:

[InjectModule<A>()]
[InjectModule<B>()]
class C:IModule

[InjectModule<A>()]
class B:IModule

// 这里出现了循环依赖
[InjectModule<C>()]
class A:IModule

// C 是入口模块
services.AddModule<C>();

因为 C 模块依赖 A、B 模块,所以 A、B 是节点 C 的子节点,而 A、B 的父节点则是 C。

C.Childs = new (){ A , B}

A.ParentModule => C
B.ParentModule => C

当把 A、B、C 三个模块以及依赖关系扫描完毕之后,会得到以下的模块依赖树。一个节点即是一个 ModuleNode 实例,一个模块被多次引入,就会出现多次。

那么,如果识别到循环依赖呢?只需要调用 ModuleNode.ContainsTree()从一个 ModuleNode 实例中,不断往上查找 ModuleNode.ParentModule 即可,如果该链表中包含相同类型的模块,即为循环依赖,需要抛出异常。

比如从 C0 开始,没有父节点,则不存在循环依赖。

从 A0 开始,A0 -> C0 ,该链路中也没有出现重复的 A 模块。

从 C1 开始,C1 -> A0 -> C0 ,该链路中 C 模块重复出现,则说明出现了循环依赖。

所以,是否出现了循环依赖判断起来是很简单的,我们只需要从 ModuleNode.ContainsTree() 往上查找即可。

在生成模块树之后,通过对模块树进行后序遍历即可。

比如,有以下模块以及依赖。

[InjectModule<C>()]
[InjectModule<D>()]
class E:IModule

[InjectModule<A>()]
[InjectModule<B>()]
class C:IModule

[InjectModule<B>()]
class D:IModule
    
[InjectModule<A>()]
class B:IModule
    
class A:IModule

// E 是入口模块
services.AddModule<E>();

伪代码示例如下:

		private static void InitModuleTree(ModuleNode moduleNode)
		{
			if (moduleNode.Childs != null)
			{
				foreach (var item in moduleNode.Childs)
				{
					InitModuleTree(item);
				}
			}
            
            // 如果该节点已经没有子节点
			// 如果模块没有处理过
			if (!moduleTypes.Contains(moduleNode.ModuleType))
			{
				InitInjectService(moduleNode.ModuleType);
			}
		}

未完待续......

文章转载自:痴者工良

原文链接:https://www.cnblogs.com/whuanle/p/18227954

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/674948.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

“去员工化”这个潮流谁也挡不住,六大诱因分析。

去员工化→恐怕是未来工作的主流&#xff0c;一方面有成本的原因&#xff0c;另一方面也有技术进步、雇佣形式创新等原因&#xff0c;这个潮流有利也有弊&#xff0c;关键看我们是如何兴利除弊。 "去员工化"是指企业在运营中减少或取消传统雇佣制度&#xff0c;更多…

HOW - vscode 使用指南

目录 一、基本介绍1. 安装 VS Code2. 界面介绍3. 扩展和插件4. 设置和自定义 二、常用界面功能和快捷操作&#xff08;重点&#xff09;常用界面功能快捷操作 三、资源和支持 Visual Studio Code&#xff08;VS Code&#xff09;是一款由微软开发的免费、开源的代码编辑器&…

vue开发网站-使用插件element、vant 遇到的问题

1. js把两个字符串放进一个另字符串里&#xff0c;用逗号分隔 let string1 "Hello"; let string2 "World"; let result ${string1},${string2}; console.log(result); // 输出: Hello,World2.js将字符串转为数组 const str "Hello, world!"…

HBuilder中怎样修改每次输出的内容hello?每次修改之后再次“运行到”的时候,怎样保证每次都重新编译?

要修改每次输出的内容&#xff0c;可以在代码中找到输出的地方&#xff0c;并将内容进行修改。例如&#xff0c;在JavaScript中&#xff0c;可以使用console.log()函数输出内容。在修改内容后&#xff0c;保存代码。 为了保证每次运行都重新编译代码&#xff0c;可以按照以下步…

LeetCode 算法:接雨水c++

原题链接&#x1f517;&#xff1a;接雨水 难度&#xff1a;困难⭐️⭐️⭐️ 题目 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 示例 1&#xff1a; 输入&#xff1a;height [0,1,0,2,1,0,1,3,2,…

使用 WM_WINDOWPOSCHANGING 跟踪窗口状态变化

在窗口位置变化过程的早期&#xff0c;系统会发送 WM_WINDOWPOSCHANGING 消息。 这个和 WM_WINDOWPOSCHANGED 消息不同&#xff0c;WM_WINDOWPOSCHANGED 消息发生在窗口位置变化之后。 一个关键的区别&#xff08;除了时间之外&#xff09;是&#xff0c;您可以通过处理 WM_WI…

OpenStreetMap部署(OSM)

参考&#xff1a;https://github.com/openstreetmap/openstreetmap-website/blob/master/DOCKER.md OpenStreeMap 部署 操作系统建议使用 Ubuntu 22 版本 安装 Docker # 更新软件包索引&#xff1a; sudo apt-get update # 允许APT使用HTTPS&#xff1a; sudo apt-get inst…

艰难求生的转型之路

起因 我个人“工作水平低&#xff0c;专业能力差。”是最核心的困难。 在坚持了快十年之后&#xff0c;博客从2015-2024。 2015201620172018201920202021202220232024 从2020年之后就已经开始全面转型之路。 所有传统赛道&#xff0c;都挤满了人&#xff0c;各种限制各种约…

【图像处理与机器视觉】灰度变化与空间滤波

基础 空间域与变换域 空间域&#xff1a;认为是图像本身&#xff0c;对于空间域的操作就是对图像中的像素直接进行修改 变换域&#xff1a;变换系数处理&#xff0c;不直接对于图像的像素进行处理 邻域 图像中某点的邻域被认为是包含该点的小区域&#xff0c;也被称为窗口 …

【Linux基础】安装redis

【Linux基础】安装redis 文章目录 【Linux基础】安装redis1、安装redis步骤2、启动redis3、redis停止 1、安装redis步骤 创建文件夹存放软件目录 [rootlocalhost ~]# mkdir /sort将Redis安装包上传到Linux到soft目录 解压安装包 cd /soft tar -xvf redis-4.0.0.tar.gz -C /usr/…

高速开箱机如何更加高效、智能化

在科技飞速发展的背景下&#xff0c;物流自动化已成为行业发展的必然趋势。其中&#xff0c;高速开箱机以其高效、精准的特性&#xff0c;在物流自动化领域大放异彩&#xff0c;成为推动行业发展的强大动力。星派将深入探讨高速开箱机在物流自动化中的应用与前景&#xff0c;展…

变现 5w+,一个被严重低估的 AI 蓝海赛道,居然用这个免费的AI绘画工具就能做!

大家好&#xff0c;我是画画的小强&#xff0c;致力于分享各类的 AI 工具&#xff0c;包括 AI 绘画工具、AI 视频工具、AI 写作工具等等。 但单纯地为了学而学&#xff0c;是没有任何意义的。 这些 AI 工具&#xff0c;学会了&#xff0c;用起来&#xff0c;才能发挥出他们的…

就凭这张图,下订华为享界S9

文 | Auto芯球 作者 | 雷慢 冲啦&#xff01;就在刚刚&#xff0c; 我们团队下订了一辆享界S9&#xff0c; 还琢磨买奔驰S级&#xff0c;宝马7系和奥迪A8的老板们&#xff0c; 是应该试试享界S9了&#xff0c; 至少先占个坑&#xff0c;8月底S9上市当天&#xff0c; 可以…

月入5000+?Midjourney制作小红书壁纸实现副业变现

一、制作步骤 使用Midjourney制作小红书壁纸的步骤比较简单&#xff0c;分为5步&#xff0c;其中最关键的步骤就是画出用户喜欢的壁纸。 1、绘画壁纸 在开始绘画之前&#xff0c;我们首先要确定好壁纸类型&#xff0c;小红书的壁纸类型有多种&#xff0c;包括剪纸类型、花草…

手机如何找回删除的视频?轻松有效的3个技巧分享!

无论是因为疏忽大意还是意外情况&#xff0c;视频被删除都是一个常见的问题&#xff0c;而视频作为一种重要的媒体形式&#xff0c;承载着我们的回忆和重要的信息&#xff0c;因此找回删除的视频具有重要的意义。该如何找回删除的视频呢&#xff1f;接下来&#xff0c;我们将详…

uniapp表格合并

最左边本来是三个td,但是要截图里面的效果,只需要rowspan"3" <tr><td rowspan"3" class"tdMoney1">A.2.8 经济来源</td><td class"tdMoney1" >A.2.8 经济1来源</td><td colspan"2"><…

4.21 Python实现将文件夹中的文件压缩

Python实现将文件夹中的文件压缩 可以使用 Python 的 shutil 和 os 模块来将文件夹 C:\Users\15640\Desktop\git\abc 中的所有文件打包成一个名为 abc.zip 的压缩包。 import shutil import os# 定义文件夹路径和压缩包名称 folder_path rC:\Users\15640\Desktop\git\abc zip_…

C语言Prim算法和Prim-Alternat找最小生成树

文章目录 1、用prim算法求最小生成树C语言Prim算法实现 2、用Prim-Alternate算法求最小生成树3、C语言Prim-Alternate算法实现 1、用prim算法求最小生成树 绿色线会标记选过的边 从v1当作起始点开始&#xff0c;可选择: (v1,v2)权值为6 &#xff08;v1,v3&#xff09;权值为3 &…

I P协议

IPv4首部 4个字节的32 bit值以下面的次序传输&#xff1a;首先是 0&#xff5e;7 bit&#xff0c;其次8&#xff5e;15 bit&#xff0c;然后1 6&#xff5e;23 bit&#xff0c;最后是24~31 bit。这种传输次序称作 big endian字节序。由于TCP/IP首部中所有的二进制整数在网络中传…

抖音直播统计、直播间无人互动直播效果软件--抖音大师!

抖音大师介绍 抖音大师是抖音直播统计、直播间无人互动直播效果软件&#xff0c;通过它&#xff0c;你可以快速添加直播互动效果&#xff01;软件使用C#开发&#xff0c;无论是内存占用还是执行效果都远比同行的效果高太多&#xff01;&#xff01;电脑所需性能大大降低&#x…