类和继承
类继承
通过继承我们可以定义一个新类,新类纳入一个已经声明的类并进行扩展。
- 可以使用已存在的类作为新类的基础。已存在类称为基类(base class),新类称为派生类(derived class)。派生类组成如下:
- 本身声明中的成员
- 基类的成员
- 声明派生类,需要在类名后加入基类规格说明
- 派生类扩展它的基类,因为它包含了基类的成员,加上它本身声明中的新增功能
- 派生类不能删除它所继承的任何成员
例:OtherClass类,继承自SomeClass
class OtherClass:SomeCLass { ↑ ↑ ... 冒号 基类 }
访问继承的成员
继承的成员可以被访问,就像它们是派生类自己声明的一样。
例:下面代码声明了类SomeClass和OtherClass
- Main创建派生类OtherClass的一个对象
- Main中接下来的两行调用基类的Method1,先是使用基类的Field1,然后是派生类的Field2
- Main中后续两行调用派生类中的Method2,再次先使用基类Field1,然后是派生类的Field2
class SomeClass { public string Field1="base class field "; public void Method1(string value) { Console.WriteLine("Base class -- Method1: {0}",value); } } class OtherClass:SomeClass { public string Field2="derived class field"; public void Method2(string value) { Console.WriteLine("Derived class -- Method2: {0}",value); } } class Program { static void Main() { var oc=new OtherClass(); oc.Method1(oc.Field1); //以基类字段为参数的基类方法 oc.Method1(oc.Field2); //以派生字段为参数的基类方法 oc.Method2(oc.Field1); //以基类字段为参数的派生方法 oc.Method2(oc.Field2); //以派生字段为参数的派生方法 } }
所有类都派生自object类
没有基类规格说明的类隐式直接派生自类object。
关于类继承的其他重要内容如下
- 一个类声明的基类规格说明中只能有一个单独的类。即单继承
- 虽然类只能直接继承一个基类,但继承的层次没有限制。
基类和派生类是相对的术语。所有类都是派生类,要么派生自object,要么派生自其他类。
屏蔽基类的成员
虽然派生类不能删除它继承的任何成员,但可以用与基类同名的成员来屏蔽(mask)基类成员。这是继承的主要功能之一,非常实用。
- 要屏蔽一个继承的数据成员,需要声明一个新的同类型成员,并使用相同名称
- 通过在派生类中声明新的带有相同签名的函数成员,可以隐藏或屏蔽继承的函数成员。(请记住,签名由名称和参数列表组成,不包括返回类型)
- 要让编译器知道你在故意屏蔽继承的成员,使用new修饰符。否则,程序可以成功编译,但编译器会警告你隐藏了一个继承的成员
- 也可屏蔽静态成员
class SomeClass { public string Field1; ... } class OtherClass:SomeClass { new public string Field1; ... }
例:OtherClass派生自SomeClass,但隐藏了两个继承的成员。
class SomeClass { public string Field1="SomeClass Field1 "; public void Method1(string value) { Console.WriteLine("SomeClass.Method1: {0}",value); } } class OtherClass:SomeClass { new public string Field1="OtherClass Field1";//屏蔽基类成员 new public void Method1(string value)//屏蔽基类成员 { Console.WriteLine("OtherCLass.Method1: {0}",value); } } class Program { static void Main() { var oc=new OtherClass(); oc.Method1(oc.Field1); } }
基类访问
如果派生类必须完全的访问被隐藏的继承成员,可以使用基类访问(base access)
Console.WriteLie("{0}",base.Field1);
例:派生类OtherClass隐藏了基类的Field1,但可以使用基类访问表达式访问它
class SomeClass { public string Field1="Field1 -- In the base class"; } class OtherClass:SomeClass { new public string Field1="Field1 -- In the derived class"; public void PrintField1() { Console.WriteLine(Field1); Console.WriteLine(base.Field1); } } class Program { static void Main() { var oc=new OtherClass(); oc.PrintField1(); } }
如果你的程序代码经常使用base进行基类访问,你可能需要重新评估类的设计。一般来说能有更优雅的设计,但在没有其他方法的时候也可使用这个特性。
使用基类的引用
如果有一个派生类对象的引用,就可以获取该对象基类部分的引用。
例:使用基类引用
- 第一行声明并初始化了变量derived,它包含一个MyDerivedClass类型对象的引用
- 第二行声明了一个基类类型MyBaseClass的变量,并把derived中的引用转换为该类型,给出对象的基类部分的引用
- 基类部分的引用被存储在变量mybc中,在赋值运算符的左边
- 其他部分的引用不能“看到”派生类对象的其余部分,因为它通过基类类型的引用“看”这个对象
MyDerivedClass derived=new MyDerivedClass(); MyBaseClass mybc=(MyBaseClass)derived;
例:两个类的声明和使用
class MyBaseClass { public void Print() { Console.WriteLine("This is the base class."); } } class MyDerivedClass:MyBaseClass { new public void Print() { Console.WriteLine("This is the derived class"); } } class Program { static void Main() { var derived=new MyDerivedClass(); var mybc=(MyBaseClass)derived; derived.Print(); mybc.Print(); } }
虚方法和覆写方法
虚方法可以使基类的引用访问“升至”派生类内。
可以使用基类引用调用派生类的方法,只需满足下面的条件。
- 派生类的方法和基类的方法有相同的签名和返回类型
- 基类的方法使用virtual标注
- 派生类的方法使用override标注
例:virtual、override演示
class MyBaseClass { virtual public void Print() ... } class MyDerivedClass:MyBaseCLass { override public void Print() ... }
下图阐明了这组virtual和override方法。注意和上一种情况(用new隐藏基类成员)相比在行为上的区别
- 当使用基类引用(mybc)调用Print方法时,方法调用被传递到派生类执行,因为:
- 基类的方法被标记为virtual
- 在派生类中有匹配的override方法
- 下图阐明了这一点,显示了一个从virtual Print方法后面开始,并指向override Print方法的箭头
下面的代码和上一节相同,但由于使用了virtual和override,产生的结果大不相同。
class MyBaseClass { virtual public void Print() { Console.WriteLine("This is the base class."); } } class MyDerivedClass:MyBaseClass { override public void Print() { Console.WriteLine("This is the derived class"); } } class Program { static void Main() { var derived=new MyDerivedClass(); var mybc=(MyBaseClass)derived; derived.Print(); mybc.Print(); } }
其他关于virtual和override的重要信息
- 覆写和被覆写的方法必须有相同的可访问性。换句话说,当被覆写为private时,覆写方法不能是public等
- 不能覆写static方法和非虚方法
- 方法、属性和索引器,以及另一种成员类型事件(将在后面阐述),都可以被声明为virtual和override
覆写标记为override的方法
覆写方法可以在继承的任何层次出现。
- 当使用对象基类部分的引用调用一个覆写的方法时,方法的调用被沿派生层次上溯执行,一直到标记为override的方法的最高派生(most-derived)版本
- 如果在更高的派生级别有该方法的其他声明,但没有被标记为override,那么它们不会被调用
下面来看看,在MyBaseClass中Print标记为virtual,在MyDerivedClass中Pring标记为override。在SecondDerived中,使用override或new声明Print,两种情况的区别。
class MyBaseClass { virtual public void Print() { Console.WriteLine("This is the base class."); } } class MyDerivedClass:MyBaseClass { override public void Print() { Console.WriteLine("This is the derived class"); } } class SecondDerived:MyDerivedClass { ... }
情况1:使用override声明Print
如果把SecondDerived的Print方法声明为override,那么它会覆写方法的全部两个低派生级别版本。如下图所示,如果一个基类的引用被用于调用Print,它会向上传递通过整个链达到类SecondDerived中的实现。
下面的代码实现了这种情况。注意Main方法最后两行代码。
- 第一条使用最高派生类SecondDerived的引用调用Print方法。这不是通过基类部分的引用的调用,所以它将调用SecondDerived中实现的方法
- 第二条语句使用基类MyBaseClass的引用调用Print方法
class SecondDerived:MyDerivedClass { override public void Print() { Console.WriteLine("This is the second derived class"); } } class Program { static void Main() { var derived=new SecondDerived(); mybc=(MyBaseClass)derived; derived.Print(); mybc.Print(); } }
情况2:使用new声明Print
使用new时,当方法通过SecondDerived的引用调用时,结果与情况1相同。但当方法通过MyBaseClass的引用调用时,方法调用只向上传递了一级,在MyDerived那就被执行。
class SecondDerived:MyDerivedClass { new public void Print() { Console.WriteLine("This is the second derived class"); } } class Program { static void Main() { var derived=new SecondDerived(); mybc=(MyBaseClass)derived; derived.Print(); mybc.Print(); } }
覆盖其他成员类型
前面几节详述了如何在方法上使用virtual/override。其实在属性和索引器上也一样。
例:属性的virtual/override
class MyBaseClass { private int _myInt=5; virtual public int MyProperty { get{return _myInt;} } } class MyDerivedClass:MyBaseClass { private int _myInt=10; override public int MyProperty { get{return _myInt;} } } class Program { static void Main() { var derived=new MyDerivedClass(); var mybc=(MyBaseClass)derived; Console.WriteLine(derived.MyProperty); Console.WriteLine(mybc.MyProperty); } }
构造函数的执行
- 要创建对象的基类部分,需要隐式调用基类的某个构造函数作为创建实例过程的一部分
- 继承层次链中的每个类在执行它自己的构造函数体之前执行它的基类构造函数
创建一个实例过程中的第一件事是初始化对象的所有实例成员。然后调用基类的构造函数,最后执行该类自己的构造函数。
警告 在构造函数中调用虚方法是极不推荐的。在执行基类的构造函数时,基类的虚方法会调用派生类的覆写方法,但这是在执行派生类的构造函数方法体之前。因此,调用会在派生类没有完成初始化之前传递到派生类。
构造函数初始化语句
默认情况下,构造对象时,将调用基类的无参数构造函数。但构造函数可以重载,所以基类可能有一个以上的构造函数。如果希望派生类使用一个指定的基类构造函数而不是无参数构造函数,必须在构造函数初始化语句中指定它。
- 使用关键字base并指明使用哪一个基类构造函数
- 使用关键字this并指明应该使用当前类的哪一个构造函数
例:构造函数示例
- 构造函数初始化语句指明要使用有两个参数的基类构造函数。且参数类型分别为string、int
- 在基类参数列表中的参数必须在类型和顺序方面与已定的基类构造函数的参数列表相匹配
public MyDerivedClass(int x,string s):base(s,x { ... }
当声明一个不带构造函数初始化语句的构造函数时,它实际上是带有base()构造函数初始化语句的简写形式,如下图。
另外一种形式的构造函数初始化语句可以让构造过程(实际上是编译器)使用当前类中其他的构造函数。
例:使用同一个类中具有两个参数的构造函数
public MyClass(int x):this(x,"Using Default String") { ... }
这种语法也常用于另一种情况:一个类有好几个构造函数,并且它们都需要在对象构造开始时执行一些公共的代码。对于这种情况,可以把公共代码提取出来作为一个构造函数,被其他所有的构造函数作为构造函数初始化语句使用,减少代码冗余。
类访问修饰符
类的可访问性有两个级别:public和internal
- 标记为public的类可以被系统内任何程序集中的代码访问
- 标记为internal的类只能被它自己所在的程序集内的类看到
- internal是默认的可访问级别
下图阐明了internal和public类从程序集外部的可访问性。类MyClass对左边程序集内的类不可见,因为MyClass被标记为internal。然而,类OtherClass对于左边的类可见,因为它是public。
程序集间的继承
迄今为止,我们一直在同一程序集内声明基类和派生类。但C#也允许从不同程序集定义基类和派生类。
要从不同程序集定义基类、派生类,必须具备以下条件。
- 基类必须声明为public,这样才能从它的程序集外部访问
- 必须在Visual Studio工程中的References节点添加对包含该基类的程序集的引用
例:程序集间继承示例
第一段代码创建了含有MyBaseClass类的程序集,该类有以下特征
- 它声明在名称为Assembly1.cs的源文件中,并位于BaseClassNS命名空间内部
- 它声明为public,以便从其他程序集访问它
- 它含有一个单独成员,一个名为PrintMe的方法
//源文件名称为Assembly1.cs using System; namespace BaseClassNS { public class MyBaseClass { public void PrintMe() { Console.WriteLine("I am MyBaseClass"); } } }
第二段代码含有DerivedClass类的声明,该源文件名称为Assembly2.cs
- DerivedClass的类体为空,但从MyBaseClass集成了方法PrintMe
- Main创建了一个类型为DerivedClass的对象并调用它继承的PrintMe方法
//源文件名称为Assembly2.cs using System; using BaseClassNS; ↑ 包含基类声明的命名空间 namespace UsesBaseClass { class DerivedClass:MyBaseClass {} class Program { static void Main() { var mdc=new DerivedClass(); mdc.PrintMe(); } } }
成员访问修饰符
对类的可访问性,只有两种修饰符:internal、public。本节阐述成员的可访问性。
类的可访问性描述了类的可见性;成员的可访问性描述了类成员的可见性。
在讲解成员访问性的细节前,先阐述一些通用内容.
- 所有显式声明在类声明中的成员都是相互可见的,无论它们的访问性如何
- 继承的成员不在类的声明中显式声明,所以,如你所见,继承的成员对派生类的成员可以是可见的,也可以是不可见的
- 有以下5个成员访问级别
- public
- private
- protected
- internal
- protected internal
- 必须对每个成员指定访问级别。如果不指定,它默认的隐式访问级别为private
- 成员不能比它的类有更高的可访问性。
访问成员的区域
public class MyClass { public int Member1; private int Member2; protected int Member3; internal int Member4; protected internal int Member5; ... }
另一个类(如类B)能否访问MyClass类中的成员取决于两个特征
- 类B是否派生自MyClass类
- 类B是否和MyClass在同一程序集
上面两个特征划分出4个集合。
- 在同一程序集且继承MyClass
- 在同一程序集但不继承MyClass
- 在不同程序集且继承MyClass
- 在不同程序集但不继承MyClass
这些特征用于定义5种访问级别,下面详细介绍。
public的可访问性
public访问级别限制性最少。所有类,包括程序集内部的类和外部的类都可以自由地访问成员。
private的可访问性
私有成员访问级别限制最严格
- private类成员只能被它自己的类成员访问。它不能被其他的类访问,包括继承它的类
- 然而,private成员能被嵌套在它的类中的类成员访问(嵌套类在第25章阐述)
protected的可访问性
protected访问级别如同private访问级别,除了一点,它允许派生自该类的类访问该成员。(注意,即使程序集外部继承该类的类也能访问该成员)
internal的可访问性
internal成员对程序集内部的所有类可见,但对程序集外部的类不可见。
protected internal的可访问性
protected internal的成员对所有继承该类的类以及所有程序集内部的类可见。
注意,这是protected和internal的并集。
成员访问修饰符小结
抽象成员
抽象成员指设计为被覆写的函数成员。抽象成员有以下特征
- 必须是一个函数成员,即字段和常量不能是抽象成员
- 必须用abstract修饰
- 不能有实现代码块。代码用分号表示
- 共有4个类型的成员可以声明为抽象:方法、属性、事件、索引
例:抽象方法和抽象属性
abstract public void PrintStuff(string s); abstract public int MyProperty { get; set; }
关于抽象成员的注意事项:
- 尽管抽象成员必须在派生类中被覆写,但不能把virtual和abstract合用
- 类似虚成员,派生类中抽象成员的实现必须指定override修饰符
抽象类
抽象类指设计为被继承的类。抽象类只能被用作其他类的基类。
- 不能创建抽象类的实例
- 抽象类使用abstract标识
- 抽象类可以包含抽象成员和普通的非抽象成员。
- 抽象类可以派生自另一个抽象类。
- 任何派生自抽象类的类必须使用override关键字实现该类所有的抽象成员,除非派生类自己也是抽象类
抽象类和抽象方法示例
abstract class AbClass { public void IdentifyBase() { Console.WriteLine("I am AbClass"); } abstract public void IndetifyDerived(); } class DerivedClass:AbClass { override public void IdentifyDerived() { Console.WriteLine("I am DerivedClass"; } } class Program { static void Main() { // AbClass a=new AbClass();//错误,抽象类不能实例化 var b=new DerivedClass(); b.IdentifyBase(); b.IdentifyDerived(); } }
抽象类的另一个例子
例:包含数据成员和函类型成员的抽象类
abstract class MyBase { public int SideLength=10; const int TriangleSideCount=3; abstract public void PrintStuff(string s); abstract public int MyInt{get;set;} public int PerimeterLength() { return TriangleSideCount*SideLength; } } class MyCLass:MyBase { public override void PrintStuff(string s) { Console.WriteLine(s); } private int _myInt; public override int MyInt { get{return _myINt;} set{_myInt=value;} } } class Program { static void Main() { var mc=new MyClass(); mc.PrintStuff("This is a string."); mc.MyInt=28; Console.WriteLine(mc.MyInt); Console.WriteLine("Perimeter Length:{0}",mc.PerimeterLength()); } }
密封类
抽象类必须用作基类,它不能被实例化。
密封类与抽象类相反。
- 密封类只能被用作独立的类,不能用作基类(被继承)
- 密封类使用sealed修饰符标注
静态类
静态类中所有成员都是静态的。静态类用于存放不受实例数据影响的数据和函数。静态类常见用途就是创建一个包含一组数学方法和值的数学库。
- 静态类本身必须标记为static
- 类的所有成员必须是静态的
- 类可以用一个静态构造函数,但不能有实例构造函数,不能创建该类的实例
- 静态类是隐式密封的,即不能继承静态类
可以使用类名和成员名,像访问其他静态成员那样访问它的成员。
例:静态类示例
static public class MyMath { public static float PI=3.14f; public static bool IsOdd(intx) { return x%2==1; } public static int Times2(int x) { return 2*x; } } class Program { static void Main() { int val=3; Console.WriteLine("{0} is odd is {1}",val,MyMath.IsOdd(val)); Console.WriteLine("{0} * 2 = {1}",val,MyMath.Time2(val)); } }
扩展方法
扩展方法允许编写的方法和声明它的类之外的类关联。
扩展方法的重要要求如下:
- 声明扩展方法的类必须声明为static
- 扩展方法本身必须声明为static
- 扩展方法必须包含关键字this作为它的第一个参数类型,并在后面跟着它所扩展的类的名称
例:扩展方法示例
namespace ExtensionMethods { sealed class MyData { private double D1,D2,D3; public MyData(double d1,double d2,double d3) { D1=d1;D2=d2;D3=d3; } public double Sum() { return D1+D2+D3; } } static class ExtendMyData { public static double Average(this MyData md) { return md.Sum()/3; } } class Program { static void Main() { var md=new MyData(3,4,5); Console.WriteLine("Sum: {0}",md.Sum()); Console.WriteLine("Average: {0}",md.Average()); } } }