C# | 一些基础,常看常新(一)

前言

C#基础指路:www.bilibili.com/video/BV1wx...

铁锰老师yyds!

本篇为个人学习笔记,仅供参考。

一、SOLID原则

Single Responsibility Principle(SRP):单一职责原则

一个类或者一个模块只做一件事。让一个类或者一个模块专注于单一的功能,减少功能之间的耦合程度。这样做在需要修改某个功能时,就不会影响到其他的功能。

Open Closed Principle(OCP):开闭原则

对扩展开放,对修改关闭。一个类独立之后就不应该去修改它,而是以扩展的方式适应新需求。

Liskov Substitution Principle(LSP):里氏替换原则

所有基类出现的地方都可以用派生类替换而不会让程序产生错误,派生类可以扩展基类的功能,但不能改变基类原有的功能。

Interface Segregation Principle(ISP):接口隔离原则

一个接口应该拥有尽可能少的行为,使其精简单一。对于不同的功能的模块分别使用不同接口,而不是使用同一个通用的接口。

Dependence Inversion Principle(DIP):依赖倒置原则

高级模块不应该依赖低级模块,而是依赖抽象接口,通过抽象接口使用对应的低级模块。


二、关于委托(delegate)

参考基础视频:委托详解_哔哩哔哩_bilibili

1 什么是委托

委托,可以理解为是C中函数指针的升级版

直接调用间接调用的本质:

  • 直接调用:通过函数名调用函数。CPU通过函数名直接获得函数所在地址,执行------》返回
  • 间接调用:通过函数指针调用函数。CPU通过读取函数指针存储的值,获得函数所在地址,执行------》返回

无论是直接or间接,CPU最终执行的机器语言指令是一样的,最终效果也是一样的。

程序中一个重要的概念:一切皆地址。程序的本质=数据+算法

变量是用来寻找数据的地址,函数是用来寻找算法的地址。

2 委托的简单使用

不同于C中函数指针要先声明再使用,C#类库中已定义了许多常用的委托类型可以直接使用。常用的委托有:Action委托、Func委托

  • Action委托:无参、返回值Void
  • Func委托:泛型委托

3 自定义委托

委托是一种类,而类是一种(引用)数据类型,所以委托也是一种数据类型。

它的声明方式与一般的类不同(不用Class XXX),而仿照函数指针的声明格式------一是为了照顾可读性,二是为了同C/C++的传统保持一致。

自定义委托的声明和使用:

需要注意的点

  1. 注意声明委托的位置(要在命名空间中声明,和其他类同级。要避免错写在类里,导致变成嵌套类)
  2. 委托与所封装的方法必须 "类型兼容" (返回值类型、参数列表个数&类型)

4 委托的一般使用

把委托当作参数传进方法。

即:通过委托,把方法A当作参数传给B方法。

好处: 假设方法B包含一个委托参数,委托封装了另一方法A。在B的方法体内,可以通过使用委托,动态调用方法A,从而形成一种动态调用方法的代码结构。

这种方法常用的实践有以下两种:

  • 实践1:模板方法。借用指定的外部方法来产生结果。

相当于方法中有一个填空题,需要传进来的委托来进行补充。一般在代码中部使用委托间接调用方法,获取返回值,之后根据返回值来进行后续的运算。

  • 实践2:回调(callback)方法。调用指定的外部方法。

把委托类型的参数传入主调方法,委托类型参数则封装了被回调的方法。由主调函数根据自己的逻辑来决定是否出发回调方法。

这类委托通常在函数末尾被使用,需要等主方法逻辑基本执行完后做出判断,委托封装的方法一般无返回值,常用来执行一些后续的工作,构成类似流水线的结构。

模板方法的使用参考:

使用模板方法的好处:

后续Product类、Box类、WrapFactory类都不用再改动,只需要不断去扩展ProductFactory产品工厂类即可。

最大限度的实现了代码复用(Reuse),可以提高工作效率,同时降低Bug的引入。

回调方法的使用参考:

回调方法,也称做好莱坞方法(类似演员面试后给导演留下名片。演员无法联络导演,而导演如果选中某位演员就通过名片联络演员------这就是回调函数的一个经典场景)

5、委托的缺点

使用委托要注意不能滥用。现实使用中一定要慎之又慎。

ps1.方法级别的紧耦合,往往是违反设计模式的

ps2.滥用委托为什么会造成内存泄露?

委托引用的方法,如果是实例方法,必定会隶属于一个对象。一旦委托引用了这个方法,方法对应的对象就必须存在于内存之中,即便没有其他引用变量引用该对象,该对象也不能释放------因为一旦释放,委托就不能再去间接调用对象的方法。所以有可能造成内存泄漏。随着泄露的内存越来越多。程序的性能会不断下降,直到程序崩溃。

6 委托的高级使用

几种常用的使用方式------

A. 多播(multicast)委托

一个委托内部封装多个方法的方式,叫多播委托。

一个委托内部封装一个方法的形式,叫单播委托。

B.隐式异步调用

何为同步 / 异步?

同步:B等待A做完,再A的基础上接着做

异步:A和B各做各的,同步进行

同步调用和异步调用的对比:

每一个运行的程序是一个进程process。每个进程拥有一个多个线程thread。最先运行的是主线程,其他的是分支线程。

同步调用是指再同一线程内依次调用;

异步调用是指在不同线程调用方法,彼此在逻辑上互不影响(物理上有可能互相抢夺资源)。异步的底层原理是多线程。

串行==同步==单线程,并行==异步==多线程

使用委托进行隐式异步调用:

多播委托中使用invoke()方法,是同步调用

想要异步调用,需要使用委托提供的另一个方法:beginInvoke()

beginInvoke方法会自动生成一个分支线程,之后在分支线程中调用封装的方法

两个参数:1、异步调用的回调;2、第二个参数一般也是null

使用线程进行显式异步调用:

在实际应用中,还有一点需要注意:应该适时地使用接口Interface取代一些对委托的使用。(比如java不具有委托的功能,完全用接口取代了委托)

重构上面<模板方法>的代码,用接口取代委托:


三、关于C#事件

1 事件的基本概念

(1-1)首先了解与事件相关的几个概念

A. 事件 Event:

事件,在日常语言中,可以理解为"能够发生的什么事情"。

而在 C# 中的定义是:一个隶属于类/对象、能够使类/对象具备通知能力成员

------ 作为类/对象的成员,事件、属性、方法等各自具备自己的功能,而<事件>的功能不在于事件本身,而是在于在事件发生时通知订阅者的能力。

------简而言之,事件的功能就是通知

B. 通知:

如何理解通知?

比如,我们说 手机具有响铃事件,就意味着------手机可以通过响铃这个事件来通知关注手机的人,也可以说,响铃这个事件让手机具备了通知关注者的能力。

当响铃事件发生时,从手机角度看------手机通知关注他的人,要求关注他的人采取行动;

从人的角度看------人得到手机的通知,可以采取行动了;

C. 事件参数:

在通知的基础上更进一步,有时伴随着通知,可能会产生一些与事件相关的数据。

比如手机来电响铃or接收微信响铃,这时手机在提醒关注者的同时也会将消息(电话、微信)发送给关注者 ------ 这种经由事件发送出来的与事件本身相关的数据 ,就是消息,被称为事件参数 EventArgs

D.事件处理器:

手机的关注者被通知之后,会去检查事件参数,之后根据参数内容采取相应的行动,比如接电话、回微信、或者可能直接忽略消息给手机关机 ------ 这种根据通知和事件参数采取行动的行为 称为响应事件 ,关注者在响应事件时具体所作的事情叫做 事件处理器 EventHandler

E. 缺少事件参数的事件:

还有一种比较特殊的,只有通知、没有参数的事件。比如开车遇到红灯自动停车、楼里火警铃响了大家自觉逃生------这类事件的特点时:无需额外的消息,事件发生的本身就足矣说明一切。

(1-2)事件的功能

由以上分析可以得出,事件的功能 = 通知(其他的对象/类) + 可选的事件参数(即详细信息)

因此,在程序中,事件用于对象与类间的动作协调与信息传递(消息推送)

------ 事件一旦发生,订阅该对象的对象们会依次接到通知,随后各自做出响应------这时各个对象协调统一的开始工作,程序开始运转。

(1-3)事件的原理

事件模型(event model),又叫发生--->响应模型。

5个部分:闹钟响了我起床 ------ 这里 闹钟、响铃、我、做饭共计4个部分,加上隐含的"订阅"关系,总计5个部分。

5个动作:第4个动作里,依次通知------通知的顺序就是订阅的顺序,先订阅先通知

(1-4)一些说明

一些含义相同的说法:

  

在日常的开发中需要注意:

2 事件模型的5个组成部分

  • 事件的拥有者(event source,对象)
  • 事件成员(event,成员)
  • 事件的响应者(event subscriber,对象)
  • 事件处理器(event handler,成员)------ 本质上是一个回调方法
  • 事件订阅 ------ 把事件处理器与事件关联在一起,本质上是一种以委托类型为基础的"约定"

事件拥有者 :事件的源头,一定是对象/类。(又称为事件的源头、事件的主体、事件消息的发送者)

事件 :本身是用来通知的工具,是被动发生的,需要事件拥有者在内部逻辑中触发。

事件响应者:订阅了事件的对象or类,它们在接收到通知后,会使用自身拥有的事件处理器来根据业务逻辑处理事件。

事件处理器:事件响应者的一个方法成员。本质上是一个回调方法。

事件订阅:解决了三个问题 ------

  1. 事件发生时,拥有者通知哪些对象

  2. 拿什么样的事件处理器才能处理事件

  • 不同的事件(比如电话、微信)所要做出的响应是不同的。
  • 当订阅者进行事件订阅时,C#编译器会进行严格的类型检查,用于订阅事件的事件处理器必须和事件遵守同一个约定
  • "约定"既约束事件发送的消息类型,又约束事件处理器处理消息的类型。只有二者的消息类型匹配时才能够订阅。

以上所说的"约定",本质上就是委托。因此常说事件是基于委托的。

  1. 事件的响应者具体用什么方法来处理事件
  • 订阅时会进行具体的指定

事件模型几种常见的组合方式:

1星:标准的事件机制模型,MVC/MVP等设计模式的雏形

2星:事件拥有者同时也是事件响应者,用自己的方法订阅自己的事件。常用的模型

3星:应用最广泛的。拥有者是响应者的一个字段成员(包含关系)

比如窗口对象包含按钮。按钮是窗口的成员。一旦点击按钮,窗口的某个订阅方法会得到通知,给窗口做出相应变化(比如页面跳转之类)

之所以应用广泛,是因为它是windows平台默认的事件订阅和处理结构------现在仍在使用。

使用中需要注意的点:

ps.对象打点弹出的下拉框中三种图标------扳手_属性(存储数据),方块_方法(做事情),闪电_事件(通知别人)

3 简单实践

实例1:

实例2:

反映1星的组合方式------拥有者和响应者是不同对象

实例3:

反映2星的组合方式------拥有者和响应者是同一个对象

实例4:

反映2星的组合方式------拥有者是响应者的一个字段成员,响应者用方法订阅着自己字段成员的事件

实现案例:窗口里一个文本框一个按钮,点击按钮,文本框显示HelloWorld。

实例5

事件处理器是可以重用的(比如两个Button绑定同一个事件)

前提是:这个事件处理器必须和被处理的事件保持约束上的一致。

ps.由此可以得出:

  • 一个事件可以挂接多个事件处理器(如实例1,Boy和Girl的Action都挂接在Elapsed事件上) ;
  • 一个事件处理器也可以被多个事件所挂接;

实例6

额外几种挂接处理器的方式:

四、为做基类而生的"抽象类"与"开闭原则"

1 什么是抽象

抽象关键字abstract

抽象方法:没有方法体(即没有逻辑实现)。也称为纯虚方法。

  • 因为虚方法virtual是有方法体的,等待子类去override
  • 而抽象方法方法体都没有,因此称为纯虚方法。
  • 而抽象方法所在的类就是抽象类(一旦包含抽象方法,类就必须也用 abstract 来修饰)

抽象类:函数成员没有被完全实现的(abstract)类。

------反义词是具体类(concrete),就是常说的 "类"。

抽象类中的抽象成员不能是private(因为需要子类去实现它,必须要允许子类访问)

编译器不允许实例化抽象类。(因为含有未实现的方法,实例化后编译器不知道该如何调用)

------ 由此得出抽象类的用处

  1. 作为基类;
  2. 可以声明变量,用基类类型变量引用子类类型的实例

2 开闭原则在抽象类中的体现

开闭原则要求不要修改已经完成的类,而是在类的基础上进行扩展。

结合抽象类,就有了这样的体现 ------

我们应该封装 那些不变的、稳定的、固定的、确定的成员 ,把不确定的 、有可能改变的成员声明为抽象成员留给子类去实现

3 结合代码重构理解抽象类的作用

假设现在我有一辆小汽车。我为它创建一个小汽车类,具有汽车Run、Stop这两个功能;半年后我又购入一辆卡车,于是新增一个卡车类,具有卡车Run、Stop功能;

这时我发现二者的Stop功能是一样的,为了去除重复代码,我提取一个车辆基类Vehicle,拥有有Stop功能,让两个类分别继承。

代码变得整洁了,但是出现了一个新的问题------当在主函数中用 Vehicle 来声明一个Car的对象,这个对象只能调用Stop,不能调用Run(因为Run方法是在子类中各自新增的)

这时如果想要让这个对象可以调用Run方法,比较传统的方式是在 Vehicle 类中也加一个 Run方法,通过逻辑判断来决定是Car还是Truck

------ 但是这种方式具有缺点,比如我又新购入一辆赛车,就需要在这里又加一条if判断

**------ 这样就是典型的违反了开闭原则(不应该在修bug&新增功能两个目的以外,改动现有类的代码)

这种情况下,解决方案有:

(1)用虚方法解决

Vehicle中定义一个虚方法Run,两个子类重写

但是!接下来我们发现一个问题:就是Vehicle的Run方法永远不会被调用。

这样看来,方法体的存在就毫无意义 ------ 因此更进一步,我们直接去掉Vehicle.Run的方法体------这时Run就变成了纯虚方法 ------ 也就是抽象方法

(2)用抽象方法解决

------存在抽象方法的类必须也加上abstract关键字,这时Vehicle类也就变成了抽象类。

这时我们再添加一个赛车类,就会发现,完全不需要改动Vehicle类,只需要在现有的基础上扩展一个新类即可------正好符合开闭原则。

以下是最终版本完整代码:

由此可以得出结论:

抽象类------专为做基类而生的类。

它的功能在于:以基类类型来声明子类类型变量,从而引用一些已经完全实现了它抽象成员的子类成员,实现多态。

ps.需要注意的一个点是:子类必须实现抽象类中未完成的方法,否则它就只能也是一个抽象类 ------ 实现抽象方法时和实现虚方法一样,都要加上override关键字。

4 抽象类的进一步进化------>纯抽象类------>接口

按之前的步骤,抽象类完成了。

这时可能会出现一个疑问:有没有一种可能,抽象类里所有的成员都是抽象的?

有可能。

按照以下步骤,我们建了一个 纯抽象类(其余部分代码不变)。

从代码可以看出:从VehicleBase,到Vehicle,再到Car系列的子类,就是一个 特别抽象------抽象------具体 的过程。

到这里,我们搬出一个热知识:在java、C#等语言里,纯抽象类,实际上就是接口。

在C#里,只需要把abstract替换为Interfase即可。

接口要求其所有成员都必须是public的,因此public默认省略。同时接口默认所有成员都是抽象的,因此abstract需要省略掉,避免重复。

------因为接口abstract已省略,因此实现它的子类也要省略override关键字。

以下为进一步进化(纯虚类替换为interface)的代码。可以看出从 接口------抽象类------具体类 的过程。

一个总结:

抽象类是未完全实现,接口是完全未实现。

五、接口 & 单元测试

1 什么是接口

关键字Interface

接口成员必须是 public

接口的本质服务调用者(消费者)与服务提供者之间的契约

作为契约,必须对供需双方透明可见(因此其成员必须是public)

接口作为契约,约束是对供需双方的。

比如以下代码,供需双方都遵守IEnumerable接口 ------

接口的产生:自顶向上(重构),自顶向下(设计)

C#中接口的实现:隐式、显式、多接口

2 依赖与耦合

在面向对象的设计中,程序与程序之间的合作,就叫做依赖。依赖的同时就出现了耦合------依赖越直接,耦合就越紧。

引入接口之后,类扩展时只需要在主函数中声明即可,不用动其它部分的代码 ------ 成功解耦,变成松耦合。

接口的作用:接口为松耦合而生。

它可以让功能的提供方变得可替换,减少不可替换带来的高风险和高成本。

  • ------ 高风险:提供方出错导致依赖它的其他功能不能正常工作
  • ------ 高成本:开发中,提供方部分代码未完成,导致团队其他部分进入等待,不能正常工作

ps.有没有办法连主函数中的代码都不用修改(即new NokiaPhone替换为new EricssonPhone 的部分)------ 有,可以使用反射!直接在配置文件中定义即可。

3、接口和依赖倒置原则

依赖倒置原则:高级模块不应该依赖低级模块,而是依赖抽象接口,通过抽象接口使用对应的低级模块。

体现如下图 ------ 原本向下依赖的箭头 变为 向上依赖的箭头,这就是依赖倒置的名字由来。

4 接口 /解耦 /依赖倒置原则在单元测试中的应用

假如我是生产电扇的厂商,电扇转速的快慢由电源大小决定------

原始的紧耦合写法:

加入接口,完成解耦:

正常开发中,比如这类测试数据一般不允许写在正常代码里,这时就要用到单元测试(把测试数据放在单元测试中)

新建单元测试方法:

右键解决方案------添加------新建项目------选择测试------推荐使用xUnit(.Net平台自己使用,兼容性比较好)

命名可以copy正式项目名,后加".Tests"后缀

在测试项目中写入测试case:

之后就可以运行测试了!

运行之前有两点需要注意:

  • 需要给单元测试项目右键添加引用(引用被测试的项目)
  • 同时保证被测试的类和方法是public

运行单元测试的方法:

选中项目文件------右键运行测试

在弹出的测试资源管理器中查看测试信息:

如果测试case报错,可以加断点debug寻找问题点,方法和正常debug代码相同。

5 单元测试的进一步优化

按照如上方法,会发现单元测试中存在一个很大的问题:为了测试不同的情况,要不停创建接口的实现类,造成类越来越多

如何解决这个问题------ Mock !

在Nuget管理中安装Moq。

使用Moq可以直接创建实现接口方法的实例,越过创建类的步骤------简化单元测试。

简化后的测试案例:


一个总结:

接口方法(纯虚)------ abstract方法 ------ virtual方法 都属于虚方法的范畴,抽象程度依次递减

virtual、override、实现方法都是有了方法体的方法

6 接口隔离原则

Interface Segregation Principle:一个接口应该拥有尽可能少的行为 ,使其精简单一。对于不同的功能的模块分别使用不同接口,而不是使用同一个通用的接口。

体现在接口中,结合之前的"接口契约论"可知:契约的作用在于,同时约束供需双方,保证供方不会少给,需方不会多要

  • 供方------不少给:是硬性规定,因为实现接口时,必须实现接口中所有方法。
  • 需方------不多要:是软性规定,编译器无法识别,需要设计模式来进行规范

那么,如何诊断需方是否"多要":

传给调用者的接口类型里是否存在一直没被调用的函数成员,如果有,就说明接口不够精简------即,违背了接口隔离原则。

在这种情况下,实现了这个接口的类往往违背了单一职责原则(要求一个类只做一件事/一组相关的事)

------ 很多情况下,接口隔离原则和单一职责原则说的是一回事。一个在服务调用者角度,一个在服务提供者角度。

所以,该如何实现接口隔离原则?

把本质不同的功能隔离开,用不同接口封装起来。

相关的三个案例见:

接口隔离,反射,特性,依赖注入_哔哩哔哩_bilibili

(00:12:00 ------ 00:39:00)

相关推荐
bug菌23 分钟前
Java GUI编程进阶:多线程与并发处理的实战指南
java·后端·java ee
夜月行者2 小时前
如何使用ssm实现基于SSM的宠物服务平台的设计与实现+vue
java·后端·ssm
Yvemil72 小时前
RabbitMQ 入门到精通指南
开发语言·后端·ruby
sdg_advance2 小时前
Spring Cloud之OpenFeign的具体实践
后端·spring cloud·openfeign
猿java3 小时前
使用 Kafka面临的挑战
java·后端·kafka
碳苯3 小时前
【rCore OS 开源操作系统】Rust 枚举与模式匹配
开发语言·人工智能·后端·rust·操作系统·os
kylinxjd3 小时前
spring boot发送邮件
java·spring boot·后端·发送email邮件
2401_857439696 小时前
Spring Boot新闻推荐系统:用户体验优化
spring boot·后端·ux
进击的女IT7 小时前
SpringBoot上传图片实现本地存储以及实现直接上传阿里云OSS
java·spring boot·后端
一 乐8 小时前
学籍管理平台|在线学籍管理平台系统|基于Springboot+VUE的在线学籍管理平台系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·学习