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)

相关推荐
追逐时光者5 小时前
推荐 12 款开源美观、简单易用的 WPF UI 控件库,让 WPF 应用界面焕然一新!
后端·.net
Jagger_5 小时前
敏捷开发流程-精简版
前端·后端
苏打水com6 小时前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
间彧7 小时前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧7 小时前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧7 小时前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧7 小时前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧7 小时前
Spring Cloud Gateway详解与应用实战
后端
EnCi Zheng9 小时前
SpringBoot 配置文件完全指南-从入门到精通
java·spring boot·后端
烙印6019 小时前
Spring容器的心脏:深度解析refresh()方法(上)
java·后端·spring