从零开始实现 ASP.NET Core MVC 的插件式开发(九) - 如何启用预编译视图

标题:从零开始实现 ASP.NET Core MVC 的插件式开发(九) - 升级.NET 5及启用预编译视图

作者:Lamond Lu

地址:https://www.cnblogs.com/lwqlun/p/13992077.html

源代码:https://github.com/lamondlu/Mystique

适用版本:.NET Core 3.1, .NET 5

前景回顾

  • 从零开始实现 ASP.NET Core MVC 的插件式开发(一) - 使用 Application Part 动态加载控制器和视图

  • 从零开始实现 ASP.NET Core MVC 的插件式开发(二) - 如何创建项目模板

  • 从零开始实现 ASP.NET Core MVC 的插件式开发(三) - 如何在运行时启用组件

  • 从零开始实现 ASP.NET Core MVC 的插件式开发(四) - 插件安装

  • 从零开始实现 ASP.NET Core MVC 的插件式开发(五) - 使用 AssemblyLoadContext 实现插件的升级和删除

  • 从零开始实现 ASP.NET Core MVC 的插件式开发(六) - 如何加载插件引用

  • 从零开始实现 ASP.NET Core MVC 的插件式开发(七) - 问题汇总及部分问题解决方案

  • 从零开始实现ASP.NET Core MVC的插件式开发(八) - Razor视图相关问题及解决方案

简介

在这个项目创建的时候,项目的初衷是使用预编译视图来呈现界面,但是由于多次尝试失败,最后改用了运行时编译视图,这种方式在第一次加载的时候非常的慢,所有的插件视图都要在运行时编译。而且从便携性上来说,预编译视图更好。近日,在几位同道的共同努力下,终于实现了这种加载方式。近日,在几位同道的共同努力下,终于实现了这种加载方式。


此篇要鸣谢网友 j4587698 和 yang-er 对针对当前项目的支持,你们的思路帮我解决了当前项目针对不能启用预编译视图的 2 个主要的问题

  • 在当前项目目录结构下,启动时加载组件,组件预编译视图不能正常使用

  • 运行时加载组件之后,组件中的预编译视图不能正常使用

升级.NET 5

随着.NET 5 的发布,当前项目也升级到了.NET 5 版本。

整个升级的过程比我预想的简单的多,只是修改了一下项目使用的Target fremework。重新编译打包了一下插件程序,项目就可以正常运行了,整个过程中没有产生任何因为版本升级导致的编译问题。

预编译视图不能使用的问题

在升级了.NET 5 之后,我重新尝试在启动时关闭了运行时编译,加载预编译视图 View, 借此测试.NET 5 对预编译视图的支持情况。

    public static void MystiqueSetup(this IServiceCollection services, IConfiguration configuration){...IMvcBuilder mvcBuilder = services.AddMvc();ServiceProvider provider = services.BuildServiceProvider();using (IServiceScope scope = provider.CreateScope()){...foreach (ViewModels.PluginListItemViewModel plugin in allEnabledPlugins){CollectibleAssemblyLoadContext context = new CollectibleAssemblyLoadContext(plugin.Name);string moduleName = plugin.Name;string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules", moduleName, $"{moduleName}.dll");string viewFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules", moduleName, $"{moduleName}.Views.dll");string referenceFolderPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules", moduleName);_presets.Add(filePath);using (FileStream fs = new FileStream(filePath, FileMode.Open)){Assembly assembly = context.LoadFromStream(fs);context.SetEntryPoint(assembly);loader.LoadStreamsIntoContext(context, referenceFolderPath, assembly);MystiqueAssemblyPart controllerAssemblyPart = new MystiqueAssemblyPart(assembly);mvcBuilder.PartManager.ApplicationParts.Add(controllerAssemblyPart);PluginsLoadContexts.Add(plugin.Name, context);BuildNotificationProvider(assembly, scope);}using (FileStream fsView = new FileStream(viewFilePath, FileMode.Open)){Assembly viewAssembly = context.LoadFromStream(fsView);loader.LoadStreamsIntoContext(context, referenceFolderPath, viewAssembly);CompiledRazorAssemblyPart moduleView = new CompiledRazorAssemblyPart(viewAssembly);mvcBuilder.PartManager.ApplicationParts.Add(moduleView);}context.Enable();}}}AssemblyLoadContextResoving();...}

运行项目之后,你会发现项目竟然会得到一个无法找到视图的错误。

这里的结果很奇怪,因为参考第一章的场景,ASP.NET Core 默认是支持启动时加载预编译视图的。在第一章的时候,我们创建了 1 个组件,在启动时,直接加载到主AssemblyLoadContext中,启动之后,我们是可以正常访问到视图的。

在仔细思考之后,我想到的两种可能性。

  • 一种可能是因为我们的组件加载在独立的AssemblyLoadContext中,而非主AssemblyLoadContext中,所以可能导致加载失败

  • 插件的目录结构与第一章不符合,导致加载失败

但是苦于不能调试 ASP.NET Core 的源码,所以这一部分就暂时搁置了。直到前几天,网友j4587698 在项目 Issue 中针对运行时编译提出的方案给我的调试思路。

在 ASP.NET Core 中,默认的视图的编译和加载使用了 2 个内部类DefaultViewCompilerProviderDefaultViewCompiler。但是由于这 2 个类是内部类,所以没有办法继承并重写,更谈不上调试了。

j4587698的思路和我不同,他的做法是,在当前主项目中,直接复制DefaultViewCompilerProviderDefaultViewCompiler2 个类的代码,并将其定义为公开类,在程序启动时,替换默认依赖注入容器中的类实现,使用公开的DefaultViewCompilerProviderDefaultViewCompiler类,替换 ASP.NET Core 默认指定的内部类。

根据他的思路,我新增了一个基于IServiceCollection的扩展类,追加了Replace方法来替换注入容器中的实现。

    public static class ServiceCollectionExtensions{public static IServiceCollection Replace<TService, TImplementation>(this IServiceCollection services)where TImplementation : TService{return services.Replace<TService>(typeof(TImplementation));}public static IServiceCollection Replace<TService>(this IServiceCollection services, Type implementationType){return services.Replace(typeof(TService), implementationType);}public static IServiceCollection Replace(this IServiceCollection services, Type serviceType, Type implementationType){if (services == null){throw new ArgumentNullException(nameof(services));}if (serviceType == null){throw new ArgumentNullException(nameof(serviceType));}if (implementationType == null){throw new ArgumentNullException(nameof(implementationType));}if (!services.TryGetDescriptors(serviceType, out var descriptors)){throw new ArgumentException($"No services found for {serviceType.FullName}.", nameof(serviceType));}foreach (var descriptor in descriptors){var index = services.IndexOf(descriptor);services.Insert(index, descriptor.WithImplementationType(implementationType));services.Remove(descriptor);}return services;}private static bool TryGetDescriptors(this IServiceCollection services, Type serviceType, out ICollection<ServiceDescriptor> descriptors){return (descriptors = services.Where(service => service.ServiceType == serviceType).ToArray()).Any();}private static ServiceDescriptor WithImplementationType(this ServiceDescriptor descriptor, Type implementationType){return new ServiceDescriptor(descriptor.ServiceType, implementationType, descriptor.Lifetime);}}

并在程序启动时,使用公开的MyViewCompilerProvider类,替换了原始注入类DefaultViewCompilerProvider

    public static void MystiqueSetup(this IServiceCollection services, IConfiguration configuration){_serviceCollection = services;services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();services.AddSingleton<IMvcModuleSetup, MvcModuleSetup>();services.AddScoped<IPluginManager, PluginManager>();services.AddScoped<ISystemManager, SystemManager>();services.AddScoped<IUnitOfWork, Repository.MySql.UnitOfWork>();services.AddSingleton<INotificationRegister, NotificationRegister>();services.AddSingleton<IActionDescriptorChangeProvider>(MystiqueActionDescriptorChangeProvider.Instance);services.AddSingleton<IReferenceContainer, DefaultReferenceContainer>();services.AddSingleton<IReferenceLoader, DefaultReferenceLoader>();services.AddSingleton(MystiqueActionDescriptorChangeProvider.Instance);...services.Replace<IViewCompilerProvider, MyViewCompilerProvider>();}

MyViewCompilerProvider中, 直接返回了新定义的MyViewCompiler

    public class MyViewCompilerProvider : IViewCompilerProvider{private readonly MyViewCompiler _compiler;public MyViewCompilerProvider(ApplicationPartManager applicationPartManager,ILoggerFactory loggerFactory){var feature = new ViewsFeature();applicationPartManager.PopulateFeature(feature);_compiler = new MyViewCompiler(feature.ViewDescriptors, loggerFactory.CreateLogger<MyViewCompiler>());}public IViewCompiler GetCompiler() => _compiler;}

PS: 此处只是直接复制了 ASP.NET Core 源码中DefaultViewCompilerProviderDefaultViewCompiler2 个类的代码,稍作修改,保证编译通过。

    public class MyViewCompiler : IViewCompiler{private readonly Dictionary<string, Task<CompiledViewDescriptor>> _compiledViews;private readonly ConcurrentDictionary<string, string> _normalizedPathCache;private readonly ILogger _logger;public MyViewCompiler(IList<CompiledViewDescriptor> compiledViews,ILogger logger){...}/// <inheritdoc />public Task<CompiledViewDescriptor> CompileAsync(string relativePath){if (relativePath == null){throw new ArgumentNullException(nameof(relativePath));}// Attempt to lookup the cache entry using the passed in path. This will succeed if the path is already// normalized and a cache entry exists.if (_compiledViews.TryGetValue(relativePath, out var cachedResult)){return cachedResult;}var normalizedPath = GetNormalizedPath(relativePath);if (_compiledViews.TryGetValue(normalizedPath, out cachedResult)){return cachedResult;}// Entry does not exist. Attempt to create one.return Task.FromResult(new CompiledViewDescriptor{RelativePath = normalizedPath,ExpirationTokens = Array.Empty<IChangeToken>(),});}private string GetNormalizedPath(string relativePath){...}}

针对DefaultViewCompiler,这里的重点是CompileAsync方法,它会根据传入的相对路径,在加载的编译视图集合中加载视图。下面我们在此处打上断点,并模拟进入DemoPlugin1的主页。

看完这个调试过程,你是不是发现了点什么,当我们访问DemoPlugin1的主页路由/Modules/DemoPlugin/Plugin1/HelloWorld的时候,ASP.NET Core 尝试查找的视图相对路径是·

  • /Areas/DemoPlugin1/Views/Plugin1/HelloWorld.cshtml

  • /Areas/DemoPlugin1/Views/Shared/HelloWorld.cshtml

  • /Views/Shared/HelloWorld.cshtml

  • /Pages/Shared/HelloWorld.cshtml

  • /Modules/DemoPlugin1/Views/Plugin1/HelloWorld.cshtml

  • /Views/Shared/HelloWorld.cshtml

而当我们查看现在已有的编译视图映射时,你会发现注册的对应视图路径却是/Views/Plugin1/HelloWorld.cshtml。下面我们再回过头来看看DemoPlugin1的目录结构

由此我们推断出,预编译视图在生成的时候,会记录当前视图的相对路径,而在主程序加载的插件的过程中,由于我们使用了Area来区分模块,多出的一级目录,所以导致目录映射失败了。因此如果我们将DemoPlugin1的插件视图目录结构改为以上提示的 6 个地址之一,问题应该就解决了。

那么这里有没有办法,在不改变路径的情况下,让视图正常加载呢,答案是有的。参照之前的代码,在加载视图组件的时候,我们使用了内置类CompiledRazorAssemblyPart, 那么让我们来看看它的源码。

    public class CompiledRazorAssemblyPart : ApplicationPart, IRazorCompiledItemProvider{/// <summary>/// Initializes a new instance of <see cref="CompiledRazorAssemblyPart"/>./// </summary>/// <param name="assembly">The <see cref="System.Reflection.Assembly"/></param>public CompiledRazorAssemblyPart(Assembly assembly){Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));}/// <summary>/// Gets the <see cref="System.Reflection.Assembly"/>./// </summary>public Assembly Assembly { get; }/// <inheritdoc />public override string Name => Assembly.GetName().Name;IEnumerable<RazorCompiledItem> IRazorCompiledItemProvider.CompiledItems{get{var loader = new RazorCompiledItemLoader();return loader.LoadItems(Assembly);}}}

这个类非常的简单,它通过RazorCompiledItemLoader类对象从程序集中加载的视图, 并将最终的编译视图都存放在一个RazorCompiledItem类的集合里。

    public class RazorCompiledItemLoader{public virtual IReadOnlyList<RazorCompiledItem> LoadItems(Assembly assembly){if (assembly == null){throw new ArgumentNullException(nameof(assembly));}var items = new List<RazorCompiledItem>();foreach (var attribute in LoadAttributes(assembly)){items.Add(CreateItem(attribute));}return items;}protected virtual RazorCompiledItem CreateItem(RazorCompiledItemAttribute attribute){if (attribute == null){throw new ArgumentNullException(nameof(attribute));}return new DefaultRazorCompiledItem(attribute.Type, attribute.Kind, attribute.Identifier);}protected IEnumerable<RazorCompiledItemAttribute> LoadAttributes(Assembly assembly){if (assembly == null){throw new ArgumentNullException(nameof(assembly));}return assembly.GetCustomAttributes<RazorCompiledItemAttribute>();}}

这里我们可以参考前面的调试方式,创建出一套自己的视图加载类,代码和当前的实现一模一样

MystiqueModuleViewCompiledItemLoader

    public class MystiqueModuleViewCompiledItemLoader : RazorCompiledItemLoader{public MystiqueModuleViewCompiledItemLoader(){}protected override RazorCompiledItem CreateItem(RazorCompiledItemAttribute attribute){if (attribute == null){throw new ArgumentNullException(nameof(attribute));}return new MystiqueModuleViewCompiledItem(attribute);}}

MystiqueRazorAssemblyPart

    public class MystiqueRazorAssemblyPart : ApplicationPart, IRazorCompiledItemProvider{public MystiqueRazorAssemblyPart(Assembly assembly){Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));AreaName = areaName;}public Assembly Assembly { get; }public override string Name => Assembly.GetName().Name;IEnumerable<RazorCompiledItem> IRazorCompiledItemProvider.CompiledItems{get{var loader = new MystiqueModuleViewCompiledItemLoader();return loader.LoadItems(Assembly);}}}

MystiqueModuleViewCompiledItem

    public class MystiqueModuleViewCompiledItem : RazorCompiledItem{public override string Identifier { get; }public override string Kind { get; }public override IReadOnlyList<object> Metadata { get; }public override Type Type { get; }public MystiqueModuleViewCompiledItem(RazorCompiledItemAttribute attr, string moduleName){Type = attr.Type;Kind = attr.Kind;Identifier = attr.Identifier;Metadata = Type.GetCustomAttributes(inherit: true).ToList();}}

这里我们在MystiqueModuleViewCompiledItem类的构造函数部分打上断点。

重新启动项目之后,你会发现当加载 DemoPlugin1 的视图时,这里的Identifier属性其实就是当前编译试图项的映射目录。这样我们很容易就想到在此处动态修改映射目录,为此我们需要将模块名称通过构造函数传入,以上 3 个类的更新代码如下:

MystiqueModuleViewCompiledItemLoader

    public class MystiqueModuleViewCompiledItemLoader : RazorCompiledItemLoader{public string ModuleName { get; }public MystiqueModuleViewCompiledItemLoader(string moduleName){ModuleName = moduleName;}protected override RazorCompiledItem CreateItem(RazorCompiledItemAttribute attribute){if (attribute == null){throw new ArgumentNullException(nameof(attribute));}return new MystiqueModuleViewCompiledItem(attribute, ModuleName);}}

MystiqueRazorAssemblyPart

    public class MystiqueRazorAssemblyPart : ApplicationPart, IRazorCompiledItemProvider{public MystiqueRazorAssemblyPart(Assembly assembly, string moduleName){Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));ModuleName = moduleName;}public string ModuleName { get; }public Assembly Assembly { get; }public override string Name => Assembly.GetName().Name;IEnumerable<RazorCompiledItem> IRazorCompiledItemProvider.CompiledItems{get{var loader = new MystiqueModuleViewCompiledItemLoader(ModuleName);return loader.LoadItems(Assembly);}}}

MystiqueModuleViewCompiledItem

    public class MystiqueModuleViewCompiledItem : RazorCompiledItem{public override string Identifier { get; }public override string Kind { get; }public override IReadOnlyList<object> Metadata { get; }public override Type Type { get; }public MystiqueModuleViewCompiledItem(RazorCompiledItemAttribute attr, string moduleName){Type = attr.Type;Kind = attr.Kind;Identifier = "/Modules/" + moduleName + attr.Identifier;Metadata = Type.GetCustomAttributes(inherit: true).Select(o =>o is RazorSourceChecksumAttribute rsca? new RazorSourceChecksumAttribute(rsca.ChecksumAlgorithm, rsca.Checksum, "/Modules/" + moduleName + rsca.Identifier): o).ToList();}}

PS: 这里有个容易疏漏的点,就是MystiqueModuleViewCompiledItem中的MetaData, 它使用了Identifier属性的值,所以一旦Identifier属性的值被动态修改,此处的值也要修改,否则调试会不成功。

修改完成之后,我们重启项目,来测试一下。

编译视图的映射路径动态修改成功,页面成功被打开了,至此启动时的预编译视图加载完成。

运行时加载编译视图

最后我们来到了运行加载编译视图的问题,有了之前的调试方案,现在调试起来就轻车熟路。

为了测试,我们再运行时加载编译视图,我们首先禁用掉DemoPlugin1, 然后重启项目,并启用DemoPlugin1

通过调试,很明显问题出在预编译视图的加载上,在启用组件之后,编译视图映射集合没有更新,所以导致加载失败。这也证明了我们之前第三章时候的推断。当使用IActionDescriptorChangeProvider重置Controller/Action映射的时候,ASP.NET Core 不会更新视图映射集合,从而导致视图加载失败。

    MystiqueActionDescriptorChangeProvider.Instance.HasChanged = true;MystiqueActionDescriptorChangeProvider.Instance.TokenSource.Cancel();

那么解决问题的方式也就很清楚了,我们需要在使用IActionDescriptorChangeProvider重置Controller/Action映射之后,刷新视图映射集合。为此,我们需要修改之前定义的MyViewCompilerProvider, 添加Refresh方法来刷新映射。

    public class MyViewCompilerProvider : IViewCompilerProvider{private MyViewCompiler _compiler;private ApplicationPartManager _applicationPartManager;private ILoggerFactory _loggerFactory;public MyViewCompilerProvider(ApplicationPartManager applicationPartManager,ILoggerFactory loggerFactory){_applicationPartManager = applicationPartManager;_loggerFactory = loggerFactory;Refresh();}public void Refresh(){var feature = new ViewsFeature();_applicationPartManager.PopulateFeature(feature);_compiler = new MyViewCompiler(feature.ViewDescriptors, _loggerFactory.CreateLogger<MyViewCompiler>());}public IViewCompiler GetCompiler() => _compiler;}

Refresh方法是借助ViewsFeature来重新创建了一个新的IViewCompiler, 并填充了最新的视图映射。

PS: 这里的实现方式参考了DefaultViewCompilerProvider的实现,该类是在构造中填充的视图映射。

根据以上修改,在使用IActionDescriptorChangeProvider重置 Controller/Action 映射之后, 我们使用Refresh方法来刷新映射。

    private void ResetControllActions(){MystiqueActionDescriptorChangeProvider.Instance.HasChanged = true;MystiqueActionDescriptorChangeProvider.Instance.TokenSource.Cancel();var provider = _context.HttpContext.RequestServices.GetService(typeof(IViewCompilerProvider)) as MyViewCompilerProvider;provider.Refresh();}

最后,我们重新启动项目,再次在运行时启用DemoPlugin1,进入插件主页面,页面正常显示了。

至此运行时加载与编译视图的场景也顺利解决了。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构——二叉树的层次遍历进阶

之前的一个博客 数据结构——二叉树的层次遍历看完这个&#xff0c;可以简单实现下面的问题 问题&#xff1a; 1.计算二叉树的最大宽度(二叉树的最大宽度是指二叉树所有层中结点个数的最大值。 2.用按层次顺序遍历二叉树的方法,统计树中具有度为1的结点数目。 解决问题的思路…

future.cancel不能关闭线程_多线程与高并发笔记

1. 创建线程的四种方式实现Runnable 重写run方法继承Thread 重写run方法线程池创建 Executors.newCachedThreadPool()实现Callable接口2. Thread线程操作方法当前线程睡眠指定mills毫秒Thread.sleep([mills])当前线程优雅让出执行权Thread.yield()例如Thread t1, t2&#xff0c…

ANDROID手表怎么设置壁纸,表盘背景随心换 果壳智能手表换壁纸教程

在智能手机和电脑上&#xff0c;我们一般都会换一张自己喜欢的图片作为壁纸&#xff0c;当我们想把一张喜欢的图片做成果壳GEAK Watch表盘时&#xff0c;应该怎么做呢&#xff1f;其实只要简单的几个步骤&#xff0c;你就能获得独一无二的专属表盘。首先&#xff0c;打开一张你…

如何踢掉 sql 语句中的尾巴,我用 C# 苦思了五种办法

一&#xff1a;背景 1. 讲故事这几天都在修复bug真的太忙了&#xff0c;期间也遇到了一个挺有趣bug&#xff0c;和大家分享一下&#xff0c;这是一块sql挺复杂的报表相关业务&#xff0c;不知道哪一位大佬在错综复杂的 嵌套 平行 if判断中sql拼接在某些UI组合下出问题了&#…

数据结构——二叉树的最长路径问题

题目&#xff1a; 求任意二叉树中第一条最长的路径长度,并输出此路径上各结点的值。 描述 设二叉树中每个结点的元素均为一个字符&#xff0c;按先序遍历的顺序建立二叉链表&#xff0c;编写算法求出该二叉树中第一条最长的路径。 输入 一行数据&#xff0c;为二叉树的先序序…

.NET 应用如何优雅的做功能开关(Feature Flag)

点击上方蓝字关注“汪宇杰博客”导语曾经&#xff0c;我们要在应用程序里做功能开关&#xff0c;就避免不了在配置文件里加上一堆 bool 类型的配置项&#xff0c;然后在代码里用 if else 去判断。尽管这种做法是可行的&#xff0c;但我们现在有办法让代码更加整洁&#xff0c;避…

鼠标右键 移动选定的文件夹到指定位置_iRightMouse:一款免费Mac鼠标右键增强神器...

如果你是多年的Windows用户转到macOS平台&#xff0c;你必定会发现Windows上很多非常方便的鼠标右键菜单在macOS上都是没有的&#xff0c;例如新建txt文档、一键隐藏文件等。而这些快捷功能的缺失也确实会带来一些不便&#xff0c;奇客君发现一款刚刚上线的免费右键增强工具&am…

数据结构——从叶子结点到根节点的全部路径

问题 给定一个二叉树&#xff0c;返回所有从根节点到叶子节点的路径。 说明: 叶子节点是指没有子节点的节点。 示例: 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;https://leetcode-cn.com/problems/binary-tree-paths 257.二叉树的所有路径 与…

台电x80plus装linux,纤巧却不简单——台电X80 Plus评测

8英寸平板一直是各平板品牌混战厮杀的大战场。“性价比”是这个尺寸平板好坏的最重要关键字。今天就为大家带来台电8英寸X80 Plus平板电脑的评测。X80 Plus是台电8英寸产品中的最新款&#xff0c;采用的是英特尔新一代CherrTrail架构芯片Atom X5 Z8300。HD核显由原来的Gen7升级…

数据结构——二叉树的递归算法

二叉树的结构定义&#xff1a; typedef struct BiNode {TElemType data;struct BiNode *lchild;struct BiNode *rchild; }BiNode,*BiTree;这里包含的递归算法有&#xff1a; 二叉树的先序创建&#xff1b;二叉树的先序中序后序遍历&#xff1b;二叉树的销毁算法&#xff1b;双…

使用BeetleX访问redis服务

BeetleX针对redis访问封了全async/await操作模式&#xff0c;通过它可以更高效地访问redis服务。BeetleX.Redis提供读写分离和多机故意写入处理&#xff0c;同时安全的TLS访问机制&#xff0c;在使用功能上组件支持绝大部分基础指令&#xff0c;并提供json&#xff0c;protobuf…

鸿蒙系统可以替代安卓吗,华为今天发布的鸿蒙系统,到底能不能替代安卓?

对于大部分差友们来说&#xff0c;“开发者大会”这个词一定显得陌生而又遥远&#xff0c;跟普通的产品发布会不一样&#xff0c;他们面向的对象并不是普通的消费者&#xff0c;而是各种程序猿和攻城狮。话又说回来&#xff0c;能开“ 开发者大会”&#xff0c;也说明这个企业已…

android loading封装_我们经常用的Loading动画居然还有这种姿势

背景Loading动画几乎每个Android App中都有。一般在需要用户等待的场景&#xff0c;显示一个Loading动画可以让用户知道App正在加载数据&#xff0c;而不是程序卡死&#xff0c;从而给用户较好的使用体验。同样的道理&#xff0c;当加载的数据为空时显示一个数据为空的视图、在…

数据结构——基于字符串模式匹配算法的病毒感染检测

实验四 基于字符串模式匹配算法的病毒感染检测 【实验目的】 1.掌握字符串的顺序存储表示方法。 2.掌握字符串模式匹配BF算法和KMP算法的实现。 【实验内容】 问题描述 医学研究者最近发现了某些新病毒,通过对这些病毒的分析,得知它们的DNA序列都是环状的。现在研究者已收集了…

WPF 使用 Expression Design 画图导出及使用 Path 画图

WPF 使用 Expression Design 画图导出及使用 Path 画图目录WPF 使用 Expression Design 画图导出及使用 Path 画图一、软件介绍二、Microsoft Expression Design 使用三、微语言和 Path 绘图1、"注释" 图形&#xff08;中括号&#xff09;2、"并行模式" 图…

数据结构——模式匹配kmp算法

暴力算法 //暴力算法 int index(SString S,SString T,int pos) {int ipos,j1;while(i<S[0]&&j<T[0]){if(S[i]T[j]){i;j;}else {ii-j2;j1;}}if(j>T[0])return i-T[0];else return 0;} kmp算法 next[]数组的求法&#xff1a; 例子&#xff1a;abaabcac 模式串…

互联网时代供应链

供应链是指围绕核心企业&#xff0c;从配套零件开始&#xff0c;制成中间产品以及最终产品&#xff0c;最后由销售网络把产品送到消费者手中的、将供应商&#xff0c;制造商&#xff0c;分销商直到最终用户连成一个整体的功能网链结构。供应链管理的经营理念是从消费者的角度&a…

win7 计算器 android,教你巧妙应用Win7计算器和时钟

正文最新的Win7是一种个性化设计极强的操作系统&#xff0c;在许多细节方面都做到了人性化设计。其功能的DIY性非常明显&#xff0c;是XP系统远远不能比的。今天我们要说的是Win7计算器和时钟&#xff0c;除了可以计算和时间之外我们还可以让他们有哪些妙用呢&#xff1f;我们左…

真实经历:整整一年了,他是这样从程序员转型做产品经理的

这是头哥侃码的第224篇原创每年年底&#xff0c;有不少企业都会对一年内辛勤劳作的员工量身定做一些奖项。发个奖杯&#xff0c;给点奖金&#xff0c;让那些没得奖的人看看&#xff0c;咱们公司有多么的关注员工的闪光点&#xff0c;优秀之处。用人所长&#xff0c;容人所短&am…

数据结构—— 基于二叉树的算术表达式求值

实验五 基于二叉树的算术表达式求值 数据结构——中序表达式求值&#xff08;栈实现&#xff09; 实验目的&#xff1a; 1.掌握二叉树的二叉链表存储表示和二叉树的遍历等基本算法。 2.掌握根据中缀表达式创建表达式树的算法 3.掌握基于表达式树的表达式求值算法。 实验内容&a…