C#面向对象编程的3个支柱(一)
OOP的支柱:
- 封装:隐藏一个对象的内部实现和保护数据完整性。
- 继承:促进代码重用。
- 多态:用同样的方式处理相关的对象。
封装的作用
OOP的第一个支柱是封装,是将对象用户不必了解的实现细节隐藏起来。如:你正在使用DatabaseReader类,它有Open()与Close()两个方法。
//DatabaseReader封裝了操作数据库的细节
DatabaseReader dr=new DatabaseReader();
dr.Open(@"MyCar.mdf");
//使用数据库来做一些事情,然后关闭
dr.Close();
调用者没有必要担心幕后完成DatabaseReader类的工作的众多代码。只需要创建一个实例并发送合适的信息(如:打开位于C驱动器下的MyCar.mdf文件)。
封装除了隐藏实现细节外还有隐藏数据的概念。对象的字段数据一般使用private关键字修饰,这样的话,外部世界就不能直接改变或获取对象的字段数据。而必须“很礼貌的请求”(如通过属性)。
继承的作用
继承它是基于已有类定义来创建新类定义。本质上,通过继承,子类可以继承基类核心的功能,并扩展基类的行为。
如“六边形是图形,图形是对象”,就在类型之间创建一种“is-a”的关系,“is-a”又称为经典继承。
在这里,Sharp(图形)定义了许多所有派生类都有的公共成员,由于Hexagon(六边形)类扩展了Sharp,它也就继承了友Sharp和Object(所有类型的超级基类)定义核心的功能。且Hexagon也定义了自己的相关功能。
在OOP中还有另外一种形式的代码重用:包含/委托模型(也就是has-a的关系)。这种重用形式不是用来建立父类/子类关系的。它意味着,一个类可以定义另一个类的成员变量,并向对象用户公开它的功能。
如:汽车(Car)有一个(has-a)收音机(Radio)。让Car继承Radio或反之都是不合理的。Car是一个Radio?当然不是啦!实质上,你有两个类一起合作,其中Car创建并公开Radio的功能。
class Radio
{
public void Power(bool turnOn)
{Console.WriteLine("Radio on:{0}",turnOn);}
}
class Car
{
//汽车“has-a”收音机
private Radio myRadio=new Radio();
public void TurnOnRadio(bool onOff)
{
//到内部对象的委托
myRadio.Power(onOff);
}
}
static void Main(string[] args)
{
//调用在内部被转发到Radio
Car viper=new Car();
viper.TurnOnRadio(false);
}
多态的作用
多态表示以同一种方式处理相关对象的能力。准确来说,是允许基类为所有的派生类定义一个成员集合(正式术语为多态接口)。一个类类型的多态接口由任意个虚拟(virtual)或抽象(abstract)成员组成。
虚拟成员是定义默认实现的基类成员,它可能被派生类改变(更正式的说法是重写override);而抽象方法是基类不提供默认实现的成员,只提供签名。当一个类派生自定义抽象方法的基类时,抽象方法必须被派生类所重写。当派生类重写由基类定义的成员时,其实就重定义了响应相同请求的方式。
class Sharp
{
//多态接口
public virtual void Draw()
{Console.WriteLine("这是一个图形");}
}
class Circle:Sharp
{
//...
public override void Draw()
{Console.WriteLine("这是一个圆形");}
}
class Hexagon:Sharp
{
//...
public override void Draw()
{Console.WriteLine("这是一个六边形");}
}
class Program
{
public static void Main(string[] args)
{//Sharp类型数组可以包含任何派生类型Sharp[] mySharps=new Sharp[3];
mySharps[0]=new Sharp();
mySharps[1]=new Circle();
mySharps[2]=new Hexagon();
foreach(Sharp sharp in mySharps)
{
sharp.Draw();
}
Console.Write("Press any key to continue . . . ");
Console.ReadKey(true);
}
}
C#访问修饰符
在使用封装的时候,我们必须考虑类型及类型成员对应用程序的可见性。类型及类型成员总是使用某个关键字来定义的,这个关键字控制它们的对应用程序其他部分的可见性。
这些关键字就是访问修饰符了
C#访问修饰符 |
可应用到的地方 |
作用 |
public |
类型、类型成员 |
公共的项没有限制。公共的成员可以被对象以及任何派生类访问。公共的类可以被其他外部程序集访问 |
private |
类型成员、嵌套类型 |
私有的项只能被定义它们的类(结构)访问 |
protected |
类型成员、嵌套类型 |
对象变量不能直接访问被保护的项,然而,可以被定义它们的类型及派生类访问 |
internal |
类型、类型成员 |
内部项只能在当前程序集访问。其他程序集不能使用它们 |
protected internal |
类型成员、嵌套类型 |
项在定义它们的程序集、类型以及派生类中可用。 |
PS:默认情况下,类型成员是隐式私有的,而类型是隐式内部的。
//具有私有默认构造函数的内部类型
class Radio
{
Radio(){}
}
要对外公开它们必须使用public关键字:
//具有公共默认构造函数的公共类型
public class Radio
{
public Radio(){}
}
第一个支柱:C#的封装技术
封装概念的核心是,对象的内部数据不应该从公共接口直接访问。如果对象用户想改变对象的状态就,就要使用访问方法(accessor,即getter)和修改方法(mutator,即setter)。
封装提供了一种保护状态数据完整的方法,与定义公共字段相比(很容易发生数据损坏),应该更多地定义私有数据字段,这种字段可以由调用者间接地操作。定义私有字段的方式有:
一、定义一对传统的访问方法和修改方法。
二、定义一个命名属性。
如果想与类体的私有数据字段进行交互,传统的做法是定义访问方法(即get方法)和修改方法(即set方法)。例如:
public class Person
{
private string name;
//传统访问方法
public string GetName()
{retrun name;}
//传统修改方法
public void SetName(string name)
{this.name=name}
}
尽管可以使用传统的访问方法和修改方法封转私有字段。但.NET语言还是提倡使用属性类强制数据保护。从CIL代码来说,属性总是映射到‘实际的’访问方法(get_XXX()方法)与修改方法(set_XXX()方法)。因此,类的设计者还可以在赋值逻辑之前进行必要的内部逻辑(如检查数字值的边界、装换大小写等等)。
public class Person
{
private string name;
//属性
public string Name
{
get{return name;}
set{name=value;}
}
}
Person p=new Person();
p.Name=”bruce.wong”;//设置Name属性
另:封装数据字段时,.NET基类库总是使用属性而不是使用访问方法与修改方法。因此,如果想创建和基类库有一个很好的集成的自定义类时,就不要定义传统的访问方法与修改方法。使用属性吧!
属性的获取和设置逻辑的可见性是由属性声明的访问修饰符单独控制:
public string Name
{
get{return name;}
protected set{name=value;}//设置逻辑只能被当前类和派生类调用。
}
当封装数据时,可能希望配置一个只读属性。为此,可以简单地建立一个只有get块的属性。类似的,如果配置一个只写属性,就建立一个只有set块得属性。
C#还支持静态属性。回想一下本章前面的内容,静态成员是在类级别访问的,而不能从这个类的实例(对象)访问。
public class Person
{
private static string name;
//静态属性
public static string Name
{
get{return name;}
set{name=value;}
}
}
其实属性与传统的访问方法与修改方法的目的是一样的,属性的优点是:对象用户可以只使用一个命名项就能操作数据字段。