c#基础五 面向对象高级编程(封装 继承 多态 抽象类 密封类 接口)
1.类的继承与多态性
1.1基类和扩充类
C#实现继承的方式:类继承和接口继承
继承用冒号(:)表示。被继承的叫做父类或者基类,从基类继承的类叫扩充类,又叫派生类或子类。所有类的基类System.Object
声明方式:[访问修饰符] class 扩充类名称:基类名称
{
}
若B继承自A,也可以使用强制转换操作将其转换为A 对象。如:
A b = (A)new B();或者 A b = new B();此时,B对象将被限制为A 对象的数据和行为,而无法再访问B对象中的数据和行为,除非A中的方法被B重载,将会访问B 的方法。将B强制转换为A后,还可以将A重新转换为B,但是,并非A的所有实例在任何情况下都可强制转换为B,只有实际上是B的实例的那些实例才可以强制转换为B。
扩充类不能继承基类中定义的private方法,只能继承基类的public成员或者protected成员。
初始化基类时,会首先调用基类的构造函数再调用扩充类的构造函数
1.2 多态性
定义:同一操作作用于不同类的实例,不同的类将进行不同的解释,最后产生不同的结果。即:建立一个父类的对象,它的内容可以是这个父类的,也可以是这个子类的,子类和父类都定义有同样的方法;当使用对象调用这个方法的时候,父类里的同名对象将被调用,当在父类的这个方法前加virtual关键字,子类的同名方法将被调用。
实现多态的方法:
1.通过继承实现。多个类可以继承自同一个类,每个扩充类可根据重写基类成员以提供不同的功能。
2.通过抽象类实现。抽象类本身不能被实例化,只能在扩充类中通过继承使用。抽象类的部分或全部成员不一定都要实现,但要在继承类中全部实现,抽象类中已实现的成员扔可以被重写,并且继承类仍可以实现其他功能。
3.通过接口实现。接口仅声明类需要实现的方法、属性、事件;以及每个成员需要接受和返回的参数类型,而他们的特定实现需要实现类去完成。
-
虚拟(virtual)和重写(override)
C#中,如果基类的某个功能不满足要求,可以在扩充类中重写基类的方法实现新的功能,
1 class A
2
3 {
4
5 public virtual string MyProperty { get; set; }//扩充类可以重写此属性
6
7 static void Main(string[] args)
8
9 {
10
11 }
12
13 public virtual void myMethod()//扩充类可以重写此方法
14
15 {
16
17 }
18
19 }
20
21 class B : A
22
23 {
24
25 public override void myMethod()//基类中也可以不重写此方法
26
27 {
28
29 }
30
31 }
注意:(1)虚拟方法不能声明 为静态的。因为静态的方法是应用在类一层次的,而面向对象的多态性只能在对象上运作,所以无法在类中使用。此外,override,abstract也不能跟static共存.
(2)virtual 不能喝private 一起使用,否则无法在扩充类中重写。
(3)重写方法的名称、参数个数、类型以及返回值都必须和虚拟方法一致。
-
隐藏
只有在扩充类中使用override修饰符时,才能重写基类声明为virtual的方法;否则,在继承的类中声明一个与基类方法同名的方法会隐藏基类的方法。
与方法或属性不同的是,使用new关键字时并不要求基类中的方法或属性声明为virtual,只要在扩充类的方法或属性前声明为new,就可以隐藏基类的方法或属性。理解:新方法把老方法(相对于子类)隐藏,但老方法仍可见。
举例:
父:实例:b,
子:实例 d;
(1)父方法:无virtual;子方法:new;此时若强制转换b=d,b.方法 调用父中方法
(2)父方法:有virtual;子方法:new;此时若强制转换b=d,b.方法 调用父中方法
(3)父方法:有virtual;子方法:override;此时若强制转换b=d,b.方法 调用子类中方法
什么时候用隐藏呢?
如开发人员A需要重新设计基类中的某个方法,该基类是一年前由另一组开发人员设计的,并且已经交给用户使用,可是原来的开发人员在方法前没有加virtual关键字,但又不允许修改原来的程序,这是无法用override重写基类的方法,这是就需要隐藏基类的方法。
-
在扩充类中直接调用基类的方法
可以在扩充类中用关键字base直接调用基类的方法。
1 class B
2
3 {
4
5 public virtual int myMethod()
6
7 {
8
9 return 5;
10
11 }
12
13 }
14
15 class C : B
16
17 {
18
19 public override int myMethod()
20
21 {
22
23 return base.myMethod()*4;
24
25 }
26
27 }
1.3抽象类
使用abstract修饰符,若类中的方法或属性为abstract,类必须用abstract修饰。只能用作基类,抽象方法没有实现代码(无大括号!)
抽象类和非抽象类的区别:
(1)抽象类不能直接被实例化,只能在扩充类中通过继承使用
(2)抽象类可以包含抽象成员,非抽象类不能包含抽象成员
当从抽象类派生非抽象类时,非抽象类必须实现抽象类的所有(必须是所有!)抽象成员,当然,如果继承抽象类的也是抽象类就不必实现其抽象成员,由最后一个非抽象类实现。
1 abstract class B
3 {
5 public abstract int BMethod();
6
7 }
8
9 abstract class C : B
10
11 {
13 public int CMethod()
14
15 {
16
17 return 4;
18
19 }
20
21 }
22
23 class D : B
24
25 {
27 public override int BMethod()
28
29 {
30
31 return 1;
32
33 }
34
35 }
什么时候使用抽象类呢?
如果有一个通用方法,对所有扩充类来说都是公共的,并且强制要求所有扩充类都必须实现这个方法,这种情况下就可以把该方法定义为基类中的抽象方法。
1.4 密封类
密封类是不能被其他类继承的类,用sealed关键字声明。sealed关键字也可以限制基类中的方法,防止被扩充类重写,若类中的方法是sealed,该类不是必须用sealed来修饰的。带有sealed修饰符的方法称为密封方法,sealed方法必须和override关键字一起使用。
一般情况下,只有在某个方法重写了基类中同名的方法,但又不希望该方法所在的类的扩充类重写该方法,就可以在该方法前使用修饰符sealed。
1 abstract class B
2
3 {
4
5 public abstract int BMethod();
6
7 }
8
9 class D : B
10
11 {
12
13 public override sealed int BMethod()//BMethod方法将不能再被重写
14
15 {
16
17 return 1;
18
19 }
20
21 }
1.5 继承过程中构造函数的处理
在扩充类中,继承了所有基类中声明的public或protected成员。但是构造函数则排除在外,不会被继承下来。
为什么不继承基类的构造函数呢?因为构造函数的主要用途是对类的成员初始化,包括对私有成员的初始化。如果让构造函数也能继承,由于扩充类中无法访问基类的私有成员,因此会导致创建扩充类的实例时无法对基类的私有成员进行初始化工作,从而导致程序运行失败。为了解决这个问题,c#在内部按照下列顺序处理构造函数:从扩充类依次向上寻找基类,直到找到最初的基类,然后开始执行最初的基类的构造函数、再依次向下执行扩充类的构造函数,直至执行完最终的扩充类的构造函数为止。
如下代码会发生“A方法没有0个参数的重载”错误,这是因为创建B的实例时,编译器会寻找基类A中提供的无参数的构造函数,而A中没有,从而报错。
1 class Program
2
3 {
4
5 static void Main(string[] args)
6
7 {
8
9 B b = new B(10);
10
11 Console.ReadLine();
12
13 }
14
15 }
16
17 class A
18
19 {
20
21 private int age;
22
23 public A(int age1)
24
25 {
26
27 this.age = age1;
28
29 }
30
31 }
32
33 class B : A
34
35 {
36
37 private int age;
38
39 public B(int age1)
40
41 {
42
43 this.age = age1;
44
45 }
46
47 }
要解决这个问题,需将public B(int age1)改为public B(int age1):base(age1)即可。程序执行时,会先调用System.Object的构造函数,然后调用A的带参数的构造函数,由于B的构造函数已经将age1参数传递给A,所以A的构造函数就可以利用这个传递的参数进行初始化。
2.版本控制
所有的方法默认都是非虚拟的,调用非虚拟方法时不会受到版本的影响。若基类中声明一个虚拟方法,而扩充类的方法中使用了override关键字,则执行时会调用扩充类中的方法;若没有使用override关键字,则调用基类的方法。而没有声明为virtual的非虚拟方法,则在编译时就确定了应该调用哪个方法了。
View Code
1 class Program
2
3 {
4
5 static void Main(string[] args)
6
7 {
8
9
10
11 A a = new A();//调用 A 的方法
12
13 B b = new C();//声明为B类,但初始化为C类的对象,
14
15 A c = b;//声明为C类,但初始化为B类的对象,
16
17 a.Method();
18
19 //调用方法时,先看B中的method是否为virtual,若是则看C中方法是否override,若是则调用C的方法
20
21 b.Method();
22
23 //调用c的method方法时,先看A的方法,由于是virtual,再看B,是new,说明B的Method并没有继承A的Method方法
24
25 //因此最后调用的是A类的Method的方法
26
27 c.Method();
28
29 Console.ReadLine();
30
31 }
32
33 }
34
35 class A
36
37 {
38
39 public virtual void Method()
40
41 {
42
43 Console.WriteLine("A.Method");
44
45 }
46
47 }
48
49 class B : A
50
51 {
52
53 public new virtual void Method()
54
55 {
56
57 Console.WriteLine("B.Method");
58
59 }
60
61 }
62
63 class C : A
64
65 {
66
67 public override void Method()
68
69 {
70
71 Console.WriteLine("C.Method");
72
73 }
74
75 }
另外,若B继承自A,C继承自B,那么A中的virtual方法,B中override后,C中还可以继续override。即 被override修饰的方法后就默认为virtual,即virtual具有传递性。
3.接口
接口像一个抽象类,不过,接口是完全抽象的集合成员,主要特点是 只有声明部分,没有实现部分。一般用I开头,接口中只能包含:(1)方法 属性 索引器和事件的声明,(2)不能包含构造函数(因为无法构建不能实例化的对象)(3)不能包含字段(因为字段隐含了某系内部的执行方法)(4)不能包含任何实现代码(5)接口中的方法必须都是public的,因此不能再用public修饰符声明。
接口的用途是表示设计者和调用者的一种约定。例如,提供某个方法用什么名字、需要哪些参数、以及每个参数的类型。抽象类和接口的一个主要差别:类可以继承多个接口,但只能继承一个抽象类。
使用接口还是抽象类来为组件提供多态性主要考虑以下几个方面:
(1)如果预计要创建组件的多个版本,则创建抽象类。抽象类提供简单易行的方法来控制组件版本。通过更新基类,是所有继承类都自动更新。另一方面,为了保护为使用接口而编写的现有系统,要求接口一旦创建就不能更改。如果需要接口的新版本,必须创建一个新的接口
(2)如果创建的功能将在大范围的完全不同的对象间使用,则使用接口。抽象类应主要用于关系密切的对象,而接口最适合为不相关的类提供通用功能。
(3)如果要设计小而精炼的功能块,则使用接口;如果要设计大的功能单元,则使用抽象类,设计优良的接口往往很小且相互独立,减少了发生性能问题的可能。
(4)如果要在组件的所有实现间提供通用的已实现的功能,则使用抽象类,抽象类允许部分实现类,而接口不包含任何成员的实现。
接口的声明与实现:
声明:
[访问修饰符] interface 接口名称
{
//接口体
}
实现某个接口的任何类都将拥有该接口中所有元素,因此,当需要再不相关的类中实现同样的功能时,就可以使用接口。
View Code
1 interface Ifunction1
2
3 {
4
5 int sum(int x1, int x2);
6
7 }
8
9 interface Ifunction2
10
11 {
12
13 string MyString { get; set; }
14
15 }
16
17 class MyTest : Ifunction1, Ifunction2
18
19 {
20
21 private string myString;
22
23 public MyTest()
24
25 { }
26
27 public MyTest(string str)
28
29 {
30
31 myString = str;
32
33 }
34
35 //实现接口Ifunction1的方法
36
37 public int sum(int x1, int x2) { return x1 + x2; }
38
39 //实现接口Ifunction2的属性
40
41 public string MyString { get { return myString; } set { myString = value; } }
42
43 }
44
45 class Program
46
47 {
48
49 static void Main(string[] args)
50
51 {
52
53
54
55 Ifunction1 f1 = new MyTest();
56
57 Console.WriteLine(f1.sum(10, 20));
58
59 Ifunction2 f2 = new MyTest("hi");
60
61 Console.WriteLine(f2.MyString);
62
63 Console.ReadLine();
64
65 }
66
67 }
显示方式实现接口:
由于不同接口中的方法可以重名,因此,在一个类中实现接口中的方法时就存在着多义性,这时可以显示实现接口中的方法,对于显示实现的方法,不能通过类的实例访问,而必须使用接口的实例。
View Code小总结
封装优点:
- 良好的封装能够减少耦合
- 类内部的实现可以自由的修改
- 类具有清晰的对外接口
继承优点:
1.使得所有子类公共的部分都放在了父类,使得代码得到了共享,就避免了重复
2.继承可使得修改或扩展继承而来的实现代码都较为容易。
缺点:
父类变,子类不得不变,继承会破坏包装,父类实现细节暴露给子类,其实是增大了两个类之间的耦合性。
注意:c#中,子类从它的父类中继承的成员有方法,域,属性,事件,索引指示器,但构造方法只能被调用不能继承。可以用base关键字调用父类的成员。当两个类之间具备“is-a”关系时,就可以考虑用继承。
多态特点:
1.子类以父类的身份出现
2.子类在工作时以自己的方式来实现
3.子类以父类身份出现时,子类特有的属性和方法不可以使用
要实现多态,要将父类声明为虚方法,子类将方法重写:override
注意:
重载:一般在类内部,在不改变原方法的基础上,增加新功能;两个方法必须要方法名相同,但参数类型或个数必须要有所不同。
重写:一般在不同的类中,必须要跟重写的方法返回类型及参数个数及类型都相同,需要加override关键字,被重写的方法要加 virtual关键字。
抽象类和接口的区别:
表面上看:1.抽象类可以给出一些成员的实现,而接口不包含任何成员的实现
2.抽象类的抽象成员可被子类部分实现,而接口的成员需要实现类完全实现
3.一个类只能继承一个抽象类,但可实现多个接口
深层次: 1 .类是对对象的抽象,抽象类是对类的抽象(类相关性),接口是对行为的抽象(行为相关性)
2.对于行为跨越不同类的对象,可使用接口;对于一些相似的类对象,用继承抽象类
3.从设计角度讲,抽象类是从子类中发现了公共的东西,泛化出父类,然后子类继承父类;而接口是根本不知子类的存在,方法如何实现还不确定,预先定义。
抽象类往往是通过 重构 得来的,是自底而上抽象出来的,而接口则是自顶向下设计出来的。

浙公网安备 33010602011771号