C#基础-面向对象/抽象/接口/反射

以塔防游戏为例:

面向过程:考虑它第一步、第二步......都是干什么,

如:创建敌人--移动寻路--输入检测--创建防守者--防守者攻击--敌人死亡/受伤

面向对象:考虑谁?干什么?

我们只需要去关心每个对象都应该干什么

例:敌人有快有慢,我们创建一个敌人类,在类里面定义一个属性

如:public int speed; 放到具体实例里后,修改实例里的速度。

面向过程的程序 = 算法 + 数据结构;关心解决问题的步骤
面向对象的程序 = 对象 + 交互;       关心谁在干什么

类:一个抽象的概念,如生活中的“类别”。
对象:类的具体实例,如归属某个类别的“个体”。

同类型的多个对象,行为相同,数据不同。

主要思想:
分而治之---将一个大的需求分解为许多类,每个类处理一个独立的模块。  拆分好处:独立模块便于分工,每个模块便于复用,可扩展性强。
封装变化---变化的地方独立封装,避免影响其他模块。
高内聚-----类中各个方法,都完成一项任务(单一职责的类)。复杂的实现封装在内部,对外提供简单的调用。
低耦合-----类与类的关联性依赖度要低(每个类独立)。      让一个模块的改变,尽少地影响其他模块。

高复用、高扩展、低维护、高移植

例如美团:一个订餐的功能,他有管理餐厅的类,有管理骑手的类、有地图显示骑手到哪的类、有购物车类。。。。也是由很多小模块组成的

 

 

封装

What:封装是什么

1.从数据角度讲:将一些基本数据类型复合成一个自定义类型(复合人的思考方式,便于操作数据)

2.从方法角度讲:向类的外边提供功能,隐藏实现细节

3.从设计角度讲:分而治之、封装变化、高内聚、低耦合

Why:为什么要用封装

1.松散耦合,减低程序各部分直接的依赖性

2.简化变成,使用者不必了解具体的实现细节,只需调用对外提供的功能

3.增加安全,以特定的访问权限来使用类成员,保护成员不被以为修改

How:怎么用

访问修饰符

天天在用。。。

抽象

抽象类

语法:用abstract修饰符即为抽象类;

抽象类可能包含抽象成员(方法、属性),抽象类不能创建对象

 

语义:表示一个概念的抽象(可以存储子类、直接使用子类);

只表示做什么/拥有什么数据,但往往不表达具体做法;

 

适用性:

1.当有行为,但是不需要实现时

2.当有一些行为,在做法上有很多种可能时,但又不希望客户了解具体做法

3.不希望被当成类创建时,如:

 

抽象方法

如果添加了修饰符abstract,是抽象方法,儿子必须得实现;

如果是修饰符virtual,是虚方法,儿子不加override就用爹的,加了就用儿子的

继承

Unity整个框架就是继承

 

定义:重用现有的类,在此基础上扩展(功能、概念)

优点:提高了代码复用率;统一概念,方便层次化的管理。上图

缺点:耦合度太高(父级改变,无需通知子级);尽量别用继承,只有在统一概念的时候用(想要用一个东西来代表他们,抽象)。

适用性:多个类具有相同的数据或行为(概念差不多);多个类概念上一致,且需要统一处理。

 

Student继承了Person

(左栈存引用/ /右堆存对象)

new之后,栈堆的变化↓

所以stu并不是new一个per再new一个stu,而是new的stu里面,有per的所有东西。

 

不存在:子类型引用指向父类型。因为stu需要在堆里四块地址,而per只开了三块地址。

但是Person p3可以转成Student。有一块地址可以不用。

 

多态

定义:

父类同一种动作/行为(父类的引用调用同一方法),在不同子类上有不同的实现。(父:虚/抽象;子:重写)

继承将相关概念的共性进行抽象,并提供了一种复用的方式;

多态在共性的基础上,体现类型及行为的个性化,即一个行为有多个不同的实现。

实现方法:

重写原理:

 

在Unity里,有一个Start,如果父和子都写了Start,

会仅调用子类的Start(一般都是挂子类在物体身上),父类的Start会被隐藏(隐藏方法)。

如果想调用父类的Start,用base.就行了

 

那么隐藏是怎么运作的呢?

C#说了,有同名的方法,我就默认调用子,你想要调用父,就用base把父亲的地址给我,我拿到地址去调用。

隐藏内存消耗少,在编译阶段就已经全部都决定好了。(静态)

重写更加灵活,在程序运行阶段还能修改父级方法地址。(动态)

C++是隐藏,Java是重写。C#说我都要。

Ps:里氏替换(继承后的重写,指导继承设计)

替换之后保持原功能,使用扩展重写:base.

 

动态绑定和静态绑定

 

因此我们添加两个类,一个火车类,一个飞机类,类中写方法,需要扩展时,直接加方法。

因此我们需要使用多态:

上图是两个子类,都继承了一个抽象类Transportation。

两个子类是具体类,里面也都是具体方法。

 

下图:抽象类Transportation和里面的抽象方法:(!!很重要!!)

 

下图是Person类里的,调用抽象方法的方法。

 

下图是主方法,直接传入抽象类的子类。

为了做到开闭原则(允许扩展,不允许修改),使用了一个手段 :

依赖倒置:依赖父级(运输方式),不依赖子级(飞机、火车...)。

具体解释就是,主方法里依赖的是Transportation类,不是子类

接口

和抽象类的效果一样(找一个概念,代表接口接上的东西)

定义:一组对外的行为规范。

一组:接口中可以包含多个方法。

对外:接口里的东西接口自己不会用,接口被调用了,直接交给子类的同一个方法去解决。

 

行为:接口中只能包含抽象方法成员(方法、属性)

规范:要求子类必须自行实现。(不像以往的父类:父类有了子类也都有了。而是必须要子类自己写一个一样的。)

 

于是,因为上述的“行为”让接口和抽象类有了区别。

接口不能包含普通类(int a;)  抽象类可以。

 

抽象类和接口的选择策略:抽象类是人类,接口是走路

炸弹爆炸了:

很多东西都需要一个受到伤害的方法,玩家有玩家的,敌人有敌人的,

但是玩家和敌人受伤的方法差不多,都是减血,因此用角色类来代表他们,

但是其他的,房子、树、鸭子就不行了,他们不能归为角色

但是他们又得有受到伤害的方法,因此就用接口,

炸弹直接去调用接口,这样炸弹类就不用再修改了。

 

作用:规范不同类型的行为,达到了不同类型在行为上是一致的;扩展一个已有类的行为。

扩展的意思就是:写好了一群玩家类,但是策划突然说要给所有玩家类加功能,敌人不能有。然后为了统一调用此后玩家的新加的那个功能,就需要让所有玩家再继承一个接口。

 

--------------------------------------------------------------------------------------------

必须实现所有方法。

 

当有两个接口的名字一样时,如01和04的方法都叫Fun1();

那么在引用调用接口时,都调用的同一方法。(这就是实现接口)

 

可有些时候我们不想这么做

我都写了两个接口,肯定想要不同的做法,不然写两个接口干嘛。

那么这个时候,显式实现接口,就起到作用了。

Ok,那现在,我们在类03里面都写好了两个显式01的接口。

我们在主脚本new一个类,并调用:

是的,我们发现无法通过类引用调用任何一个显式方法。

 

所以得使用接口引用:

另外一点:如果继承了某个接口,但是接口里有很多方法,有些方法用不到,因此方法里什么都没有写,但是还能点出来,就很难受,怎么办呢?

改成显式实现就行了。(笑)

 

 

倘若我们对一个任意类进行排序:

很明显,Sort函数不知道该靠什么去比。

但是报错里有一个开头的东西。用他。

A.CompareTo(B){},这会返回一个数,大于0->BA,反之AB

因此我们就让Grenada类继承IComparable接口,在里面写一个int CompareTo()

这样就实现了类与类的对比。

这是简单化,因为int里就有CompareTo()

 

另外,我们需要根据不同的数值去对手雷类排序。

使用Sort的第三个重载,它需要一个IComparer。

怎么去实现呢?写一个类,然后让重载3去调用就行。

如上图,默认排序写在手雷类里面,用IComparable接口调用。

正常是写成委托的形式

 

如果一个类,能使用foreach去进行调用,那这个类中,肯定有:

通过IEnumerator中两个方法来 移 拿 移 拿 移 拿 移 拿 移...

单独的干活的类

foreach找的hand类里的方法

Main函数中,foreach的写法。

上图就是foreach的实现细节步骤。但还有简便写法:yield。

他不是只需要返回数组中的某一个对象吗

这样写,是不是就返回了一个对象,但方法不认。

没关系,我们加个yield。

将yield前的部分分配到MoveNext方法中;

将yield后的部分分配到Current属性中;

 

重点:就是上面那个while,当item调用MoveNext()时,

进入hand.GetEnumerator(),开始执行,

执行完yield行,暂时离开,

当item再次调用MoveNext()时,重新进来。

这就是协程的原理。

 

总结:如果需要一个类,去代表一群概念类似的类,那就用继承。

如果被那一群概念类似的类,有自己的特点,那就用多态。

父:虚、抽象;子:重写。

接口隔离:可以参考Unity中的EventTrigger,巨多接口。

 

还有,面向接口编程:

倘若我有一堆方法类似的东西要去调用,那我不去调他们,

我通过接口,面向接口,把自己当做接口,去写抽象类。

然后再在Main、方法里调用。

其实就是依赖倒置,声明父,指向子,这就是面向接口编程。

反射

定义:动态获取类型信息,动态创建对象,动态访问成员的过程

作用:在编译时无法了解类型信息,在运行时获取类型信息,创建对象,访问成员

流程:得到数据类型->动态创建对象->查看类型信息(本身/成员)

 

得到类型数据:

动态创建对象:

查看类型信息:

 

OK,现在我创建了个User类:

在Main里面这样写:

然后我们通过反射区实现上面的代码:

1.创建类型:

2.创建对象:object user01 = Activator.CreateInstance(type01);

等于

3.查看/运行对象内部成员:

反射真牛

这样就可以在运行时创建你需要的类

同样的,底下的SetValue也不能写死,需要用到万能转换器:

委托

posted @   被迫吃冰淇淋的小学生  阅读(54)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
点击右上角即可分享
微信分享提示