[转]Autofac 框架初识与应用

一、前言

 这上一篇中,主要讲述了什么是IoC容器,以及了解到它是DI构造函注入的框架,它管理着依赖项的生命周期以及映射关系,同时也介绍实践了在ASP.Net Core中,默认提供的内置IoC容器,以及它的实例注册方式和相应的生命周期。

但考虑到在实际项目中,如果需要一个个添加实例,会略显麻烦,为了达到可以简化我们工作量,因此我们也可以引入其他的Ioc容器框架,实现更多的功能和扩展。

这里选择用Autofac,这也是在.net下比较流行的,其他的框架不做说明,可自行查阅了解。

二、说明

AutoFac是一个开源的轻量级的依赖注入容器,也是.net下比较流行的实现依赖注入的工具之一。

将Autofac整合到你的应用的基本流程如下:

  • 按照 控制反转 (IoC) 的思想构建你的应用.
  • 添加Autofac引用.
  • 在应用的 startup 处
  • 创建 ContainerBuilder.
  • 注册组件.
  • 创建容器,将其保存以备后续使用.
  • 应用执行阶段
  • 从容器中创建一个生命周期.
  • 在此生命周期作用域内解析组件实例.

三、开始

3.1 默认容器

在上一篇中定义的三个接口,分别测试Singleton,Scope,Transient三种,一个 TestService服务,

在内置的IoC容器中,在Startup.cs类文件ConfigureServices方法中,注入依赖方式如下:

public void ConfigureServices(IServiceCollection services)
{services.AddControllers();services.AddTransient<ITransientService, TransientService>();services.AddSingleton<ISingletonService, SingletonService>();services.AddScoped<IScopedService, ScopedService>();services.AddScoped<ITestService, TestService>();
}

其他不清楚的可以回看上一篇说明

3.2 Autofac框架

现在我们使用其他的IoC容器框架来替换默认的内置IoC,这里选择使用Autofac框架

.net core 2.x和3.x 使用autofac注入方式不一样,此文章是针对.net core 3.x

首先,我们需要从nuget引用相关的包.

Autofac.Extensions.DependencyInjection(这个包扩展了一些微软提供服务的类.来方便替换autofac)

然后在Program.cs 新增一行代码

        public static IHostBuilder CreateHostBuilder(string[] args){//var assemblyName = typeof(Startup).GetTypeInfo().Assembly.FullName;return Host.CreateDefaultBuilder(args).UseServiceProviderFactory(new AutofacServiceProviderFactory())  //设置工厂来替换实例.ConfigureWebHostDefaults(webBuilder =>{webBuilder.UseStartup<Startup>();});}

UseServiceProviderFactory 设置工厂来替换实例。

然后在Startup类增加ConfigureContainer方法,在方法中注入依赖:

    public void ConfigureContainer(ContainerBuilder builder){// Register your own things directly with Autofac, like:builder.RegisterType<TransientService>().As<ITransientService>();builder.RegisterType<SingletonService>().As<ISingletonService>().SingleInstance();builder.RegisterType<ScopedService>().As<IScopedService>().InstancePerLifetimeScope();builder.RegisterType<TestService>().As<ITestService>().InstancePerLifetimeScope();}

说明

ASP.NET Core 引入了具有强类型容器配置的能力。 它提供了一个ConfigureContainer方法,您可以使用Autofac单独注册,而不是使用ServiceCollection注册。

使用ConfigureContainer配置

  • 在配置WebHostBuilderProgram.Main方法中,调用AddAutofac将Autofac挂钩到启动管道中。
  • Startup类的ConfigureServices方法中,使用其他库提供的扩展方法将内容注册到IServiceCollection中。
  • Startup类的ConfigureContainer方法中,将内容直接注册到AutofacContainerBuilder中。

3.3 测试

启动运行项目,访问接口/Test

效果如下:

对比之前默认容器可以发现,在两次的请求访问都一样,可以得到了 4个Transient实例,2个Scope实例,1个Singleton实例。

四、说明

下面主要针对Autofac中的注册组件、解析服务两大步骤,以及其中容器中对应实例的生命周期,进行说明。

4.1 注册组件

通过创建 ContainerBuilder 来注册组件,并且告诉容器哪些组件,暴露了哪些服务。

使用 Register() 方法来注册实现:

ContainerBuilder 包含一组 Register() 注册方法,而组件暴露服务,可用使用 ContainerBuilder 上的 As() 方法。

即在容器初始化时候,向容器组件添加对象的操作过程。

通过梳理Autofac所有可用的注册组件方法,显示如下图展示的流程图。

这里我们只说明下几种便捷的注册方法

4.1.1 反射注册

直接注册的组件必须是具体的类型,并可用暴露抽象和接口作为服务,但不能注册一个抽象和接口组件。

使用RegisterType<T>()或者RegisterType(typeof(T))方法:

builder.RegisterType<TestService>().As<ITestService>();
// 或者
builder.RegisterType(typeof(TestService)).As(typeof(ITestService))

在多个构造函数时,如果需要,也可手动指定一个构造函数。

使用 UsingConstructor 方法和构造方法中代表参数类型的类型。

builder.RegisterType<TestService>().UsingConstructor(typeof(TransientService), typeof(SingletonService));

4.1.2 实例注册

提前生成对象的实例并加入容器,以供注册组件时使用。

使用RegisterInstance()方法

// new出一个对象注册:
var output = new StringWriter();
builder.RegisterInstance(output).As<TestService>();

如果单例中存在实例且需要在容器中被组件使用时,

builder.RegisterInstance(MySingleton.Instance).ExternallyOwned();

4.1.3 Lambda表达式注册

当组件创建不再是简单调用构造方法时,可用利用lambda表达式来实现一些常规反射无法实现的操作。

比如一些复杂参数注册,参数注入,以及选择参数值实现等。

  builder.Register(x => new TransientService()).As<ITransientService>();
// 或者指定参数builder.Register(x => new TestService(x.Resolve<ITransientService>(), x.Resolve<IScopedService>(), x.Resolve<ISingletonService>())).As<ITestService>().InstancePerLifetimeScope();

4.1.4 泛型注册

支持泛型注册操作,使用 RegisterGeneric() 方法:

builder.RegisterGeneric(typeof(NHibernateRepository<>)).As(typeof(IRepository<>)).InstancePerLifetimeScope();

4.1.5 条件注册

在一些特殊场景,可能需要通过加上判断条件,来决定是否执行该条注册语句。

两种方法:

  • OnlyIf() - 提供一个表达式, 表示只有满足条件,才会执行语句。
builder.RegisterType<Manager>().As<IManager>().OnlyIf(reg =>reg.IsRegistered(new TypedService(typeof(IService))) &&reg.IsRegistered(new TypedService(typeof(HandlerB))));
  • IfNotRegistered() - 表示没有其他服务注册的情况下,就执行语句。

方法在 ContainerBuilder.Build() 时执行并且以实际组件注册的顺序执行。

builder.RegisterType<ServiceA>().As<IService>();
builder.RegisterType<ServiceB>().As<IService>().IfNotRegistered(typeof(IService));

4.1.6 属性注入

构造方法参数注入是一种传值给组件的首选的方法。

在构造函数中是直接使用服务类型作为参数,然后AutoFac解析该类时,就会去容器内部已存在的组件中查找,然后将匹配的对象注入到构造函数中去。

但你同样也可以使用属性方法注入来传值。

是将容器内对应的组件直接注入到类内的属性中去,在注册该属性所属类的时候,需要使用PropertiesAutowired()方法额外标注。

这里不讨论属性注入的好坏,也不做说明服务层属性怎么注入,只讨论说明控制器中属性如何实现注入

  1. 注册组件方法,并使用属性注入PropertiesAutowired()标注。
builder.RegisterType<TransientService>().As<ITransientService>().PropertiesAutowired();
  1. 在控制器中使用属性来接收, 其中注入属性必须标注为public
    [ApiController][Route("[controller]")]public class TestController : ControllerBase{public ITransientService _transientService { get; set; }  [HttpGet]public JsonResult Get(){var data1 =   _transientService.GetGuid();return new JsonResult(new { data1});}}
  1. 运行测试,发现如下

发现_transientService为null,所以根本没有注入成功。

这是因为控制器本身的实例(以及它的处理)是由框架创建和拥有的,而不是由容器所有

因此我们需要改变控制器本身的创建及其拥有。

  1. 在Startup.cs中修改ConfigureServices方法,替换从IServiceProvider中解析控制器实例的所有者。
        public void ConfigureServices(IServiceCollection services){//替换控制器的所有者services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());services.AddControllers();}

注意,替换的方法一定要在AddControllers之前。

  1. ContainerBuilder中通过注册控制器,并使用属性注入功能实现.
public void ConfigureContainer(ContainerBuilder builder)
{ //找到所有的controller进行注册,并使用属性注入功能var controllerTypesInassembly = typeof(Startup).Assembly.GetExportedTypes().Where(type => typeof(ControllerBase).IsAssignableFrom(type)).ToArray();builder.RegisterTypes(controllerTypesInassembly).PropertiesAutowired();builder.RegisterType<TransientService>().As<ITransientService>().PropertiesAutowired();
}

这样就可以在Controller中进行属性注入了;

  1. 再次运行查看,发现已经成功注入了。

4.1.7 程序集注册

当我们需要实现批量注册的时候,也可以使用程序集的方式来注册,这也是常用的方法。

可通过指定过滤类型,服务,扫描模块等方式来找到需要注册的组件。

var assemblies = Assembly.GetExecutingAssembly();builder.RegisterAssemblyTypes(assemblies)//程序集内所有具象类 
.Where(c => c.Name.EndsWith("Service"))
.PublicOnly()//只要public访问权限的
.Where(cc => cc.IsClass)//只要class型(主要为了排除值和interface类型) 
.AsImplementedInterfaces();//自动以其实现的所有接口类型暴露(包括IDisposable接口)

说明:

  • RegisterAssemblyTypes() :接收包含一个或多个程序集的数组作为参数
  • RegisterAssemblyModules() : 接收模块作为参数,进行模块扫描注册
  • PublicOnly() :指定公有方法被注册
  • Where() :要过滤注册的类型
  • Except() :要排除的类型
  • As() :反射出其实现的接口
  • AsImplementedInterfaces() : 自动以其实现的所有接口类型暴露(包括IDisposable接口)

4.2 暴露服务

上面提到了注册组件时, 我们得告诉Autofac, 组件暴露了哪些服务。

在上面注册实现中,大部分使用到了As() 方法。

当然,Autofac也提供了其他标注来暴露服务的方法。

4.2.1 默认暴露自身类型服务

常用的几种方法如下:

builder.RegisterType<CallLogger>();//不标注,默认以自身类型暴露服务
builder.RegisterType<CallLogger>().AsSelf();
builder.RegisterType<CallLogger>().As<CallLogger>();
builder.RegisterType<CallLogger>().As(typeof(CallLogger));

4.2.2 多个暴露服务类型

以其实现的接口(interface)暴露服务,暴露的类型可以是多个,比如CallLogger类实现了ILogger接口和ICallInterceptor接口。

暴露服务后, 可以解析基于该服务的组件了. 但请注意, 一旦将组件暴露为一个特定的服务, 默认的服务 (组件类型) 将被覆盖。

所以,为了防止被其他服务覆盖,可以使用 AsSelf() 方法。

Copybuilder.RegisterType<CallLogger>().As<ILogger>().As<ICallInterceptor>().AsSelf();

这样你既可以实现组件暴露一系列特定的服务, 又可以让它暴露默认的服务。

4.2.3 程序集注册指定暴露类型

  1. 可通过指定接口类型暴露服务,使用As() 方法
publi void ConfigureContainer(ContainerBuilder builder)
{builder.RegisterAssemblyTypes(assemblies)//程序集内所有具象类 .Where(cc =>cc.Name.EndsWith("Repository")|//筛选cc.Name.EndsWith("Service")).As(x=>x.GetInterfaces()[0])//反射出其实现的接口,并指定以其实现的第一个接口类型暴露
}
  1. 指定所有实现的接口类型进行暴露

使用AsImplementedInterfaces()函数实现,相当于一个类实现了几个接口(interface)就会暴露出几个服务,等价于上面连写多个As()的作用。

publi void ConfigureContainer(ContainerBuilder builder)
{
builder.RegisterAssemblyTypes(asm).Where(t => t.Name.EndsWith("Repository")).AsImplementedInterfaces();//自动以其实现的所有接口类型暴露(包括IDisposable接口)
}

4.3 解析服务

在 注册完组件并暴露相应的服务后, 可以从创建的容器或其生命周期中解析服务。

使用 Resolve() 方法来解析实现:

通过梳理Autofac所有可用的解析服务方法,显示如下图展示的流程图。

在 注册完组件并暴露相应的服务后, 你可以从创建的容器或其子 生命周期 中解析服务. 让我们使用 Resolve() 方法来实现:

var builder = new ContainerBuilder();
builder.RegisterType<MyComponent>().As<IService>();
var container = builder.Build();using(var scope = container.BeginLifetimeScope())
{var service = scope.Resolve<IService>();
}

4.3.1 解析时传参

当解析服务时, 需要传参,可以使用Resolve() 方法来接受可变长度的参数。

  • 可用参数类型

NamedParameter - 通过名称匹配目标参数

TypedParameter - 通过类型匹配目标参数 (需要匹配具体类型)

ResolvedParameter - 灵活的参数匹配

  • 反射组件的参数
var reader = scope.Resolve<ConfigReader>(new NamedParameter("configSectionName", "sectionName"));
  • Lambda表达式组件的参数

  • 不显式调用Resolve传参

4.3.2 隐式关系类型

这里不做详细说明,详见官方文档

4.4 生命周期

下面讲下AutoFac定义的几种生命周期作用域,并与.NET Core默认的生命周期作了简要的对比。

4.4.1 暂时性

每次在向服务容器进行请求时都会创建新的实例,相当于每次都new出一个。

注册方式:

使用InstancePerDependency()方法标注,如果不标注,这也是默认的选项。以下两种注册方法是等效的:

//不指定,默认就是瞬时的
builder.RegisterType<TransientService>().As<ITransientService>();//指定其生命周期域为瞬时
builder.RegisterType<TransientService>().As<ITransientService>().InstancePerDependency();

对比

与默认的容器中自带的生命周期AddTransient相同,也是每次都是全新的实例。
使用AddTransient()注册:

 services.AddTransient<ITransientService, TransientService>()

4.4.2 作用域内

在每次Web请求时被创建一次实例,生命周期横贯整次请求。即在每个生命周期作用域内是单例的。

注册方式:

使用InstancePerLifetimeScope()方法标识:

builder.RegisterType<ScopedService>().As<IScopedService>().InstancePerLifetimeScope();

对比

与默认的容器中自带的生命周期AddScoped相同,.NET Core框架自带的容器全权接管了请求和生命周期作用域的创建,使用Scoped()可以实现相同的效果。
使用AddScoped()注册:

 services.AddScoped<IScopedService, ScopedService>();

4.4.3 匹配作用域内

即每个匹配的生命周期作用域一个实例。
该类型其实是上面的“作用域内”的其中一种,可以对实例的共享有更加精准的控制.。我们通过允许给域“打标签”,只要在这个特定的标签域内就是单例的。

  • 注册
    使用InstancePerMatchingLifetimeScope(string tagName)方法注册:
var builder = new ContainerBuilder();
builder.RegisterType<Worker>().InstancePerMatchingLifetimeScope("myrequest");

当你开始一个生命周期时, 提供的标签值和它就关联起来了。

  • 解析
// myrequest标签子域一
using(var scope1 = container.BeginLifetimeScope("myrequest"))
{for(var i = 0; i < 100; i++){var w1 = scope1.Resolve<Worker>();using(var scope2 = scope1.BeginLifetimeScope()){var w2 = scope2.Resolve<Worker>();//解析了2次,但2次都是同一个实例(w1和w2指向同一个内存块Ⅰ)}}
}// //myrequest标签子域二
using(var scope3 = container.BeginLifetimeScope("myrequest"))
{for(var i = 0; i < 100; i++){//因为标签域内已注册过,所以可以解析成功var w3 = scope3.Resolve<Worker>();using(var scope4 = scope3.BeginLifetimeScope()){var w4 = scope4.Resolve<Worker>();//因为和上面不是同一个子域,所以解析出的实例w3, w4是同一个实例,但与之前的w1, w2并不是同一个实例 }}
}//无标签子域三
using(var noTagScope = container.BeginLifetimeScope())
{// 如果你尝试从一个名称并不匹配的生命周期中解析一个每个匹配生命周期作用域的组件你会得到一个异常!var fail = noTagScope.Resolve<Worker>();
}

如果你尝试从一个名称并不匹配的生命周期中解析一个每个匹配生命周期作用域的组件你会得到一个异常!

4.4.4 全局单例

即全局只有一个实例,即每一个后续请求都使用同一个实例。

注册方式:

使用SingleInstance()方法标识:

 builder.RegisterType<SingletonService>().As<ISingletonService>().SingleInstance()

对比

与默认的容器中自带的生命周期AddSingleton相同。
使用AddSingleton();注册:

services.AddSingleton<ISingletonService, SingletonService>();

还有其他生命周期补充:

  • 每个请求一个实例(Instance Per Request) : 其实是一种的“匹配作用域内单例”的一种。

  • 每次被拥有一个实例(Instance Per Owned)

  • 线程作用域(Thread Scope)

这几种在这不做详细说明,具体可以查看官网。

4.5 小结

在.NET Core中默认的容器自带生命周期只有3种类型,而相比于autofac,其显得更加丰富复杂些。

五、总结

本篇主要介绍autofac框架的使用,从注册组件,到暴露服务,及解析服务的各个过程。

同时也与.NET Core框架默认容器相比,更加丰富了一些注册方法和更复杂的生命周期,在应用上,也更加轻巧快捷,特别是在批量注册上更显实用。

文章展示的思维导图

https://www.processon.com/view/link/6072ed8863768912ae50b483

参考资料:Autofac官方网站:

https://autofaccn.readthedocs.io/en/latest/getting-started/index.html

好啦,这篇文章就先讲述到这里吧,希望对大家有所帮助。

如果有不对的或不理解的地方,希望大家可以多多指正,提出问题,一起讨论,不断学习,共同进步。🤣


---------------------
作者:nidongla
来源:CSDN
原文:https://blog.csdn.net/nidongla/article/details/115675058
版权声明:本文为作者原创文章,转载请附上博文链接!
内容解析By:CSDN,CNBLOG博客文章一键转载插件

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

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

相关文章

【ArcGIS微课1000例】0051:Geodatabase子类型操作全解

子类型是要素类中具有相同属性的要素的子集&#xff0c;或表中具有相同属性的对象的子集。可 通过它们对数据进行分类。 子类型是特征类(或对象类)中特征(或对象)的次级分类。例如一个公路线要素类可以根 据其字段类型的值细分为“高速公路”和“普通公路”两个子类型。 子类…

在Winform程序中设置管理员权限及为用户组添加写入权限

在我们一些Winform程序中&#xff0c;往往需要具有一些特殊的权限才能操作系统文件&#xff0c;我们可以设置运行程序具有管理员权限或者设置运行程序的目录具有写入的权限&#xff0c;如果是在操作系统里面&#xff0c;我们可以设置运行程序以管理员身份运行&#xff0c;或者设…

数据库性能系列之索引(上)

前言上一次&#xff0c;我们从优化子查询的角度&#xff0c;讲解了一些简单的数据库性能优化方面的知识。通过优化子查询的顺序&#xff0c;包括合理使用IN和EXISTS&#xff0c;可以起到部分查询的效率提升。但对于其他大多数场景&#xff0c;如单表记录很大&#xff0c;或多表…

【ArcGIS微课1000例】0052:创建地理数据库注记(标准注记、要素关联注记、尺寸注记)

本文讲述创建地理数据库注记(标准注记、要素关联注记、尺寸注记)的方法。 文章目录 一、创建标准注记二、创建与要素关联的注记三、创建尺寸注记一、创建标准注记 标准注记不与地理数据库中的要素关联。标准注记的一个例子是,地图上标记某山脉的文字,没有特定的要索代表该…

Lambda表达式超详解

目录 背景 Lambda表达式的用法 函数式接口 Lambda表达式的基本使用 语法精简 变量捕获 匿名内部类 匿名内部类中的变量捕获 Lambda的变量捕获 Lambda表达式在类集中的使用 Collection接口 List接口 Map接口 总结 背景 Lambda表达式是Java SE 8中的一个重要的新特性.…

【ArcGIS微课1000例】0043:ArcGIS绘制国界线的3种方法

本文讲解ArcGIS绘制国界线的3种方法。 文章目录 1. 直接修改国界线符号2. 缓冲区工具3. 制图表达1. 直接修改国界线符号 直接修改国界线/省界线的符号。点击“线要素”出现符号选择器,点击【编辑符号】按钮,编辑成下面右图的形式。缺点:只能在边界一侧出现缓冲样式,如下面…

C# 获取系统已安装的.NET版本

本文经原作者授权以原创方式二次分享&#xff0c;欢迎转载、分享。原文作者&#xff1a;唐宋元明清原文地址&#xff1a; https://www.cnblogs.com/kybs0/p/16478587.htmlC# 获取系统已安装的.NET版本获取系统已安装的.NET版本&#xff0c;来确定当前应用可运行的环境。获取系…

.NET 6 Minimal API 的经验分享

Minimal API 是 .NET 6 提供的最新功能 &#xff0c; 对比传统的 ASP.NET Core Web API 方式更加直接 , 你可以用几行代码编写好 REST API 。 没有了祖传的 Startup.cs 和 Controller &#xff0c;通过简单的代码就可以完成 API 的开发。在第二阶段的 .NET 挑战赛中就以 .NET 6…

JavaWeb之Filter过滤器

原本计划这一篇来总结JSP&#xff0c;由于JSP的内容比较多&#xff0c;又想着晚上跑跑步减减肥&#xff0c;所以今天先介绍Filter以及它的使用举例&#xff0c;这样的话还有些时间可以锻炼锻炼。言归正传&#xff0c;过滤器从字面理解她的话有拦网、过滤的功能&#xff0c;可以…

【ArcGIS微课1000例】0054:尺寸注记的创建与编辑

尺寸注记要素是一种特殊类型的文本,用于显示地图上的长度或距离,可以创建各种形状的尺寸注记要素,如对齐、简单对齐、水平线状、垂直线状和旋转线状等。 文章目录 一、创建尺寸注记1. 直接创建尺寸注记要素2. 通过已有尺寸注记要素创建二、编辑尺寸注记1. 删除尺寸注记要素2…

利用python实现批量查询ip地址归属地址

今天需要查询nginx访问的客户端ip是否和调度一样&#xff01;先是用shell把文件中的ip截取出来&#xff1a; python脚本如下&#xff1a;&#xff08;哈哈&#xff0c;新手写的很草率&#xff09;#!/usr/bin/env#-- coding: utf-8 - import jsonimport urllibimport socketimpo…

基于.NetCore开发博客项目 StarBlog - (16) 一些新功能 (监控/统计/配置/初始化)

系列文章基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客&#xff1f;基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目基于.NetCore开发博客项目 StarBlog - (3) 模型设计基于.NetCore开发博客项目 StarBlog - (4) markdown博客批量导入基于.N…

堪比JMeter的.Net压测工具 - Crank 入门篇

1. 前言 Crank 是.NET 团队用来运行基准测试的基准测试基础架构&#xff0c;包括&#xff08;但不限于&#xff09;来自TechEmpower Web 框架基准测试的场景,是2021年.NET Conf 大会上介绍的一项新的项目&#xff0c;其前身是Benchmarks。 Crank目标之一是为开发人员提供一种工…

【GlobalMapper精品教程】016:按照指定字段批量生成不同用地类型的矢量图层

Globalmapper中可以很方便的根据指定的字段,对矢量数据进行批量提取,生成不同类型的多个矢量数据,本文以土地利用现状数据为例,基于DLMC,提取出不同用地类型的矢量图层。 参考阅读:【ArcGIS遇上Python】ArcGIS Python按照指定字段批量筛选不同类型的图斑(以土地利用数据…

javascript闭包—围观大神如何解释闭包

闭包的概念已经出来很长时间了&#xff0c;网上资源一大把&#xff0c;本着拿来主意的方法来看看。 这一篇文章 学习Javascript闭包&#xff08;Closure&#xff09; 是大神阮一峰的博文&#xff0c;作者循序渐进&#xff0c;讲的很透彻。下面一一剖析。 1.变量的作用域 变量的…

.NET 20周年专访 - 张善友:.NET 技术是如何赋能并改变世界的

点击蓝字关注我们今年是 .NET 发布20周年&#xff0c;值此20周年之际&#xff0c;微软 Reactor 特别策划了 .NET 20周年系列主题专访。我们邀请了数位中国 .NET 领域的技术专家以及社区名人&#xff0c;来聊聊他们与 .NET 的情缘、认识 .NET 的契机、选择 .NET 的理由&#xff…

【ArcGIS错误异常100问】之005:ArcGIS字段计算器python中文编码问题解决

问题描述&#xff1a; 现因工作的需要&#xff0c;对照2017最新版&#xff1a;《土地利用现状分类》&#xff08;GBT 21010-2017&#xff09;&#xff0c;需根据DLMC对DLBM进行批量修改&#xff0c;如旱地是0103&#xff0c;其他林地是0307等&#xff0c;共计19种用地类型。 问…

【ArcGIS微课1000例】0055:根据图层创建自定义图例符号案例教程

在利用ArcGIS作图时,有时候需要根据线状或面状图层自己的矢量形状去创建图例项目符号,本文讲解根据图层创建自定义图例符号。 本实验使用的数据为配套案例数据包中的0055.rar中的水库数据。 文章目录 1. 添加“新建图例图面形状”工具2. 根据图层形状创建符号3. 绘制形状符号…

真魔法!图形化管理 Kafka 超轻量的自动化工具

Kafka Magic[1] 是一个用于处理 Apache Kafka 集群的 GUI 工具。它可以查找和显示消息、在 Topic 之间转换和移动消息、查看和更新模式、管理 Topic 以及自动化复杂任务。Kafka Magic 通过方便的用户界面促进 Topic 管理、QA 和集成测试。Kafka Magic Community Edition 可免费…