Q:类是什么?对象又是什么?
A:准确定义不知道,类大概是对东西分类的意思
Q:看来你真是菜鸟啊,一切事物皆为对象,即所有的东西都是对象,对象可以被看到,听到,问到......准确的说,对象是一个自包含的实体,用一组可识别的特性和行为来标识。
面向对象编程,英文叫“object-oriented programming” 其实就是针对对象进行编程的意思。
类与实例
用VS建立一个window应用程序最终我们将实现动物运动会的例子
首先实现这样一个功能,点击一个“猫叫”的按钮,会弹出小猫的叫声'喵-'
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show("喵");
}
A是这个意思吗?
Q是,程序是编写出来了,现在的问题是如果我们需要在宁外一个按钮中让小猫叫一声,或者需要小猫多叫几声,怎么办?
A那就多写几个 MessageBox.Show("喵");
Q那不就重复了吗?
A我知道你意思,写个函数就可以解决了,其他需要“猫叫”的地方都可以
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show(shout() );
}
string shout()
{
return "喵";
}
Q很好,现在的问题是,万一别的地方,或者其他窗体也需要'猫叫‘如何处理?
A这个好像学过的,在shout 前面加一个public,别的窗体就可以访问了
Q对,没错,但你觉得这个代码放在这个窗体里合适吗?这样好比居委会的电视放你家,而别人家没有,都来你们家看电视你喜欢这样吗?
A公用电视应该放在居委会
Q所以猫叫的函数应该放在一个更合适的地方,这就是'类’;类就是具有相同的属性和功能的对象的抽象的集合,我们来看代码
class Cat
{
public string shout()
{
return "喵";
}
}
Q 这里class表示定义的关键字,Cat就是类的名称,Shout是类的方法
A 哈,定义这个玩意还挺简单嘛
Q这里有个注意的点:对外公开的代码要用public修饰符
A明白,怎么运用这个类呢?
Q很简单,只要将类实例一下就可以了
A什么是实例化?
Q实例,就是一个真实的对象,比如说我们都是‘人’,而你和我就是人的实例了,而实例化就是创建对象的过程,使用NEW关键字来创建
private void button1_Click(object sender, EventArgs e)
{
//将Cat实例化
Cat cat = new Cat();
//Cat cat = new Cat();==
//Cat cat ;声明一个cat对象,对象名为cat,
//cat = new Cat();将cat对象实例化
MessageBox.Show(cat.shout() );
}
class Cat
{
public string shout()
{
return "喵";
}
}
QCat实例化后,等同于生出了一只小猫cat,此时就可以让小猫cat.shout();了
在任何需要小猫叫的地方都可以实例化他
A,明白,这样调用他确实方便多了
构造方法
Q下面,我们希望出生的小猫应该有个名字,比如叫‘咪咪’,当咪咪叫的时候,最好是能说‘我的名字叫咪咪,喵’。此时就需要考虑用构造方法。
A构造方法?这是做什么用的?
Q构造方法,又叫构造函数,其实就是的类进行初步始化。构造方法与类同名,无返回值,也不需要void在new时候调用。
A那就是说,在类创建时,就是调用构造方法的时候了
Q“是呀,在‘Cat cat=new Cat();’ 中,new 后面的 Cat()其实就是构造方法。
A”“不对呀,在类当中没有写过构造方法 Cat(),怎么可以调用呢?”
Q“问得好,实际情况是这样,所有类都有构造方法,如果你不编码则系统默认生成空的构造方法
若你有定义的构造方法,那么默认的构造方法就会失效了。也就是说,由于你没有在 Cat 类中定义过构造方法,所以C#语言会生成一个空的构造方法 Cat()。当然,这个空的方法是什么也不做,只是为你能顺利地实例化而己。”
A“那不是很好吗?我们还需要构造方法做什么呢?”
Q刚才不是说过吗?构造方法是对类的初始化,如果如果我们希望每一个小猫一诞生就有姓名,那么就应该写一个有参数的方法
Q这样一来,我们在客户端生成小猫时,就必须要给小猫起名字了
class Cat
{
private string name = "";//声明cat私有字段变量name
public Cat (string name)//d定义cat构造方法,参数是输入一个字符串
{
this.name = name;//将参数赋值给私有变量name
}
public string shout()
{
return "我的名字"+name +"喵";
}
}
private void button1_Click(object sender, EventArgs e)
{
//将Cat实例化
Cat cat = new Cat("咪咪");
//Cat cat = new Cat();==
//Cat cat ;声明一个cat对象,对象名为cat,
//cat = new Cat();将cat对象实例化
MessageBox.Show(cat.shout() );
}
运行效果如下
方法重载
A如果我们事先没有期号小猫的名字,难道这个实例就创建不了了吗?
Q“是的,有些父母刚生下孩子时,姓名没有起好也是很正常的事。就目前的代码,你如果写 ‘Cat cat=new Cat();’是会直接报“Cat 方法没有采用。个参数的重载,的错误,原因就是必领要给小猫起名字。如果当真需要不起名字也要生出小猫来。可以用‘方法重载’。”
A“方法重载?好像也学过的东西,具体如何说?”
Q方法重载提供了创建同名的多个方法的能力,但这些方法需使用不同的参数类型。注意并不是只有构造方法可以重载,普通方法也是可以重载的。”
class Cat
{
private string name = "";//声明cat私有字段变量name
public Cat (string name)//d定义cat构造方法,参数是输入一个字符串
{
this.name= name;//将参数赋值给私有变量name
}
public Cat ()
{
this.name = "无名";
}
public string shout()
{
return "我的名字"+name+"喵";
}
}
A这样的话,如果写 ‘Cat cat = new Cat();的话,就不会报错了。而猫叫时会是 ‘我的名子叫
无名喵“
Q对的,注意意方法重载时,两个方法必须要方法名相同,但参数类型或个数必须要有所不同,否则重載就没有意义了。你觉得方法重载的好处是什么?
A“哈,我想应该是方法重載可在不改变原方法的基础上,新增功能。”
Q“说得很好,方法重载算是提供了函数可扩展的能力。比如刚才这个例子,有的小猫起好名字了,
就用带 string 参数的构造方法,有的没有名字,就用不带参数的,这样就达到了扩 展的目的。
A“如果我需要分清楚猫的姓和名,还可以再重载一 个 public Cat(string firstName,string lastVame),对吧?”
Q“对的。非常好。下面,我们觉得小猫叫的次数太少,希望是我让它叫儿声,它就叫儿声,如何做?”
“那是不是在构造方法里再加一个叫的次数?”
“那样当然是可以,但叫几声并不是必须要实例化的时候就声明的,我们可以之后再规定叫几声,所以这时应该考虑用‘属性’。
属性与修饰符
“属性是一个方法或一对方法,但在调用它的代码看来,它是一个字段,即属性适合于以字段的方式使用方法调用的场合。这里还需要解释一下字段的意思,字段是存储类要满足其设计所需要的数据,字段是与类相关的变量。比如刚才的 Cat 类中的‘private string name = m;,name 其实就是一个字段,它通常是私有的类变量。那么属性是什么样呢?我们现在增加一个“猫叫次数 ShoueNum’的属性。”
private int shoutNum = 3;
public int ShuotNum//声明一个内部字段,注意是private,默认叫的次数是3
{
//get表示外界调用是可以得到shoutNum的值
get
{
return shoutNum;//shoutNum是属性,注意是public,当中有两个方法
}
//set表示外界调用是可以设置shoutNum的值
set
{
shoutNum = value;
}
}
Q“刚才没有强调 public 和 private 的区别,它们都是修饰符,public 表示它所修饰的类成员可以允许其他任何类来访问,俗称公有的。而 private 表示只允许同一个类中的成员访问,其他类包括它的子类无法访问,俗称私有的。如果在类中的成员没有加修饰符,则被认为是 private 的。修饰符还有其他三个,我们以后再讲。通常宇段都是 private,即私有的变量,而属性都是 public, 即公有的变量。那么在这里 shoutNum 就是私有的字段,而 ShoutNum 就是公有的对外属性。由于是对外的,所以属性的名称
一般首字母大写,而字段则一般首字母小写或前加“-",
A“属性的get 和set 是什么意恩?”
Q“属性有两个方法 get 和set。get 访问器返回与声明的属性相同的数据类型,表示的意思是调用时可以得到内部字段的值或引用;set 访问器没有显式设置参数,但它有一个隐式参数,用关键宇 value 表示,它的作用是调用属性时可以给内部的字段或引用赋值。”
A“那又何必呢,我把字段的修饰符改为 public,不就可以做到对变量的既读又写了吗?”
Q“是的,如果仅仅是可读可写,那与声明了public 的字段没什么区别。但是对于对外界公开的数据,我们通常希望能做更多的控制,这就好像我们的房子,我们并不希望房子是全透明的,那样你在家里的所有活动全部都被看得清清楚楚,亳无隐私可言。通常我们的房子有门有窗,但更多的是不透明的墙。
这门和窗其实就是 public,而房子内的东西,其实就是 private。而对于这个房子来说,门窗是可以控制的,我们并不是让所有的人都可以从门随意进出,也不希望蚊子苍蝇来回出入。这就是属性的作用了,如果你把字段声明为public, 那就意味着不设防的门窗,任何时候,调用者都可以读取或写入,这是非糟糕的一件事,如果把对外的数据写成属性,那情况会好很多
private int shoutNum = 3;
public int ShuotNum//声明一个内部字段,注意是private,默认叫的次数是3
{
//get表示外界调用是可以得到shoutNum的值
get
{
return shoutNum;//shoutNum是属性,注意是public,当中有两个方法
}
//set表示外界调用是可以设置shoutNum的值
set
{
//控制猫叫的次数最多只能叫10声
if (value <= 10)
shoutNum = value;
else
shoutNum = 10;
}
如果去掉set代码就变成了只读属性
做一个循环,以便让小猫叫相应的次数
public string shout()
{
string result = "";
for (int i=0;i <shoutNum; i++)
{
result += "喵";
}
return "我的名字" + name + "" + result;
}
此时调用的时候只需要给属性赋值就可以了
private void button1_Click(object sender, EventArgs e)
{
Cat cat = new Cat("小咪");
cat.ShuotNum = 5;
MessageBox.Show(cat.shout() );
}
生成结果
Q如果我们不给属性赋值,小猫会叫吗?
A当然会,应该会叫三声把,因为字段shoutNum的初始值是3
Q很好,宁外需要强调的是,变量私有叫字段,公有的是属性
对于方法也有私有方法和公有方法,一般无需对外公开的方法都应该设置其修饰符为private。这才有利于封装
封装
Q“现在我们可以讲面向对象的三大特性之一‘封装’了。每个对象都包含它能进行操作所需要的所有信息,这个特性称为封装,因此对象不必依赖其他对象来完成自己的操作。这样方法和属性包装在类中,通过类的实例来实现。”
A“是不是刚才提炼出 Cat 类,其实就是在做封装?
Q“是呀,封装有很多好处,第一、良好的封装能够减少耦合,至少我们让 Cat 和 Forml 的耦合分离了。第二、类内部的实现可以自由地修改,这也是显而易见的,我们已经对 Cat 做了很大的改动。第三、类具有清晰的对外接口,这其实指的就是定义为public 的 ShoutNum 属性和 Shout 方法。”
Q“封装的好处很好理解呀,比如刚才举的例子。我们的房子就是一个类的实例,室内的装饰与摆这只能被室内的居佳者欣赏与使用,如果没有四面墙的遮挡,室内的所有活动在外人面前一览无遗。由于有了封装,房屋内的所有摆设都可以随意地改变而不用影响他人。然而,如果没有门窗,一个包裹得严严实实的黑箱子,即使它的空间再宽阔,也设有实用价值。房屋的门窗,就是封装对象暴露在外的属性和方法,专门供人进出,以及流通空气、带来阳光。
Q“现在我需要增加一个狗叫的功能,就是加一个按钮‘狗叫’,点击后会弹出 ‘我的名字叫Xx 汪汪汪’如何做?”
A“那简单呀,仿造 Cat 加一个 Dog 类就可以了。然后再增加一个buton2 按钮,写上click 事件代码。”
private void button2_Click(object sender, EventArgs e)
{
Dog dog = new Dog("旺财");
dog.ShoutNum2 = 5;
MessageBox.Show(dog .shout());
}
运行结果为
但是代码重复
继承
Q “我们还是先离开软件编程,来想想我们的动物常识,猫和狗都是什么?。
A“都是给人添麻烦的东西。除了吃喝拉救睡,什么也不干的家伙。”
Q“拜托,正经一些。猫和狗都是动物,准确地说,他们都是哺乳动物。哺乳动物有什么特征?”A“哦,这个小时候学过,哺乳动物是胎生、哺乳、恒温的动物。”
Q “OK,因为猫和狗是哺乳动物,所以猫和狗就同样具各胎生、哺乳、恒温的特征。所以我们可以这样说,由于猫和狗是哺乳动物,所以貓和狗与哺乳动物是继承关系。”
A“哦,原来继承就是这个意思。
Q“是的,回到编程上,对象的继承代表了一种 is-a’的头系,如果两个对象A和B,可以描述为‘B
是A’,则表明B可以继承A。“猫是哺乳动物,就说明了猫与哺乳动物之间继承与被继承的关系。实际上,继承者还可以理解为是对被继承者的特殊化,因为它除了具备被继承者的特性外,还具备自己独有的个性。例如,猫就可能拥有抓老鼠、爬树等‘哺乳动物”对象所不具备的屆性。因而,在继承关系中,继承者可以完全替换被继承者,反之则不成立。所以,我们在描述继承的“is-a’ 关系时,是不能相互颠倒的。说‘哺乳动物是猫’ 显然有些真名其妙。继承定义了类如何相互关联,共享特性。继承的工作方式是,定义父类和子类,或叫做基类和派生类,其中于类继承父类的所有特性。子类不但继承了父类的所有特性,还可以定义新的特性。”
A“ Sis-a’这个比较好理解。”
Q“学习继承最好是记住三句话,如果子类继承于父类,第一、子类拥有父类非private 的属性和功能:第二、子类具有自己的属性和功能,即子类可以扩展父类没有的属性和功能:第三、子类还可以以自己的方式实现父类的功能(方法重写)。”
“这里有些不理解,什么叫非private,滩道除了public 还有别的修饰符吗?
“当然有,刚才讲了private和 public,现在再讲一个protected 修饰行。protected 表示继承时子类可以对基类有完全访问权
先写父类animal
class Animal
{
protected string name = "";
public Animal (string name)
{
this.name = name;
}
public Animal ()
{
this.name = "无名";
}
protected int shoutName = 3;
public int ShoutName
{
get
{
return shoutName;
}
set
{
shoutName = value;
}
}
}
在写cat和dog的代码
继承的格式是子类:父类
子类构造方法同样要调用父类的构造方法,用base关键字
class Cat : Animal
{
public Cat ():base ()
{
}
public Cat (string name):base (name )
{
}
public string Shout()
{
string result = " ";
for (int i=0;i<shoutName;i ++)
{
result += "喵,";
}
return "我的名字叫" + name + " " + result;
}
}
class Dog: Animal
{
public Dog() : base()
{
}
public Dog(string name) : base(name)
{
}
public string Shout()
{
string result = " ";
for (int i = 0; i < shoutName; i++)
{
result += "喵,";
}
return "我的名字叫" + name + " " + result;
}
}
Q“此时容户端的代码完全一样,没有受到影响,但重复的代码却因此减少了。”小菜说。
A“差不太多嘛,子类还是有些复杂,没简单到哪去?”史熙说道。
Q“如果现在需要加牛、羊、猪等多个类似的类,按你以前的写法就需要再复制三遍,也就是有五个类。如果我们需要改动起始的叫声次数,也就是让 shoutNum 由3改为5,你需要改几个类?”
A“我懂你意思了,那需要改 5个类,现在有了 Animal, 就只要改一个类就行了,继承可以使得重复
减少。
Q“说得很好。不用继承的话,如果要修改功能,就必须在所有重复的方法中修改,代码越多,出错的可能就越大,而继承的优点是,继承使得所有子类公共的部分都放在了父类,使得代码得到了共享,这就避免了重复,另外,继承可使得修改或扩展继承而来的实现都较为容易。”
A“嗯,继承是个好东西,我以前时常 Ctrl+C 加 CtrltV 的,这样表面上很快,但其实重复的代码越多,以后要更改的难度越大。看来以后我要多多用继承。”
Q“慢,等等,你说以后要多多用继承?那可不一定是好事,用是可以,但一定要慎重。”
A〞有这么严重吗,反正只要是有重复的时候,就继承一个于类不就行了吗?
Q“哈,那真是大错特错了,那我问你,你先写 Cat,而后要你与一个Dog,由于都差不多,你有没有考虑过直接让狗去继承猫呢?”
A“咦,这其实也差不多哦,没什么问题呀。如果再有了羊呀、牛呀也都分别继承猫就可以了。”〞Q问题就在这里了。这就使得猫会的行为,狗都会。现在编写的这只猫只会叫,以后它应该还可以会抓老鼠、爬树等行为,你让狗继承了猫,就意味着狗也就会抓老鼠、 会爬树。你觉得这合理吗?”
A“狗拿耗子,那是多管闲事了。看来不能让狗继承猫,那样很容易造成不必要的麻烦。”
Q“继承是有缺点的,那就是父类变,则子类不得不变。让-狗去继承于猫,显然不是什么好的设计,另外,继承会破坏包装,父类实现细节暴露给子类,这其实是增大了两个类之间的耦合性。
A“什么叫轉合性?”
Q“严格定义你自己去查吧,简单理解就是藕断丝连,两个类尽管分开,但如果关系密切,一方的变化都会影响到另一方,这就是轉合性高的表现,继承显然是一种类与类之问强耜合的关系。”
A“明白,你说了这么多,那什么时候用继承才是合理的呢?”
Q“我最先不是说过吗?当两个类之问具备“is-a’ 的关系时,就可以考虑用继承了,因为这表示一个
类是另一个类的特殊种类,而当两个类之间是‘has-a’的关系时,表示某个角色具有某一项责任,此时不合适用继承。比如人有两只手,手不能继承人;再比如飞机场有飞机,这飞机也不能去继承飞机场。
“OK,也就是说,只有合理的应用继承才能发挥好的作用。”
多态
Q“下面我们再来增加需求,如果我们要举办一个动物运动会,求参加的有各种各样的动物,其中有一项是‘叫声比赛”。界面就是放两个拔钮,一个是“动物报名”,就是确定动物的种类和报名的顺序,另一个是“叫声比赛’,就是报名的动物换个地叫出声音来比赛。注意来报名的都是什么动物,我们并不知道。可能是猫、 可能是狗,也可能是其他的动物,当然它们都需要会叫才行。
A“有点复杂,我除了会加两个按钮外,不知道如何做了。”
Q“先分析一下,来报名的都是动物,参加叫声比赛必领会叫。这说明什么?”
A“说明都有叫的方法,哦,也就是Animal类中有 Shout 方法。”
Q“是呀,所谓的‘动物报名’,其实就是建立一个列物对象数组,让不同的动物对象加入其中。所谓的‘叫声比賽’,其实就是遍历这个数组来让动物们“Shout()”就可以了。”
A“哦,我大概明白你意思了。那看吞我写的代码,我说得应该是类似的样子,但问题是我们不知道是哪个动物来报名,最终叫的时候到底是猫在叫还是狗在叫呢?,
private Animal[] arryAnimal;//声明一个动物数组
private void button3_Click(object sender, EventArgs e)
{
arryAnimal = new Animal[5];//实例化最多可报名5个的动物数组对象
}
/// <summary>
/// 叫声比赛 按钮事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button4_Click(object sender, EventArgs e)
{
foreach (Animal item in arryAnimal)//遍历整个数组让他们都shout();
{
MessageBox.Show(item.Shout());//是什么动物在叫我也不知道
}
}
Q“是呀,就之前讲到的知识,是不足以解决这个问题的,所以我们引入面向对象的第三大特性-多态。
A“啊,多态,多态是我大学里听得很多,但一直都不懂的东西,实在不明白它是什么意恩。”
Q“多态表示不同的对象可以执行相同的动作,但要通过它们自己的实现代码來执行。看定义显然不太明白,我来华不例子你就好理解了。我们的国料 一京即一以的都是子承父业,代代相使的艺术。假设有这样一对父子,父亲是非常有名的京剧艺术家,儿子长大成人,模仿父亲的戏也惟妙惟肖。有一天,父亲突然发高烧,上不了台表演,而票都早就卖出,退票显然会大大影响声誉。怎么办呢?由于京戏都是需要化妆才可以上合的,于是就快定让儿子代父亲上台表演。”“化妆后谁认识谁呀,只要唱得好就可以糊弄过去了。”
“是呀,这里面有几点注意,第一,子类以父类的身份出现,儿子代表老子表演,化妆后就是以父亲身份出现了。第二、子类在工作时以自己的方式来实现,儿子模仿得再好,那也是模仿,儿子只能用自己理解的表现方式去模份父亲的作品;第三、子类以父类的身份出现时,子类特有的属性和方法不可以使用,儿于经过多年学习,其实已经有了自己的创作,自己的绝活,但在此时,代表父亲表演时,绝活是不能表现出来的。当然,如果父亲还有别的儿子会表演,也可以在此时代表父亲上场,道理也是一样的。这就是多态。”
A“听听好像都懂,怎么用呢?”
Q“是呀,怎么用呢,我们还需要了解一些概念,虛方法和方法重写。为了使子类的实例完全接替来目父类的类成员,父类必须将该成员声明为虛拟的。这是通过在该成员的返回类型之前添加virtual关键字来实现。通常虛拟的是方法,但其实除了字段不能是虛拟的,属性、事件和索引器都可以是虛拟的。
尽管方法可以是虛拟的,但虚方法还是有方法体,可以实际做些事情。然后,子奕可以选择使用override关键宇,将父类实现替换为它自己的实现,这就是方法重写 Override,或者叫做方法覆写。我们来看下例子。”
“由于 Cat 和 Dog 都有 Shout 的方法,只是叫的声音不同,所以我们可以让 Animal 有一个 Shout 的虚方法,然后 Cat 和Dog 去車写这个 Shout,用的时候,就可以用猫或狗代替 Animal 叫唤,来达到多态的目的。”
class Animal
{
......
public virtual string Shout()//virtual表示方法是虚方法,可以被子类重写
{
return "";
}
}
class Cat : Animal
{
public Cat ():base ()
{
}
public Cat (string name):base (name )
{
}
public override string Shout()
{
string result = " ";
for (int i=0;i<shoutName;i ++)
{
result += "喵,";
}
return "我的名字叫" + name + " " + result;
}
}
class Dog: Animal
{
public Dog() : base()
{
}
public Dog(string name) : base(name)
{
}
public override string Shout()
{
string result = " ";
for (int i = 0; i < shoutName; i++)
{
result += "汪,";
}
return "我的名字叫" + name + " " + result;
}
}
private Animal[] arryAnimal;//声明一个动物数组
private void button3_Click(object sender, EventArgs e)
{
arryAnimal = new Animal[5];//实例化最多可报名5个的动物数组对象
arryAnimal[0] = new Cat("小花");
arryAnimal[1] = new Cat("小发");
arryAnimal[2] = new Cat("小蜜");
arryAnimal[3] = new Cat("小雪");
arryAnimal[4] = new Cat("小明");
}
/// <summary>
/// 叫声比赛 按钮事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button4_Click(object sender, EventArgs e)
{
foreach (Animal item in arryAnimal)//遍历整个数组让他们都shout();
{
//由于有了多态性,所以叫的时候,程序会自动去找item是什么对象,然后用哪个重写方法
MessageBox.Show(item.Shout());//是什么动物在叫我也不知道
}
}
输出结果为
先点击报名,然后叫声比赛,将有五个展示框全部弹出
“我明白了,Animal相当于京剧表演的老爸,Cat 和Dog相当于儿子,儿子代表父亲表演 Shout,但Cat 叫出来的是“喵’,Dog 叫出来的是“汪’,这就是所谓的不同的对象可以执行相同的动作,但要通过它们自己的实现代码来执行。”
“说得好,是这个意思,不过一定要注意了,这个对象的声明必须是父类,而不是子类,实例化的对象是子类,这才能实现多态。多态的原理是当方法被调用时,无论对象是否被转换为其父类,都只有位于对象继承链最末端的方法实现会被调用。也就是说,虚方法是按照其运行时类型而非编译时类型进行动态绑定调用的。[AMNFP]”
动物 animal=new 猫();
或者
猫 cat = new 猫();
动物 animal = cat;
“不过老实说,即使这样,我也还是不太理解这样做有多大的好处。多态被称为面向对象三大特性,我感觉不到它有和封装、继承同样的作用。”
“慢慢来,要深刻理解并会合理利用多态,不去研究设计模式是很难做到的。也可以反过来说,没有学过设计模式,那么对多态、乃至对面向对象的理解多半都是肤浅和片面的。我相信会有那种天才,可以听一知十,刚学的东西就可以灵活自如地应用,甚至要造汽车,他都能再去发明轮子。但对于绝大多数程序员,还是需要踏踏实实地学习基本的东西,并在不断地实践中成长,最终成为高手。”
“蔡老师,受教了。下面我们做什么?”
(注:多态中样例改编自《编程的奥秘》,作者金旭亮,电子工业出版社出版。)
重构
现在又来了小牛和小羊来报名,需要参加‘叫声比赛’,你如何做?,
/// <summary>
/// 牛
/// </summary>
class Cattle:Animal
{
public Cattle ():base ()
{
}
public Cattle (string name ):base (name )
{
}
public override string Shout()
{
string result = " ";
for (int i = 0; i < shoutName; i++)
{
result += "哞,";
}
return "我的名字叫" + name + " " + result;
}
}
/// <summary>
/// 羊
/// </summary>
class Sheep : Animal
{
public Sheep() : base()
{ }
public Sheep (string name) : base(name)
{ }
public override string Shout()
{
string result = "";
for (int i = 0; i < shoutName; i++)
{
result += "咩,";
}
return "我的名字叫" + name + " " + result;
}
}
Q等等,你有没有发现,猫狗牛羊四个类,除丁构造方法之外,还有重复的地方?
A是呀,我发现了,Shout 里除了四种动物叫的声音不同外,几乎没有任何差异。”
Q这有什么坏处?
A“重复呀,如果你有需求说,把“我的名字叫xxx’改成“我叫 xxx’,我就得更改四个类的代码了。
Q“非常好,所以这里有重复,我们还是应该要改造它。”“我先把重复的这个 Shout 的方法体放到 Animal 类中,去掉 virtual。”
A“这样如何能行,动物叫什么声音呢?叫‘喵??叫“汪,?都不行,动物是个抽象的概念,它是不会有叫的声音的。”
Q“别急,我们把叫的声音部分改成另一个方法getShoutsound 不就行了!”
public Animal ()
{
......
public string Shout()//去掉virtual,普通的公共方法
{
return "";
string result;
for (int i = 0; i < shoutName; i++)
{
result += "getShoutSound"+",";
}
return "我的名字叫" + name + " " + result;
}
/// <summary>
/// 得到叫声的虚方法,让子类重写,只需要给继承的子类使用,所以用protected修饰符
/// </summary>
/// <returns></returns>
protected virtual string getShoutSound()
{
return "";
}
}
此时的子类就及其简单了,除了叫声和构造方法不同,所有重复的都转移到了父类,真是漂亮至极
/// <summary>
/// 猫
/// </summary>
class Cat : Animal
{
public Cat() : base()
{
}
public Cat(string name) : base(name)
{
}
protected override string getShoutSound()
{
return "喵";
}
}
/// <summary>
/// 狗
/// </summary>
class Dog : Animal
{
public Dog() : base()
{
}
public Dog(string name) : base(name)
{
}
protected override string getShoutSound()
{
return "汪";
}
}
/// <summary>
/// 牛
/// </summary>
class Cattle : Animal
{
public Cattle() : base()
{
}
public Cattle(string name) : base(name)
{
}
protected override string getShoutSound()
{
return "哞,";
}
}
/// <summary>
/// 羊
/// </summary>
class Sheep : Animal
{
public Sheep() : base()
{ }
public Sheep(string name) : base(name)
{ }
protected override string getShoutSound()
{
return "咩,";
}
}
A“有点疑问,这样改动,子类,比如 Cat 就没有 Shout 方法了,外面如何调用呢?”“嗨,你把继承的基本忘记了?继承第一条是什么?”
Q“哈,是子类拥有所有父类非private的属性和方法。对的对的,由于子类继承父类,所以是 public
的 Shout 方法是一定可以为所有子类所用的。”史熙高兴地说,“我渐渐能感受到面向对象编程的魅力了,的确足非常的简捷。由于不重复,所以需求的更政都不会影响到其他类。”
A这里其实就是在用一个设计模式,叫模板方法。
Q啊,原来就是设计模式呀,very Good,太棒了,哈,我竞然学会了设计模式。
A“疯了?发什么神经呀。”“这才是知道了皮毛,得意什么,还早着呢。”
抽象类
Q我们再来观察,你会发现,Animal 类其实根木就不可能实例化的,你想呀,说一只猫长什么样,可以想象,说 new Animal();即实例化一个动物。一个动物长什么样?,
A•不知道,动物是一个抽象的名词,没有具体对象与之对应。”
Q“是呀,所以我们完全可以考患把实例化没有任何意义的父类,改成抽象类,同样的,对于 Animal类的getShoutsound 方法,其实方法体没有任何意义,所以可以将 virtual修饰行改为 abstract, 使之成为批象方法。C#允许把类和方法声明为 abstract,即抽象类和抽象方法。”Q“这样一来,Animal 就成了抽象类了。抽象类需要注意几点,第一,抽象类不能实例化,刚才就说过,‘动物,实例化是没有意义的:第二,抽象方法是必须被子类重写的方法,不重写的话,它的存在叉有什么意义呢?其实抽象方法可以被看成是没有实现体的虚方法;第三,如果类中包含抽象方法,那么类就必须定义为抽象类,不论是否还包含其他一般方法。”
加abstract关键字,表明是抽象类
abstract class Animal
{
protected abstract string getShoutSound();
}
没有返回值result
A“这么说的话,一开始就可以把 Animal 类设成抽象类了,根本没有必要存在虚方法的父类。是这样吧?
Q“的确是这样,我们应该考虑让抽象类拥有尽可能多的共同代码,拥有尽可能少的数据[J&DPT]。”A“那到底什么时候应该用抽象类呢?”
Q“抽象类通常代表一个抽象概念,它提供一个继承的出发点,当设计一个新的抽象类时,一定是用来继承的,所以,在一个以继承关系形成的等级结构里面,树叶节点应当是具体类,而树枝节点均应当是抽象类[J&DA]。也就是说,具体类不是用来继承的。我们作为编程设计者,应该要努力做到这一点。比如,若猫、狗、牛、羊是最后一级,那么它们就是具体类,但如果还有更下面一级的金丝猫继承于猫、哈巴狗继承于狗,就需要考虑把猫和狗改成抽象类了,当然这也是需要具体情况具体分析的。”
接口
Q“在动物运动会里还有一项非常特殊的比赛是为了给有特异功能的动物展示其特殊才能的。
A”“哈,有特异功能?有意思。不知是什么动物?”
Q“多的是呀,可以来比赛的比如有机器猫叮哨,石猴孙悟空,肥猪猪八戒,再比如蜘蛛人、蝙蝠俠等。
A”“啊,这都是什么动物呀,根本就是人们虚构之物。”
Q让猫狗比赛叫声难道就不是虚构?你当它们会愿意相互攀比?其实我的目的只是为了让两个动物尽量的不相干而已。现在叮哨会从肚皮的口袋里变出东西,而孙语空可以拔根毫毛变出东西,且有七十二般变化,八戒有三十六般变化。它们各属于猫、猴、猪,现在需要让它们比赛谁变东西的本领大。你来分析一下如何做?
A变出东西’应该是叮当、孙悟空、猪八戒的行为方法,要想用多态,就得让猫、猴、猪有‘变出东西’的能力,而为了更具有普遍意义,千脆让动物具有 “变出东西’的行为,这样就可以使用多态了。”
Q“哈哈,史熙呀,你犯了儿乎所有学面向对象的人都会犯的错误,‘变出东西,它是动物的方法吗?如果是,那是不是所有的动物都必须具备 “变出东西’的能力呢?”
A“这个,确实不是,这其实只是三个特殊动物具备的方法。那应该如何做?
Q“这时候我们就需要新的知识了,那就是接口 interface。 接口是把隐式公共方法和属性组合起来,以封装特定功能的一个集合。一旦类实现了接口,类就可以支持接口所指定的所有属性和成员。声明接口在语法上与声明抽象类完全相同,但不允许提供接口中任何成员的执行方式。所以接口不能实例化,不能有构造方法和字段;不能有修饰符,比如 public、 private 等;不能声明虛拟的或静态的等。还有实现接口的类就必须要实现接口中的所有方法和属性。
“怎么接口这么麻烦。
“要求是多了点,但一个类可以支持多个接口,多个类也可以文持相同的接口。所以接口的概念可以让用户和其他开发人员更容易理解其他人的代的。哦,对了,记佳,接口的命名,前面要加一个大写子母I,这是规范。”
〝听不懂呀,不如讲讲实例吧。”
我们先创建一个接口,它用来变东西的
注意接口用interface来声明,接口名称前要加I,接口中的方法或属性前面不能有修饰符,方法没有方法体
/// <summary>
/// 声明一个ichang的接口,此接口有一个方法changething参数是一个字符串变量,返回一字符串
/// </summary>
interface IChange
{
string ChangeThing(string thing);
}
然后我们创建机器猫 猴子 的类
/// <summary>
/// 机器猫的类
/// </summary>
class MachineCat : Cat, IChange
{
public MachineCat() : base()
{
}
public MachineCat(string name) : base(name )
{
}
public string ChangeThing(string thing )//实现接口方法,注意不能加override修饰符
{
return base.Shout() + "我有万能口袋,我可变出:" + thing;
}
}
class StoneMonkey : Monkey , IChange
{
public StoneMonkey ():base ( )
{
}
public StoneMonkey (string name ):base (name )
{ }
public string ChangeThing(string thing)//实现接口方法,注意不能加override修饰符
{
return base.Shout() + "我会72变,我可变出:" + thing;
}
}
添加按钮,并实现下面的代码
private void button5_Click(object sender, EventArgs e)
{
MachineCat mcat = new MachineCat("小叮当");
StoneMonkey wukong = new StoneMonkey("孙悟空");
IChange[] arry = new IChange[2];
arry[0] = mcat;
arry[1] = wukong;
MessageBox.Show(arry[0].ChangeThing("各种各样的东西!"));//利用多态,实现不同的ChangeThing
MessageBox.Show(arry[1].ChangeThing("各种各样的东西!"));
}
A我,我明白了,其实这和抽象类是很类似的,由于我现在要让两个完全不相千的对象,叮当和孙悟来做同样的事情,所以我不得不让他们去实现做这件“变出东西,的接口,这样的话,“当我们调用变出东西的接口的方法时,程序就会根据我实现接口的对象做出反应如果是叮当就用万能你口袋,如果是孙悟空,就是72变,利用了多态性完成了两个不同对象本不可能完成的任务。
Q“说得非常好,同样是飞,鸟用翅膀飞,飞机用引擎加机翼飞,而超人呢?举起两手,握紧拳头就
飞,它们是完全不同的对象,但是,如果硬要把它们放在一起的话,用飞行行为的接口,比如命名
IFly 的接口来处理就是非常好的办法。”
“A但是我对抽象类和接口的区别还是不太清楚。”
Q“问到点子上了,这两个概念的异同点是网上讨论面向对象问题时讨论得最多的话题之一,从表象上来说,抽象类可以给出一些成员的实现,接口却不包含成员的实现,抽象类的抽象成员可被子类部分其现,接口的成员需要 实现类完全实现,一个类只能继承一个抽象类,但可实现多个接口等等。但这些犯是从两者的形态上去区分的。我觉得还有三点是能帮助我们去区分抽象类和接口的。第一,类是对对像的抽象;抽象类是对类的抽象;接口是对行为的抽象。按口是对类的局部 (行为)进行的抽象,而抽系类是对类整体(字段、属性、方法)的抽象。如果只关注行为抽象,那么也可以认为接口就是抽象类。总之,不论是接口、抽象类、类甚至对象,都是在不同层次、不同角度进行抽象的结果,它们的共性就是抽象。第二,如果行为跨越不同类的对象,可使用接口;对于一些相似的类对象,用继承抽象类。比如猫呀狗呀它们其实都是动物,它们之间有很多相似的地方,所以我们应该让它们去继承动物这个抽象类,而飞机、麻雀、超人是完全不相关的类,小叮当是动漫角色,孙悟空是古代神话人物,这也是不相关的类,但它们又是有共同点的,前三个都会飞,而后两个都会变出东西,所以此时让它们去实现相同的接口来达到我们的设计目的就很合适了。”
A“哦,明白,其实实现接口和继承抽象类并不冲突的,我完全可以让超人继承人类,再实现飞行接
口,是吗?”
Q“对,超人除了内裤外穿以外,基本就是一个正常人的样子,让他继承人类是对的,但他本事很大,除了飞天,他还具有刀枪不入、力大无穷等等非常人的能力,而这些能力也可能是其他对象具备的,所以就让超人去实现飞行、力大无穷等行为接口,这就可以让超人和(机比飞行,和大象比力气了,这就是一个类只能继承一个抽象类,却可以实现多个接口的做法。”
A“那还有一点呢?”
Q“嗯,这一点更加关键,那就是第三,从设计角度讲,抽象类是从子类中发现了公共的东西,泛化出父类,然后子类继承父类,而接口是根本不知子类的存在,方法如何实现还不确认,预先定义。这里其实说明的是抽象类和接口设计的思维过程。回想一下我们今天刚开始讲的时候,先是有一个 Cat 类,然后再有一个 Dog 类,观察后发现它们有类似之处,于是泛化出 Animal 类,这也体现了敏捷开发的思想,通过重构改善既有代码的设计。事实上,只有小猫的时候,你就去设计一个动物类,这就极有可能会成为过度设计了。所以说抽象类往往都是通过重构得来的,当然,如果你事先意识到多种分类的可能,那么事先就设计出抽象类也是完全可以的。而接口就完全不是一回事,比如我们是动物还动会的主办方,在策划时,大家就坐在一起考虑需要组织什么样的比賽,大家商议后,觉得应该设置如跑得最快、跳得最高、飞得最远、叫得最响、力气最大等等比赛项目,而此时,主办方其实还不太清楚会有什么样的动物来参加运动会,所有的这些比赛项日都可能是完全不相同的动物在比,它们将如何去实现这些行为也不得而知,此时,能做的事就是事先定义这些比赛项目的行为接口。”
A“啊,你的意思是不是说,抽象类是自底而上抽象出来的,而接口则是自顶向下设计出来的。
Q”“对,可以这么说。其实仅仅理解这一点是不够的,要想真正把抽象类和接口用好,还是需要好好用心地去学习设计模式。只有真正把设计模式理解好了,那么你才能算是真正会合理应用抽象类和接口了
集合
“下面我们再来看看,客广端的代码中,“动物报名’用的是 Animal 类的对象数组,你设置了数组的长度为5,也就是说最多只能有五个动物可以报名参加‘叫声比赛’,多了就不行了。这显然是非常不合理的,应该考虑改进。你能说说数组的优缺点吗?”
“数组优点,比如说数组在内存中连续存储,因此可以快速而容易地从头到尾遍历元素,可以快速修改元素等等。缺点嘛,应该是创建时必须要指定数组变量的大小,还有在两个元素之问添加元素也比较困难。”
“说得不错,的确是这样,这就可能使得数组长度设置过大,造成内存空间浪费,长度设置过小造成溢出。所以.NET Framework 提供了 用于数据存储和检索的专用类,这些类统称集合。这些类提供对堆栈、队列、列表和哈希表的支持。大多数集合类实现相同的接口。我们现在介绍当中最常用的一种,
ArrayList."
“集合?它和数组有什么区别?”
“别急,首先 ArrayList 是命名空间 System. Collections 下的一部分,它是使用大小可按需动态增加的数组实现 Iist 接口 MSDNI。”
,“哦,没学接口前不太懂,现在知道了,你的意思是说,Iist 接口定义了很多集合用的方法,Array List
对这些方法做了具体的实现?”
“对的,ArrayList 的容量是 ArrayList 可以保存的元素数。ArrayList 的默认初始容量为 0。随着元素添加到 ArrayList 中,容量会根据需要通过重新分配自动增加。使用整数索引1可以访问此集合中的元素。此集合中的索引从零开始。™MSDNI”
“是不是可以这样理解,数组的容量是固定的,而 ArayList 的容量可根据需要自动扩充。”“是的,由于实现了 Iist,所以 ArrayList 提供添加、插入或移除某一范围元素的方法。下面我们来
看看如何做。”
using System.Collections;//增加此命名空间
public partial class Form1 : Form
{
IList arryAnimal;//声明一集合变量,可以用接口Ilist,也可以直接声明ArrayList arryAnimal;
// private Animal[] arryAnimal;//声明一个动物数组
private void button3_Click(object sender, EventArgs e)
{
/* /* arryAnimal = new Animal[5];//实例化最多可报名5个的动物数组对象
arryAnimal[0] = new Cat("小花");
arryAnimal[1] = new Cat("小发");
arryAnimal[2] = new Cat("小蜜");
arryAnimal[3] = new Cat("小雪");
arryAnimal[4] = new Cat("小明");
*/
arryAnimal = new ArrayList();//实例化对象、
arryAnimal.Add (new Cat("小花"));
arryAnimal.Add(new Cat("小鸣"));
arryAnimal.Add(new Cat("小红"));
arryAnimal.Add(new Cat("小黑"));
arryAnimal.Add(new Cat("小黄"));
arryAnimal.Add(new Cat("小军"));
arryAnimal.Add(new Cat("小华"));
MessageBox.Show(arryAnimal.Count.ToString());//随着add的增加,集合count可以得到当前元素的个数
}
}
对于叫声比赛的代码,没有什么变化
private void button4_Click(object sender, EventArgs e)
{
foreach (Animal item in arryAnimal)//此时遍历的是ArraylIst集合
{
//由于有了多态性,所以叫的时候,程序会自动去找item是什么对象,然后用哪个重写方法
MessageBox.Show(item.Shout());//是什么动物在叫我也不知道
}
}
如果因一些原因要退赛,该如何做、
private void button3_Click(object sender, EventArgs e)
{
arryAnimal = new ArrayList();//实例化对象、
arryAnimal.Add (new Cat("小花"));
arryAnimal.Add(new Cat("小鸣"));
arryAnimal.Add(new Cat("小红"));
arryAnimal.Add(new Cat("小黑"));
arryAnimal.Add(new Cat("小黄"));
arryAnimal.Add(new Cat("小军"));
arryAnimal.Add(new Cat("小华"));
arryAnimal.RemoveAt(1);
arryAnimal.RemoveAt(2);
MessageBox.Show(arryAnimal.Count.ToString());//随着add的增加,集合count可以得到当前元素的个数
}
QA程序在执行 arryAnimal.RemoveAt(1);的时候,也就是小鸣被移除了结合,此时小红的索引次序还是原来的2吗?
A我明白了应该这样才对
arryAnimal.RemoveAt(1);
arryAnimal.RemoveAt(1);
Q“总结一下,集合 AmrayList相比数组有什么好处?”
A“主要就是它可以根据使用大小按需动态增加,不用受事先设置其大小的控制。还有就是可以随意地添加、插入或移除某一范围元素,比数组要方便。”
Q“对,这是 ArrayList 的优势,但它也有不足,ArrayList 不管你是什么对象都是接受的,因为在它眼里所有元素都是 Object, 这就使得如果你‘array Animal.Add(123);’ 或者 ‘array Animal.AddrFfello Worldr;”在编译时 都是没有问题的,但在执行时,“foreach (Animal item in arrayAnimaly’ 需要明确集合中的元素是 Animal 类型,而123 是整型,Helloworld 是字符串型,这就会在运行到此处时报错,显然,这是典型的类型不匹配错误,换句话说,ArrayList 不是类型安全的。还有就是 Array List 对于存放值类型的数据,比如 int、 string 型 (string 是一种拥有值类型特点的特殊引用类型)或者结构 struct 的数据,用 ArrayList就意味着都需要将值类型装箱为 Object 对象,使用集合元素时,还需要执行拆箱操作,这就带来了很大的性能损耗。”
A“等等,我不太懂,装箱和拆箱是什么意思?,
Q“所谓装箱就是把值类型打包到 0bject 引用类型的一个实例中。比如整型变量 i被“装箱”并赋值
给对象o。
int i=123;
object o=(object) i;//boxing
所谓的拆箱就是指从对象中提取值类型,此例中对象o拆箱并将其赋值给整形变量i
o=123;
i=(int)o;//unboxing
“相对于简单的赋值而言,装箱和拆箱过程需要进行大量的计算。对值类型进行装箱时,必须分配并构造一个全新的对象。其次,拆箱所需的强制转换也需要进行大量的计算IMSDN]。总之,装箱拆箱是耗资源和时间的。而 ArrayList 集合在使用值类型数据时,其实就是在不断地做装箱和拆箱的工作,这显然是非常糟糕的事情。”
“啊,那从这点上来看,它还不如数组来得好了,因为数组事先就指定了数据类型,就不会有类型
安全的问题,也不存在装箱和拆箱的事情了。看来他们各 有利弊呀。”
“说得非常对,C#在2.0 版之前的确也没什么好办法,但2.0出来后,就推出了新的技术来解決这个问题,那就是泛型。
泛型
“泛型是具有占位符(类型参数)的类,结构,接口和方法,这些占位符是类,结构,接口和方法所存储或使用的一个或多个类型的占位符。泛型集合类可以行类型参数用作它所存储的对象的类型的占位符:类型参数作为其字段的裝型和其方法的参数类型出现MSDN。我该给你的是 MSDN 的原话,听每有些抽黎,我们直授米看例子。首先泛型集合需要 System Colloctions Generic的而名空同。而 List 炎居ArayList 类的泛型等效类。该类使用大小可技需动态增加的数组实现Iist 泛型接口。其实用法上共維就是在 IIist 和 Iist 后面加 “T" ,这个“T"就是你需要指定的集合的数据或对象类型
IList<Animal> arryAnimal;//声明一个泛型变量,用接口Ilist
//注意 IList<Animal> 表示集合变量只能接受Animal类型其他不可以
private void button3_Click(object sender, EventArgs e)
{
arryAnimal = new List<Animal >();//实例化ilist对象、此时也需要指定List<T>的T是Anmail
arryAnimal.Add (new Cat("小花"));
arryAnimal.Add(new Cat("小鸣"));
arryAnimal.Add(new Cat("小红"));
arryAnimal.Add(new Cat("小黑"));
arryAnimal.Add(new Cat("小黄"));
arryAnimal.Add(new Cat("小军"));
arryAnimal.Add(new Cat("小华"));
MessageBox.Show(arryAnimal.Count.ToString());//随着add的增加,集合count可以得到当前元素的个数
}
“此时,如果你再写 ‘array Animal.Ada(123),或者‘arrayAnimalAad(Tielloworidr):结果将是?”“哈,编译就报错,因为 Add 的参数必须是要 Animal 或者 Animal的子类型才行。”“至于‘叫声比赛’的写法就完全相同,这里就不写了。来,说说你对泛型集合 List的感受。”
“我是这样想的,其实 List 和 ArrayList 在功能上是一样的,不同就在于,它在声明和实例化时都需要指定其内部项的数据或对象类型,这就避免了刚才讲的类型安全问题和装箱拆箱的性能问题了。强,够强,怎么想到的,真是厉害。”
“是呀,这个改造的确是非常的好,不过显然C#语言的设计者也并不是一开始就明白这一点,也足通过实践和用户的反馈才 在 C#2.0版中改进过来的。巨人也有会走李路的时候,何记我们常人。通常情况下,都建议使用泛型集合,因为这样可以获得类型安全的直接优点而不需要从基集合类型派生井实现类型特定的成员。此外,如果集合元素为值类型,泛型集合类型的性能通常优子对区的非泛型集合类型(并优于从非泛型基集合类型派生的类型),因为使用泛型时不必对元素进行装箱IMSDN]。”
“当然是泛型好呀,它可是集 ArrayList 集合和 Atray数组优点于一号的好东西,有了它,ArrayList
就显得太老士了。”
“至于泛型的知识还有很多,这里就不细讲了,你自己去找资料研究吧。”
委托与事件
A老师,能不能给我讲讲委托与事件,我认真地看过 书,也研究过,但实话说,我至今也不是很明白它的作用和好处。”
Q“哈,委托是对函数的封装,可以当作给方法的特征指定一个名称。而事件则是委托的一种特殊形式,当发生有意义的事情时,事件对象处理通知过程[PC#]。事件其实就是设计模式中观察者模式在.NET中的一种实现方式。要详细讲就有得讲了,不过在这可以举个例子,就当抽砖引玉吧。”
A好的,好的。”
Q“你先建一个控制合 应用程序。我们的需求是,有一只猫叫 Tom,有两只老鼠叫 Jerry 和 Jack, Tom只要一叫“喵,我是 Tom’,两只老鼠就说‘老猫来了,快跑’。你来分析一下,这里应该有几个类,如何处理类之间的关系?”
A“当然应该是有 Cat 和Mouse 类,在Main 函数中执行,当 Cat 的 Shout 方法触发时,Mouse 就执行Run 方法。不过这里如何让 shout 触发时,通知两只老鼠呢?显然老猫不会认识老鼠,也不会主动通知它们 ‘我来了,你们快跑’。”
Q“你说得没错,的确是这样。所以在 Cat 类当中,是不应该关联 Mouse 类的。此时用委托事件的方式就是最好的处理办法了。注意,委托是一种引用方法的类型。一旦为委托分配了方法,委托将与该万法具有完全相同的行为[MSDN]。委托对象用关键宇 delegate 来声明。而事件是说在发生其他类或对象关注的事情时,类或对象可通过事件通知它们[MSDN]。事件对象用event 关键字声明。
class Cat
{
private string name;
public Cat(string name)
{
this.name = name;
}
//声明委托CatShoutEventHander
public delegate void CatShoutEventHander();
//声明时间CatShout,他的事件类型是委托CatShoutEventHander
public event CatShoutEventHander CatShout;
public void shout()
{
Console.WriteLine("喵,我是{0}.", name);
if (CatShout != null)
{
CatShout();//表明当执行shout()方法时,如果CatShout中有对象等登记事件,则执行CatShout
}
}
}
Q我问你,为什么catshout()是无参数无返回值的方法
A因为事件Catshout的类型是委托CatShoutEventHander,而CatShoutEventHander,就是无参数,无返回值的
Q说得对,就这么简单。
下面写老鼠的方法
class Mouse
{
private string name;
public Mouse (string name)
{
this.name = name;
}
public void Run()
{
Console .WriteLine ("老猫来了,{0}快跑!",name);
}
}
关键是Main函数的写法
class Program
{
static void Main(string[] args)
{
Cat cat = new Cat("Tom");
Mouse mouse1 = new Mouse("Jery");
Mouse mouse2 = new Mouse("Jack");
;
cat.CatShout += new Cat.CatShoutEventHander(mouse1.Run);
cat.CatShout += new Cat.CatShoutEventHander(mouse2.Run);
cat . Shout();
Console.Read();
}
}
+=是ad-CatShout的意思
Q这里需要解释一下,new Cat. CatShoutEventHtandler(mouse1.Run)的含义是实例化一个委托,而委托的实例其实就是Mouse 的Run 方法。而‘cat.Catshout +=’ 表示的就是 ‘cat.add Catshout (newCat. CatShoutEventHandler(mousel.Run),的意思。’”
A“哦,原来‘+=’就是增加委托实例对象的意思,以前我一直不明白这里的含义。那要是这么说,应该也有‘二’,移除委托实例的操作了?”
Q“当然当然,所谓_=不过就是 ‘remove Catshout()’的含义了,使用了它,就等于减少一个需要触发事件时通知的对象。我们来看运行后的结果。”
“我还有个问题,我看到我在号,NET 应用程序或者 Web 程序时,总是看到 DDE 生成的事件参数。比如 private void buttonl_ Click(object sender, EventArgs o),这里的sender 和e有什么用呀?”“问得好,我们来改造一下这个例千,你就知道这两个参数是做什么用的了。’
“首先我们增加一个卖类CatshoutBventArgs, 让它继承 EventArgs, EventArgs 是包含事件数据的类的基类[MSDN]。换句话说,这个老的作用航足用来在事件触发时传递数据用的。我现在写了它的一个子类 catshourEventArgs ,当中有属任Narne表不的就是 Caishout 事件触发时,需要传递 Cat对象的名字。
public class CatShotEventArgs : EventArgs
{
private string name;
public string Name
{
get { return Name; }
set { Name = value; }
}
}
“然后改写Cat类的代码,对委托 CaistourEventandler进行 重定 义,增加两个参数,第一个参数object对象 sender 是指向发送通知的对象,而第二个参数 CatShoutEventArgs 的 args,包含了 所有通知接受者需要附件的信息。在这里显然就是老猫的名字信息。”
/// <summary>
/// 老鼠类
/// </summary>
class Mouse
{
private string name;
public Mouse(string name)
{
this.name = name;
}
public void Run(object sender, CatShotEventArgs args)//逃跑的方法中增加了两个参数,并且可以在显示时,说出老猫的名字
{
Console.WriteLine("老猫{0}来了,{1}快跑!", args.Name, name);
}
}
/// <summary>
/// 猫类
/// </summary>
class Cat
{
private string name;
public Cat(string name)
{
this.name = name;
}
//声明委托CatShoutEventHander,此时委托所代表的方法有两个参数,object参数和CatShotEventArgs参数
public delegate void CatShoutEventHander(object sender,CatShotEventArgs args );
//声明时间CatShout,他的事件类型是委托CatShoutEventHander
public event CatShoutEventHander CatShout;
public void Shout()
{
Console.WriteLine("喵,我是{0}.", name);
if (CatShout != null)
{
CatShotEventArgs e = new CatShotEventArgs();//声明委托CatShoutEventHander,并给Name属性赋值为猫的名字
e.Name = this.name;
CatShout(this ,e );//当事件触发时,通知所有登记过的对象,并将发送通知的自己及需要的数据传递过去
}
}
主函数不变,而显示的结果不一样了