C#(面向对象)
一、面向对象
1.概念
类可以派生形成子类(派生类),派生子类的类称为父类。对应一个系统最基本的类称为基类,一个基类可以有多个派生类,从基类派生出的类(子类)还可以进行派生。
对象是类的具体化,是具有属性和方法的实体(实例)。对象通过唯一的标识名以区别于其他对象,对象有固定的对外接口,它是对象与外界通信的通道。
2.对象、类与实例化
对象是类的具体化,是具有属性和方法的实体(实例)。对象通过唯一的标识名以区别于其他对象,对象有固定的对外接口,它是对象与外界通信的通道。
二、面向对象程序设计三原则
1.封装
所谓“封装”,就是用一个框架把数据和代码组合在一起,形成一个对象。遵循面向对象数据抽象的要求,一般数据都被封装起来,外部不能直接访问对象的数据,只能说明对象提供的公共方法(也称接口,是对象之间联系的渠道)。在 C#中,类是支持对象封装的工具,对象则是封装的基本单元。
封装的对象之间进行通信的一种机制叫作消息传递。消息是向对象发出的服务请求,是对象之间交互的途径。消息包含要求接收对象去执行某些活动的信息,以及完成要求所需的其他信息(参数)。发送消息的对象不需要知道接收消息的对象如何对请求予以响应,接收者接收到消息,它就承担了执行指定动作的责任,作为消息的答复,接收者将执行某个方法来满足所接收的请求。
2.继承
继承是面向对象编程技术的一块基石,通过它可以创建分等级层次的类。例如,创建一个汽车的通用类,它定义了汽车的一般属性(如车轮、方向盘、发动机、车门)和操作方法(如前进、倒退、刹车、转弯等)。这个已有的类可以通过继承的方法派生出新的子类,如卡车、轿车、客车等,它们都是汽车类中更具体的类,每个具体的类还可增加自己的一些特有的属性,如图3.2所示,更一般的表示如图3.3所示。
如果一个类有两个或两个以上的直接基类,这样的继承结构就称为多重继承或多继承。现实中这种模型屡见不鲜,如一些类似于沙发床的组合功能的产品,它既有沙发的功能,又有床的功能,沙发床应允许同时继承沙发和床的特征,如图3.4所示,更一般的表示如图3.5所示。
目前很多编程语言已不再支持多继承,C#也对多继承的使用进行了限制,规定通过接口来实现多继承。接口可以从多个基接口继承,可以包含方法、属性、事件和索引器。一个典型的接口就是一个方法声明的列表,接口本身不提供它所定义的成员的实现,故不能被实例化,而是由实现接口的类以适当的方式定义接口中声明的方法,如图3.6所示。
3.多态
多态就其字面意思是:多种形式或多种形态。在面向对象编程中,多态是指同一个消息或操作在作用于不同的对象时,可以有不同的反应,产生不同的执行结果。例如,问甲同学:“现在几点钟?”甲回答说:“3 点 15 分。”又问乙同学:“现在几点钟?”乙回答说:“大概3点多钟。”再问丙同学:“现在几点钟?”丙回答说:“不知道。”这就是同一个消息发送给不同的对象,不同的对象可以作出不同的反应的例子。
多态分两种:静态多态和动态多态。当在同一个类中直接调用一个对象的方法的时候,系统在编译时,根据传递的参数个数、类型及返回值类型等信息决定实现何种操作,这就是所谓的静态多态(静态绑定)。而当在一个有着继承关系的类层次结构中间接调用一个对象的方法的时候,调用要经过基类的操作,只有到系统运行时,才能根据实际情况决定实现何种操作,这就是动态多态(动态绑定)。C#同时支持这两种多态,在实现方式上可以有三种方式:接口多态、继承多态、通过抽象类实现的多态。
三、类的封装与继承
1.属性封装
C#提供了属性(property)这个更好的方法,把字段域和访问它们的方法相结合。对类的用户而言,属性值的读/写与字段域语法相同;对编译器来说,属性值的读/写是通过类中封装的特别方法get访问器和set访问器实现的。
属性的声明方法如下:
[属性集信息] [属性修饰符] 类型 成员名
{
访问器声明
}
其中:
v 属性修饰符—与方法修饰符相同,包括 new、static、virtual、abstract、override 和 4种访问修饰符的合法组合,它们遵循相同的规则。
v 类型—指定该声明所引入的属性的类型。
v 成员名—指定该属性的名称。
v 访问器声明——声明属性的访问器,可以是一个 get 访问器或一个 set 访问器,或者两个都有。
语法形式:
get // 读访问器
{
… // 访问器语句块
}
set // 写访问器
{
… // 访问器语句块
}
get 访问器的返回值类型与属性的类型相同,所以在语句块中的 return 语句必须有一个可隐式转换为属性类型的表达式。
set 访问器没有返回值,但它有一个隐式的值参数,其名称为 value,它的类型与属性的类型相同。
同时包含 get 和 set 访问器的属性是读/写属性,只包含 get 访问器的属性是只读属性,只包含 set 访问器的属性是只写属性。
【例3.1】 对TextBox类的text、fontname、fontsize、multiline域提供[属性方式的读](【例3.1】 对TextBox类的text.txt)[/](【例3.1】 对TextBox类的text.txt)[写访问。](【例3.1】 对TextBox类的text.txt)
using System;
namespace Ex3_1
{
class TextBox
{
private string text;
private string fontname;
private int fontsize;
private bool multiline;
public TextBox()
{
text = "text1";
fontname = "宋体";
fontsize = 12;
multiline = false;
}
public string Text
{
// Text属性,可读可写
get
{ return text; }
set
{ text = value; }
}
public string FontName
{
// FontName属性,只读属性
get
{ return fontname; }
}
public int FontSize
{
// FontSize属性,可读可写
get
{ return fontsize; }
set
{ fontsize = value; }
}
public bool MultiLine
{
// MultiLine属性,只写
set
{ multiline = value; }
}
}
class Test
{
static void Main(string[] args)
{
TextBox Text1 = new TextBox();
// 调用Text属性的get访问器
Console.WriteLine("Text1.Text= {0} ", Text1.Text);
// 调用Text属性的set访问器
Text1.Text = "这是文本框";
Console.WriteLine("Text1.Text= {0} ", Text1.Text);
// 调用FontName属性的get访问器
Console.WriteLine("Text1.Fontname= {0} ", Text1.FontName);
// 调用FontSize属性的set访问器
Text1.FontSize = 36;
// 调用FontSize属性的get访问器
Console.WriteLine("Text1.FontSize= {0} ", Text1.FontSize);
// 调用MultiLine属性的set访问器
Text1.MultiLine = true;
Console.Read();
}
}
}
程序运行结果如图3.7所示。
在这个示例中,类TextBox定义了4个属性,在类体外使用这些属性是用 Text1.*的形式,这与字段域的访问非常类似。编译器根据它们出现的位置调用不同的访问器。如果在表达式中引用该属性则调用 get 访问器,若给属性赋值则调用set访问器,赋值号右边的表达式值传给value。
属性是字段的自然扩展,当然属性也可作为特殊的方法使用,并不要求它和字段域一一对应,所以属性还可以用于各种控制和计算。
【例3.2】 定义 Label类,设置 Width 和 Heigh属性,分别存放两点之间在水平坐标轴和垂直[坐标轴上的投影长度](【例3.2】 定义 Label类.txt)[。](【例3.2】 定义 Label类.txt)
using System;
namespace Ex3_2
{
class Point
{
int x, y;
public int X
{
get
{ return x; }
}
public int Y
{
get
{ return y; }
}
public Point ( )
{ x=y=0; }
public Point (int x, int y)
{
this.x=x;
this.y=y;
}
}
class Label
{
Point p1=new Point ( );
Point p2=new Point (5, 10);
public int Width // 计算两点之间在水平坐标轴上的投影长度
{
get
{
return p2.X-p1.X;
}
}
public int Height // 计算两点之间在垂直坐标轴上的投影长度
{
get
{
return p2.Y-p1.Y;
}
}
}
class Test
{
static void Main(string[] args)
{
Label Label1 = new Label();
Console.WriteLine("Label1.Width= {0} ", Label1.Width);
Console.WriteLine("Label1.Height= {0} ", Label1.Height);
Console.Read();
}
}
}
2.类的继承
1.引例
图3.9是采用类的层次图表示继承的例子。
下面用程序实现图3.9所示的类的层次结构。
【例3.3】 类的继承
using System;
namespace Ex3_3
{
// 定义基类Shape
public class Shape
{
protected string Color;
public Shape(){ ;}
public Shape(string Color)
{
this.Color = Color;
}
public string GetColor()
{
return Color;
}
}
// 定义Circle类,从Shape类中派生
public class Circle : Shape
{
private double Radius;
public Circle(string Color, double Radius)
{
this.Color = Color;
this.Radius = Radius;
}
public double GetArea()
{
return System.Math.PI * Radius * Radius;
}
}
// 派生类Rectangular,从Shape类中派生
public class Rectangular : Shape
{
protected double Length, Width;
public Rectangular()
{
Length = Width = 0;
}
public Rectangular(string Color, double Length, double Width)
{
this.Color = Color;
this.Length = Length;
this.Width = Width;
}
public double AreaIs()
{
return Length * Width;
}
public double PerimeterIs() // 周长
{
return (2 * (Length + Width));
}
}
// 派生类Square,从 Rectangular类中派生
public class Square : Rectangular
{
public Square(string Color, double Side)
{
this.Color = Color;
this.Length = this.Width = Side;
}
}
class TestInheritance
{
static void Main(string[] args)
{
Circle Cir = new Circle("orange", 3.0);
Console.WriteLine("Circle color is {0},Circle area is {1}", Cir.GetColor(), Cir.GetArea());
Rectangular Rect = new Rectangular("red", 13.0, 2.0);
Console.WriteLine("Rectangualr color is {0},Rectangualr area is {1},
Rectangular perimeter is {2}",Rect.GetColor(), Rect.AreaIs(), Rect.PerimeterIs());
Square Squ = new Square("green", 5.0);
Console.WriteLine("Square color is {0},Square Area is {1},
Square perimeter is {2}", Squ.GetColor(),Squ.AreaIs(), Squ.PerimeterIs());
}
}
}
程序运行结果如图3.10所示。
【例3.3】程序中的Square类也可改写成:
public class Square: Rectangular
{
public Square(string Color,double Side):base(Color,Side,Side){ }
}
关键字base的作用是调用Rectangular类的构造函数,并将Square类的变量初始化。
如果将Square类改写成:
public class Square: Rectangular
{
public Square(string Color,double Side){ }
}
这种情况调用的就是父类的无参构造函数,而非有参构造函数,它等同于:
public class Square: Rectangular
{
public Square(string Color,double Side):base(){ }
}
【例3.4】 [base](【例3.4】 base调用基类的方法.txt)[调用基类的方法。](【例3.4】 base调用基类的方法.txt)
在上例中,Employee类的 GetInfoEmployee 方法使用了 base 关键字,调用基类 Person 的GetInfoPerson方法。
using System;
namespace Ex3_4
{
public class Person
{
protected string Phone = "444-555-666";
protected string Name = "李明";
public void GetInfoPerson()
{
Console.WriteLine("Phone: {0}", Phone);
Console.WriteLine("Name: {0}", Name);
}
}
class Employee : Person
{
public string ID = "ABC567EFG";
public void GetInfoEmployee()
{
// 调用基类Person的GetInfo方法
base.GetInfoPerson();
Console.WriteLine("Employee ID: {0}", ID);
}
}
class TestClass
{
static void Main(string[] args)
{
Employee Employees = new Employee();
Employees.GetInfoEmployee();
}
}
}
程序运行结果如图3.11所示。
3.System.Object类
C#中所有类都派生于 System.Object 类。在定义类时如果没有指定派生于哪一个类,系统就默认其派生于Object类。
于是,【例3.3】中Shape的定义就等同于:
public class Shape :System.Object
{
//TODO…
}
System.Object类常见的公有方法是:
(1)Equals——若两个对象具有相同值,方法返回true。
(2)GetHashCode——方法返回对象值的散列码。
(3)ToString——通过在派生类中重写该方法,返回一个表示对象状态的字符串。
(4)GetType——根据内置对象类型指针获得当前对象的实际类型。
四、派生类的构造函数
当创建派生类的对象时,会展开一个链式的构造函数调用,在这个过程中,派生类构造函数在执行它自己的函数体之前,首先显式或隐式地调用基类构造函数。类似地,如果这个基类也是从另一个类派生而来的,那么这个基类的构造函数在执行之前也会先调用它的基类构造函数,以此类推,直到Object类的构造函数被调用为止。
【例3.5】 [继承中函数的构造顺序。](【例3.5】 继承中函数的构造顺序.txt)
using System;
…
namespace Ex3_5
{
public class Grandsire
{
public Grandsire()
{
Console.WriteLine("调用Grandsire的构造函数");
}
}
public class Father : Grandsire
{
public Father()
{
Console.WriteLine("调用Father的构造函数");
}
}
public class Son : Father
{
public Son()
{
Console.WriteLine("调用Son的构造函数");
}
}
class BaseLifeSample
{
static void Main(string[] args)
{
Son s1 = new Son();
Console.Read();
}
}
}
上段代码中,C#的执行顺序是这样的:根据层次链找到最顶层的基类 Grandsire,先调用基类的构造函数,再依次调用各级派生类的构造函数,运行结果如图3.12所示。
下面程序描述了派生类构造函数的格式,以及在初始化对象时构造函数的调用次序。
【例3.6】 [派生类构造函数及其调用。](【例3.6】 派生类构造函数及其调用.txt)
using System;
namespace Ex3_6
{
class Point
{
private int x, y;
public Point()
{
x = 0; y = 0;
Console.WriteLine("Point() constructor : {0} ", this);
}
public Point(int x, int y)
{
this.x = x;
this.y = y;
Console.WriteLine("Point(x,y) constructor : {0} ", this);
}
}
class Circle : Point
{
private double radius;
public Circle() // 默认约定调用基类的无参构造函数Point()
{
Console.WriteLine("Circle () constructor : {0} ", this);
}
public Circle(double radius): base()
{
this.radius = radius;
Console.WriteLine("Circle (radius) constructor : {0} ", this);
}
public Circle(int x, int y, double radius): base(x, y)
{
this.radius = radius;
Console.WriteLine("Circle (x, y, radius) constructor : {0} ", this);
}
}
class Test
{
static void Main(string[] args)
{
Point a = new Point();
Circle b = new Circle(3.5);
Circle c = new Circle(1, 1, 4.8);
Console.Read();
}
}
}
程序运行结果如图3.13所示。
五、多态的实现
1.方法重载
一个方法的名字、形参个数、修饰符及类型共同构成了这个方法的签名,应用中经常需要为同名的方法提供不同的实现,如果一个类中有两个或两个以上的方法同名,但它们的形参个数或类型有所不同,这是允许的,这属于不同的方法签名。若仅仅是返回类型不同的同名方法,编译器是不能识别的。下面通过一个例子来介绍方法的重载。
【例3.7】 [不同版本的方法求最大值](【例3.7】 不同版本的方法求最大值.txt)[。](【例3.7】 不同版本的方法求最大值.txt)
using System;
namespace Ex3_7
{
class Myclass // 该类中有4个不同版本的max方法
{
public int max(int x, int y)
{
return x >= y ? x : y;
}
public double max(double x, double y)
{
return x >= y ? x : y;
}
public int max(int x, int y, int z)
{
return max(max(x, y), z);
}
public double max(double x, double y, double z)
{
return max(max(x, y), z);
}
}
class Test
{
static void Main(string[] args)
{
Myclass m = new Myclass();
int a, b, c;
double e, f, g;
a = 10; b = 20; c = 30;
e = 1.5; f = 3.5; g = 5.5;
// 调用方法时,编译器会根据实参的类型和个数调用不同的方法
Console.WriteLine("max({0},{1})= {2} ", a, b, m.max(a, b));
Console.WriteLine("max({0},{1},{2})= {3} ", a, b, c, m.max(a, b, c));
Console.WriteLine("max({0},{1})= {2} ", e, f, m.max(e, f));
Console.WriteLine("max({0},{1},{2})= {3} ", e, f, g, m.max(e, f, g));
Console.Read();
}
}
}
程序运行结果如图3.14所示。
2.运算符重载
运算符重载包括一元运算符重载、二元运算符重载以及用户定义的数据类型转换,下面简单介绍前两种情况。
如果有一个复数Complex类对一元运算符“++”重载,可以写成:
public static Complex operator ++(Complex a)
{
//TODO…
}
对二元运算符“+”可以写成:
public static Complex operator +(Complex a, Complex b)
{
//TODO…
}
一元运算符有一个参数,二元运算符有两个参数。重载运算符必须以 public static 修饰符开始。
可以重载的运算符包括:
v 一元运算符——+ - ! ~ ++ - - true false;
v 二元运算符——+ - * / % & | ^ << >> == != > < >= <=。
下面的运算符要求必须同时重载,不能只单独重载中间的一个:
v 一元运算符——true和false;
v 二元运算符——==和!=,>和<,>=和<=。
【例3.8】 运算符重载的实现。
using System;
namespace Ex3_8
{
class Complex
{
double r, v; //r+ v i
public Complex(double r, double v)
{
this.r=r;
this.v=v;
}
// 二元运算符“+”重载
public static Complex operator +(Complex a, Complex b)
{
return new Complex(a.r+b.r, a.v+b.v);
}
// 一元运算符“-”重载
public static Complex operator -(Complex a)
{
return new Complex(-a.r,-a.v);
}
// 一元运算符“++”重载
public static Complex operator ++(Complex a)
{
double r=a.r+1;
double v=a.v+1;
return new Complex(r, v);
}
public void Print()
{
Console.Write(r+" + "+v+"i\n");
}
}
class Test
{
static void Main(string[] args)
{
Complex a=new Complex(3,4);
Complex b=new Complex(5,6);
Complex c=-a;
c.Print();
Complex d=a+b;
d.Print();
a.Print();
Complex e=a++; // 先赋值后++
a.Print();
e.Print();
Complex f=++a; // 先++后赋值
a.Print();
f.Print();
}
}
}
程序运行结果如图3.15所示。