C#学习(15)------反射、特性、依赖注入

反射常常与接口、依赖反转原则一起使用。反射事实上是.Net框架的内容,不是C#语言的内容。

对于托管类语言,反射很重要。单元测试、依赖注入、泛型编程,都基于反射机制。

反射的实质其实就是:给我一个对象,我能在不知道它是什么静态类型,且不使用new操作符的情况下,再创建出一个与它同类型的新对象,并且能够访问其方法。

从定义来看,反射有两方面好处。

一方面,我们知道,使用new操作符,即是创造了一个紧耦合,直接地将所在类与new后的静态类型紧耦合在了一起。而使用反射,直接避免紧耦合的发生,降低了耦合度。

另一方面,在具体的工程中,在编写程序阶段,因为用户的请求很多变,你很难预测用户需求,不可能写成百上千的if-else语句使用静态的类型枚举出所有可能结果。这就体现了反射的动态效果。反射是在编写程序阶段,不确定具体逻辑的时候存在的。程序需要以不变应万变的能力,这个能力就是反射。

但是注意,不要过多使用反射,以影响程序的性能。

        static void Main()
        {
            ITank tank = new HeavyTank();
            //----------------------------
            var t = tank.GetType();
            object o = Activator.CreateInstance(t);
            MethodInfo fireMe = t.GetMethod("Fire");
            MethodInfo runMe = t.GetMethod("Run");
            fireMe.Invoke(o, null);
            runMe.Invoke(o, null);
        }

反射长相如此。

但是一般不会直接使用反射,而是会使用封装好了的反射。而封装好了的反射最重要的功能就是依赖注入。

依赖注入最重要的是容器container,即sevice provider。其中装着很多类型和对应的接口,要实例的时候就向他要就行。

using Microsoft.Extensions.DependencyInjection;
        static void Main()
        {
            var serviceCollection = new ServiceCollection();//容器
            serviceCollection.AddScoped(typeof(ITank),typeof(HeavyTank));
            //typeof拿到动态类型描述;第一个参数是接口类型,第二个是实现接口类型的类型。
            var serviceProvider = serviceCollection.BuildServiceProvider();
            //-----------------一次性的注册-------------------------------接下来不再有new
            ITank tank = serviceProvider.GetService<ITank>();
            tank.Fire();
            tank.Run();
        }

这样做的好处是什么呢?比如说程序有一个地方要改,不要heavytank了,全部变成lighttank。这时候如果用new操作符,就得改多处,而且还不能用一键替换,因为你不知道哪里要改哪里不用改。如果使用这样的封装反射,就只用改第二行就行。

但是反射的强大不仅仅在此。

    class Program
    {
        static void Main()
        {
            var serviceCollection = new ServiceCollection();//容器
            serviceCollection.AddScoped(typeof(ITank), typeof(HeavyTank));
            serviceCollection.AddScoped(typeof(IVehicle), typeof(Car));
            serviceCollection.AddScoped<Driver>();
            //typeof拿到动态类型描述;第一个参数是接口类型,第二个是实现接口类型的类型。
            var serviceProvider = serviceCollection.BuildServiceProvider();
            //-----------------一次性的注册-------------------------------
            var driver = serviceProvider.GetService<Driver>();
            driver.Drive();
        }
    }
    class Driver
    {
        private IVehicle _vehicle;
        public Driver(IVehicle vehicle)
        {
            _vehicle = vehicle;
        }
        public void Drive()
        {
            Console.WriteLine("I am driving a {0}.",_vehicle.GetType().Name);
        }
    }

Driver这个类本来要求产生实例时需要构造传入一个IVehicle,现在没传。容器会自动帮你在它已有的类型里面找到一个IVehicle接口的类,然后帮你自动创造一个实例传进去。比如这里就传进去了一个Car。

注意此处似乎类型还挺严格的。

Driver要求一个IVehicle类,虽然IVehicle->ITank,但是不允许呢。

同样的,如果有一个TankDriver类,要求一个ITank,给一个IVehicle

也是不允许的。

再注意一个点,如果有多个IVehicle,会取最新的那个。这个新体现在代码先后上,比如说:

这时候司机开的就是Truck了。 

于是我在想,这个“新版本”会不会跟子类父类有关呢?因而此处,我创建一个Car的子类Rolls_Royce:

 

这样依然是后面的被引用,没啥好说。

 

这样是Car被引用,依然是后面的被引用。 

这说明所谓的“最新的”,其实只是添加到容器的先后顺序而已,不是这种子父类的版本问题。

于是我在想,是不是容器是采用类似栈什么的结构?遍历下来,自然是遍历得少省事,所以优先选取先放进去的。

 这里,依赖注入体现在,把容器创建的实例注入到构造器。

依赖注入在别的语言也是有的,比如在java中,这玩意叫作自动牵线。

 看完了依赖注入这个例子,我们来看看反射是如何进行解耦合,产生更松的耦合的,即,反射是如何让程序有“以不变应万变”的能力的。

这常常用在插件式编程上。

什么是插件式编程呢?就是一个玩意,由一个主体程序,和若干个插件组成。插件不与主体程序一起编译,但是会与其一起工作。插件往往由第三方提供,而且可能在主体程序完成后再插。这样的编程模式,有利于形成以主体程序为中心的生态圈。

这类似于游戏mod、创意工坊之类的吧。

因而,一般的主体程序都会发布包含程序接口(API)的开发包(SDK)。

在这样的模式中,主体程序就是所谓“不变”,插件就是所谓“万变”。因为开发主体程序时显然不能预测出未来可能有的插件,并枚举出来。

而为了避免第三方开发者犯小错误【如把Run方法写成run方法,导致找不到这个方法】,需要用SDK里的API【就是接口,写程序时也有留意到吧,接口对方法很严格的】来约束开发者开发,同时也能帮助开发者减轻劳动。

下面,我们通过实例来理解这个思想。

这次例子是那种婴儿车,上面配置有小动物和数字按钮。按一下小动物的头像,再按一下数字,就能让小动物叫数字次的声音。而动物数量可能会有其他人想多加一些,这时候就需要有插件了。

下面,我们先来编写主体程序。

首先,我们获取程序所在文件夹:

 

 在此处建立一个Animal文件夹:

接下来,我们编写主体程序,包括导入Animal里面的动物和编写使用逻辑两个方面。

首先是导入Animal里的动物的代码逻辑:

namespace AnimalBaby
{
    class Program
    {
        static void Main(string[] args)
        {
            //load

            var folder = Path.Combine(Environment.CurrentDirectory, "Animal");
            var files = Directory.GetFiles(folder);
                 //创建用于存储动物类型的数据组
            var animalTypes = new List<Type>();
                 //两个循环。第一层遍历所有文件,第二层遍历一个文件中的所有类。
            foreach (var file in files)
            {
                        //得到每个小dll文件里的所有类
                var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
                var types = assembly.GetTypes();
                foreach (var t in types)
                {
                    if (t.GetMethod("Voice")!=null)//找到dll文件中的动物类
                    {
                        animalTypes.Add(t);
                    }
                }
            }

            //load end
        }
    }
}

 接下来就是激动人心的使用反射了,相当于模仿一个插u盘的过程:

 注意一下反射在这里的不可替代性。t是动态的,反射可以根据t自由选取所需小动物,并且前面已经对没有“Voice”方法的t过滤了,故而可以放心调用。真是完美啊,太牛逼了,反射!

写完了主体程序,接下来让我们写各种各样的小动物吧。【略】

最终主体程序代码如下:

namespace AnimalBaby
{
    class Program
    {
        static void Main(string[] args)
        {
            //load
            var folder = Path.Combine(Environment.CurrentDirectory, "Animal");
            var files = Directory.GetFiles(folder);
            var animalTypes = new List<Type>();
            foreach (var file in files)
            {
                var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
                var types = assembly.GetTypes();
                foreach (var t in types)
                {
                    if (t.GetMethod("Voice")!=null)
                    {
                        animalTypes.Add(t);
                    }
                }
            }
            //load end

            //具体逻辑
            while (true)
            {
                for (int i = 0; i < animalTypes.Count; i++)
                {
                    Console.WriteLine("{0} : {1}", i + 1, animalTypes[i].Name);
                }
                Console.WriteLine("=====================");
                Console.WriteLine("Please choose the animal.");
                int choice = int.Parse(Console.ReadLine());
                if (choice > animalTypes.Count || choice < 1)
                {
                    Console.WriteLine("You have made an ilegal choice!");
                    continue;
                }
                Console.WriteLine("How many times you want it to cry?");
                int times = int.Parse(Console.ReadLine());

                var t = animalTypes[choice - 1];//小动物类型,Type
                var m = t.GetMethod("Voice");//保存叫的方法
                object o = Activator.CreateInstance(t);//使用反射创建实例
                m.Invoke(o, new object[] { times });
            }
        }
    }
}

运行效果:

 事到如今,尚感到一些不足,就是可能开发者会把Voice这个方法写错。为了约束这一点,且提供便利,我们可以使用SDK。

下面就来看看怎么弄出一个SDK来。

感觉学完这节课,跟着做了个小项目啊~总结一下吧。

这个项目的核心难点,首先一个就是使用反射,动态根据插件里有什么东西,我就创建什么样的实例来执行那个东西所具有的方法。这里的方法应该是各个东西之间的共性,可变的是种类,不变的是共同的方法。

注意反射的基本过程。得到type t-得到方法m-声明object o,以t为type-o为参数调用方法m

第二个,就是装载程序的方法。我们可以看到整体思路是,先找到文件夹所在地址,再筛选出其中的所有dll文件,再创建一个装这些类型的Type数据组,然后把所有dll文件、所有符合要求的类都筛选出来,存入到Type数据组之中。

关于这些类型的使用方法,这里用到了两个。一个是得到他的名字,一个是用它来声明实例(反射)。由此可见,这个方法十分具有通用性,对那些需要读取一大堆类的项目来说。

 

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值