1、第3章 C#语言基础,3,3.5 类和结构,3.5.1 定义类和结构,3.5.2 定义属性,3.5.3 定义索引器,3.5.4 方法重载,3.5.5 使用ref和out类型参数,3.5.6 抽象类和接口,方法参数的功效就是能使信息在方法中传入或传出,当声明一个方法时,包含的参数说明是形式参数(形参)。当调用一个方法时,给出的对应实际参量是实在参数(实参),传入或传出就是在实参与形参之间发生的,在C#中实参与形参有四种传递方式。 值参数 引用参数 输出参数 参数数组,3.5.5 使用ref和out类型参数, 值参数在方法声明时不加修饰的形参就是值参数,它表明实参与形参之间按值传递。当这个方法被调
2、用时,编译器为值参数分配存储单元,然后将对应的实参的值拷贝到形参中。实参可以是变量、常量、表达式,但要求其值的类型必须与形参声明的类型相同或者能够被隐式的转化为这种类型。这种传递方式的好处是在方法中对形参的修改不影响外部的实参,也就是说数据只能传入方法而不能从方法传出,所以值参数有时也被称为入参数。,3.5.5 使用ref和out类型参数, 值参数【例】下面的程序演示了当方法Sort传递的是值参数。using System;class Myclass public void Sort (int x, int y, int z) int tmp; / 将x, y, z按从小到大排序if (xy)
3、 tmp=x; x=y; y=tmp; if (xz) tmp=x; x=z; z=tmp; if (yz) tmp=y; y=z; z=tmp; ,3.5.5 使用ref和out类型参数, 值参数【例】下面的程序演示了当方法Sort传递的是值参数。class Teststatic void Main() Myclass m = new Myclass ( );int a, b, c;a=30; b=20; c=10;m.Sort (a, b, c);Console.WriteLine (“a=0, b=1, c=2“ , a, b, c ); Console.Read ();,值传递.cs,
4、3.5.5 使用ref和out类型参数, 引用参数引用参数与值参数不同,引用参数并不创建新的存储单元,它与方法调用中的实在参数变量同处一个存储单元。因此,在方法内对形参的修改就是对外部实参变量的修改。,3.5.5 使用ref和out类型参数, 引用参数 【例】下面的程序演示了当方法Sort传递的是引用参数。using System;class Myclass public void Sort (ref int x, ref int y, ref int z) int tmp; / 将x, y, z按从小到大排序if (xy) tmp=x; x=y; y=tmp; if (xz) tmp=x;
5、x=z; z=tmp; if (yz) tmp=y; y=z; z=tmp; ,3.5.5 使用ref和out类型参数, 引用参数 【例】下面的程序演示了当方法Sort传递的是引用值参数class Teststatic void Main() Myclass m = new Myclass ( );int a, b, c;a=30; b=20; c=10;m.Sort (ref a, ref b, ref c);Console.WriteLine (“a=0, b=1, c=2“ , a, b, c ); Console.Read ();,引用传递.cs,3.5.5 使用ref和out类型参数
6、, 引用参数使用ref参数的注意点:(1) ref关键字仅对跟在它后面的参数有效,而不能应用于整个参数表。例如Sort方法中x、 y、z都要加ref修饰。(2) 在调用方法时,也用ref修饰实参变量,因为是引用参数,所以要求实参与形参的数据类型必须完全匹配,而且实参必须是变量,不能是常量或表达式。(3) 在方法外,ref参数必须在调用之前明确赋值,在方法内,ref参数被视为初始值已赋过。,3.5.5 使用ref和out类型参数, 输出参数在参数前加out修饰符的被称为输出参数,它与ref参数很相似,只有一点除外,就是它只能用于从方法中传出值,而不能从方法调用处接受实参数据。在方法内out参数被
7、认为是未赋过值的,所以在方法结束之前应该对out参数赋值。【例】下面的程序演示求一个数组元素中的最大值、最小值以及平均值。希望得到三个返回值,显然用方法的返回值不能解决,而且这三个值必须通过计算得到,初始值没有意义,所以解决方案可以定义三个out参数。,3.5.5 使用ref和out类型参数, 输出参数 using System; class Myclass public void MaxMinArray (int a, out int max, out int min,out double avg ) int sum ; sum = max = min = a0 ;for (int i=1;
8、 imax) max=ai;if (aimin) min=ai;sum+=ai;avg=sum / a.Length; ,3.5.5 使用ref和out类型参数, 输出参数 class Test static void Main() Myclass m = new Myclass ( );int score = 87,89,56,90,100,75,64,45, 80, 84 ;int smax, smin;double savg;m.MaxMinArray(score, out smax, out smin, out savg);Console.Write (“Max=0, Min=1, A
9、vg=2 “ , smax, smin, savg ); Console.Read (); ,输出参数.cs,3.5.5 使用ref和out类型参数, 参数数组一般而言,调用方法时其实参必须与该方法声明的形参在类型和数量上相匹配,但有时候我们更希望灵活一些,能够给方法传递任意个数的参数,比如在三个数中找最大、最小和在5个数中找最大、最小甚或任意多个数中找最大、最小能使用同一个方法。C#提供了传递可变长度的参数表的机制,即使用params关键字来指定一个参数可变长的参数表。【例】下面程序演示了Myclass类中的方法MaxMin有一个参数数组类型的参数,那么在调用这个方法是所具有的灵活性。,3.
10、5.5 使用ref和out类型参数, 参数数组 using System; class Myclass public void MaxMin (out int max, out int min, params int a ) if (a.Length=0) / 如果可变参数为零个,可以取一个约定值max=min=-1; return ;max = min = a0 ;for (int i=1; imax) max=ai;if (aimin) min=ai; ,3.5.5 使用ref和out类型参数, 参数数组 class Test static void Main() Myclass m =
11、new Myclass ( );int score = 87,89,56,90,100,75,64,45, 80, 84 ;int smax, smin;m.MaxMin (out smax, out smin); / 可变参数的个数可以是零个Console.WriteLine (“Max=0, Min=1 “ , smax, smin ); m.MaxMin (out smax, out smin, 45, 76, 89, 90 ); Console.WriteLine (“Max=0, Min=1 “ , smax, smin ); m.MaxMin (out smax, out smin
12、, score); / 可变参数也可接受数组对象Console.WriteLine (“Max=0, Min=1 “ , smax, smin ); Console.Read (); ,参数数组.cs,3.5.5 使用ref和out类型参数,从上面例程中可以看出设立可变参数非常方便,也很实用。但在使用时要注意以下几点:(1) 一个方法中只能声明一个params参数,如果还要其他常规参数,则params参数应放在参数表的最后。(2) 用params修饰符声明的参数是一个一维数组类型,例如,可以是int , string , double , 或int , string 等,但不能是int , s
13、tring ,等。(3) 由于params参数其实是一个数组,所以在调用时可以为参数数组指定零个或多个参数,其中每个参数的类型都应与参数数组的元素类型相同或能隐式地转换。,3.5.5 使用ref和out类型参数,(4) 当调用具有params参数的方法时,可以作为一个元素列表如:( 45, 76, 89, 90 ); 或作为一个数组(如:score)传递给params参数。(5) 无论采用哪种方式来调用方法,params参数都是作为一个数组被处理。所以在方法内可以使用数组的长度属性来确定在每次调用中所传递参数的个数。(6) params参数在内部会进行数据的复制,不可能将params修饰符与r
14、ef和out修饰符组合起来用。所以在这个方法中即使对参数数组的元素进行了修改,在这个方法之外的数值也不会发生变化。,3.5.5 使用ref和out类型参数,3.5.6 抽象类和接口,到现在为止,我们使用的类都可以直接用来声明一个对象类型,并可以实例化。但C#中也可以有例外抽象类。这种类是为了需要而定义的一种非常基本的类,实际上并不想直接使用它,而是为了派生出新的类。 抽象类是基类的一种特殊类型。除了拥有普通的类成员之外,还有抽象类成员。抽象类成员中的方法和属性,只有声明(使用关键字abstract),而没有实现部分。由于对实例而言,没有实现的成员是不合法的,所以抽象类永远也不能实例化。这种不能
15、实例化的类也有它的作用空间,它们可以在类层次结构的上层,对于派生于该类的其他类而言,抽象类就确定了子类的基本结构和意义,从而使程序框架更容易建立。,包含一个或多个抽象函数的类本身必须声明为abstract,但是,抽象类可以包含非抽象的成员。从抽象类派生的类必须对基类中包含的所有抽象方法提供实现过程,否则,它也为抽象类。抽象函数为隐式的虚函数,所以为继承的抽象类提供了实现代码的方式与覆盖一个虚方法相似。另外,属性和索引也可以声明为abstract。抽象类不能自己实例化,需要使用其子类来实例化。抽象类是针对某些某些应用而产生的,抽象类的对象可以指向子类的实例,并且抽象方法可以被子类方法重写(ove
16、rride)。 实例:抽象类.cs,3.5.6 抽象类和接口,有时候,我们并不希望自己编写的类被继承,或者已经认定没有必要继承了。于是,C#提出了密封类的概念。类声明为密封后,就不能用来派生新的类。密封类具有不能用来继承的限制,但它也有自身的长处。一个类声明为密封的(sealed)有利于提高稳定性。因为,继承性是对基类的内部的某种程度的保护性访问。如果类是密封的,那么就完全避免了由派生类引起崩溃的可能性。同时,编译器也能针对密封类做相应的优化,例如,可以避免增加与虚拟方法相关联的系统总开销。 实例:密封类.cs,3.5.6 抽象类和接口,多继承在现实世界中是很普遍的,比如孩子会继承父母两方面的
17、特征等。C#中的类和抽象类都不能进行多继承,而通过接口可以实现多继承。接口很像类,但是接口中不能包含任何数据成员,接口中可以包含方法、属性、事件和索引器等的声明,但是接口本身不提供对它所声明的成员的实现。类通过“属性”和“行为”来描述事物,而接口只能包含行为的描述。比如我们可以把电器的“开”和“关”行为单独定义为一个接口,这样凡是继承自这个接口的电器都有了“开”和“关”这两个方法,可以让电灯、电视等继承并实现接口。,3.5.6 抽象类和接口,1) 接口的定义C#接口的定义如:public interface ISwitch void On();void Off();接口的定义与类的定义类似,使
18、用interface定义代替class。另外,接口里面的方法成员不能有方法实现(与抽象类类似)。接口的命名通常是以I开头,如IPartA、IPartB。接口不能被实例化。所有接口成员都不包含访问修饰符,所有成员默认是public。,3.5.6 抽象类和接口,2) 接口的实现接口自身并不包含任何实现代码,类可以继承自接口,继承了以后就必须实现接口里面的方法。public class Light : ISwitch private string id;public Light(string _id) this.id = _id; public void On()Console.WriteLine(
19、“电灯开了“);public void Off()Console.WriteLine(“电灯关了“); 实例:接口.cs,3.5.6 抽象类和接口,接口和接口之间可以互相继承,并且允许多重继承: interface IA void PlayA(); interface IB void PlayB(); interface IC:IA,IB ,3.5.6 抽象类和接口,一个类可以同时继承自类、抽象类和接口。类和抽象类要写在前面,接口写在后面。class ClassB:ClassA,IA,IB 抽象类和接口在很多方面具有很大的相似性,抽象类和接口都是抽象体,但是其区别体现在以下两个方面:1、接口中
20、不定义任何与实现有关的内容;2、抽象类是实体的抽象,接口则是行为的抽象。,3.5.6 抽象类和接口,3) 显式接口实现一个类可以同时继承自多个接口,如果两个接口中有同名的方法,就要用显式接口来实现。显式接口的实现语法注意以下两个方面:1、在实现的方法上带上接口名;2、显式接口方法访问修饰符不能为public类型(否则对象在调用方法时不知道调用那个接口的方法)。实例:显式接口.cs,3.5.6 抽象类和接口,3.2 C#的数据类型,C#的数据类型分为值类型和引用类型两大类。 值类型包括整数类型、布尔类型、实数类型、字符类型、结构类型和枚举类型等。值类型被分配在堆栈上,值类型的变量直接包含了数据,
21、可以直接访问其值; 引用类型包括类类型、数组类型、代理类型、接口类型等。引用类型总是分配在托管堆上,引用类型的变量通常仅包含一个指向实例的指针,系统通过该指针来引用其实例。,代理(委托)类型,通过参数把数据传递给方法是我们习惯的一种做法,那么如何把方法作为参数传递给另一个方法?C#的代理相当于在C/C+中的函数指针。函数指针用指针获取一个函数的入口地址,实现对函数的操作。代理与C/C+中的函数指针不同在于代理是面向对象的,是引用类型,因此对代理的使用要先定义后实例化,最后才调用。代理指定了方法的返回类型和形式参数类型,但没有指定具体的实现过程,只要这些方法与代理的签名(返回类型及形式参数)相同
22、。同一个代理在运行期间可以表示不同的方法实现过程。只是在运行时,为代理赋一个适当的方法,当调用此代理时,它将执行此方法实际所封装的代码。,代理是函数的封装,它代表一委托是函数的封装,它代表一“类”函数。它们都符合一定的签名:拥有相同的参数列表、返回值类型。同时,委托也可以看成是对函数的抽象,是函数的“类”。此时,委托的实例将代表一个具体的函数。为什么要使用委托?1、更加灵活的方法调用; 2、用于异步回调; 3、多线程编程中使用委托来指定启动一个线程时调用的方法; 4、C#中的事件模型。用它们指明处理给定事件中的方法,代理(委托)类型,1)代理的定义代理定义了方法的返回类型和参数类型,也创建了一
23、种新类型,代理也是一个类,它派生于基类System.Delegate。和其他类一样,必须首先定义代理,然后才能实例化。代理定义的语法:修饰符 delegate 返回值类型 (参数);例如:delegate int MyDelegate(int nID, string sName);定义了一个名称为MyDelegate的代理,这个代理代表的方法带有两个参数,返回一个整型数。,代理(委托)类型,2)代理的实例化定义了代理之后,必须要实例化代理,还需要代理的方法处理程序,这个方法实现具体的功能,其参数列表必须相同,并且必须返回同样的类型。然后再将这个方法赋给代理对象。 定义:delegate int
24、 MyDelegate(int nID, string sName); 实例化:MyDelegate d1 = new MyDelegate(wr.InstanceMethod);,代理(委托)类型,3)通过代理调用方法通过代理的定义和实例化工作后,我们就可以使用代理了。使用代理就好像它就是代理的方法本身一样,和直接使用方法的格式相同。我们可以通过下面的语句来调用代理:d1(5, “aaa”); 通过代理MyDelegate实现对方法InstanceMethod的调用,调用还必须有一个前提条件是:方法InstanceMethod的参数和定义MyDelegate的参数一致,并且返回值为int。方
25、法InstanceMethod定义:public int InstanceMethod(int nID, string sName)代理.cs,代理(委托)类型,4)多播委托多播委托允许将多个委托对象组成一个委托链(比如某个事件造成连锁反应)。委托链可以承载很多方法,一旦触发该链,那么将调用链中所有的方法。可以使用“+”把委托对象加入委托,使用“-”从委托链中移出一个委托对象。 实例:多播委托.cs 注意:1.多播委托的委托方法一般是没有返回值的方法(void)2.多播委托只支持“+”、“-”、“+=”、“-=”。,代理(委托)类型,(1)类的字段成员的定义,(2)类的方法的定义,(3)类的属
26、性的定义,(4)类的索引器的定义,类的成员的定义,(5)类的事件的定义,事件是Windows编程中很重要的一个概念,事件可以由用户的操作触发,也可能由程序逻辑触发。事件最常见的用途是用于窗体编程,当发生像点击按钮、移动鼠标等事件时,相应的程序将收到通知,再执行代码。事件作为C#中类的一种成员,为类和类的实例定义发出通知的能力,从而将事件和可执行代码捆绑在了一起。事件的发布者定义和发布事件,事件的接收者接收事件并调用自己的方法处理事件,这就是计算机中事件机制的运行原理(类似报纸订阅)。,(5)类的事件的定义,1) 事件的定义在C#中,事件实际上是代理的一种特殊形式。事件是为处理过程特制的、更为专
27、业化的代理,因此,事件比普通的代理更易于使用,更强健。下面是定义事件的格式:修饰符 event type name;其中event是事件定义的关键字。name为事件名称,因为所有的事件是一类特殊的代理,它们都是基于代理的,所以type必须是代理类型。例如:delegate void MyDelegate(string msg); /定义代理public event MyDelegate CalaculateFinished; /定义事件,(5)类的事件的定义,2) 触发事件接收到事件通知后,如何处理?在代理中确定了相关的处理方法,该方法必须符合事件委托类型的签名(返回类型和参数类型)。 pub
28、lic void OnCalaculateFinished(string msg) /触发事件if (CalaculateFinished != null)CalaculateFinished(msg);该方法中触发事件的语句是CalaculateFinished(msg);它的签名与定义事件时所用到的代理MyDelegate的签名完全一致。,(5)类的事件的定义,3) 事件处理方法事件发生了,如何将它和事件处理的程序联系起来呢?这就要用到订阅器,它是订阅事件的对象。例如:在上面我们已完成了事件的定义和触发,事件触发后要执行什么样的操作不是在类的内部定义,而是需要在类的调用处定义。将事件处理程序添加到类实例的事件上。,(5)类的事件的定义,3) 事件处理方法如:MyClass c = new MyClass();c.CalaculateFinished += new MyDelegate(c_CalaculateFinished);private static void c_CalaculateFinished(string msg) /事件处理方法Console.WriteLine(msg + “计算机完成“); 注意:事件只能使用“+=”运算符把事件处理程序关联到事件成员上和“-=”运算符从事件成员中删除事件处理程序。事件.cs,(5)类的事件的定义,