面向对象基础笔记
最近在看程杰的《大话设计模式》,这里对面向对象基础做个笔记。
使用语言:C#
有条件,推荐看原书。
01 类与实例
提问:类是什么?对象是什么?
一切事物皆可为对象,准确描述为:对象是一个自包含的实体,用一组可识别的特性和行为来标识。
面向对象编程,英文叫Object-Oriented Programming,即针对对象来进行编程。
类是具有相同的属性和功能的对象的抽象的集合。
例如:
# ‘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()。当然,这个空的方法是什么也不做,只是为了能顺利地实例化而已。
提问:那为什么还需要构造方法?
构造方法是为了对类进行初始化。比如我们希望每个小猫一诞生就有姓名,那么就应该写一个有参数的构造方法。
因此,将代码修改为:
因此,后面在客户端要生成小猫时,就必须给小猫取名。
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 封装
每个对象都包含它能进行操作所需要的所有信息,这个特性称为封装。因此对象不必依赖其他对象来完成自己的操作。
这样方法和属性包装在类中,通过类的实例来实现。
封装的好处:
- 良好的封装能够减少耦合;
- 内部的实现可以自由地修改;
- 类具有清晰的对外接口,这其实指的就是定义为public的ShoutNum属性和Shout方法。(感觉接口可理解为与对外交流的窗口)
如果我们现在需要增加一个狗叫的功能,如何操作呢?
可以仿造Cat加一个Dog类。然后再增加一个button2按钮,写上click事件代码。例如:
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。
继承者还可以理解为是对被继承者的特殊化,因为它除了具备被继承者的特性外,还具备自己独有的个性。
例如,猫就可能拥有抓老鼠、爬树等‘哺乳动物’对象所不具备的属性。
继承的工作方式是,定义父类和子类,或叫做基类和派生类,其中子类继承父类的所有特性。
子类不但继承了父类的所有特性,还可以定义新的特性。
如果子类继承于父类,有一些性质:
- 子类拥有父类非private的属性和功能;
- 子类具有自己的属性和功能,即子类可以扩展父类没有的属性和功能;
- 子类还可以以自己的方式实现父类的功能(方法重写)
修饰符包括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
例如:
这样的子类,除了叫声和构造方法的不同,所有的重复都转移到了父类。子类的定义如下:
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就成了抽象类了
抽象类需要注意:
- 抽象类不能实例化;
- 抽象方法是必须被子类重写的方法(不重写的话,它的存在没有意义。抽象方法可以被看成是没有实现体的虚方法);
- 如果类中包含抽象方法,那么类就必须定义为抽象类,不论是否还包含其他一般方法。
抽象类拥有尽可能多的共同代码,拥有尽可能少的数据
提问,那什么时候应该用抽象类呢?
抽象类通常代表一个抽象概念,它提供一个继承的出发点。
当设计一个新的抽象类时,一定是用来继承的,所以,在一个以继承关系形成的等级结构里面,树叶节点应当是具体类,而树枝节点均应当是抽象类。
即,具体类不是用来继承的
比如,若猫、狗、牛、羊是最后一级,那么它们就是具体类,但如果还有更下面一级的金丝猫继承于猫、哈巴狗继承于狗,就需要考虑把猫和狗改成抽象类了,当然这也是需要具体情况具体分析的
(就像上面的结构图,我的理解是最低一层才是具体类)
10 接口
如在上面新结构图中,我们还需要叮当猫,孙悟空,猪八戒实现变出东西的比赛。这时候,将“变出东西”的行为设置在动物中,就不合适,因为不是所有动物都可以变出东西。
引入接口的概念:
接口是把隐式公共方法和属性组合起来,以封装特定功能的一个集合。
一旦类实现了接口,类就可以支持接口所指定的所有属性和成员。
声明接口在语法上与声明抽象类完全相同,但不允许提供接口中任何成员的执行方式(所以,接口不能实例化,不能有修饰符,不能声明虚拟的或静态等。
一个类可以支持多个接口,多个类也可以支持相同的接口
接口的命名,前面要加一个大写字母‘I’,这是规范!
创建一个接口,它是用来‘变东西’用的
注意接口用interface声明,而不是class,接口名称前要加‘I’。
接口中的方法或属性前面不能有修饰符、方法没有方法体。
接下来创建叮当猫的类:
猴子的类Monkey和孙悟空的类StoneMonkey与上面非常类似
这样,调用接口的‘变出东西’的方法时,程序就会根据实现接口的对象来做出反应
抽象类与接口的理解:
- 类是对对象的抽象;抽象类是对类的抽象;接口是对行为的抽象
- 如果行为跨越不同类的对象,可使用接口;对于一些相似的类对象,用继承抽象类
- 从设计角度讲,抽象类是从子类中发现了公共的东西,泛化出父类,然后子类继承父类,而接口是根本不知子类的存在,方法如何实现还不确认,预先定义。
即抽象类是自底而上抽象出来的,而接口则是自顶向下设计出来的
11 集合
数组优点,在内存中连续存储,可以快速而容易地从头到尾遍历元素,可以快速修改元素等等。
数组缺点,创建时必须要指定数组变量的大小,还有在两个元素之间添加元素也比较困难
而且数组长度设置过大,造成内存空间浪费,长度设置过小造成溢出
所以.NET Framework提供了用于数据存储和检索的专用类,这些类统称集合。这些类提供对堆栈、队列、列表和哈希表的支持。
大多数集合类实现相同的接口
ArrayList是命名空间System.Collections下的一部分,它是使用大小可按需动态增加的数组实现IList接口
ArrayList的容量是ArrayList可以保存的元素数。ArrayList的默认初始容量为0。随着元素添加到ArrayList中,容量会根据需要通过重新分配自动增加
使用整数索引可以访问此集合中的元素。此集合中的索引从零开始
可以简单理解,数组的容量是固定的,而ArrayList的容量可根据需要自动扩充
由于实现了IList,所以ArrayList提供添加、插入或移除某一范围元素的方法
代码修改为:
代码1
当有对象被删除时,集合的变化时影响全局的,始终都保证元素的连续性。
ArrayList的不足:
- ArrayList是什么对象都是接受的,因为在它眼里,所有元素都是Object,这就使得‘arrayAnimal.Add(123);’或者‘arrayAnimal.Add(“HelloWorld”);’在编译时都是没有问题的,但在执行时,‘foreach (Animal item in arrayAnimal)’需要明确集合中的元素是Animal类型,而23是整型,HelloWorld是字符串型,这就会在运行到此处时报错。这是典型的类型不匹配错误,换句话说,ArrayList不是类型安全的;
- 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在功能上是一样的,不同就在于,它在声明和实例化时都需要指定其内部项的数据或对象类型,这就避免了刚才讲的类型安全问题和装箱拆箱的性能问题了
通常情况下,都建议使用泛型集合
- 泛型集合具有可以获得类型安全的直接优点,而不需要从基集合类型派生并实现类型特定的成员;
- 如果集合元素为值类型,泛型集合类型的性能通常优于对应的非泛型集合类型
13 委托与事件
委托是对函数的封装,可以当作给方法的特征指定一个名称。
事件是委托的一种特殊形式,当发生有意义的事情时,事件对象处理通知过程
委托对象用关键字delegate来声明。
事件是说在发生其他类或对象关注的事情时,类或对象可通过事件通知它们。事件对象用event关键字声明
public delegate void CatShoutEventHandler();
public event CatShoutEventHandler CatShout;
这里声明了一个委托,委托名称叫做CatShoutEventHa
这个委托所能代表的方法是无参数、无返回值的方法。
然后声明了一个对外公开的public事件CatShout,它的事件类型是委托CatShoutEventHandler。表明事件发生时,执行被委托的方法