C#语言入门详解-27、28、29;抽象类与开闭原则;接口,依赖反转,单元测试;接口隔离,反射,特性,依赖注入

文章目录
二十七、抽象类与开闭原则

接口和抽象类是现在面向对象设计的基石。
Solid设计原则:五个设计原则的简称:单一职责原则(srp)、开放关闭原则(ocp)、替换原则(Lsp)、接口隔离原则(Isp)、依赖反转原则(Dip)。这些基本原则孕育出了设计模式。
下面通过实例来贯彻这一节课:
实例的名字可以叫做“为做基类而生的“抽象类”与“开放/关闭原则””
设计原则的重要性以及在敏捷开发中的重要性
抽象类
- 一旦一个类里有了抽象方法或其他抽象成员的话,那么这个类就变成了抽象类,类的前面也要加上abstract。
- 这个被abstract修饰的方法,他只有返回值、方法名和参数列表,他没有方法体,连{}都没有,这个就是一个完全没有被实现的方法,是抽象方法。
abstract class Student//一旦一个类里有了抽象方法或其他抽象成员的话,那么这个类就变成了抽象类,类的前面也要加上abstract
{
abstract public void Study();//这个被abstract修饰的方法,他只有返回值、方法名和参数列表,他没有方法体,连{}都没有,这个就是一个完全没有被实现的方法,是抽象方法
}
抽象类指的是函数成员没有被完全实现的类,类里面可以有若干个函数成员,但是这些函数成员里面有至少一个是像上面那样没有被实现的,也就是abstract,这样的类就是抽象类。如果所有的成员都是被实现的,那么就是抽象类的对立面,也就是具体类(Concrete Class)。
- 注意:
- 这个没有被实现的函数成员不能是private的,因为最终还要靠子类去实现,public、internal、protected的都可以。
- 一旦一个函数成员被abstract修饰的话,他是不可以有任何逻辑实现的,连{ }都没有,没有方法体。
- 一个类里有了抽象方法或其他抽象成员的话,那么这个类就变成了抽象类,类的前面也要加上abstract。
- 编译器不允许去实例化一个抽象类,那么抽象类就剩下两个作用:一是作为基类,让别人从自己这派生出去,在派生类里把自己这些没有实现的函数成员实现了;二是抽象类作为基类,他可以去声明变量,然后用一个基类类型的变量去引用一个子类类型的实例(当变量的类型和实例的类型有代差的时候,会产生多态的效果)。
- 抽象方法被子类去实现,看上去有点像用override方法去重写 virtual方法,所以抽象方法在某些编程语言当中也被称为纯虚方法,那么为什么要被叫做纯虚方法呢?因为虚方法有自己的方法体,等着子类用override方法去重写这个方法,而抽象方法连方法体都没有,所以abstract方法也叫做纯虚方法。
开闭原则
开闭原则的大意是说,如果不是为了修bug,或者添加新的功能的话,闲着没事别老去修改一个类的代码,特别是这个类当中的函数成员的代码,用一个历史悠久的描述就是:我们应该去封装那些不变的、稳定的、固定的和确定的成员,而把那些不确定的有可能改变的成员声明为抽象成员,并且留给子类去实现,所以抽象类和开闭原则天生就是一对。

为了解决上面出现的问题,先用上节课提到的虚方法和重写的方法改进:
using System;
namespace ConsoleApp26
{
class Program
{
static void Main(string[] args)
{
Vehicle v = new Car();
v.Run();
}
}
class Vehicle
{
public void Stop()
{
Console.WriteLine("Stopped!");
}
public virtual void Run()
{
Console.WriteLine("Vehicle is running...");
}
}
class Car:Vehicle//首先买了个小汽车,具有开起来和停下来的方法
{
public override void Run()
{
Console.WriteLine("Car is running...");
}
}
class Truck:Vehicle//过了一段时间又买了一辆小卡车,卡车其实也是跑起来和停下来
{
public override void Run()
{
Console.WriteLine("Truck is running...");
}
}
}
//为做基类而生的“抽象类”与“开放 / 关闭原则”
上面程序中其实Vehicle类中的Run方法并不会执行,没有什么实际意义,所以可以将Vehicle类的Run声明为纯虚函数,相应的Vehicle也变成抽象类,实现我们的抽象方法的时候也要加上override。
这样新添加一个类,并不需要对基类的成员进行修改,符合开闭原则:
using System;
namespace ConsoleApp26
{
class Program
{
static void Main(string[] args)
{
Vehicle v = new Car();
v.Run();
}
}
abstract class Vehicle
{
public void Stop()
{
Console.WriteLine("Stopped!");
}
public abstract void Run();
}
class Car:Vehicle//首先买了个小汽车,具有开起来和停下来的方法
{
public override void Run()
{
Console.WriteLine("Car is running...");
}
}
class Truck:Vehicle//过了一段时间又买了一辆小卡车,卡车其实也是跑起来和停下来
{
public override void Run()
{
Console.WriteLine("Truck is running...");
}
}
class RaceCar : Vehicle//这样新添加一个类,并不需要对基类的成员进行修改,符合开闭原则
{
public override void Run()
{
Console.WriteLine("RaceCar is running...");
}
}
}
//为做基类而生的“抽象类”与“开放 / 关闭原则”
利用基类类型的变量来引用子类的实例,就可以来玩多态,多态的使用可以让代码更加的整齐划一。
注意:
- 我们不但要会用这种我们重构好了的这种方式,也要知道刚才那两种不太合适的方式问题出现在什么地方,这样就能快速识别出来工作中项目的一些问题,并且知道怎么解决。
那么某个类当中所有的成员都是抽象的时候,可以这么写:
using System;
namespace ConsoleApp26
{
class Program
{
static void Main(string[] args)
{
Vehicle v = new Car();
v.Run();
}
}
//特别抽象-》抽象-》具体
abstract class VehicleBase//纯抽象类,纯虚类
{
abstract public void Stop();
abstract public void Fill();
abstract public void Run();
}
abstract class Vehicle:VehicleBase//推到Vehicle类实现了两个
{
public override void Stop()
{
Console.WriteLine("Stopped!");
}
public override void Fill()
{
Console.WriteLine("Pay and fill...");
}
}
class Car:Vehicle//首先买了个小汽车,具有开起来和停下来的方法
{
public override void Run()
{
Console.WriteLine("Car is running...");
}
}
class Truck:Vehicle//过了一段时间又买了一辆小卡车,卡车其实也是跑起来和停下来
{
public override void Run()
{
Console.WriteLine("Truck is running...");
}
}
class RaceCar : Vehicle//这样新添加一个类,并不需要对基类的成员进行修改,符合开闭原则
{
public override void Run()
{
Console.WriteLine("RaceCar is running...");
}
}
}
//为做基类而生的“抽象类”与“开放 / 关闭原则”
在C++中,这种纯虚类的使用很常见,但是在C#中,只需要将abstract class替换成interface,interface接口要求里边的所有的成员都是public的,若以默认的public就可以省去了,接口的写法如下:

这种接口->抽象类->具体的写法是个不错的架构。
总结
- 接口和抽象类都是“软件工程产物”:
大家看到,如果不遵循软件工程,违反开闭原则,那么用一个类就可以把所有问题搞定,如果我想让代码好维护好测试,就得来点软件工程的东西,没必要去测试用不到的东西,也就是得用抽象类 - 具体类->抽象类->接口:越来越抽象,内部实现的东西越来越少:
什么东西算实现呢?如果是一个方法成员,那么方法体就是实现;如果是数据成员,比如字段,也是实现。 - 抽象类是未完全实现逻辑的类(可以有字段和非public成员,它们代表了“具体逻辑”):
抽象类时未完全实现,接口是完全未实现。抽象类有实现逻辑的部分,也有未实现逻辑的部分 - 抽象类为复用而生:专门作为基类来使用,也具有解耦功能
- 封装确定的,开放不确定的,推迟到合适的子类中去实现:
这就是“开闭原则” - 接口时完全未实现逻辑的“类”(纯虚类;只有函数成员;成员全部public):
这里public是隐式public,不用写也不能写。 - 接口为解耦而生:“高内聚,低耦合”,方便单元测试
- 接口是一个“协约”,早已为工业生产所熟知(有分工必有协作,有协作必有协约)
- 他们都不能实例化,只能用来声明变量、引用具体类(consrete class)的实例
二十八、接口、依赖反转、单元测试

接口和解耦合
接口当中的成员方法肯定是public,而抽象类中的方法只要求不是private。
契约同时约束提供者和使用者,当多个提供者和多个使用者共同遵循同一个契约时,它们可以自由组对。
下面见实例:
C#是强类型,用来处理整型数组的方法不能用来处理ArrayList类型
在没有引入接口的时候,要想完成对一个整型数组和ArrayList类型的求和和平均值需要四个方法:
using System;
using System.Collections;
namespace ConsoleApp27
{
class Program
{
static void Main(string[] args)
{
//对存放在int数组和ArrayList中的整数进行求和和求平均值
int[] nums1 = new int[] { 1, 2, 3, 4, 5 };
ArrayList nums2 = new ArrayList { 1, 2, 3, 4, 5 };//ArrayList存在于System.Collections里面,是非泛型的
Console.WriteLine(Sum(nums2));
Console.WriteLine(Avg(nums2));
}
static int Sum(int[] nums)
{
int sum = 0;
foreach (var n in nums)//foreach语句背后的秘密:要求nums这个对象必须是可被迭代的
{
sum += n;
}
return sum;
}
static double Avg(int[] nums)
{
int sum = 0;double count = 0;
foreach (var n in nums)
{
sum += n;
count++;
}
return sum / count;
}
//重载,参水列表不一样
static int Sum(ArrayList nums)//ArrayList里面存储的是object类型
{
int sum = 0;
foreach (var n in nums)//foreach语句背后的秘密:要求nums这个对象必须是可被迭代的
{
sum +=(int) n;
}
return sum;
}
static double Avg(ArrayList nums)
{
int sum = 0; double count = 0;
foreach (var n in nums)
{
sum += (int)n;
count++;
}
return sum / count;
}
}
}
以上是没有使用接口/契约的情况,非常不方便。接口时供需双方都要遵循的一个契约,在这个例子中,供方是整型数组和ArrayList,需方是Sum和Avg两个函数,需求方只要求传进来的这个参数可以被迭代就可以了。那么如何知道这个需求方能不能被迭代呢?整型数组的基类是Array,而Array类:

可以看到Array实现了IEnumerable接口,也就是Array遵循这个契约,保证可以被迭代,ArrayList也是同样:

现在供方遵循了IEnnumerable协议,那么需方也遵循即可:

下面对接口进一步介绍,在面向对象世界里,合作的术语就叫做依赖,依赖的同时就出现了耦合,依赖越直接,耦合就越紧,下面一个例子演示什么是依赖什么是耦合:
现实当中汽车都有引擎,汽车能不能正常工作是依赖于引擎的,引擎不工作汽车肯定开不起来
紧耦合时的弊端,如果你的基础类出了问题,那你上面这个类也不会正常工作:
using System;
using System.Collections;
namespace ConsoleApp27
{
class Program
{
static void Main(string[] args)
{
var engine = new Engine();
var car = new Car(engine);
car.Run(3);
Console.WriteLine(car.Speed);
}
class Engine
{
public int RPM { get;private set; }//这里设置转速不能被外界更改
public void Work(int gas)
{
this.RPM = 1000 * gas;
}
}
class Car
{
private Engine _engine;//Car里面有一个Engine类型的字段,这两个类已经紧耦合在一起了
public Car(Engine engine)
{
this._engine = engine;
}
public int Speed { get; private set; }
public void Run(int gas)
{
_engine.Work(gas);
this.Speed = _engine.RPM / 100;
}
}
}
}
那么解决紧耦合的办法就是引入接口,下面实例,实现人与手机的解耦:
using System;
using System.Collections;
namespace ConsoleApp27
{
class Program
{
static void Main(string[] args)
{
//var user = new PhoneUser(new NokiaPhone());
//当诺基亚手机换了的时候只需要换成安立信的即可
var user = new PhoneUser(new EricssonPhone());//松耦合,只需要将坏掉的类换掉就行,不用更改任何而代码
user.UsePhone();
}
}
class PhoneUser
{
private Iphone _phone;
public PhoneUser(Iphone phone)//构造器
{
this._phone = phone;
}
public void UsePhone()
{
_phone.Dail();
_phone.PickUp();
_phone.Send();
_phone.Receive();
}
}
interface Iphone
{
void Dail();//打电话
void PickUp();//接电话
void Send();//发短息
void Receive();//收短信
}
class NokiaPhone : Iphone
{
public void Dail()
{
Console.WriteLine("Nokia calling...");
}
public void PickUp()
{
Console.WriteLine("Hello!This is Tim!");
}
public void Receive()
{
Console.WriteLine("Nokia message ring...");
}
public void Send()
{
Console.WriteLine("Hello!");
}
}
class EricssonPhone : Iphone
{
public void Dail()
{
Console.WriteLine("Ericsson calling...");
}
public void PickUp()
{
Console.WriteLine("Hello!This is Tim!");
}
public void Receive()
{
Console.WriteLine("Ericsson message ring...");
}
public void Send()
{
Console.WriteLine("Hello!");
}
}
}
记住一句话:在代码当中如果有可以替换的地方,那么就一定会有接口的存在,接口就是为了松耦合而生,为了解耦而生。解耦合最大的好处就是可以让功能的提供方变得可替换,降低紧耦合带来的提供方不能替换的高风险和高成本
依赖反转原则和单元测试
解耦在代码当中的表现就是依赖反转,也可以译为依赖倒置。 依赖反转和单元测试为什么要一起讲呢?因为单元测试其实就是依赖反转在开发当中的直接应用和直接受益者。
依赖反转原则:依赖(耦合)指的是服务的提供者和服务的使用者之间的一个依赖关系,服务的使用者依赖在服务的提供者之上,依赖越直接,耦合就越紧密;反转指的是

上图为:自顶向下逐步求精,函数和函数之间,在封装后就是类与类之间的依赖关系。那依赖反转原则,其实就是给我们一种新的思路,用来平衡自顶向下逐步求精这种单一的思维方式。是平衡而不是否定。

从右上角的流程图来看,Car和Truck依赖于Ivehicle接口,而不像左上角流程图所示的人依赖于车,依赖反转就是这么来的,如果有多个服务提供者和使用者都遵循一个协议,那么此时就可以产生很多组合:

再往后面走就可以解锁很多的设计模式了。
接下来演示接口、解耦、依赖倒置原则是怎么被单元测试所应用的:
新建测试项目:用这个xUnit,因为.NET Core自己整个就用的xUnit,所以兼容性最好;一般测试项目的名称命名为被测试项目的名字.Tests,



Program.cs
using System;
using System.Collections;
namespace ConsoleApp27
{
class Program
{
static void Main(string[] args)
{
var fan = new DeskFan(new PowerSupply());
Console.WriteLine(fan.Work());
}
}
//生产电扇的厂商,电扇有个电源,当这个电流比较大时转的比较快,电扇一般都有电流保护的功能,当这个电流过大时会断开
public interface IPowerSupply
{
int GetPower();
}
public class PowerSupply: IPowerSupply
{
public int GetPower()
{
return 100;
}
}
public class DeskFan
{
private IPowerSupply _powerSupply;//紧耦合
public DeskFan(IPowerSupply powerSupply)
{
this._powerSupply = powerSupply;
}
public string Work()
{
int power = _powerSupply.GetPower();
if (power <= 0)
{
return "Won't work.";
}
else if (power < 100)
{
return "Slow.";
}
else if (power < 200)
{
return "Work fine";
}
else { return "Explode!"; }
}
}
}
Desk FanTests.cs
using System;
using Xunit;
namespace ConsoleApp27.Tests
{
public class DeskFanTests
{
[Fact]
public void PowerLowerThanZero_OK()
{
var fan = new DeskFan(new PowrSupplyLowerThanZero());
var expected = "Won't work.";
var actual = fan.Work();
Assert.Equal(expected, actual);
}
[Fact]
public void PowerHigherThan200_Warning()
{
var fan = new DeskFan(new PowerSupplyHigherThan200());
var expected = "Warning!";
var actual = fan.Work();
Assert.Equal(expected, actual);
}
}
class PowrSupplyLowerThanZero : IPowerSupply
{
public int GetPower()
{
return 0;
}
}
class PowerSupplyHigherThan200 : IPowerSupply
{
public int GetPower()
{
return 220;
}
}
}

会发现测试结果有一个过了一个没过,现在工作环境中有一种东西叫可持续集成,每次当有人把我们的代码提交到代码库的时候,我们的代码管理工具都会把我们当前所有的unitTestCase都run一遍,如果原先能测试过的过不去了,就说明最近一次check in产生了回退,所以需要debugg,具体debugg的过程参见视频28讲视频链接1:02:00位置处。
**现在单元测试里面,为了测试不同的情况,我们得不停的去创建接口的实现类,看起来很丑,**解决办法如下:
首先,安装Moq


安装上之后,我们回到测试项目里面,下面用Moq来直接创建实现接口方法的实例,越过创建类的这一步,代码如下所示:


二十九、接口隔离、反射、特性、依赖注入
特性和依赖注入都是基于.NET反射机制的,他们之间有密切的关系。反射大多数时候都是和接口配合起来使用的,也会跟依赖反转原则一起使用。

接口隔离原则(接28节)
大接口在传给我们的功能调用者的时候,必然会有一部分功能用不到,也就违反了接口隔离原则。接口隔离原则指的是你的调用者不能多要,再这种情况下所带来的问题就是:实现这个接口的类,违反了单一职责原则(一个类只做一件事或一组事),接口隔离原则是站在接口调用者的角度上,单一职责原则是建立在服务提供者的角度上,解决方案是把这个胖接口拆分,把本质不同的功能隔离开,再用接口封装起来。
下面示例演示:
男生的开车技术一般比女生好一些,有一天女生给男生打电话说在路上追尾了,然后男生安慰女生说没事没事,下次给你买个坦克就不怕追尾了。
using System;
namespace ConsoleApplication2
{
class Program
{
static void Main(string[] args)
{
var driver = new driver(new LightTank());
driver.Drive();
}
}
class driver
{
private ITank _tank;
public driver(ITank tank)
{
this._tank = tank;
}
public void Drive()
{
_tank.Run();
}
}
interface Ivehicle
{
void Run();
}
class Car : Ivehicle
{
public void Run()
{
Console.WriteLine("Car is running...");
}
}
class Truck : Ivehicle
{
public void Run()
{
Console.WriteLine("Truck is running...");
}
}
interface ITank
{
void Fire();
void Run();
}
class LightTank : ITank
{
public void Fire()
{
Console.WriteLine("Boom");
}
public void Run()
{
Console.WriteLine("Ka Ka Ka...");
}
}
class MediumTank : ITank
{
public void Fire()
{
Console.WriteLine("Boom!");
}
public void Run()
{
Console.WriteLine("Ka! Ka! Ka!...");
}
}
class HeavyTank : ITank
{
public void Fire()
{
Console.WriteLine("Boom!!");
}
public void Run()
{
Console.WriteLine("Ka!! Ka!! Ka!!...");
}
}
}
现在可以开着坦克上路了,但是坦克里面有Run和Fire两个方法,但是Fire方法不可能用到,这里就违反了接口隔离原则。在C#里面,类和类继承的时候,只能有一个基类;而当你拿一个类或者接口去继承其他接口的时候,可以有多个基接口。
using System;
namespace ConsoleApplication2
{
class Program
{
static void Main(string[] args)
{
var driver = new driver(new LightTank());//符合接口隔离原则:我的服务者不会多要,只关注Tank里面的Run方法
driver.Drive();
}
}
class driver
{
private Ivehicle _vehicle;
public driver(Ivehicle vehicle)
{
this._vehicle = vehicle;
}
public void Drive()
{
_vehicle.Run();
}
}
interface Ivehicle
{
void Run();
}
class Car : Ivehicle
{
public void Run()
{
Console.WriteLine("Car is running...");
}
}
class Truck : Ivehicle
{
public void Run()
{
Console.WriteLine("Truck is running...");
}
}
interface IWeapon
{
void Fire();
}
interface ITank:IWeapon,Ivehicle//一个接口对其他多个接口的继承
{
}
class LightTank : ITank
{
public void Fire()
{
Console.WriteLine("Boom");
}
public void Run()
{
Console.WriteLine("Ka Ka Ka...");
}
}
class MediumTank : ITank
{
public void Fire()
{
Console.WriteLine("Boom!");
}
public void Run()
{
Console.WriteLine("Ka! Ka! Ka!...");
}
}
class HeavyTank : ITank
{
public void Fire()
{
Console.WriteLine("Boom!!");
}
public void Run()
{
Console.WriteLine("Ka!! Ka!! Ka!!...");
}
}
}
注意在使用接口隔离原则和单一职责原则的时候要注意一个度,注意接口的颗粒度(分工太细)。
下面请看第二种违反接口隔离原则的情况:
传给调用者的这个胖接口,它本身是由两个原本设计的很好的小接口合并起来的,本来应该传一个小接口进来,但是他却传了一个合并了小接口的大接口进来,这时候会产生一个问题:你有可能吧原本合格的这个服务提供者给挡在门外了:
比如:将Driver类里面的字段改成ITank类型,这样主程序里面能够传入的实例就只剩下三个了,Car和Truck就不能用了,这就是典型的传入接口过大。


- 下面通过另一个例子在解释一些这个问题:
在28节接口和解耦合中有这样一个例子:对一个整形数组和ArrayList进行求和和求均值,当时修改完Sum静态方法用的是IEnumerable接口,现在我们用ICollection接口,他们二者之间的关系是:

ICollection继承自IEnumerable,在保留可迭代的基础上增加了计数和复制值的功能,但是我们这个例子并用不到这些功能,我们将那个例子改为如下:
using System;
using System.Collections;
namespace ConsoleApplication2
{
class Program
{
static void Main(string[] args)
{
int[] nums1 = { 1, 2, 3, 4, 5 };
ArrayList nums2 = new ArrayList { 1, 2, 3, 4, 5 };
Console.WriteLine(Sum(nums1));
Console.WriteLine(Sum(nums2));
}
static int Sum(ICollection nums)
{
int sum = 0;
foreach (var n in nums)
{
sum += (int)n;
}
return sum;
}
}
}
这里Sum传入的参数为ICollection ,那么有没有这样一种集合,他只实现了我们的IEnumerable,并没有实现ICollection 接口,在,NET框架中找不到这个类,只能自己实现一个
using System;
using System.Collections;
namespace ConsoleApplication2
{
class Program
{
static void Main(string[] args)
{
int[] nums1 = { 1, 2, 3, 4, 5 };
ArrayList nums2 = new ArrayList { 1, 2, 3, 4, 5 };
Console.WriteLine(Sum(nums1));
Console.WriteLine(Sum(nums2));
var roc = new ReadOnlyCollection(nums1);
Console.WriteLine(Sum(roc));
foreach (var n in roc)
{
Console.WriteLine(n);
}
}
static int Sum(IEnumerable nums)
{
int sum = 0;
foreach (var n in nums)
{
sum += (int)n;
}
return sum;
}
}
class ReadOnlyCollection : IEnumerable
{
private int[] _array;
public ReadOnlyCollection(int [] array)
{
_array = array;
}
public IEnumerator GetEnumerator()//实现IEnumerable接口,要求外界迭代我的实例的时候,你要给人家一个迭代器IEnumerator
{
return new Enumerator(this);
}
public class Enumerator : IEnumerator
{
private ReadOnlyCollection _collection;
private int _head;
public Enumerator(ReadOnlyCollection collection)
{
_collection = collection;
_head = -1;
}
public object Current
{
get
{
object o = _collection._array[_head];//整数是struct类型,IEnumerator要返回object类型,所以必须要进行装箱
return o;
}
}
public bool MoveNext()
{
if (++_head < _collection._array.Length)
{
return true;
}
else { return false; }
}
public void Reset()
{
_head = -1;
}
}
}
}

- 下面第三个例子,用来展示C#语言一个独有的能力,叫做显式接口实现,C#在接口隔离方面做的比其他的语言都要好,都要彻底,它不但可以实现接口隔离,甚至能够把隔离出来的接口隐藏起来,直到你显式的使用这种接口类型的变量去引用实现了这个接口的实例的时候,这个接口里的方法才能够被你看见,才能够被你使用。
例子背后的故事:这个著名的电影,男主角杀手两面性,一面暖男,一面杀手,下面看着两个接口如何和在一起:
如果有个接口里面的方法,我们不是那么轻易的就让人调的话,那么就不能轻易让别人看到,这个时候就用到接口的显式实现了
**void IKiller.Kill() ** 显式接口实现,这里加了IKiller.,意思是只有我们把这个类的实例当做IKiller类型的实例来用的时候,换句话说:只有当我们拿一个IKiller类型的变量来引用我们这个WarmKiller类类型的实例的时候,这个方法才能够被调用。
注意:父类引用变量指向子类时,自动调用父类的方法和变量,无法调用子类的新方法和变量。但如果子类重写了父类的某个方法,通过父类引用变量调用时实际调用子类重写的方法。如果通过父类引用变量调用的是静态方法,即使子类中该方法被重写,调用时也仍然与父类一致,因为父类的静态方法在父类加载时就已经调用了,在对象建立之前就存在了,无法被之后出现的子类对象复写。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApplication4
{
class Program
{
static void Main(string[] args)
{
var wk = new WarmKiller();
wk.;//看不到Kill方法
IKiller killer = new WarmKiller();
killer.Kill();//这时候才能够调用kill方法,但是现在调用不了love方法,怎么解决:
//方法1:
var wk1 = killer as WarmKiller;//在变回去,比较普遍的方法
wk1.Love();
//方法2:
var wk2 = (IGentleman)killer;
wk2.Love();
var wk3 = (WarmKiller)killer;
wk3.Love();
}
}
interface IGentleman
{
void Love();
}
interface IKiller
{
void Kill();
}
class WarmKiller : IGentleman,IKiller
{
public void Love()
{
Console.WriteLine("I love you forever!");//现在用的普通的接口隔离和普通的类的实现
}
void IKiller.Kill()//显式接口实现,这里加了IKiller.,意思是只有我们把这个类的实例当做IKiller类型的实例来用的时候,换句话说:只有当我们拿一个IKiller类型的变量来引用我们这个WarmKiller类类型的实例的时候,这个方法才能够被调用
{
Console.WriteLine("Let me kill the enemy...");
}
}
}
反射(reflection)

反射不是C#语言的功能,而是.NET框架的功能,当用.NET框架的地方,你用什么编程语言都能使用反射功能。
- 镜子里面成的谁的像呢?意思就是,你给我一个对象,我能再不用new操作符的情况下,也不知道你给我的这个对象他具体是什么静态类型的情况下,我能给你创建出来一个同类型的对象,还能访问这个对象带有的各个成员。
- 可以实现进一步解耦,因为你一用new操作符,后面就得跟类型,一有类型就有依赖,而且这种类型以来还是紧耦合,反射创造的对象带来的耦合可以忽略不计。
- C#和Java这种托管类型的语言和C、C++这种原生类型的语言最大的区别中,反射肯定能算的上其中之一
- 之前学过的单元测试,一会学的依赖注入,未来要学的泛型编程,都是基于反射机制的。
- 一般不会直接只用反射,而是使用已经封装好的反射
我们为什么要使用反射?有时候我们程序的逻辑不是在写程序的时候就确定的,有时候这逻辑是到了用户和这个程序交互的时候才能确定,程序处在运动时,动态状态;程序还在编写时叫做静态状态。如果我们想让程序员在静态的情况下去预测和枚举用户有可能做的操作的话,这个程序就会变得非常的臃肿,这时候就需要程序有一种以不变应万变的能力,就是反射。
下面用两个例子见识一下反射功能:
第一个例子来看反射的原理,以及与反射密切相关的一个重要技能,叫做依赖注入。
- 当前.NET有两个大的版本,一个是可以运行在Windows上的.net FrameWork 版本,一个是可以跨平台的.netCore版本,这两个平台都有反射机制,但是他们的类库在调用的时候不太一样,现在用的是.netCore的程序,当在用.net FrameWork去做反射的时候,这个API查查手册就知道了,原理是一样的,这是其一;
- 第二,反射毕竟是动态在内存里去拿到对象与它绑定的类型的描述,再用这些描述去创建新的对象,这些过程总归是对程序的一些性能是有影响的,所以注意不要在程序中盲目的、过多的使用反射机制
直接使用反射-实例
基于接口隔离的例子进行修改:

以上便是直接使用反射。
但是一般不直接使用反射,而是使用封装好的反射,封装好的反射,其中最重要的一个功能就是依赖注入(DI,Dependency Injection)。
反射的第一个用途:依赖注入(DependencyInjection)
依赖注入和依赖反转的关系
依赖反转是DIP,依赖反转是一个概念,而依赖注入是在概念的基础之上,结合我们的接口,结合我们的反射机制所形成的这么一个应用
依赖注入-实例
依赖注入要借助于依赖注入框架

依赖注入最重要的就是他有一个东西叫做容器,我们把各种各样的类型和接口都放在容器(service provider)里面,回头我们要实例的时候就朝这个容器说你给我个实例,注册类型的时候就可以告诉容器你创建对象的时候是每次朝你要,你给我创建一个新对象呢,还是你创建一个单例模式,每次朝你要的时候都给我同一个实例,这里不关注容器怎么用,只关注DependencyInjection。
接口的实现者就是服务的提供者
using System;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
namespace ConsoleApplication2
{
class Program
{
static void Main(string[] args)
{
var sc = new ServiceCollection();//接口的实现者就是服务的提供者.ServiceCollection就是我们说的容器
//下面往容器里装东西
sc.AddScoped(typeof(ITank), typeof(HeavyTank));//在我们当前这个函数Scoped里面,你可以去要对象.现在就把一对类型放进了我们的容器
var sp = sc.BuildServiceProvider();//service provider
//分割线以上是一次性的注册,注册可以在程序启动的时候注册
//==========华丽的分割线==========
//分割线以下代表着你在程序的其他各个地方,只要你能看到我们这个ServiceProvider的地方,你都可以这么用,不再有new操作符,我们从continer里面去要对象
ITank tank = sp.GetService<ITank>();//GetService作用为service provider你能不能给我创建一个对象呢,get一个什么service呢,get一个ITank的service,上面注册了
tank.Fire();
tank.Run();
}
}
class driver
{
private Ivehicle _vehicle;
public driver(Ivehicle vehicle)
{
this._vehicle = vehicle;
}
public void Drive()
{
_vehicle.Run();
}
}
interface Ivehicle
{
void Run();
}
class Car : Ivehicle
{
public void Run()
{
Console.WriteLine("Car is running...");
}
}
class Truck : Ivehicle
{
public void Run()
{
Console.WriteLine("Truck is running...");
}
}
interface IWeapon
{
void Fire();
}
interface ITank : IWeapon, Ivehicle//一个接口对其他多个接口的继承
{
}
class LightTank : ITank
{
public void Fire()
{
Console.WriteLine("Boom");
}
public void Run()
{
Console.WriteLine("Ka Ka Ka...");
}
}
class MediumTank : ITank
{
public void Fire()
{
Console.WriteLine("Boom!");
}
public void Run()
{
Console.WriteLine("Ka! Ka! Ka!...");
}
}
class HeavyTank : ITank
{
public void Fire()
{
Console.WriteLine("Boom!!");
}
public void Run()
{
Console.WriteLine("Ka!! Ka!! Ka!!...");
}
}
}
- 使用依赖注入有什么好处呢?
好处就在于比如说我整个开发程序过程当中呢,我在程序的千千万万个地方都用到了这个tank所引用的这个实例,突然有一天我的程序升级了,说我这个ITank接口对应的实现类不再是HeavyTank,而是MediumTank,如果在程序的所有地方你都使用的ITank tank = new HeavyTank()的话,那么你程序当中有多少个new操作符,你就得改多少次把HeavyTank改成MediumTank,但是并不保证所有new的地方都要变,用了这种依赖注入只需要改一个地方就可以了 sc.AddScoped(typeof(ITank), typeof(MediumTank));
这个就是依赖注入的最基本的用法,下面演示更厉害的
static void Main(string[] args)
{
var sc = new ServiceCollection();//接口的实现者就是服务的提供者.ServiceCollection就是我们说的容器
//下面往容器里装东西
sc.AddScoped(typeof(ITank), typeof(MediumTank));//在我们当前这个函数Scoped里面,你可以去要对象.现在就把一对类型放进了我们的容器
sc.AddScoped(typeof(Ivehicle), typeof(Car));//如果小男孩要给小女孩买一个坦克,只需要把Car改成LightTank就行了
sc.AddScoped<Driver>();
var sp = sc.BuildServiceProvider();//service provider
//分割线以上是一次性的注册,注册可以在程序启动的时候注册
//==========华丽的分割线==========
//分割线以下代表着你在程序的其他各个地方,只要你能看到我们这个ServiceProvider的地方,你都可以这么用,不再有new操作符,我们从continer里面去要对象
var driver = sp.GetService<Driver>();//之前我们想创建一个Driver类的实例的时候,需要往他构造器里面传一个IVehicle类型的实例,但是现在我们的Driver和IVehicle都已经注册在我们的Continer里面了,当我们的continer创建我们的Driver的实例的时候
driver.Drive();
}
之前我们想创建一个Driver类的实例的时候,需要往他构造器里面传一个IVehicle类型的实例,但是现在我们的Driver和IVehicle都已经注册在我们的Continer里面了,当我们的continer创建我们的Driver的实例的时候,它就会去找,既然需要一个IVehicle类型的实例,那IVehicle注册的是谁呢(Car),我就把这个实例创建出来,然后塞给你的这个构造器。如果小男孩要给小女孩买一个坦克,只需要把IVehicle注册时候绑定的Car改成LightTank就行了,你不用去关心new这个Driver的时候传进去的是什么类型的对象,这就是依赖注入的强大功能,注入体现在:用我们注册的类型创建的实例给注入到Driver构造器里面去了
如何使用反射追求更松的耦合-以不变应万变的能力
- 更松的耦合一般用在插件式编程,开发多少插件是不知道的,你不可能在写主体程序的时候就把未来的所有插件预测出来、枚举出来。一般情况下,主体程序都会发布包含有程序开发接口(API)的程序开发包(SDK),使用SDK中的API呢,第三方在开发插件的时候就会比较容易,插件也能比较标准、高效的与主体程序对接。API(程序开发接口)里面不一定都是接口,也有可能是一组函数、一组类、一组接口。
- 那么既然有了反射,为什么还要用SDK里面的API呢?是因为我们用反射的时候呢,可以很容易的把耦合降低到忽略不计,当耦合忽略不计的时候写程序就非常自由了,一自由没有约束就很容易出错(比如方法大小写写错,找不到方法),所以为了避免第三方插件开发者犯这种没有必要小错,我们就用SDK里面的API约束一下开发者的开发,也减轻以下开发者的劳动。
- 在下面的实例中进行演示:
实例背景:我现在是一个婴儿车的生产厂商,第一方程序员,我们的婴儿车有一个小面板,这个小面板上面有一些按钮,第一排是一堆小动物的头像,第二排是一排数字123456789,现在小婴儿按一下小动物的头像,再按一下数字,这个小动物就会叫这么多次,这个婴儿车呢会默认的提供几种小动物,如果销路比较广呢,我们就应该允许第三方的开发者帮助我们开发更多小动物的头像和叫声,我们只需要给我们的面板留一个usb的插口,其他厂商就可以把他们生产出来的小动物装在u盘里边,卖给婴儿车的主人。
- 首先,作为主体程序是一个可执行程序,选择.netcore下面的App
- 这个主体程序,我们会从这个程序运行的地方,下面有一个文件夹,叫做Animals里面,去把第三方小动物插件加载进来,去调用所有小动物类的Voice方法,约定好Voice的V要大些,而且Voice方法还能接收一个整数的参数,就是小动物叫几次。
- 这个主题程序里主要写利用反射来加载插件,并且拿到这些动物类,然后创建实例并且调用这个实例的Voice方法。


主体程序:
using System;
using System.IO;
using System.Collections.Generic;
using System.Runtime.Loader;
namespace BabyStoller
{
internal class Program
{
static void Main(string[] args)
{
var folder = Path.Combine(Environment.CurrentDirectory, "Animals");//Environment.CurrentDirectory为程序运行的目录,合起来就是当前目录加子目录
var files = Directory.GetFiles(folder);//这里我们需要把这个path里面所有的所有.dll文件给load进来
var animalTypes = new List<Type>();
foreach (var file in files)
{
var assemably = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
//Load进来这个assemably之后,我们需要把assemably里面所有的动物类型加载到animalTypes里面去
var types = assemably.GetTypes();
foreach (var t in types)
{
if (t.GetMethod("Voice")!=null)
{
animalTypes.Add(t);
}
}
}
while (true)
{
for (int i = 0; i < animalTypes.Count; i++)
{
Console.WriteLine($"{i + 1}.{animalTypes[i].Name}");
}
Console.WriteLine("===================");
Console.WriteLine("Please choose animal:");
int index = int.Parse(Console.ReadLine());//让用户在命令行输入一个整数,这个整数必须介于1和animalTypes.Count之间
if (index>animalTypes.Count||index<1)
{
Console.WriteLine("No such an animal.Try again!");
continue;
}
Console.WriteLine("Please input the times:");
int times = int.Parse(Console.ReadLine());//在输入一个数字,小动物叫多少次
var t = animalTypes[index - 1];
var m = t.GetMethod("Voice");
var o = Activator.CreateInstance(t);//用这个类型创建一个对象,在这我需要调用这个方法,这次在调用这个方法的时候,要传进去一个参数times
m.Invoke(o, new object[] { times }); //传数组是因为静态时期编译器无法识别你镜像的函数的参数列表是什么,只能用一个数组接收你的参数列表,在运行时再把参数注入进这个方法上
//这里和前面反射例子一样,只是多了个参数,参数用object数组,如果多个参数不同类型逗号隔开就可以,因为不管参数什么类型基类都是object
}
}
}
}
主体程序写完后,看看第三方的开发商,包括第一方开发商在内,如何开发小动物的插件
- 创建新的项目:.NET Core的类库



下面模仿插优盘,把这两个类库搁到我们主体程序的Animals文件夹里面


- 以上用的是纯的反射,很容易犯错误,如果插件开发商的Voice的v写成了小写,那么我们的主体程序就会把这个类过滤掉,就看不到他了,为了帮助第三方的插件开发商,一般第一方会发布一个SDK,帮助第三方快速开发。
- 这个SDK,第一准备一个叫做IAnimal的接口,接口里面就一个Voice方法,所有开发动物类的厂商,你都要是实现这个接口,这样我保证里边就有Voice这个方法,回头创建出来这个对象之后,我把这个对象直接转化成接口类型的对象,然后就可以不用这种弱类型的
m.Invoke(o, new object[] { times });方式来调用,直接用接口里面的Voice方法就可以了。还有我考虑到开发商有时开发的小动物有可能没开发完,他也一块放到了类库里面了,现在我再给他加一个类型,一种artibute类型,如果你觉得你这个小动物开没有开发完的话,你就把这个artibute呢attach到这个小动物上,然后我再bload它就可以了,下面看这个SDK怎么开发: - 这个SDK跟主体程序要分开,是另一个类库,然后这个类库要以dll的形式,我不会把原码给第三方,让他们开发程序的时候引用这个dll就可以了


除了接口外,再加一个class,叫做UnFinashedAttribut,UnFinashedAttribut类他一般要求后面加一个Attribut后缀,但是当你用这个类的时候,这个后缀可以不写。这个类里什么也不用做,然后这个SDK就可以发布了。
(就是未完成的插件可以用这个特性进行标记,当sdk的提供商扫描到这个特性的时候就知道这个插件还没有完成,就会忽略它的调用)

生成解决方案后,打开SDK所在位置

接下来跟第三方的人说,你们可以下载这个SDK搞开发,要配上一个说明书说明里面有什么接口有什么类,之后呢作为主体厂商,我自己的程序首先的引用我的这个SDK:

然后回到第三方开发动物插件的地方

对于每个动物类来说要修改的代码并不多,只需要引用接口就可以了

开发过程中呢,发现牛的这个类没有开发完,不想让它出现在面板上

重新生成解决方案后,再把新的dll放到主体程序的Animals文件夹里

接下来是主体程序更新:

有了SDK之后,我们就不用这种笨的办法了,我们只需要判断一下这个类型,它是不是IAnimal接口的实现类型,并且没有被我们UnFinashedAttributed所修饰就可以了(Attributed是特征/特性,其作用是让你在使用反射的时候,通过反射拿到一个方法或一个类,看他有没有被某一个attribute所修饰,然后再去做决定调用它还是不调用它,放弃还是保留)
应用SDK的方法:

更多推荐



所有评论(0)