对着月亮唱歌的幸福生活

即便是莲藕内心真空之所,也有根根柔丝穿过。
  博客园  :: 首页  :: 联系 :: 管理

Object-Oriented:接口(Interface)

Posted on 2008-07-23 13:33  对月而歌  阅读(488)  评论(0)    收藏  举报

 

《Beginning c# Objects》读书笔记系列

1.: 继承(Inheritance)

2.: 多态(Polymorphism)

3.: 抽象类(Abstract Class)

4.: 接口(Interface)

5.:静态特征(Static Feature)

 

    回忆一下,做为一个类型的类,是对真实世界事物的一种抽象。在这种抽象中,一些不重要的细节被忽略了。所以,可以发现,抽象类要比具体类更接近抽象模型,以为在抽象类中我们忽略了一个或多个行为的执行细节。

    现在,让我们来了解关于抽象的更多概念。利用抽象类,我们避免了为被声明为抽象的方法编写方法体。但这些类中的attribute又如何呢?在关于Course类的例子中,我们定义了自己认为所有课程类型都可能需要的数据结构(attribute):

    string courseName;

    string courseNumber;

    int creditValue;

    Collection enrolledStudents;

    professor instructor;

    但是,如果我们只想指定公共行为,而不去想费心去声明attribute,又该怎么办呢?归根到底,attribute 通常会被声明为私有,我们并不打算规定将来的派生类必须使用怎样的数据结构来实现期望的公共行为,而是把这些问题留给派生类去决定。

    打个比方,我们打算定义在“大学教书”的意思。对于一个对象,也许需要执行以下服务,才能达到“教书”目的:

      *同意讲授特定课程;

      *为课程选定教科书;

      *为课程准备提纲;

      *批准特定学生选修课程的申请。

    所有这些行为都可通过指定方法头而被形式化,从而表现出一个有能力教书的对象会被怎样要求执行这些行为:

 

public bool AgreeToTeach(Course c)
public void DesignateTextbook(TextBook b.Course c)
public Syllabus DefineSyllabus(Course c)
public bool ApproveEnrollment(Student s,Course c)

    这一系列的方法头,共同定义了应用程序中的一个角色的意义(例如教科书),这些方法头组成的结构被称为接口。和类一样,接口也有名称;让我们把上面的借口叫做ITeacher.(C#关于借口的命名约定是使用Pascal命名法风格;前面加上大写字母I,表示该类型是一个接口。)

    要定义接口。我们把为接口定义的方法头放一起,每个方法头后面加上一个分号(;),然后把他们放到一组大括号里,第一个大括号的前面加上关键字Interface和接口名称。如下所示:

 

public interface ITeacher{
  
bool AgreeToTeach(Course c);
  vodi DesignateTextbook(Textbook b,Course c);
  Syllabus DefineSyllabus(Course c);
  
bool ApproveEnrollment(Student s,Course c);
}

    接口可以是公共的也可以是私有的,不过一般都声明为公共的。和类一样,每个接口也往往放到单独

的代码文件中,文件名与其容纳的接口名称一致:如接口ITeacher应该放到文件ITeacher.Cs里面。

    接口中的方法都隐含地是公共及抽象方法,所以,声明它们时无需再再用public和abstract关键字,实际上,如果我们硬要给接口中的方法头部加上public关键字:

  public interface ITeacher{

      public bool AgreeToTeach(Course c);

      //等等。

    编译器将报错:

    error CS0106:the modifier'public' is not valid for this item

    如果给借口中方法头错误地abstract关键字,会得到类似报错。

1.实现一个接口

    定义了类似ITeacher这样的接口之后,我们就能指定不同的类作为教师(Teacher)了,例如Person(人)对象,只需要用下面的语法声明该类有意实现ITeacher接口即可。

    //实现一个接口。

  public class Professor : ITeacher

     {细节从略}

    注意,声明一个类为一个实现接口的语法,和标示关系的语法没有区别,都是在接口/类名称后面加上冒号:

    //通过继承扩展一个类... ...

    public class Professor : Person

      {细节从略}

接口(ITeacher)和类(Persong的命名区别。接口的名称往往以大写字母I开头,后面跟另一个大写字母(在原来的名称前面加大写字母I:

       当声明一个类为接口的实现时: public class Professor : ITeacher{... ... 接口的实现类必须提供接口所声明的所有方法(这些方法隐含地生命为抽象方法)的具体版本,这样才能满足编译器的要求。举个例子,如果我们按照以下代码编写Professor类,实现ITeacher接口定义的三个方法,但却忽略了第四个方法:

public class Professor : Iteacher
{
 privater 
string name;
 
private string employeeId;
 
//等等。
 
//也定义了所有的Property;细节从略
 
//实现ITeacher接口生命的四个方法中的三个,
 
//为它们编写了方法体。
 public bool AgreeToTeach(Course c){
 
//方法的逻辑在这里体现.细节从略 
}
 
public void DesignaterTextbook(TextBook b,Course c){
 
//方法的逻辑在这里体现.细节从略 
}
public Syllabus DefineSullabus(Course c){
 
//方法的逻辑在这里体现.细节从略 
}
//注意,没有提供ApproveEnrollment方法的实现
}
//如果试图编译上面的类,就会得到以下编译错误信息:
error CS0535:'Professor' does not implement interface 'ITeacher.ApproveEnrollment(Student,Course)'

 

2.另一种“is a ”关系

    在前面学到,继承被看作是一种“is a ”关系(即派生类型同时也是基类型)。

对接口的实现,是另一种形式的"is a "关系:

      *如果Professor类扩展自Person 类,则教授(professor)也是人(person);

      *如果Professor实现ITeacher接口,则教授(professor)同时也是教师(teacher).

    同样,如果类A实现接口X,则所有派生自A的类都得实现同一个接口。例如如果从Professor类派生出AdjunctProfessor类,如果Professor类实现了ITeacher接口,则副教授(adjunct professor)也是教师:

 

public class Professor : ITeacher{
//ITeacher所需要的所有方法将本类实现 
//细节从略。
}
public class AdjunctProfessor : Professor{
//ITeacher 所需要的所有方法,至少上从Professor继承下来的那些方法
//都将本类实现 细节从略
}
//这很容易理解,因为AdjunctProfessor类继承ITeacher接口要求的所有方法,或者覆载这些方法;AdjunctProfessor类也将有能力执行ITeacher
//接口的服务。

3.抽象类vs接口

    实现一个接口,概念上类似于在扩充一个抽象类时填充其抽象方法。那么,实现一个接口和扩展一个抽象类,俩者有何区别呢?在设计应用程序时,为什么会二选一?

      *对于接口,我们只指定抽象行为,而抽象类经常会指定“具体的”数据结构(attribute),以及一些抽象行为和具体行为的混合体。所以,从程度上看,接口要比抽象类更为抽象(而抽象类则比具体类来得抽象),因为接口留下了更多的想象空间。

注意,抽象类也可以是一系列抽象方法头的组合,如果我们愿意这样做的话;比如:

public abstract class Person{
//故意不为这个类声明attribute
//而且所有的方法都是抽象方法。
public abstract void Print();
public abstract double ComputSalary();
//等等
}

不过,对于这种情形,还不如直接干脆声明一个接口好了。

      *当从一个抽象类派生出一个非抽象方法时,派生类通过覆载的手段,提供抽象方法的具体实现,所以,派生类的方法头一定会包括override关键字

      *当一个类实现一个接口时,该实现类也要提供接口中声明的所有方法的具体实现。然而,实现类并不覆载它们。反之,我们是在初次描述方法轮廓,所以,在实现类的方法头中不包括override。(如果加上了这个关键字,编译器会告诉我们“没有合适的方法可以覆载”)

    借口和抽象类的语法沙锅内的不同之处如下所示:

 

使用抽象类的例子 使用接口的例子

那Teacher类型声明为抽象类

{

 //抽象类可能会声明数据结构。

 string employeedId;

 //等等。

 //使用关键字abstract声明抽象方法

  public abstract void AgreeToTeach(Course c);

  public abstract void DesignateTextbook

     (TextBook b,Course c); 

 //等等

 //抽象类可能会声明具体方法。

  public void Print(){

  Console.WriteLine(name);

 //等等。

  }

}

把Teacher类型声明为接口:

 

public interface ITeacher

{

 //接口不声明attribute.

 //不能加上pbulic 或abstract关键字,

 void AgreeToTeach(Course c);

 void DesignateTextbook( TextBook b,Course c);

 //等等。

 //接口不声明具体方法。

}

使用抽象类的例子 使用接口的例子

从Teacher类派生Professor类:

public class Professor : Teacher

{

 //Professor类从父类继承所有attribute.

 //而且可以有选择地增加attribute

 //细节从略

 //覆盖从Teacher类继承的抽象方法。

  public override void AgreeToTeach(Course c){

  //方法提逻辑在这里体现,细节从略

    }

  //其他抽象方法的实现

  //可能增加的其他方法

 //细节从略

}

Professor类实现ITeacher接口:

public class Professor : Iteacher

{

 //类必须提供租户的数据结构,

 //而接口则无法提供。

 string name ;

  string employeeId;

  //等等。

  //实现ITeacher接口的方法

  //无须使用override关键字

  public void AgrweeToTeach(Course c)

{

  //方法体逻辑在这里体现,细节从略。

}

 //其他抽象方法的 实现

 //可能增加的其他方法。

 //细节从略

}

      *从抽象类派生的类不一定要覆载所有的抽象方法、提供具体版本;如果一个或多个抽象方法没有被覆载,则该派生类也是一个抽象类;

      *实现一个接口的类必须提供借口所需的所有抽象方法的具体版本;实现一个接口,是一种“要么全要,要么不要”的事情;

      *俩者之间的另一重要区别在于,一个类只能从一个基类派生,而一个类却可以实现多个接口。因为这是一个强有力的语言特征,我们下面来说明。

4.实现多个接口

    举个例子,如果我们创建第二个窗口,名为IAdministrator,它定义了以下方法头:

public interface IAdministrator
{
 
public bool approveNewCourse(Course c);
 
public bool hireProfessor(Professor p);
}
//我们可以把Professor类声明为同时实现ITeacher和IAdministrator接口,这样,这个类就得实现俩个接口的所有方法:
public class Professor : ITeacher,IAdministrator
{
 
//Professor 类必须ITeacher接口要求的方法细节从略
 
//Professor类必须实现IAdministrator接口的方法细节从略
}

 

注意

 注意,如果一个类实现的俩个或多个借口中的方法拥有同样的方法签名,则在实现类中只需要实现其中一个--该方法竟同时满足所有接口的实现要求

 

    一个类实现超过了一个接口,它的对象就有能力扮演应用程序中的多种身份或角色:这样的对象可以为不同类型的引用变量所维系.根据上述定义,Professor即是ITeacher又是IAdministrator,下面的客户端代码是可能存在的:

   

//实体化一个Professor对象,将其句柄放在
//一个类型为Professor的应用变量中
professor p = new Professor();
//然后生命俩个引用变量,类型分别为Professor类
//实现俩个接口类型.
ITeacher t ;
IAdministrator a ;
= p;//把 Professor对象的句柄放在类型为Teacher的引用变量中
       
//这是可以的,因为教授同时也是教师
= p;//把Professor对象的句柄放在类型为Administrator的引用变量中,
      
//这是可以的,因为教授可能同时也是行政人员!
//这个概念等同于一个人的你,被不同人当作不同角色看待的情形:在经理眼里你是雇员,在父母眼里你是孩子,在孩子眼里你
//是父母,如此等等.
//我们可以把一个对象当作Professor来要求 
 
//Department是为Professor类定义的Property
p.Department = "Computer Science";
 
//或者当做一个Teacher
 
//AgreeToTeach是为ITeacher接口定义的方法
t.AgreeToTeach(c);//注意p.agreeToTeach(c);也同样可以工作 
 
//或者当作一个行政人员
 
//ApproveNewCourse四为IAdministrator接口定义的方法 
a.approveNewCourse(c);//注意 p.approveNewCourse(c);也同样可以工作 
//因为他身兼三职!    

5.接口实体化

    接口不能实体花,因为接口中没有构造器.也就是说,如果把ITeacher定义为一个接口,则不能将他实体化:

    ITeacher t = new ITeacher(); //不能这样做!编译器将报错;

    error CS0144 :Cannot creat an instace of the abstract class or interface 'ITeacher'

    虽然我们不能实体化一个接口,但却能把引用变量声明为一个接口类型:

    ITeacher t ; //这样可以.

    上面一行可以通过,下面一行也可以

    ITeacher t = new Professor();

    为什么?如果等号右边的表达式类型与左边变量的类型匹配,编译器就允许赋值操作.Professor类实现恶劣ITeacher接口,教授就是教师,所以这种赋值是允许的.

6.接口的重要性

    接口,是支持接口的OO语言中不为众人所了解的特性,从而也不被充分利用.这相当不幸运,因为如果应用得当,接口会是非常有力的技术,

    只要有可能,就应该把类的公共部分设计为接口,而不是设计为特定的类类型,这样就可以让方法在多哥方面拥有更多的灵活性,诸如:

      *方法的形参

      *方法的返回值

下面用2个例子来展示接口的威力

例子一:

    在本例中,假定

      *Professor 是Person的一个派生类;

      *Student是Persong的一个派生类;

      *Professor和Student是兄弟类----相互不继承;

      Person 实现ITeacher接口,所有Professor和Strudent也见解地实现ITeacher接口.

   我们从设计一个名为Course的类开始,该类有一个类型为Professor的私有attribute,名为teachingAssistant,和访问这个attribute的一个property;

 

public class Course{
  
private Professor teachingAssistant;
  
//其他特征从略
  public Professor TeachingAssistant{
    
get{
      
return teachingAssistant;
 }
    
set{
      teachingAssistant 
= value;
 }
}
//其他特征从略
然后,我们可能会在可户代码中这样使用Course类;
//客户代码.
Course c = new Course("Math 101");
Professor p 
= new Professor ("John Smith");
c.TeachingAssistant 
= p;
  
//之后,如果我们想把私有attribute teachingAssistant的类型从Professor改为Student,则也要修改
  
//公共property TeachingAssistant;
public class Course {
  
//把类从Professor变为Student  
  Private Student teachingAssistant;
  
//还有这里 
  public Student TeachingAssistant{
  
//细节从略.
  }
  
//等等
}

    之前编写的客户代码不能再用----下面黑体字标出的代码无法通过编译----因为TeachinaAssistant已经被修改,现在它被赋予一个Student引用:

  //客户代码

Course c new Course ("Math 101");
Prifessor p 
= new Prifessor("John Smith");
c.TeachingAssistant 
= p;//本行无法被编译过.

  //现在,看看修改Course类的设计会怎样.比如,我们原来就把Professor类声明为ITeacher接口类型,而且把TeachingAssistant这个prperty声明为ITeacher类型:

   

public class Course
{
private ITeacher teachingAssistant;
//细节从略
public ITeacher TeachingAssistant{
//细节从略
}

//等等
}

    这样我们为可户代码创造了更大的可能行,可以把一个Professor指定为助教:
//客户代码
Course c = cnew Course("Math 101");
Professor p 
= new Professor ("Jojn Smith");
c.TeachingAssistant 
= p;
//或把一个Student指定为助教:
//客户代码
Course c = new Course("Math 101");
Student s 
= new Student("George Jones");
c.TeachingAssistant 
= s;
或者任意实现ITeacher接口类型.

例子二:

    常用的一个C#预定义接口是IList接口要求时下以下方法:

 

int Add(object value)
void Clear()
bool Contains (object value)
int IndexOf(object vlaue)
void Insert(int index,object value)
void Remove(object value)
void RemoveAt(int index)
      该接口被数种C#预定义群集的方法,该方法接受一个普遍IList引用,而不是特定集群类型的引用,则这样的方法更我通用;客户代码可以传如任意类型的群集.
pbulic class SomeClass
{
 
//细节从略
 public vodi SomeMethod(IList list){
 
//在方法内部,可以使用IList接口定义的任何方法操作list参数 
}