面向对象基础笔记

最近在看程杰的《大话设计模式》,这里对面向对象基础做个笔记。

使用语言:C#

有条件,推荐看原书。

01 类与实例

提问:类是什么?对象是什么?

一切事物皆可为对象,准确描述为:对象是一个自包含的实体,用一组可识别的特性和行为来标识。

面向对象编程,英文叫Object-Oriented Programming,即针对对象来进行编程。

类是具有相同的属性和功能的对象的抽象的集合。

例如:

bash 复制代码
# 'class'是表示定义类的关键字,'Cat'就是类的名称,'Shout'就是类的方法。"
# 注意事项:第一,类名称首字母记着要大写。多个单词则各个首字母大写;第二,对外公开的方法需要用'public'修饰符。(class的类默认为私有,区别于struct结构体的默认权限为公有)

class Cat
{
    public string Shout()
    {
        return "喵";
    }
}​​

应用这个类,就需要将这个类实例化。

实例化就是创建对象的过程,常使用new关键字来创建。

Cat cat = new Cat();其实做了两件事,如下解析:

02 构造方法

构造方法,又叫构造函数,其实就是对类进行初始化

构造方法与类同名,无返回值,也不需要void,在new时候调用。

'Cat cat=new Cat();'中,new后面的Cat()其实就是构造方法。

提问:在类当中没有写过构造方法Cat(),怎么可以调用呢?

实际情况是,所有类都有构造方法,如果不编码则系统默认生成空的构造方法,若有定义的构造方法,那么默认的构造方法就会失效了。也就是说,由于没有在Cat类中定义过构造方法,所以C#语言会生成一个空的构造方法Cat()。当然,这个空的方法是什么也不做,只是为了能顺利地实例化而已。

提问:那为什么还需要构造方法?

构造方法是为了对类进行初始化。比如我们希望每个小猫一诞生就有姓名,那么就应该写一个有参数的构造方法。

因此,将代码修改为:

因此,后面在客户端要生成小猫时,就必须给小猫取名。

bash 复制代码
private void button1_Click(object sender, EventArgs e)
{
    Cat cat = new Cat("咪咪");
    MessageBox.Show(cat.Shout());
}​​

03 方法重载

如果没有给小猫取好名字,这个实例就创建不了。

如果写'Cat cat= new Cat();'会直接报'Cat方法没有采用0个参数的重载'的错误。

如果当真需要不起名字也要生出小猫来。可以用'方法重载'。
方法重载提供了创建同名的多个方法的能力 ,但这些方法需使用不同的参数类型。注意并不是只有构造方法可以重载,普通方法也是可以重载的。

则类的定义变为:

此时,如果写'Cat cat = new Cat();'的话,就不会报错了。而猫叫时会是'我的名字叫无名 喵'。

注意方法重载时,两个方法必须要方法名相同,但参数类型或个数必须要有所不同,否则重载就没有意义了。

提问:方法重载的好处是什么?

方法重载可在不改变原方法的基础上,新增功能。
方法重载算是提供了函数可扩展的能力 。比如,有的小猫起好名字了,就用带string参数的构造方法,有的没有名字,就用不带参数的,这样就达到了扩展的目的。

如果还需要分清楚猫的姓和名,还可以再重载一个public Cat(string firstName, string lastName)

04 属性与修饰符

属性是一个方法或一对方法,但在调用它的代码看来,它是一个字段,即属性适合于以字段的方式使用方法调用的场合

字段是存储类要满足其设计所需要的数据,字段是与类相关的变量。

比如在Cat类中的'private string name = "";'name其实就是一个字段,它通常是私有的类变量。

public表示它所修饰的类成员可以允许其他任何类来访问,俗称公有的。而private表示只允许同一个类中的成员访问,其他类包括它的子类无法访问,俗称私有的。

现在增加一个'猫叫次数ShoutNum'的属性:

在上面的代码中,shoutNum是私有的字段,ShoutNum是公有的对外属性。

由于是对外的,所以属性的名称一般首字母大写,而字段则一般首字母小写或前加'_'。

提问:属性的get和set是什么意思?

属性有两个方法get和set。

get访问器返回与声明的属性相同的数据类型,表示的意思是调用时可以得到内部字段的值或引用。

set访问器没有显式设置参数,但它有一个隐式参数,用关键字value表示,它的作用是调用属性时可以给内部的字段或引用赋值。

由于有了'叫声次数'的属性,就需要对Shout方法就需要改进:

此时调用时,就只需要给属性赋值:

如果不给属性赋值,小猫会叫三声"喵",因为字段shoutNum的初始值是3。

特别需要注意:变量私有的叫字段,公有的是属性

一般无需要对外界公开的方法都应该设置其修饰符为private(私有)。这才有利于'封装'。

05 封装

每个对象都包含它能进行操作所需要的所有信息,这个特性称为封装 。因此对象不必依赖其他对象来完成自己的操作。

这样方法和属性包装在类中,通过类的实例来实现。

封装的好处:

  1. 良好的封装能够减少耦合;
  2. 内部的实现可以自由地修改;
  3. 类具有清晰的对外接口,这其实指的就是定义为public的ShoutNum属性和Shout方法。(感觉接口可理解为与对外交流的窗口)
    如果我们现在需要增加一个狗叫的功能,如何操作呢?
    可以仿造Cat加一个Dog类。然后再增加一个button2按钮,写上click事件代码。例如:
bash 复制代码
private void button2_Click(object sender, EventArgs e)
{
    Dog dog = new Dog("旺财");
    dog.ShoutNum = 5;
    MessageBox.Show(dog.Shout());
}​​

但是,Cat和Dog有非常类似的代码,下面就引入面向对象的第二大特性"继承"。

06 继承

可以这样理解,猫和狗都是哺乳动物,所以它们都能继承到哺乳动物的一些基本特征,即,猫和狗与哺乳动物是继承关系。

回到编程上,对象的继承代表了一种'is-a'的关系,如果两个对象A和B,可以描述为'B是A',则表明B可以继承A。

继承者还可以理解为是对被继承者的特殊化,因为它除了具备被继承者的特性外,还具备自己独有的个性。

例如,猫就可能拥有抓老鼠、爬树等'哺乳动物'对象所不具备的属性。

继承的工作方式是,定义父类和子类,或叫做基类和派生类,其中子类继承父类的所有特性。

子类不但继承了父类的所有特性,还可以定义新的特性。

如果子类继承于父类,有一些性质:

  1. 子类拥有父类非private的属性和功能;
  2. 子类具有自己的属性和功能,即子类可以扩展父类没有的属性和功能;
  3. 子类还可以以自己的方式实现父类的功能(方法重写)

修饰符包括private,protected和public。

protected表示继承时子类可以对基类有完全访问权。即,用protected修饰的类成员,对子类公开,但不对其他类公开。

所以子类继承于父类,则子类就拥有了父类的除private外的属性和功能

对比观察Cat和Dog类:

可以发现把部分代码都是相通的,可以直接建立一个父类,动物Animal类。

可以尽量吧相同的代码放到动物类中。

即,得到动物类:

然后写Cat和Dog的代码。让它们继承Animal。

C#中,子类从它的父类中继承的成员有方法、域、属性、事件、索引指示器,但对于构造方法,不能被继承,只能被调用。

对于调用父类的成员,可以用base关键字。

此时的子类可以这样写:

继承的优点是,继承使得所有子类公共的部分都放在了父类,使得代码得到了共享,这就避免了重复。

另外,继承可使得修改或扩展继承而来的实现都较为容易。

继承的缺点,是父类变,子类不得不变。

07 多态

下面如果要增加需求:

举办一个动物运动会,其中有一项是各种动物进行'叫声比赛'。界面就是放两个按钮,一个是'动物报名',就是确定动物的种类和报名的顺序,另一个是'叫声比赛',报名的动物需要挨个地叫出声音来比赛。

注意来报名的都是什么动物,我们并不知道。

分析:参加的动物,都必须叫,说明都有叫的方法(有Shout方法)。

所谓的'动物报名',就是建立一个动物对象数组,让不同的动物对象加入其中。再'叫声比赛',就是遍历这个数组,来运行动物们的'Shout()'。

所以,这里引入面向对象的第三大特性------多态

多态表示不同的对象可以执行相同的动作,但要通过它们自己的实现代码来执行

补充一点概念,虚方法和方法重写:

为了使子类的实例完全接替来自父类的类成员,父类必须将该成员声明为虚拟的。通过在返回类型之前添加virtual关键字来实现。

子类可以选择使用override关键字,将父类实现替换为它自己的实现,这就是方法重写Override,或者叫做方法覆写

例如:

不过需要注意:对象的声明必须是父类,而不是子类,实例化的对象是子类,这才能实现多态。

08 重构

如果现在又来了小牛和小羊来报名,需要参加'叫声比赛',如何操作呢?

分析:把重复的这个Shout的方法体放到Animal类中,去掉virtual,但是需要将叫的声音部分改成另一个方法getShoutSound

例如:

这样的子类,除了叫声和构造方法的不同,所有的重复都转移到了父类。子类的定义如下:

bash 复制代码
class Cat : Animal
{
    public Cat () : base()
    { }
 
    public Cat (string name) : base(name)
    { }
 
    protected override string getShoutSound ()
    {
      return "喵";
    }
}​​

​​class Dog : Animal
{
    public Dog () : base()
    { }
 
    public Dog (string name) : base(name)
    { }
 
    protected overridestring getShoutSound ()
    {
      return "汪";
    }
}​​

​​class Sheep : Animal
{
    public Sheep () : base()
    { }
 
    public Sheep (string name) : base(name)
    { }
 
    protected override string getShoutSound()
    {
      return "咩";
    }
}​​

​​class Cattle : Animal
{
    public Cattle () : base()
    { }
 
    public Cattle (string name):base(name)
    { }
 
    protected override string getShoutSound ()
    {
      return "哞";
    }
}​​

09 抽象类

对于Animal类的getShoutSound方法,其实方法体没有任何意义,所以可以将virtual修饰符改为abstract,使之成为抽象方法。

这样,Animal就成了抽象类了

抽象类需要注意:

  1. 抽象类不能实例化;
  2. 抽象方法是必须被子类重写的方法(不重写的话,它的存在没有意义。抽象方法可以被看成是没有实现体的虚方法);
  3. 如果类中包含抽象方法,那么类就必须定义为抽象类,不论是否还包含其他一般方法。

抽象类拥有尽可能多的共同代码,拥有尽可能少的数据

提问,那什么时候应该用抽象类呢?

抽象类通常代表一个抽象概念,它提供一个继承的出发点。

当设计一个新的抽象类时,一定是用来继承的,所以,在一个以继承关系形成的等级结构里面,树叶节点应当是具体类,而树枝节点均应当是抽象类。

即,具体类不是用来继承的

比如,若猫、狗、牛、羊是最后一级,那么它们就是具体类,但如果还有更下面一级的金丝猫继承于猫、哈巴狗继承于狗,就需要考虑把猫和狗改成抽象类了,当然这也是需要具体情况具体分析的

(就像上面的结构图,我的理解是最低一层才是具体类)

10 接口

如在上面新结构图中,我们还需要叮当猫,孙悟空,猪八戒实现变出东西的比赛。这时候,将"变出东西"的行为设置在动物中,就不合适,因为不是所有动物都可以变出东西。

引入接口的概念:

接口是把隐式公共方法和属性组合起来,以封装特定功能的一个集合。

一旦类实现了接口,类就可以支持接口所指定的所有属性和成员。

声明接口在语法上与声明抽象类完全相同,但不允许提供接口中任何成员的执行方式(所以,接口不能实例化,不能有修饰符,不能声明虚拟的或静态等。

一个类可以支持多个接口,多个类也可以支持相同的接口
接口的命名,前面要加一个大写字母'I',这是规范!

创建一个接口,它是用来'变东西'用的

注意接口用interface声明,而不是class,接口名称前要加'I'。

接口中的方法或属性前面不能有修饰符、方法没有方法体。

接下来创建叮当猫的类:

猴子的类Monkey和孙悟空的类StoneMonkey与上面非常类似

这样,调用接口的'变出东西'的方法时,程序就会根据实现接口的对象来做出反应

抽象类与接口的理解:

  1. 类是对对象的抽象;抽象类是对类的抽象;接口是对行为的抽象
  2. 如果行为跨越不同类的对象,可使用接口;对于一些相似的类对象,用继承抽象类
  3. 从设计角度讲,抽象类是从子类中发现了公共的东西,泛化出父类,然后子类继承父类,而接口是根本不知子类的存在,方法如何实现还不确认,预先定义。

抽象类是自底而上抽象出来的,而接口则是自顶向下设计出来的

11 集合

数组优点,在内存中连续存储,可以快速而容易地从头到尾遍历元素,可以快速修改元素等等。

数组缺点,创建时必须要指定数组变量的大小,还有在两个元素之间添加元素也比较困难

而且数组长度设置过大,造成内存空间浪费,长度设置过小造成溢出

所以.NET Framework提供了用于数据存储和检索的专用类,这些类统称集合。这些类提供对堆栈、队列、列表和哈希表的支持。

大多数集合类实现相同的接口

ArrayList是命名空间System.Collections下的一部分,它是使用大小可按需动态增加的数组实现IList接口

ArrayList的容量是ArrayList可以保存的元素数。ArrayList的默认初始容量为0。随着元素添加到ArrayList中,容量会根据需要通过重新分配自动增加

使用整数索引可以访问此集合中的元素。此集合中的索引从零开始

可以简单理解,数组的容量是固定的,而ArrayList的容量可根据需要自动扩充

由于实现了IList,所以ArrayList提供添加、插入或移除某一范围元素的方法

代码修改为:

代码1

当有对象被删除时,集合的变化时影响全局的,始终都保证元素的连续性。

ArrayList的不足:

  1. ArrayList是什么对象都是接受的,因为在它眼里,所有元素都是Object,这就使得'arrayAnimal.Add(123);'或者'arrayAnimal.Add("HelloWorld");'在编译时都是没有问题的,但在执行时,'foreach (Animal item in arrayAnimal)'需要明确集合中的元素是Animal类型,而23是整型,HelloWorld是字符串型,这就会在运行到此处时报错。这是典型的类型不匹配错误,换句话说,ArrayList不是类型安全的;
  2. ArrayList对于存放值类型的数据,比如int、string型(string是一种拥有值类型特点的特殊引用类型)或者结构struct的数据,用ArrayList就意味着都需要将值类型装箱为Object对象,使用集合元素时,还需要执行拆箱操作,这就带来了很大的性能损耗

其中,装箱就是把值类型打包到Object引用类型的一个实例中。拆箱就是指从对象中提取值类型。

相对于简单的赋值而言,装箱和拆箱过程需要进行大量的计算。对值类型进行装箱时,必须分配并构造一个全新的对象。其次,拆箱所需的强制转换也需要进行大量的计算

C#在2.0版本后推出了新的技术来解决这个问题,那就是泛型

12 泛型

泛型是具有占位符(类型参数)的类、结构、接口和方法,这些占位符是类、结构、接口和方法所存储或使用的一个或多个类型的占位符

泛型集合需要System.Collections.Generic的命名空间,而List类是ArrayList类的泛型等效类。

用法上关键就是在IList和List后面加'',这个'T'就是需要指定的集合的数据或对象类型

例如:

提问:这是如果写'arrayAnimal.Add(123);'或者'arrayAnimal.Add("HelloWorld");'将是什么结果?
编译就报错,因为Add的参数必须是要Animal或者Animal的子类型才行

List和ArrayList在功能上是一样的,不同就在于,它在声明和实例化时都需要指定其内部项的数据或对象类型,这就避免了刚才讲的类型安全问题和装箱拆箱的性能问题了

通常情况下,都建议使用泛型集合

  1. 泛型集合具有可以获得类型安全的直接优点,而不需要从基集合类型派生并实现类型特定的成员;
  2. 如果集合元素为值类型,泛型集合类型的性能通常优于对应的非泛型集合类型

13 委托与事件

委托是对函数的封装,可以当作给方法的特征指定一个名称。

事件是委托的一种特殊形式,当发生有意义的事情时,事件对象处理通知过程

委托对象用关键字delegate来声明。

事件是说在发生其他类或对象关注的事情时,类或对象可通过事件通知它们。事件对象用event关键字声明

bash 复制代码
public delegate void CatShoutEventHandler();​​
​​public event CatShoutEventHandler CatShout;​​

这里声明了一个委托,委托名称叫做CatShoutEventHa

这个委托所能代表的方法是无参数、无返回值的方法。

然后声明了一个对外公开的public事件CatShout,它的事件类型是委托CatShoutEventHandler。表明事件发生时,执行被委托的方法

相关推荐
岑梓铭5 分钟前
《考研408数据结构》第六章(5.1+5.2+5.3树、二叉树、线索二叉树)复习笔记
数据结构·笔记·考研·408·1024程序员节
源代码•宸5 分钟前
Qt6 学习——一个Qt桌面应用程序
开发语言·c++·经验分享·qt·学习·软件构建·windeployqt
周杰伦_Jay27 分钟前
【 RocketMQ 全解析】分布式消息队列的架构、消息转发与快速实践、事务消息
分布式·算法·架构·rocketmq·1024程序员节
sprintzer39 分钟前
10.16-10.25力扣计数刷题
算法·leetcode
王哈哈^_^1 小时前
【数据集】【YOLO】【目标检测】建筑垃圾数据集 4256 张,YOLO建筑垃圾识别算法实战训推教程。
人工智能·深度学习·算法·yolo·目标检测·计算机视觉·数据集
摇滚侠1 小时前
全面掌握 PostgreSQL 关系型数据库,PostgreSQL 介绍,笔记02
数据库·笔记·postgresql
摇滚侠1 小时前
Spring Boot3零基础教程,生命周期监听,自定义监听器,笔记59
java·开发语言·spring boot·笔记
讽刺人生Yan1 小时前
RFSOC学习记录(一)RF data converter总览
学习·fpga开发·rfsoc
CoovallyAIHub1 小时前
不看异常,怎么学会识别异常?用“异常”指导异常检测!——NAGL方法解析(附代码地址)
深度学习·算法·计算机视觉
张人玉2 小时前
WPF 控件速查 PDF 笔记(可直接落地版)
笔记·microsoft·wpf