接口与多态性
==========================
一 接口概述
==========================
接口是对象之间通信的基本方式,在C#中接口是使用 interface 关键字定义的。例如:
interface IComparable
{
int CompareTo(object obj);
}
接口描述可属于任何类或结构的一组相关行为。接口可由方法、属性、事件、索引器或这四种成员类型的任何组合构成。接口不能包含字段。接口成员一定是公共的。
类和结构可以像类继承基类或结构一样从接口继承,但有两个例外:其一是类或结构可继承多个接口。其二是当类或结构继承接口时,它继承成员定义但不继承实现。例如:
public class Minivan : Car, IComparable
{
public int CompareTo(object obj)
{
return 0;
}
}
若要实现接口成员,类中的对应成员必须是公共的、非静态的,并且与接口成员具有相同的名称和签名。类的属性和索引器可以为接口上定义的属性或索引器定义额外的访问器。例如,接口可以声明一个带有 get 访问器的属性,而实现该接口的类可以声明同时带有 get 和 set 访问器的同一属性。但是,如果属性或索引器使用显式实现,则访问器必须匹配。
接口和接口成员是抽象的;接口不提供默认实现。IComparable 接口向对象的用户宣布该对象可以将自身与同一类型的其他对象进行比较,接口的用户不需要知道相关的实现方式。
接口可以继承其他接口。类可以通过其继承的基类或接口多次继承某个接口。在这种情况下,如果将该接口声明为新类的一部分,则类只能实现该接口一次。如果没有将继承的接口声明为新类的一部分,其实现将由声明它的基类提供。基类可以使用虚拟成员实现接口成员;在这种情况下,继承接口的类可通过重写虚拟成员来更改接口行为。
接口具有下列属性:
1 接口类似于抽象基类:继承接口的任何非抽象类型都必须实现接口的所有成员。
2 不能直接实例化接口。
3 接口可以包含事件、索引器、方法和属性。
4 接口不包含方法的实现。
5 类和结构可从多个接口继承。
6 接口自身可从多个接口继承。
==========================
二 显式接口实现
==========================
如果类实现两个接口,并且这两个接口包含具有相同签名的成员,那么在类中实现该成员将导致两个接口都使用该成员作为它们的实现。例如:
interface IControl
{
void Paint();
}
interface ISurface
{
void Paint();
}
class SampleClass : IControl, ISurface
{
public void Paint()
{
}
}
然而,如果两个接口成员执行不同的函数,那么这可能会导致其中一个接口的实现不正确或两个接口的实现都不正确。可以显式地实现接口成员 -- 即创建一个仅通过该接口调用并且特定于该接口的类成员。这是使用接口名称和一个句点命名该类成员来实现的。例如:
public class SampleClass : IControl, ISurface
{
void IControl.Paint()
{
System.Console.WriteLine("IControl.Paint");
}
void ISurface.Paint()
{
System.Console.WriteLine("ISurface.Paint");
}
}
类成员 IControl.Paint 只能通过 IControl 接口使用,ISurface.Paint 只能通过 ISurface 使用。两个方法实现都是分离的,都不可以直接在类中使用方法名Paint()。例如:
SampleClass obj = new SampleClass();
IControl c = (IControl)obj;
c.Paint();
ISurface s = (ISurface)obj;
s.Paint();
显式实现还用于解决两个接口分别声明具有相同名称的不同成员(如属性和方法)的情况:
interface ILeft
{
int P { get;}
}
interface IRight
{
int P();
}
为了同时实现两个接口,类必须对属性 P 和/或方法 P 使用显式实现以避免编译器错误。例如:
class Middle : ILeft, IRight
{
public int P() { return 0; }
int ILeft.P { get { return 0; } }
}
示例
本示例声明一个 接口IDimensions 和一个类 Box,该类显式实现接口成员 getLength 和 getWidth。通过接口实例 dimensions 访问这些成员。
interface IDimensions
{
float getLength();
float getWidth();
}
class Box : IDimensions
{
float lengthInches;
float widthInches;
Box(float length, float width)
{
lengthInches = length;
widthInches = width;
}
float IDimensions.getLength()
{
return lengthInches;
}
float IDimensions.getWidth()
{
return widthInches;
}
static void Main()
{
Box mybox = new Box(30.0f, 20.0f);
IDimensions dimensions = (IDimensions)mybox;
System.Console.WriteLine("Length: {0}",dimensions.getLength());
System.Console.WriteLine("Width: {0}",dimensions.getWidth());
}
}
==========================
三 多态性概述
==========================
多态性从字面上看是多种形态,在C#中多态性指的是对象具有多种数据类型,当一个类继承于某个基类或者接口时,则这个类就具有多态性,它可以是自己的类型,也可以是任何基类或接口类型。C# 中的每种类型都是多态的。类型可用作它们自己的类型或用作 Object 实例,因为任何类型都自动将 Object 当作基类型。
当派生类从基类继承时,它会获得基类的所有方法、字段、属性和事件。若要更改基类的数据和行为,您有两种选择:可以使用新的派生成员替换基成员,或者可以重写虚拟的基成员。
★使用新的派生成员替换基成员:
使用新的派生成员替换基类的成员需要使用 new 关键字。如果基类定义了一个方法、字段或属性,则 new 关键字用于在派生类中创建该方法、字段或属性的新定义。
示例
public class BaseClass
{
public int WorkField;
public int DoWork()
{
return 10;
}
public int WorkProperty
{
get { return 20; }
}
}
public class DerivedClass : BaseClass
{
public new int WorkField;
public new int DoWork()
{
return 100;
}
public new int WorkProperty
{
get { return 200; }
}
}
class Mainclass
{
static void Main()
{
DerivedClass ff = new DerivedClass();
System.Console.WriteLine(ff.DoWork());
System.Console.WriteLine(ff.WorkProperty);
}
}
使用 new 关键字时,调用的是新的类成员而不是已被替换的基类成员。这些基类成员称为隐藏成员。如果将派生类的实例强制转换为基类的实例,就仍然可以调用隐藏类成员。
示例
public class BaseClass
{
public int WorkField;
public int DoWork()
{
return 10;
}
public int WorkProperty
{
get { return 20; }
}
}
public class DerivedClass : BaseClass
{
public new int WorkField;
public new int DoWork()
{
return 100;
}
public new int WorkProperty
{
get { return 200; }
}
}
class Mainclass
{
static void Main()
{
DerivedClass ff = new DerivedClass();
System.Console.WriteLine(ff.DoWork());
System.Console.WriteLine(ff.WorkProperty);
//-----------------------------------------
DerivedClass B = new DerivedClass();
BaseClass A = (BaseClass)B;
System.Console.WriteLine(A.DoWork());
System.Console.WriteLine(A.WorkProperty);
}
}
★ 重写虚拟的基成员
为了使派生类的实例完全接替来自基类的类成员,基类必须将该成员声明为虚拟的。这是通过在该成员的返回类型之前添加 virtual 关键字来实现的。然后,派生类可以选择使用 override 关键字而不是 new,将基类实现替换为它自己的实现。
字段不能是虚拟的,只有方法、属性、事件和索引器才可以是虚拟的。当派生类重写某个虚拟成员时,即使该派生类的实例被当作基类的实例访问,也会调用该成员。
public class BaseClass
{
public virtual int DoWork()
{
return 10;
}
public virtual int WorkProperty
{
get { return 20; }
}
}
public class DerivedClass : BaseClass
{
public override int DoWork()
{
return 100;
}
public override int WorkProperty
{
get { return 200; }
}
}
class Mainclass
{
static void Main()
{
DerivedClass ff = new DerivedClass();
System.Console.WriteLine(ff.DoWork());
System.Console.WriteLine(ff.WorkProperty);
//-----------------------------------------
DerivedClass B = new DerivedClass();
BaseClass A = (BaseClass)B;
System.Console.WriteLine(A.DoWork());
System.Console.WriteLine(A.WorkProperty);
}
}
使用虚拟方法和属性可以预先计划未来的扩展。由于在调用虚拟成员时不考虑调用方正在使用的类型,所以派生类可以选择完全更改基类的外观行为。
★ 停止虚拟继承
通常虚拟成员会不断的继承下去,无论在派生类和最初声明虚拟成员的类之间已声明了多少个类,虚拟成员都将永远为虚拟成员。如果类 A 声明了一个虚拟成员,类 B 从 A 派生,类 C 从类 B 派生,则类 C 继承该虚拟成员,并且可以选择重写它,而不管类 B 是否为该成员声明了重写。若要停止这种虚拟继承,可以将重写声明为密封的来实现。这需要在类成员声明中将 sealed 关键字放在 override 关键字的前面。例如:
public class A
{
public virtual int DoWork() { }
}
public class B : A
{
public override int DoWork() { }
}
public class C : B
{
public sealed override int DoWork() { }
}
在上面的示例中,方法 DoWork 对从 C 派生的任何类都不再是虚拟的。它对 C 的实例仍然是虚拟的 -- 即使将这些实例强制转换为类型 B 或类型 A。派生类可以通过使用 new 关键字替换密封的方法,如下面的示例所示:
public class D : C
{
public new int DoWork() { }
}
在此情况下,如果在 D 中使用类型为 D 的变量调用 DoWork,被调用的将是新的 DoWork。如果使用类型为 C、B 或 A 的变量访问 D 的实例,对 DoWork 的调用将遵循虚拟继承的规则,即把这些调用传送到类 C 的 DoWork 实现。
★ base访问基类成员
已替换或重写某个方法或属性的派生类仍然可以使用关键字base访问基类的该方法或属性。
示例
public class A
{
public virtual int DoWork() { return 10; }
}
public class B : A
{
public override int DoWork() { return 100; }
}
public class C : B
{
public override int DoWork()
{
return base.DoWork();
}
}
class Mainclass
{
static void Main()
{
C myc = new C();
System.Console.WriteLine(myc.DoWork());
}
}
建议虚拟成员在它们自己的实现中使用 base 来调用该成员的基类实现。允许基类行为发生使得派生类能够集中精力实现特定于派生类的行为。未调用基类实现时,由派生类负责使它们的行为与基类的行为兼容。
小结:
本节中学习了在派生类中如何更改基类的数据和行为,当基类是一个普通类时,我们可以在派生类中使用new关键字,将基类成员隐藏起来,然后使用新的派生成员代替基成员,还有一种办法就是将类定义成虚拟类,其中的成员也定义成虚的,这是通过关键字virtual实现的,此时在派生类中可以使用new关键字和override关键字,使用override关键字修饰的成员会代替基类的相应成员,要注意的是override、virtual 和 new 关键字还可以用于属性、索引器和事件中,但不能应用于字段。
谈到虚拟成员其实我们在上一章中已经接触过了,不过那个虚成员是定义在抽象类中的并且没有实现,这里的虚成员是定义在虚拟类中的并且实现是存在的。到底虚成员有什么用处呢,除了分散在课文中所谈到用处外,它还具有许多方面的意义,例如,在基类中引入与派生类中的某个成员具有相同名称的新成员在 C# 中是完全支持的,不会导致意外行为,还可以使不同库中的基类与派生类之间的版本控制不断向前发展,同时保持向后的兼容性。
==========================
四 重写 ToString
==========================
C# 中的每个对象都继承 ToString 方法,此方法返回该对象的字符串表示形式。例如,所有 int 类型的变量都有一个 ToString 方法,从而允许将变量的内容作为字符串返回:
class Mainclass
{
static void Main()
{
int x = 42;
System.Console.WriteLine(x.ToString());
}
}
通过重写 ToString 方法,您可以控制对象返回的默认字符串。这在调试或跟踪程序的执行时可能很有用:
class SampleObject
{
int number = 42;
public override string ToString()
{
return "Object: SampleObject. Value: " + number;
}
}
class Mainclass
{
static void Main()
{
SampleObject o = new SampleObject();
System.Console.WriteLine(o.ToString());
}
}
产生的输出如下:
Object: SampleObject. Value: 42