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也不能写死,需要用到万能转换器:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统