类与实例
首先,对象是一个自包含的实体,用一组可识别的特性和行为来标识。
面向对象编程,英文叫Object-Oriented Programming,其实就是针对对象进行编程的意思。
类,是具有相同的属性和功能的对象的抽象的集合。
实例,就是一个真实的对象。而实例化就是创建对象的过程,使用new关键字来创建。如:
Cat cat = new Cat();
它其实做了两件事:
Cat cat; // 声明一个Cat的对象,对象名为cat
cat = new Cat(); // 将此cat对象实例化
构造方法
构造方法,又叫构造函数,其实就是对类进行初始化。构造方法与类同名,无返回值,也不需要void,在new时候调用。
所有类都有构造方法,如果不编码则系统默认生成空的构造方法,若已有定义的构造方法,那么默认的构造方法就会失效。
构造方法可以有参数。
方法重载
方法重载提供了创建同名的多个方法的能力,但这些方法需使用不同的参数类型。并不是只有构造方法可以重载,普通方法也是可以重载的。
注意,方法重载时,两个方法必须要方法名相同,但参数类型或个数必须要有所不同,否砸重载就没有意义了。
所以,方法重载其实就是可在不改变原方法的基础上,新增功能。
属性与修饰符
属性是一个方法或一对方法,但在调用它的代码看来,它是一个字段,即属性适合于以字段的方式使用方法调用的场合。
而字段是存储类要满足其设计所需要的数据,字段是与类相关的变量。
看一段代码:
private int shoutNum =3; // 声明一个内部字段,注意是private,默认值是3
public int ShortNum{ // ShoutNum属性,注意是public,当中有两个方法
// get表示外界调用时可以得到shoutNum的值
// set表示外界可以给内部的shoutNum赋值
get{
return shoutNum;
}
set{
shoutNum=value;
}
}
这里,public表示它所修饰的类成员可以允许其他任何类来访问,俗称公有的。而private表示只允许同一个类中的成员访问,其他类包括它的子类无法访问,俗称私有的。如果在类中的成员没有加修饰符,则被认为是private的。
通常,字段都是private,即私有的变量;而属性都是public,即公有的对外属性。由于是对外的,所以属性的名称一般首字母大写,而字段则一般首字母小写或前加‘_’。
属性有两个方法get和set。get访问器返回与生命的属性相同的数据类型,表示的意思是调用时可以得到内部字段的值或引用赋值;set访问器没有显式设置参数,但它有一个隐式参数,用关键字value表示,它的作用是调用属性时可以给内部的字段或引用赋值。
把对外的数据写成属性,而不是直接设置成public,对于数据的封装和隐藏很有效。
封装
封装是面向对象的三大特性之一。每个对象都包含它能进行操作所需要的所有信息,这个特性称为封装,因此对象不必依赖其他对象来完成自己的操作。
封装有很多好处,第一、良好的封装能够减少耦合;第二、类内部的实现可以自由地修改;第三、类具有清晰的对外接口。
继承
对象的继承代表了一种‘is-a’的关系,如果两个对象A和B,可以描述为‘B是A’,则表明B可以继承A。继承者可以理解为是对被继承者的特殊化,因为它除了具备被继承者的特性外,还具备自己独有的个性。
继承定义了类如何相互关联,共享特性。继承的工作方式是,定义父类和子类,或叫做基类和派生类,其中子类继承父类的所有特性。子类不但继承了父类的所有特性,还可以定义新的特性。
如果子类继承于父类,第一、子类拥有父类非private的属性和功能;第二、子类具有自己的属性和功能,即子类可以扩展父类没有的属性和功能;第三、子类还可以以自己的方式实现父类的功能(方法重写)。
protected表示继承时子类可以对基类有完全访问权。也就是说,用protected修饰的类成员,对子类公开,但不对其他类公开。
注意:对于构造方法,它不能被继承,只能被调用,对于调用父类的成员,可以用base关键字。
class Cat: Animal{ // 继承格式就是子类:父类
public Cat():base() // 子类构造方法需要调用父类同样参数类型的构造方法,用base关键字代表父类
{}
public Cat(string name):base(name)
{}
public string Shout()
{}
}
不用继承的话,如果要修改功能,就必须在所有重复的方法中修改,代码越多,出错的可能就越大,而继承的优点是,继承使得所有子类公共的部分都放在了父类,使得代码得到了共享,这就避免了重复。另外,继承可使得修改或扩展继承而来的实现都较为容易。
继承也是有缺点的,那就是父类变,则子类不得不变。同时,继承会破坏包装,父亲实现细节会暴露给子类。而且,继承显然是一种类与类之间强耦合的关系,耦合性过高,一方的变化会影响到另一方。
而当两个类之间是'has-a'的关系时,表示某个角色具有某一项责任,此时不适合用继承。比如人有两只手,不能让手去继承人吧。
多态
多态是面向对象的第三大特性。表示不同的对象可以执行相同的动作,但要通过它们自己的实现代码来执行。
注意:第一、子类以父类的身份出现;第二、子类在工作时以自己的方式来实现;第三、子类以父类的身份出现时,子类特有的属性和方法不可以使用。
为了使子类的实例完全接替来自父类的类成员,父类必须将该成员声明为虚拟的。这是通过在该成员的返回类型之前添加virtual关键字来实现。除了字段不能是虚拟的,属性、事件和索引器都可以是虚拟的。
尽管方法可以是虚拟的,但虚方法还是有方法体,可以实际做些事情(区分后面提到的抽象abstract)。然后,子类可以选择使用override关键字,将父类实现替换为它自己的实现,这就是方法重写Override,或者叫做方法覆写。
来看代码:
class Animal{
.....
public virtual string Shout(){ // 注意修饰符中增加了一个virtual,它表示此方法是虚方法,可以被子类重写
return "";
}
}
class Cat : Animal{
public Cat():base(){}
public Cat(string name):base(name){}
public override string Shout(){ // 增加了override,表示方法重写
string result="";
for(int i=0;i<shouotNum;i++)
result += "喵,";
return "我的名字叫" +name;
+ " " +result;
}
}
多态的原理是,当方法被调用时,无论对象是否被转换为其父类,都只有位于对象继承链最末端的方法实现会被调用。也就是说,虚方法是按照其运行时类型而非编译时类型进行动态绑定调用的。
重构
我们发现,之前代码中,继承自Animal的不同动物的Shout函数里,除了叫的声音不同外,几乎没有任何差异,却需要进行方法重写,这是有重复的,如何改造呢?
如果仅把重复的Shout的方法体放到Animal类中,去掉virtual,可行吗?显然是不行的,因为你没办法设定叫的声音,是叫“喵”还是“汪”,还是其他?其实,既然只有叫的声音不同,那我们把叫的声音改成另一个方法 getShoutSound 就行了:
class Animal{
......
public string Shout(){ // 去掉virtual,成为普通的公共方法
string result = "";
for(int i=0; i<shoutNum; i++)
result+= getShoutSound() + "."; // 此处是原先子类的唯一不同之处,
// 所以改成调用一个虚方法getShoutSound
return "我的名字叫" + name + " " +result;
}
protected virtual string getShoutSound(){ // “得到叫声”,虚方法,让子类重写,只需给继承的子类使用,所以用protected修饰符。
return "";
}
}
此时的子类就极其简单了:
class Cat :Animal{
public Cat():base() {}
public Cat(string name):base(name) {}
protected override string getShoutSound(){
return "喵";
}
}
其他动物,只需替换return中的“喵”即可。
抽象类
我们完全可以考虑把实例化没有任何意义的父类改成抽象类。同样的,对于Animal的getShoutSound方法,其实方法体没有任何意义,所以可以将virtual修饰符改为abstract,使之成为抽象方法。C#允许把类和方法声明为abstract,即抽象类和抽象方法。
abstract class Animal{ // 加abstract关键字,表明是抽象类
......
protected abstract string getShoutSound(); // 在方法返回值前加abstract表明此方法是抽象方法,抽象方法没有方法体,直接在括号后加“;”
}
注意:第一、抽象类不能实例化;第二、抽象方法必须是被子类重写的方法(不重写的话,它的存在有什么意义呢?);第三、如果类中包含抽象方法,那么类就必须定义为抽象类,不论是否还包含其他一般方法。
应该让抽象类拥有尽可能多的代码、尽可能少的数据。
抽象类通常代表一个抽象概念,它提供一个继承的出发点,当设计一个新的抽象类时,一定是用来继承的。所以,在一个以继承关系形成的等级结构里面,树叶结点应当是具体类,而树枝结点均应当是抽象类。也就是说,具体类不是用来继承的。
接口
接口是把隐式公共方法和属性组合起来,以封装特定功能的一个集合,一旦类实现了接口,类就可以支持接口所指定的所有属性和成员。声明接口在语法上与声明抽象类完全相同,但不允许提供接口中任何成员的执行方式。所以接口不能实例化,不能有构造方法和字段;不能有修饰符,比如private、public等;不能声明虚拟的或静态的等。还有实现接口的类就必须要实现接口中的所有方法和属性。
一个类可以支持多个接口,多个类也可以支持相同的接口。接口的命名,前面要加一个大写字母‘I’,这是规范。
接口用interface声明,接口中的方法或属性前面不能有修饰符、方法没有方法体。
interface IChange{ // 声明一个IChange接口,此接口有一个方法ChangeThing,
// 参数是一个字符串变量,返回一字符串
string ChangeThing (string thing);
}
然后来创建一个类实现接口:
class MachineCat:Cat,IChange{ // 机器猫继承于猫,并实现IChange接口
// 注意Cat与IChange是用‘,’分割
public MachineCat():base(){}
public MachineCat(string name):base(name){}
public string ChangeThing(string thing){ //实现接口的方法,注意不能加override修饰符
return base.Shout() + "我有万能的口袋,我可变出:" +thing; // base.Shout()表示调用父类Cat的方法
}
}
实现代码:
private void button_Click(object sender, EventArgs e){
MachineCat mcat=new MachineCat("叮当"); // 创建两个类的实例
StoneMonkey wukong = new StoneMonkey("孙悟空");
IChange[] array=new IChange[2]; // 声明一个接口数组,将两个类实例赋值给数组
array[0]=mcat;
array[1]=wukong;
MessageBox.Show(array[0].ChangeThing("各种各样的东西!")); // 利用多态性,实现不同的ChangeThing
MessageBox.Show(array[1].ChangeThing("各种各样的东西!"));
}
抽象类与接口的区别?
抽象类可以给出一些成员的实现,接口却不包含成员的实现,抽象类的抽象成员可被子类部分实现,接口的成员需要实现类完全实现;一个类只能继承一个抽象类,但可实现多个接口。
注意:第一、类是对对象的抽象,抽象类是对类的抽象,接口是对行为的抽象;第二、如果行为跨越不同类的对象,可使用接口;对于一些相似的类对象,用继承抽象类,实现接口和继承抽象类并不冲突;第三、从设计角度讲,抽象类是从子类中发现了公共的东西,泛化出父类,然后子类继承父类,而接口是根本不知道子类的存在,方法如何实现还不确认,预先定义。
集合
我们都知道,数组的优点在于数组在内存中是连续存储的,因此可以快速而容易地从头到尾遍历元素,可以快速修改元素等等。缺点在于,其创建时必须要指定数组变量的大小,还有在两个元素之间添加元素也比较困难。这就可能使得数组长度设置过大,造成内存空间浪费;长度设置过小造成溢出。
所以.NET Framework提供了用于数据存储和检索的专用类,这些类统称集合。这些类提供对堆栈、队列、列表和哈希表的支持。大多数集合类实现相同的接口。
ArrayList是命名空间System.Collections下的一部分,它是使用大小可按需动态增加的数组实现IList接口(IList接口定义了很多集合用的方法,ArrayList对这些方法做了具体的实现)。
ArrayList的容量是ArrayList可以保存的元素数。ArrayList的默认初始容量为0。随着元素添加到ArrayList中,容量会根据需要通过重新分配自动添加。使用整数索引可以访问此集合中的元素。此集合中的索引从0开始。
来看代码:
using System.Collections; // 增加此命名空间
public partial class Form1 :Form{
IList arrayAnimal; // 声明一集合变量,可以用接口IList,也可以直接声明“ArrayList array Animal”
// 动物报名按钮事件
private void button_Click(object sender,EventArgs e){
arrayAnimal =new ArrayList(); // 实例化ArrayList对象,注意,此时并没有制定arrayAnimal的大小,这与数组并不相同
arrayAnimal.Add(new Cat("小花")); // 调用集合的Add方法增加对象,其参数是object,所以new Cat和new Dog都没有问题
arrayAnimal.Add(new Dog("阿毛"));
arrayAnimal.Add(new Dog("小黑"));
arrayAnimal.Add(new Cat("娇娇"));
arrayAnimal.Add(new Cat("咪咪"));
MessageBox.Show(arrayAnimal.Count.ToString()); // 随着Add对象的增加,集合的Count可以得到当前的元素个数
}
}
遍历:
foreach(Animal item in arrayAnimal){
MessageBox.Show(item.Shout());
}
移除:
arrayAnimal.RemoveAt(1);
然而,ArrayList不是类型安全的,不管传入什么对象,它都可以接受,这就使得你的程序可能在编译时没有问题,而运行时会报错。还有就是ArrayList对于存放值类型的数据,比如int、string型(string是一种拥有值类型特点的特殊引用类型)或者结构struct的数据,用ArrayList就意味着都需要将值类型装箱为Object对象,使用集合元素时,还需要执行拆箱操作,这就带来了很大的性能损耗。
所谓装箱,就是把值类型打包到Object引用类型的一个实例中。比如整型变量i被“装箱”并赋值给对象o:
int i=123;
object o=(object)i; //boxing
所谓拆箱就是指从对象中提取值类型:
o=123;
i=(int) o; // unboxing
相对于简单的赋值而言,装箱和拆箱过程需要进行大量的计算。对值类型进行装箱时,必须分配并构造一个全新的对象。其次,拆箱所需的强制类型转换也需要进行大量的计算。
为了解决这个问题,人们提出了泛型。
泛型
泛型是具有占位符(类型参数)的类、结构、接口和方法,这些占位符是类、结构、接口和方法所存储或使用的一个或多个类型的占位符。
首先泛型集合需要System.Collections.Generic的命名空间。而List类是ArrayList类的泛型等效类。该类使用大小可按需动态增加的数组实现IList泛型接口。其实用法上关键就是在IList和List后面加‘<T>’,这个‘T'就是你需要指定的集合的数据或对象类型。
来看代码:
using System.Collections.Generic; // 增加泛型集合命名空间
public partiial class Form1:Form{
IList<Animal>arrayAnimal; // 关键在这里:声明一泛型集合变量,用接口IList,
// 注意:IList<Animal>表示此集合变量只能接受Animal类型,其他不可以
// 也可以直接声明“List <Animal>arrayAnimal”
// 动物报名按钮事件
private void button_Click(object sender, EventArgs e){
arrayAnimal=new List<Animal>(); // 实例化List对象,注意,此时也需要指定List<T>的'T'是Animal
arrayAnimal.Add(new Dog("阿毛"));
arrayAnimal.Add(new Dog("小黑"));
arrayAnimal.Add(new Cat("娇娇"));
arrayAnimal.Add(new Cat("咪咪"));
MessageBox.Show(arrayAnimal.Count.ToString());
}
}
通常情况下,都建议使用泛型集合,因为这样可以获得类型安全的直接优点而不需要从基集合类型派生并实现类型特定的成员。
泛型是集ArrayList集合和Array数组优点于一身的好东西。
委托与事件
委托是对函数的封装,可以当作给方法的特征指定一个名称。而事件则是委托的一种特殊形式,当发生有意义的事情时,事件对象处理通知过程。
委托是一种引用方法的类型。一旦为委托分配了方法,委托将与该方法具有完全相同的行为。委托对象用关键字delegate来表明。而事件是说在发生其他类或对象关注的事情时,类或对象可通过事件通知它们。事件对象用event关键字声明。
了解了基本概念后,现在有一个需求,有一只猫叫Tom,有两只老鼠叫Jerry和Jack,Tom只要一叫,两只老鼠就说‘老猫来了,快跑’。该如何写代码?
来看实际代码:
class Cat{
private string name;
public Cat(string name){
this.name=name;
}
public delegate void CatShoutEventHandler(); // 声明委托CatShoutEventHandler
public event CatShoutEventHandler CatShout; // 声明事件CatShout,它的事件类型是委托CatShoutEventHandler
public void Shout(){
Console.WriteLine("喵,我是{0}。",name);
if(CatShout!=null){
CatShout(); // 表明当执行Shout()方法时,如果CatShout中有对象登记事件,则执行CatShout()
}
}
}
为什么这里的CatShout()是无参数、无返回值的方法呢?因为事件CatShout的类型是委托CatShoutEventHandler,而CatShoutEventHandler就是无参数,无返回值的。
再来看Mouse:
class Mouse{
private string name;
public Mouse(string name){
this.name=name;
}
public void Run(){
Console.WriteLine("老猫来了,{0}快跑!",name);
}
}
关键是Main函数的写法:
static void Main(string[] args){
Cat cat=new Cat("Tom"); // 实例化老猫Tom以及小老鼠Jerry和Jack
Mouse mouse1=new Mouse("Jerry");
Mouse mouse2=new Mouse("Jack");
cat.CatShout += new Cat.CatShoutEventHandler(mouse1.Run); // 表示将Mouse的Run方法通过实例化委托Cat.CatShoutEventHandler
// 登记到Cat的事件CatShout当中,
// 其中的“+=”表示“add CatShout”的意思
cat.CatShout += new Cat.CatShoutEventHandler(mouse2.Run);
cat.Shout();
Console.Read();
}
而所谓“-=“就是remove_CatShout()的含义。
接下来,我们增加一个类CatShoutEventArgs,让它继承EventArgs,EventArgs是包含事件数据的类的基类。换句话说,这个类的作用就是用来在事件触发时传递数据用的。
public class CatShoutEventArgs : EventArgs{
private string name;
public string Name{
get {return name;}
set { name=value;}
}
}
然后改写Cat类的代码,对委托CatShoutEventHandler进行重定义。增加两个参数,第一个参数object对象sender是指向发送通知的对象,而第二个参数CatShoutEventArgs的args,包含了所有通知接受者需要附件的信息。
class Cat{
private string name;
public Cat(string name){
this.name=name;
}
public delegate void CatShoutEventHandler(object sender, CatShoutEventArgs args); // 声明委托CatShoutEventHandler
public event CatShoutEventHandler CatShout; // 声明事件CatShout,它的事件类型是委托CatShoutEventHandler
public void Shout(){
Console.WriteLine("喵,我是{0}。",name);
if(CatShout!=null){
CatShoutEventArgs e = new CatShoutEventArgs(); // 声明并实例化一个CatShoutEventArgs
e.Name=this.name; // 并给Name属性赋值为猫的名字
CatShout(this,e); // 当事件触发时,通知所有登记过的对象,
// 并将发送通知的自己以及需要的数据传递过去
}
}
}
Mouse类中,可以显示老猫的名字了:
class Mouse{
private string name;
public Mouse(string name){
this.name=name;
}
public void Run(object sender, CatShoutEventArgs args){
Console.WriteLine("老猫{0}来了,{1}快跑!",args.Name,name);
}
}
Main函数没有变化。
本章完。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
本文是作者阅读《大话设计模式》后,根据书中内容总结出的个人感悟。在此感谢程杰先生及其著作,本人受益匪浅。
本文是连载文章,此为序章,在了解设计模式前先熟悉面向对象的基本概念,转变之前的思维。
下一章:https://blog.youkuaiyun.com/qq_36770641/article/details/82712283 简单工厂模式
(本文遵循《大话设计模式》中的讲述顺序和方法,代码部分使用C#)
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------