一、接口和抽象类简要概述
- 接口和抽象类都是"软件工程产物"
- 具体类 -> 抽象类 -> 接口:越来越抽象,内部实现的东西越来越少
- 如果是一个方法成员的话,它的方法体就是它的实现
- 字段也代表一种实现,它实现了类怎么去存储数据
- 抽象类是未完全实现逻辑的类(可以有字段和非public成员,它们代表了"具体逻辑")
- 抽象类为复用而生:专门作为基类来使用,也具有解耦功能
- 封装确定的,开放不确定的,推迟到合适的子类中去实现
- 接口是完全未实现逻辑的"类"("纯虚类";只有函数成员;成员全部public)
- 接口为解耦而生:"高内聚,低耦合",方便单元测试
- 接口是一个"协约",早已为工业生产所熟知(有分工必有协作,有协作必有协约)
- 它们都不能实例化,只能用来声明变量、引用具体类(concrete class)的实例
二、实际案例
为作基类而生的"抽象类" & 开放/关闭原则(开闭原则)
1. 抽象类的感性认识
一个被abstract所修饰的方法,只有返回值、方法名、和参数列表,没有方法体(连花括号都没有,不能有任何逻辑实现),这个就是一个完全没有被实现的方法,也就是抽象方法。abstract public void Study();抽象方法不能是private,因为它会被抽象类的子类去实现,private的话子类都没办法访问,何谈去实现这个抽象方法;internal、public、protected都可以,因为子类可以访问到
一旦一个类中有了抽象方法或者其他抽象成员,这个类就成了抽象类,抽象类前面必须加abstract修饰
csharp
abstract class Student // 抽象类
{
abstract public void Study(); // 抽象方法
}
抽象类中含有为被实现的函数成员(没有具体的行为),编译器不允许实例化一个抽象类
一个类不能被实例化,就只剩下俩个作用:
- 作为基类;让别人从自己这边派生,在派生类中把没有实现的函数成员给实现了
- 抽象类作为基类,可以去声明变量,用基类类型的变量去引用一个子类类型的实例,在子类里面已经实现了这个抽象类中未被实现的方法;也就是多态:父类变量引用子类实例;当变量和实例之间有代差时,会产生多态效果
- 抽象方法被子类所实现有点像用override方法去重写virtual方法,所以抽象方法在某些编程语言中(c++)也被成为纯虚方法;
- 虚方法有方法体,等着子类用override去重写这个方法;抽象方法连方法体都没有,所以abstract方法又被称为纯虚方法
抽象类:函数成员没有被完全实现 的类
抽象类中可以有若干个函数成员,但函数成员里面至少有一个像刚才那样没有被实现的
若这些函数成员都被实现了,那就是具体类(concrete class)
2. 开闭原则的感性认识
如果不是为了修Bug或者添加新功能的话,闲着没事别老去修改一个类的代码,特别时这个类当中的函数成员代码
我们应该封装那些不变的、稳定的、固定的、和确定的成员,把哪些不确定的、有可能改变的成员声明为抽象成员,并且留给子类去实现
3. 实际案例分析
案件分析:你有一辆小汽车(car),有run和stop功能;十年后,你买了一辆卡车(truck),也有run和stop功能;二十年后,你可以会买一辆拖拉机(tractor),也有run和stop功能;...
其中无论什么样类型的车,stop功能是完全一样的,只不过run方法各有不同
此时就可以抽离出来它们的共性,都是交通工具(vehicle),都有stop和run功能
- stop功能完全一样,可以直接放在vehicle这个基类中
- run功能各不相同,可以通过多态去实现(父类用virtual虚函数,子类用override)
csharp
using System;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Vehicle v_car = new Car();
v_car.Run();
v_car.Stop();
Vehicle v_truck = new Truck();
v_truck.Run();
v_truck.Stop();
Vehicle v_tractor = new Tractor();
v_tractor.Run();
v_tractor.Stop();
}
}
class Vehicle
{
public void Stop() { Console.WriteLine("Stopped!!!"); }
public virtual void Run() { Console.WriteLine("Vehicle running"); }
}
class Car : Vehicle
{
public override void Run() { Console.WriteLine("Car running"); }
}
class Truck : Vehicle
{
public override void Run() { Console.WriteLine("Truck running"); }
}
class Tractor : Vehicle
{
public override void Run() { Console.WriteLine("Tractor running"); }
}
}
问题发现了:基类Vehicle中的public virtual void Run() { Console.WriteLine("Vehicle running"); }压根没有用,因为子类都override重写父类的方法了,故可以把方法体给直接删掉
没有方法体的虚函数,可以直接用abstract修饰,得到抽象方法,public abstract void Run();
有抽象方法的类必须是抽象类,abstract class Vehicle
抽象类的子类去通过override重写抽象方法,这样就看的简洁很多
csharp
using System;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Vehicle v_car = new Car();
v_car.Run();
v_car.Stop();
Vehicle v_truck = new Truck();
v_truck.Run();
v_truck.Stop();
Vehicle v_tractor = new Tractor();
v_tractor.Run();
v_tractor.Stop();
}
}
abstract class Vehicle
{
public void Stop() { Console.WriteLine("Stopped!!!"); }
public abstract void Run(); // 改动唯一位置
}
class Car : Vehicle
{
public override void Run() { Console.WriteLine("Car running"); }
}
class Truck : Vehicle
{
public override void Run() { Console.WriteLine("Truck running"); }
}
class Tractor : Vehicle
{
public override void Run() { Console.WriteLine("Tractor running"); }
}
}
若抽象类里面全是抽象方法会咋样?
定义一个VehicleBase类,里面全是抽象方法,让Vehicle类去继承这个VehicleBase类
csharp
abstract class VehicleBase
{
abstract public void Stop();
abstract public void Run();
}
完整代码:
csharp
using System;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Vehicle v_car = new Car();
v_car.Run();
v_car.Stop();
Vehicle v_truck = new Truck();
v_truck.Run();
v_truck.Stop();
Vehicle v_tractor = new Tractor();
v_tractor.Run();
v_tractor.Stop();
}
}
abstract class VehicleBase
{
abstract public void Stop();
abstract public void Run();
}
abstract class Vehicle: VehicleBase
{
public override void Stop() { Console.WriteLine("Stopped!!!"); }
// public abstract void Run(); // 因为VehicleBase基类已经有了抽象方法Run,这里就没必要再次去声明了
}
class Car : Vehicle
{
public override void Run() { Console.WriteLine("Car running"); }
}
class Truck : Vehicle
{
public override void Run() { Console.WriteLine("Truck running"); }
}
class Tractor : Vehicle
{
public override void Run() { Console.WriteLine("Tractor running"); }
}
}
把VehicleBase抽象类(abstract)替换成接口interface
- 接口要求所有成员都必须是public,故原先的public就可以去掉了
- 接口本身就包含纯抽象类的含义,所有的成员一定都是抽象的,所以abstract也可以去掉
- 接口的抽象成员abstract被去掉了,所以子类对应重写override也可需要去掉
- 接口作为基类命名方式一般以I开头,如IVehicle
- 接口有ABC,抽象类可以继承接口,实现AB,对于C可以通过abstract保留抽象,让另一个类去继承抽象类即可
csharp
using System;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Vehicle v_car = new Car();
v_car.Run();
v_car.Stop();
Vehicle v_truck = new Truck();
v_truck.Run();
v_truck.Stop();
Vehicle v_tractor = new Tractor();
v_tractor.Run();
v_tractor.Stop();
}
}
interface IVehicle // 定义接口
{
void Stop();
void Run();
}
abstract class Vehicle: IVehicle
{
public void Stop() { Console.WriteLine("Stopped!!!"); }
public abstract void Run(); // 可以不实现接口中的Run方法,把Run方法搞成抽象方法,留给子类去实现
}
class Car : Vehicle
{
public override void Run() { Console.WriteLine("Car running"); }
}
class Truck : Vehicle
{
public override void Run() { Console.WriteLine("Truck running"); }
}
class Tractor : Vehicle
{
public override void Run() { Console.WriteLine("Tractor running"); }
}
}
三、接口
接口是由抽象类进化而来
- 接口中的"抽象方法"必须都是public的,因为必须都是public,所以都不需要写
- 这个成员访问级别public,决定了接口的本质
- 接口的本质:服务的调用者/服务的消费者 与服务的提供者之间的契约(contract);契约就必须透明,对双方都可见;契约使得自由合作成为可能
- 自由合作:既约束服务的使用者,也约束服务的提供者
- 抽象类中的抽象方法只要不是private就行
- 抽象类中的protected修饰的成员只能够让自己的子类看得到
- internal,出了当前这个程序集别人也都看不见
- protected和internal都不是给功能调用者准备的,各自都有各自特定的可见目标
接口与单元测试
- 接口的产生:自底向上(重构),自顶向下(设计)
- C#中接口的实现(隐式,显式,多接口)
- 语言对面向对象设计的内建支持:依赖反转,接口隔离,开/闭原则
1. 接口案例
对一组整数求和和求平均值操作
整数分别由int类型的数组和ArrayList数组存放
常规做法:重载两个函数即可
缺点:代码重复
csharp
using System;
using System.Collections;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
int[] nums1 = new int[] { 1, 2, 3, 4, 5};
ArrayList nums2 = new ArrayList() { 11, 12, 13, 14, 15 };
Console.WriteLine($"int数组求和:{Sum(nums1)}");
Console.WriteLine($"int数组求平均值:{Average(nums1)}");
Console.WriteLine($"ArrayList求和:{Sum(nums2)}");
Console.WriteLine($"ArrayList求平均值:{Average(nums2)}");
}
static int Sum(int[] nums)
{
int sum = 0;
foreach (int num in nums)
{
sum += num;
}
return sum;
}
static double Average(int[] nums)
{
int sum = 0;double count = 0;
foreach (int num in nums)
{
sum += num;
count++;
}
return sum / count;
}
static int Sum(ArrayList nums)
{
int sum = 0;
foreach (int num in nums)
{
sum += num; // ArrayList存储的元素类型是object,正常需要强转一下,sum += (int)num; 新版编译器会自动转换类型
}
return sum;
}
static double Average(ArrayList nums)
{
int sum = 0; double count = 0;
foreach (int num in nums)
{
sum += num; // ArrayList存储的元素类型是object,正常需要强转一下,sum += (int)num; 新版编译器会自动转换类型
count++;
}
return sum / count;
}
}
}
可以明显看到,代码重复了
int数组和ArrayList数组都实现了可迭代的IEnumerable接口
所有 C# 数组(int []、string []、double []、自定义类 [])都隐式实现:
IEnumerable
IEnumerable
ICollection
IList
这是 CLR 底层规定,数组是特殊类型,编译器会自动让它支持枚举。

故,可通过传入IEnumerable接口类型变量进行迭代求和和求平均值
csharp
using System;
using System.Collections;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
int[] nums1 = new int[] { 1, 2, 3, 4, 5};
ArrayList nums2 = new ArrayList() { 11, 12, 13, 14, 15 };
Console.WriteLine($"int数组求和:{Sum(nums1)}");
Console.WriteLine($"int数组求平均值:{Average(nums1)}");
Console.WriteLine($"ArrayList求和:{Sum(nums2)}");
Console.WriteLine($"ArrayList求平均值:{Average(nums2)}");
}
static int Sum(IEnumerable nums)
{
int sum = 0;
foreach (int num in nums)
{
sum += num;
}
return sum;
}
static double Average(IEnumerable nums)
{
int sum = 0;double count = 0;
foreach (int num in nums)
{
sum += num;
count++;
}
return sum / count;
}
}
}
2. 依赖
人是群居生物,在这个社会上生活避免不了合作
在面向对象世界里的合作成为依赖
依赖的同时就出现了耦合,依赖越直接,耦合越紧
依赖案例
现实世界中,汽车有引擎,汽车能否正常工作,依赖于引擎
引擎不工作,汽车肯定开不起来
一个Engine引擎类
- int型的RPM转速属性,set方法为private,因为没办法从外界设置发动机转速,发动机转速是输出
- public类型、返回值为void的Work方法、参数是int类型的gas表示油耗大小,RPM = gas * 1000;
csharp
class Engine
{
public int RPM { get; private set; } // 转速,外界只能get,不可以set
public void Work(int gas)
{
this.RPM = gas * 1000; // 假设转速 = 油耗 * 1000
}
}
一个Car汽车类
- private的Engine类型字段(此时Car已经依赖到Engine上了)
- 构造函数传入一个Engine
- int型的Speed属性,set方法也设置为private
- public类型、返回值为void的Run方法,参数是int类型的gas油耗,Car的Run表示让汽车动起来,也就是调用Engine和Work方法
- Speed = Engine的RPM / 100,速度=发动机转速/100
csharp
class Car
{
private Engine engine_; // 此时Car已经依赖到Engine上了
public Car(Engine engine)
{
this.engine_ = engine; // 创建Car对象的时候,必须给它一个Engine对象
}
public int Speed { get; private set; }
public void Run(int gas)
{
engine_.Work(gas); // 调用Engine的Work方法
this.Speed = engine_.RPM / 100; // 假设车速 = 转速/100
}
}
主函数调用:
csharp
static void Main(string[] args)
{
Engine engine = new Engine();
Car car = new Car(engine);
car.Run(10); // 传入一个油耗gas
Console.WriteLine($"Car此时的速度为:{car.Speed}"); // 油耗10 == 转速10*1000 == 速度1000/100
}
完整代码:
csharp
using System;
using System.Collections;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Engine engine = new Engine();
Car car = new Car(engine);
car.Run(10); // 传入一个油耗gas
Console.WriteLine($"Car此时的速度为:{car.Speed}"); // 油耗10 == 转速10*1000 == 速度1000/100
}
class Engine
{
public int RPM { get; private set; } // 转速,外界只能get,不可以set
public void Work(int gas)
{
this.RPM = gas * 1000; // 假设转速 = 油耗 * 1000
}
}
class Car
{
private Engine engine_; // 此时Car已经依赖到Engine上了
public Car(Engine engine)
{
this.engine_ = engine; // 创建Car对象的时候,必须给它一个Engine对象
}
public int Speed { get; private set; }
public void Run(int gas)
{
engine_.Work(gas); // 调用Engine的Work方法
this.Speed = engine_.RPM / 100; // 假设车速 = 转速/100
}
}
}
}
Car和Engine是紧耦合的,也就是说一旦Engine出现了问题,Car必定会出现问题
3. 消除依赖
引入接口可以解决紧耦合问题
接口本质就是一个契约,用来约束功能
假设手机本质就三个功能:打电话、接电话、发短信、收短信
当我有一台手机的时候,那我肯定知道手机有这四个功能
约束消费者:消费者肯定相信手机一定有这四个功能
约束手机厂家:无论哪个厂商,只要宣称自己的产品是手机,就必须去实现这四个功能
接口好处:
你一开始用的是xiaomi,突然手机坏了,那么就算换成huawei、vivo、oneplus等品牌手机,你也可以拿来直接用,因为你就用这四个功能,人和手机之间是解耦的
案例实现
- 调用手机接口IPhone,有四个功能
csharp
interface IPhone
{
void Dail();
void PickUp();
void Send();
void Receive();
}
- xiaomi手机实现IPhone接口
csharp
class XiaomiPhone : IPhone
{
public void Dail()
{
Console.WriteLine("Dail Xiaomi Phone");
}
public void PickUp()
{
Console.WriteLine("PickUp Xiaomi Phone");
}
public void Receive()
{
Console.WriteLine("Receive Xiaomi Phone");
}
public void Send()
{
Console.WriteLine("Send Xiaomi Phone");
}
}
- Huawei手机实现IPhone接口
csharp
class HuaweiPhone : IPhone
{
public void Dail()
{
Console.WriteLine("Dail Huawei Phone");
}
public void PickUp()
{
Console.WriteLine("PickUp Huawei Phone");
}
public void Receive()
{
Console.WriteLine("Receive Huawei Phone");
}
public void Send()
{
Console.WriteLine("Send Huawei Phone");
}
}
- 用户类
- 内部有个private的IPhone接口类型字段
- 构造器接受一个IPhone接口类型的变量
- 一个UsePhone方法,调用手机的四个方法
csharp
class PhoneUse
{
private IPhone phone_;
public PhoneUse(IPhone phone)
{
this.phone_ = phone;
}
public void UsePhone()
{
phone_.Dail();
phone_.PickUp();
phone_.Receive();
phone_.Send();
}
}
- 主函数
- 创建手机用户,传入一个IPhone实例,xiaomi和huawei都实现了IPhone这个接口
- 调用UsePhone方法
csharp
static void Main(string[] args)
{
//PhoneUse phoneUse = new PhoneUse(new XiaomiPhone());
PhoneUse phoneUse = new PhoneUse(new HuaweiPhone()); // 换手机了,只需要修改手机对象即可
phoneUse.UsePhone();
}
完整代码:
csharp
using System;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
//PhoneUse phoneUse = new PhoneUse(new XiaomiPhone());
PhoneUse phoneUse = new PhoneUse(new HuaweiPhone()); // 换手机了,只需要修改手机对象即可
phoneUse.UsePhone();
}
interface IPhone
{
void Dail();
void PickUp();
void Send();
void Receive();
}
class XiaomiPhone : IPhone
{
public void Dail()
{
Console.WriteLine("Dail Xiaomi Phone");
}
public void PickUp()
{
Console.WriteLine("PickUp Xiaomi Phone");
}
public void Receive()
{
Console.WriteLine("Receive Xiaomi Phone");
}
public void Send()
{
Console.WriteLine("Send Xiaomi Phone");
}
}
class HuaweiPhone : IPhone
{
public void Dail()
{
Console.WriteLine("Dail Huawei Phone");
}
public void PickUp()
{
Console.WriteLine("PickUp Huawei Phone");
}
public void Receive()
{
Console.WriteLine("Receive Huawei Phone");
}
public void Send()
{
Console.WriteLine("Send Huawei Phone");
}
}
class PhoneUse
{
private IPhone phone_;
public PhoneUse(IPhone phone)
{
this.phone_ = phone;
}
public void UsePhone()
{
phone_.Dail();
phone_.PickUp();
phone_.Receive();
phone_.Send();
}
}
}
}
在代码当中,如果有可以替换的地方,那么就一定有接口的存在
接口就是为了解耦合而生的
松耦合最大的好处:让功能的提供方变得可替换,从而降低功能的提供方不能被替换所带来的高风险和高成本
- 高风险:服务的提供方本身有毛病,会导致依赖在它上面的其他类都不可以工作
- 高成本:服务提供方的程序员开发进度慢了,有可能导致整个团队的工作受阻
故开发的时候要尽可能的追求松耦合
四、依赖反转(依赖倒置)原则
解耦在代码中的表现就是依赖反转
依赖反转(dependency inversion principle)
依赖:服务的使用者和服务的提供者之间的依赖关系,服务的使用者依赖在服务的提供者上
依赖越直接,耦合越紧密;服务的提供者出现问题时,服务的使用者也会出现相应问题
人类解决问题的思维方式:自顶向下逐步求精

大问题的解决有赖于下面的小问题和中问题的解决,调用者依赖于功能的提供者之上的,主要箭头的方向,被依赖的放在下面
依赖反转(依赖倒置):提供一种新的思路,来平衡这种自顶向下逐步求精的这一单一思维方式
案例
无接口、无依赖倒置时的情况

- 司机Driver依赖车Car
- Driver类有个Car字段,调用Driver方法时会调用Car的Run方法
- Driver和Car是解耦合的
- Trucker和Truck也是解耦合的
- Racer和RaceCar也是解耦合的
问题:让Driver去试开Truck,传不过去,就相当于上面的求和和求平均值,用处理int数组的方法去处理ArrayList是有问题的
平常开Car,特殊情况也会开Truck,按无接口、无依赖倒置时的情况,Truck是没办法开的
通过接口实现依赖反转
创建IVehicle接口,接口有个Run方法;让Car和Truck类去实现IVehicle接口,这样Car
和Truck类里面也都有了Run方法
让Driver的字段不再是具体的Car或者Truck,而是IVehicle接口类型字段,这样就可以把Car类型的实例或Truck类型的实例交给Driver去引用,这样Driver去调用Driver方法时,访问的是IVehicle里面的Run方法,根据多态原则,最后给的是Car实例就调用Car的Run,给的Truck实例就调用Truck的Run
这样就利用接口把Driver和各种各样的车解耦了

当类实现一个接口时,类和接口之间的关系也是紧耦合
Car和Truck去实现IVehicle接口,这个箭头方向发送了变化,箭头的线有之前的向下,改成了现在的向上,依赖关系发生了反转,从图上看反转的是箭头
继续优化
当有多个服务的使用者和多个服务的提供者,都遵循一个接口时,就可以多种配对
Car和Truck两个服务的提供者,都实现了IVehicle接口
DriverBase抽象类,里面有IVehicle接口类型的字段,还有一个Drive方法
从这个DriverBase抽象类中派生出两个子类,一个是普通的Driver另一个是AIDriver,都有IVehicle接口类型字段,在Drive时,会分别调用自己的IVehicle类型的实例的Run方法
通过这样的桥接,Driver即可以开普通的车Car,也可以开卡车Truck
AIDriver可以开Car,也可以开Truck

再进一步优化就是设计模式了
五、单元测试(了解)
单元测试就是依赖反转的直接受益者
案例:生成电扇的厂家,里面有电源,电扇转的快输出高电流,转速慢输出低电流
电扇有自我保护功能,电流过大会断开或警报
分析:依赖关系------电扇依赖在电源上
先按常规,写个紧耦合,然后再通过接口把耦合松开,最后再进行单元测试
常规情况------紧耦合
- 电源类PowerSupply
- 返回值为int的GetPower方法,电源是固定电源,永远输出固定值100
- 电扇类DeskFan
- private的PowerSupply类型的字段
- 构造函数中传入PowerSupply对象
- 返回值为void的Work方法,负责拿到PowerSupply类中的电源值,根据这个电源值进行对应判断输出
- 主函数
- 创建一个DeskFan对象,传入PowerSupply对象
- 调用DeskFan对象的Work方法
csharp
using System;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
DeskFan fan = new DeskFan(new PowerSupply());
Console.WriteLine(fan.Work());
}
class PowerSupply
{
public int GetPower() { return 110; }
}
class DeskFan
{
private PowerSupply powerSupply_;
public DeskFan(PowerSupply powerSupply)
{
this.powerSupply_ = powerSupply;
}
public string Work()
{
int power = powerSupply_.GetPower();
if (power <= 0) return $"Power is {power}. Won't work.";
else if (power < 100) return $"Power is {power}. Slow.";
else if (power < 200) return $"Power is {power}. Working.";
else return "Warning.";
}
}
}
}
可以看到,若要改变状态,只能去修改PowerSupply的GetPower方法
若有其他设备用到这个PowerSupply,则修改后会影响到其他设备工作
故,引入接口,进行解耦
接口------解耦合
- 定义接口IPowerSupply,包含返回值为int的GetPower方法,因为是接口,故不需要实现
- PowerSupply类去实现IPowerSupply接口
- DeskFan类中PowerSupply改为IPowerSupply接口
csharp
using System;
namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
DeskFan fan = new DeskFan(new PowerSupply());
Console.WriteLine(fan.Work());
}
public interface IPowerSupply
{
int GetPower();
}
public class PowerSupply : IPowerSupply
{
public int GetPower() { return 110; }
}
public class DeskFan
{
private IPowerSupply powerSupply_;
public DeskFan(IPowerSupply powerSupply)
{
this.powerSupply_ = powerSupply;
}
public string Work()
{
int power = powerSupply_.GetPower();
if (power <= 0) return $"Power is {power}. Won't work.";
else if (power < 100) return $"Power is {power}. Slow.";
else if (power < 200) return $"Power is {power}. Working.";
else return "Warning.";
}
}
}
}
单元测试
打开测试资源管理器,测试------测试资源管理器

添加一个测试项目

添加一个xUnit测试项目

一般测试项目命名:被测试项目名字.Tests,例如这里的ConsoleApp1.Tests

目前本人工作上暂时用不到这个,故作为了解,不再详细学习了,需要了解的同学们可以自行网上查阅资料学习
六、总结

接口方法是虚线------纯虚方法
abstract方法是长虚线------比纯虚方法稍微实现了一点,但没有完全实现
virtual方法、override方法和普通方法都是实线------地地道道已经有逻辑了,有方法体
- 接口方法无论由哪些类来实现它时,方法的修饰符一定是public
- 抽象类到virtual方法 或者 抽象类到非virtual的实现方法,要加override
- virtual方法到override方法也要加override
- 有个接口,接口里面有方法,想直接拿一个类来实现它,看到的那个方法一定是public
- 用一个抽象类去实现一个接口,看到的方法一定是public abstract方法
- 用一个类实现接口方法,并且这个方法想让它可以被其他子类重写,看到的方法时public virtual方法
- 被重写后看到的方法是public override