基础系列(8)——面向对象
一、 封装
封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(我们称之为类)。被封装的对象通常被称为抽象数据类型。
面向对象程序设计中一般以类作为数据封装的基本单位。类将数据和操作数据的方法结合成一个单位。在设计类时,不希望直接存取类中的数据,而是希望通过方法来存取数据。如此就可以达到封装数据的目的,方便以后维护、升级,也可以在操作数据时多一层判断,提高安全性。
在C#中可使用类来达到数据封装的效果,这样就可以使数据与方法封装成单一元素,以便于通过方法存取数据。除此之外,还可以控制数据的存取方式。封装在C#中可使用类来达到数据封装的效果,这样就可以使数据与方法封装成单一元素,以便于通过方法存取数据。除此之外,还可以控制数据的存取方式。
(一) 封装的意义
封装的意义在于保护或者防止代码(数据)被我们无意中破坏。在面向对象程序设计中数据被看作是一个中心的元素并且和使用它的函数结合的很密切,从而保护它不被其它的函数意外的修改。
封装还可以解决数据存取权限问题,使用封装可以将数据隐藏起来,形成一个封闭的空间,用户可以设置哪些数据只能在这个空间中使用,哪些数据可以在空间外部使用。如果一个类中包含敏感数据,则有些用户可以访问,有些用户却不能访问。如果不对这些数据的访问加以限制,那么后果是很严重的。所以,在编写程序时,要对类的成员使用不同的访问修饰符,从而定义它们的访问级别。 封装提供了一个有效的途径来保护数据不被意外的破坏。相比我们将数据(用域来实现)在程序中定义为公用的(public)我们将它们(fields)定义为私有的(privat)在很多方面会更好。私有的数据可以用两种方式来间接的控制。第一种方法,我们使用传统的存、取方法。第二种方法我们用属性(property)。
使用属性不仅可以控制存取数据的合法性,同时也提供了“读写”、“只读”、“只写”灵活的操作方法。
(二)访问修饰符
1、Public 访问修饰符
Public 访问修饰符允许一个类将其成员变量和成员函数暴露给其他的函数和对象。任何公有成员可以被外部的类访问。
下面的实例说明了这点:
using System; namespace RectangleApplication { class Rectangle { //成员变量 public double length; public double width; public double GetArea() { return length * width; } public void Display() { Console.WriteLine("长度: {0}", length); Console.WriteLine("宽度: {0}", width); Console.WriteLine("面积: {0}", GetArea()); } }// Rectangle 结束 class ExecuteRectangle { static void Main(string[] args) { Rectangle r = new Rectangle(); r.length = 4.5; r.width = 3.5; r.Display(); Console.ReadLine(); } } }
当上面的代码被编译和执行时,它会产生下列结果:
长度: 4.5
宽度: 3.5
面积: 15.75
在上面的实例中,成员变量 length 和 width 被声明为 public,所以它们可以被函数 Main() 使用 Rectangle 类的实例 r 访问。
成员函数 Display() 和 GetArea() 可以直接访问这些变量。
成员函数 Display() 也被声明为 public,所以它也能被 Main() 使用 Rectangle 类的实例 r 访问。
2、Private 访问修饰符
Private 访问修饰符允许一个类将其成员变量和成员函数对其他的函数和对象进行隐藏。只有同一个类中的函数可以访问它的私有成员。即使是类的实例也不能访问它的私有成员。
下面的实例说明了这点:
using System; namespace RectangleApplication { class Rectangle { //成员变量 private double length; private double width; public void Acceptdetails() { Console.WriteLine("请输入长度:"); length = Convert.ToDouble(Console.ReadLine()); Console.WriteLine("请输入宽度:"); width = Convert.ToDouble(Console.ReadLine()); } public double GetArea() { return length * width; } public void Display() { Console.WriteLine("长度: {0}", length); Console.WriteLine("宽度: {0}", width); Console.WriteLine("面积: {0}", GetArea()); } }//end class Rectangle class ExecuteRectangle { static void Main(string[] args) { Rectangle r = new Rectangle(); r.Acceptdetails(); r.Display(); Console.ReadLine(); } } }
当上面的代码被编译和执行时,它会产生下列结果:
请输入长度:
4.4
请输入宽度:
3.3
长度: 4.4
宽度: 3.3
面积: 14.52
在上面的实例中,成员变量 length 和 width 被声明为 private,所以它们不能被函数 Main() 访问。成员函数 AcceptDetails() 和 Display() 可以访问这些变量。由于成员函数 AcceptDetails() 和 Display() 被声明为 public,所以它们可以被 Main() 使用 Rectangle 类的实例 r 访问。
3、Protected 访问修饰符
Protected 访问修饰符允许子类访问它的基类的成员变量和成员函数。这样有助于实现继承。我们将在继承的章节详细讨论这个。更详细地讨论这个。
4、Internal 访问修饰符
Internal 访问说明符允许一个类将其成员变量和成员函数暴露给当前程序中的其他函数和对象。换句话说,带有 internal 访问修饰符的任何成员可以被定义在该成员所定义的应用程序内的任何类或方法访问。
下面的实例说明了这点:
using System; namespace RectangleApplication { class Rectangle { //成员变量 internal double length; internal double width; double GetArea() { return length * width; } public void Display() { Console.WriteLine("长度: {0}", length); Console.WriteLine("宽度: {0}", width); Console.WriteLine("面积: {0}", GetArea()); } }//end class Rectangle class ExecuteRectangle { static void Main(string[] args) { Rectangle r = new Rectangle(); r.length = 4.5; r.width = 3.5; r.Display(); Console.ReadLine(); } } }
当上面的代码被编译和执行时,它会产生下列结果:
长度: 4.5
宽度: 3.5
面积: 15.75
在上面的实例中,请注意成员函数 GetArea() 声明的时候不带有任何访问修饰符。如果没有指定访问修饰符,则使用类成员的默认访问修饰符,即为 private。
5、Protected Internal 访问修饰符
Protected Internal 访问修饰符允许在本类,派生类或者包含该类的程序集中访问。这也被用于实现继承。
(三) 现实中的封装:
我们的房子就是一个类的实例,室内的装饰与摆设只能被室内的居住者欣赏与使用,如果没有四面墙的遮挡,室内的所有活动在外人面前一览无遗。由于有了封装,房屋内的所有摆设都可以随意地改变而不用影响他人。然而,如果没有门窗,一个包裹得严严实实的黑箱子,即使它的空间再宽阔,也没有实用价值。房屋的门窗,就是封装对象暴露在外的属性
和方法,专门供人进出,以及流通空气、带来阳光
(四) 封装的例子:
举一个我们项目开发中经常用到的例子,对数据库操作,封装成一个常用的帮助类,我们进行重复调用,下面为其中封装的一个方法
private DataTable Getdate(string sqlstr, SqlParameter parameter) { SqlConnection conn = new SqlConnection(str);(这个str是连接数据库) SqlCommand comm = new SqlCommand(sqlstr,conn); if (parameter!=null) { comm.Parameters.AddWithValue(parameter.ParameterName, parameter.Value); } SqlDataAdapter adapter = new SqlDataAdapter(comm); DataTable dt = new DataTable(); adapter.Fill(dt); return dt; }
二、继承
继承是OOP最重要的特性之一。任何类都可以从另外一个类继承,即这个类拥有它所继承类的所有成员。在OOP中,被继承的类称为父类或基类。
C#提供了类的继承机制,但C#只支持单继承,不支持多重继承,即在C#中一次只允许继承一个类,不能同时继承多个类。利用继承机制,用户可以通过增加、修改或替换类中方法对这个类进行扩充,以适应不同的应用要求。利用继承,程序开发人员可以在已有类的基础上构造新类。继承使得类支持分类的概念。在日常生活中很多东西比较有条理,那是因为它们有着很好的层次分类。如果不用层次分类,则要对每个对象定义其所有的性质。使用继承后,每个对象就可以只定义自己的特殊性质。每一层的对象只需定义本身的性质,其他性质可以从上一层继承下来。
在C#中,接口允许多继承,可以通过继承多个接口来实现类似于C++中的多重继承。在继承一个基类时,成员的可访问性是一个重要的问题。子类不能访问基类的私有成员,但是可以访问其公共成员。子类和外部代码都可以访问公共成员。这就是说,只使用这两个可访问性,就可以让一个成员被基类和子类访问,同时也可以被外部的代码访问。
为了解决这个问题,C# 还提供了第3种可访问性:protected。只有派生类才能访问protected成员,基类和外部代码都不能访问protected成员。
除了成员的保护级别外,用户还可以为成员定义其继承行为。基类的成员可以是虚拟的,成员可以由继承它的类重写。子类可以提供成员的其他执行代码。这种执行代码不会删除原来的代码,仍可以在类中访问原来的代码,但外部代码不能访问它们。如果没有提供其他执行方式,外部代码就访问基类中成员的执行代码。
虚拟成员不能是私有成员,因为成员不能同时由子类重写,也不能访问它。基类还可以定义为抽象类。抽象类不能直接实例化,要使用抽象类就必须继承这个类,然后再实例化。
继承主要实现重用代码,节省开发时间。
(一)、C#中的继承符合下列规则
1.继承是可传递的。如果C从B中派生,B又从A中派生,那么C不仅继承了B中声明的成员,同样也继承了A中的成员。Object类作为所有类的基类。
2.派生类应当是对基类的扩展。派生类可以添加新的成员,但不能除去已经继承的成员的定义。
3.构造函数和析构函数不能被继承。除此之外的其它成员,不论对它们定义了怎样的访问方式,都能被继承。基类中成员的访问方式只能决定派生类能否访问它们。
4.派生类如果定义了与继承而来的成员同名的新成员,就可以覆盖已继承的成员。但这并不因为这派生类删除了这些成员,只是不能再访问这些成员。
5.类可以定义虚文法、虚属性以及虚索引指示器,它的派生类能够重载这些成员,从而实现类可以展示出多态性。
(二)、new关键字
如果父类中声明了一个没有friend修饰的protected或public方法,子类中也声明了同名的方法。则用new可以隐藏父类中的方法。(不建议使用)
(三)、base关键字
base关键字用于从派生类中访问基类的成员:
1.调用基类上已被其他方法重写的方法。
2.指定创建派生类实例时应调用的基类构造函数。
(四)基类和派生类
一个类可以派生自多个类或接口,这意味着它可以从多个基类或接口继承数据和函数。
C# 中创建派生类的语法如下:
<acess-specifier> class <base_class> { ... } class <derived_class> : <base_class> { ... }
假设,有一个基类 Shape,它的派生类是 Rectangle:
using System; namespace InheritanceApplication { class Shape { public void setWidth(int w) { width = w; } public void setHeight(int h) { height = h; } protected int width; protected int height; } // 派生类 class Rectangle: Shape { public int getArea() { return (width * height); } } class RectangleTester { static void Main(string[] args) { Rectangle Rect = new Rectangle(); Rect.setWidth(5); Rect.setHeight(7); // 打印对象的面积 Console.WriteLine("总面积: {0}", Rect.getArea()); Console.ReadKey(); } } }
当上面的代码被编译和执行时,它会产生下列结果:
总面积: 35
(五)基类的初始化
派生类继承了基类的成员变量和成员方法。因此父类对象应在子类对象创建之前被创建。您可以在成员初始化列表中进行父类的初始化。
下面的程序演示了这点:
using System; namespace RectangleApplication { class Rectangle { // 成员变量 protected double length; protected double width; public Rectangle(double l, double w) { length = l; width = w; } public double GetArea() { return length * width; } public void Display() { Console.WriteLine("长度: {0}", length); Console.WriteLine("宽度: {0}", width); Console.WriteLine("面积: {0}", GetArea()); } }//end class Rectangle class Tabletop : Rectangle { private double cost; public Tabletop(double l, double w) : base(l, w) { } public double GetCost() { double cost; cost = GetArea() * 70; return cost; } public void Display() { base.Display(); Console.WriteLine("成本: {0}", GetCost()); } } class ExecuteRectangle { static void Main(string[] args) { Tabletop t = new Tabletop(4.5, 7.5); t.Display(); Console.ReadLine(); } } }
当上面的代码被编译和执行时,它会产生下列结果:
长度: 4.5
宽度: 7.5
面积: 33.75
成本: 2362.5
(六)C# 多重继承
多重继承指的是一个类别可以同时从多于一个父类继承行为与特征的功能。与单一继承相对,单一继承指一个类别只可以继承自一个父类。
C# 不支持多重继承。但是,您可以使用接口来实现多重继承。下面的程序演示了这点:
using System; namespace InheritanceApplication { class Shape { public void setWidth(int w) { width = w; } public void setHeight(int h) { height = h; } protected int width; protected int height; } // 基类 PaintCost public interface PaintCost { int getCost(int area); } // 派生类 class Rectangle : Shape, PaintCost { public int getArea() { return (width * height); } public int getCost(int area) { return area * 70; } } class RectangleTester { static void Main(string[] args) { Rectangle Rect = new Rectangle(); int area; Rect.setWidth(5); Rect.setHeight(7); area = Rect.getArea(); // 打印对象的面积 Console.WriteLine("总面积: {0}", Rect.getArea()); Console.WriteLine("油漆总成本: ${0}" , Rect.getCost(area)); Console.ReadKey(); } } }
当上面的代码被编译和执行时,它会产生下列结果:
总面积: 35
油漆总成本: $2450
三、多态
(一)、多态定义
派生类的实例由基类的实例加上派生类新增的成员组成。派生类的引用指向整个类对象,包括基类部分。如果有一个派生类对象的引用,就可以获取对象基类部分的引用(强制类型转换)。
同一种操作作用于不同的类的对象,不同的类的对象进行不同的执行,最后产生不同的执行结果。简单理解:让一种对象表现出来多种类型。
如下简单的例子:
使用这种方式,派生类可以访问基类和派生类中的所有成员,但是基类只能访问基类中的成员。这是我们想要的结果吗?
(二) 多态的实现方式(虚方法):
1、定义:
在基类中用virtual关键字声明的方法叫做虚方法,在派生类中可以通过override关键字进行重写
2、使用场景:
1. 明确定义了基类,对基类中的方法进行重写时,可以考虑使用虚方法。
2.派生类的方法和基类的方法有相同的签名和返回类型。
3.覆写和被覆写的方法具有相同等级的可访问性。
3、例子:
class Person { private string _name; public string Name { get { return _name; } set { _name = value; } } public Person(string name) { this.Name = name; } public virtual void SayHello() { Console.WriteLine("我是人类"); } } class Chinese:Person { public Chinese(string name) : base(name) { } public override void SayHello() { Console.WriteLine("你好,我的姓名是{0},我是中国人",this.Name); } } class Japanese:Person { public Japanese(string name) : base(name) { } public override void SayHello() { Console.WriteLine("你好,我的姓名是{0},我是日本人", this.Name); } } class Program { static void Main(string[] args) { Chinese c1 = new Chinese("小明"); Chinese c2 = new Chinese("小李"); Japanese j1 = new Japanese("井泉宜良"); Japanese j2 = new Japanese("龟田君"); Person[] Pers = { c1, c2, j1, j2 }; //Pers[0] = c1; //Pers[1] = c2; //Pers[2] = j1; //Pers[3] = j2; 上面数组的另外一种写法 for (int i = 0; i < Pers.Length; i++) { Pers[i].SayHello(); } //Person p = new Chinese("小明");//Japanese("bingbian"); //p.SayHello(); Console.ReadKey(); } }
4、加深拓展
(三) 多态的实现方式(抽象方法)
1、定义:
描述共性的类就是抽象类,抽象类中的方法用abstract关键字修饰。其中抽象类并不考虑其实现的具体方式。
2、成员:
抽象方法和非抽象方法;属性;字段;构造函数;实例成员
3、使用场景:
1在抽象类定义之前,如果其他类中,没有公共的基类,那么可以通过抽象出来共同的基类来实现共性的方法
2 如果父类中的方法有默认的实现,并且父类需要实例化,这时候可以考虑将 父类定义成一个普通类,用虚方法来实现多态
4、例子:
class Program { static void Main(string[] args) { Animal an = new Dog(); an.Speak(); Console.ReadKey(); } } public abstract class Animal { public abstract void Speak(); } class Dog:Animal { public override void Speak() { Console.WriteLine("狗汪汪的叫"); } } class Cat:Animal { public override void Speak() { Console.WriteLine("猫喵喵的叫"); } }
5、注意事项:
1. 抽象类成员必须标记为abstract,并且不能有任何实现,抽象成员必须下抽象类中
2.抽象类不能被实例化
3.子类继承抽象类后,必须把父类中的所有抽象成员都重写(除非子类也是一个抽象类,则不需要重写)
4.抽象成员的访问修饰符不能是private
5.在抽象类中可以包含实例成员,并且抽象类的实例成员可以不被子类实现
6.抽象类中是有构造函数的,虽然不能被实例化
7.如果父类的抽象方法中参数,那么,继承这个抽象父类的子类在重写父类的方法时候必须传入对应的参数。如果抽象父类的抽象方法中有返回值,那么子类在重写这个抽象方法的时候也必须要传入返回值
8.如果父类中的方法有默认的实现,并且父类需要实例化,这时候可以考虑将 父类定义成一个普通类,用虚方法来实现多态
9.如果父类中的方法没有默认的实现。父类也不能实例化,则可以将该类定义为抽象类
(四) 多态的实现方式(接口)
1、接口的定义:
接口是一种编程规范、能力,c#本身并不支持类的多重继承,但是通过接口可以实现类的多重继承
2、成员:
方法;索引器;事件;自动属性(没有字段和方法体的属性);不能有构造函数和字段
3、使用场景:
要继承多个基类时,基类可以抽象为接口
4、例子:
class Program { static void Main(string[] args) { Ichlssable ch = new Teacher(); ch.CHLSS(); Console.ReadKey(); } } interface Ichlssable { void CHLSS(); } class Driver { public void KaiChe() { Console.WriteLine("我是一名司机,可以开车"); } } class Teacher:Driver,Ichlssable { public void Teach() { Console.WriteLine("我是一名老师,可以教书"); base.KaiChe(); } public void CHLSS() { Console.WriteLine("我是人类,可以吃喝拉撒睡"); } }
5、注意事项:
1.一个类继承了接口,这个类就需要实现接口中的所有成员
2.接口不能被实例化,也就是说不能用new创建对象
3.接口的成员中不能加访问修饰符,默认的为public,且不能被修改,接口中的成员不能有任何实现
4.接口和接口之间可以继承,并且可以多继承
5.接口不可以继承一个类,而类可以继承一个接口(接口只能继承类,类可以继承类和接口)
6.实现接口的子类必须实现该接口的全部成员
7.一个类可以同时继承一个类和多个接口,那么被继承类的名必须写在接口名称的前面,并用,隔开
8.显示实现接口的目的是为了解决方法的重命名问题,当继承的接口中的方法和参数一模一样时u,就要用的显示的实现接口。,当一个抽象实现接口的时候,需要子类去实现接口。
9.接口中不能包含实现的方法