目录
一、类继承
通过继承可以定义一个新类,新类纳入一个已经生命的类并进行扩展。
- 可以使用一个已经存在的类作为新类的基础,已存在的类称为基类,新类称为派生类。派生类成员的组成如下:
- 本身声明中的成员;
- 基类的成员;
- 要声明一个派生类,需要在类名后加入基类规格说明。基类规格说明由冒号和用作基类的类名称组成。派生类直接继承自列出的基类;
- 派生类扩展它的基类,因为它包含了基类的成员,还有它本身声明中的新增功能;
- 派生类不能删除它所继承的任何成员;
class OtherClass : SomeClass //冒号+基类名(SomeClass)=基类规格说明
{
……
}
二、访问继承的成员
继承的成员可以被访问,就像它们是派生类自己声明的一样。
class SomeClass //基类
{
public string Field1 = "base class field";
public void Method1(string value)
{
Console.WriteLine($"Base class -- Method1: {value}");
}
}
class OtherClass : SomeClass //派生类
{
public string Field2 = "derived class field";
public void Method2(string value)
{
Console.WriteLine($"Derived class -- Method2: {value}");
}
}
class Program
{
static void Main(string[] args)
{
OtherClass oc = new OtherClass();
oc.Method1(oc.Field1); //以基类字段为参数的基类方法
oc.Method1(oc.Field2); //以派生字段为参数的基类方法
oc.Method2(oc.Field1); //以积累字段为参数的派生方法
oc.Method2(oc.Field2); //以派生字段为参数的派生方法
}
}
执行结果:
三、所有类都派生自object类
除了特殊的类object,所有的类都是派生类,即使它们没有基类规格说明。类object是唯一的非派生类,因为它是继承层次结构的基础。
没有基类规格说明的类隐式地直接派生自类object,不加基类规格说明只是指定object为基类的简写。这两种形式是语义等价的。
关于类继承的其他重要内容如下:
- 一个类声明的基类规格说明中只能有一个单独的类,这称为单继承;
- 虽然类只能直接继承一个基类,但派生的层次没有限制。也即是说,作为基类的类可以派生自另外一个类,而这个类又派生自另外一个类……,直至最终到达object;
基类和派生类是相对的术语,所有的类都是派生类,要么派生自object,要么派生自其他的类。所以,通常称一个类为派生类时,我们的意思时它直接派生自某类而不是object。
四、屏蔽基类的成员
虽然派生类不能删除它继承的任何成员,但可以用与基类成员名称相同的成员来屏蔽基类成员。这是继承的主要功能之一,非常实用。
例如,我们要继承包含某个特殊方法的基类,该特殊方法虽然适合声明它的类,但不一定适合派生类。在这种情况下,我们希望在派生类中声明新成员以屏蔽基类中的方法。在派生类中屏蔽基类成员的一些要点如下:
- 要屏蔽一个继承的数据成员,需要声明一个新的相同类型的成员,并使用相同的名称;
- 通过在派生类中声明新的带有相同签名的函数成员,可以屏蔽继承的函数成员。记住:签名包括方法名、参数个数、参数类型和顺序、参数修饰符,不包括返回类型;
- 要让编译器知道你在故意屏蔽继承的成员,可使用new修饰符。否则,程序可以成功编译,但编译器会警告你隐藏了一个继承的成员;
- 也可以屏蔽静态成员;
class SomeClass //基类
{
public string Field1;
}
class OtherClass : SomeClass //派生类
{
new public string Field1; //用同样的名称,使用new修饰符,显式屏蔽基类成员
}
class SomeClass //基类
{
public string Field1 = "SomeClass Field1";
public void Method1(string value)
{
Console.WriteLine($"SomeClass.Method1: {value}");
}
}
class OtherClass : SomeClass //派生类
{
new public string Field1 = "OtherClass Field1"; //屏蔽基类成员
new public void Method1(string value) //屏蔽基类成员
{
Console.WriteLine($"OtherClass.Method1: {value}");
}
}
class _4_ShieldBaseMember
{
static void Main()
{
OtherClass oc = new OtherClass(); //使用屏蔽成员
oc.Method1(oc.Field1); //使用屏蔽成员
}
}
执行结果:
五、基类访问
如果派生类必须访问被隐藏的继承成员,可以使用基类访问表达式,基类访问表达式由关键字base后面跟着一个点和成员的名称组成,如下所示:
Console.WriteLine("{0}", base.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 _5_BaseClassAccess
{
static void Main()
{
OtherClass oc = new OtherClass();
oc.PrintField1();
}
}
执行结果:
如果你的程序代码经常使用这个特性(即访问隐藏的继承成员),你可能需要重新评估类的设计。一般来说存在更优雅的设计,但是在没其它办法的时候也可以使用这个特性。
六、使用基类的引用
派生类的实例由基类的实例和派生类新增的成员组成,派生类的引用指向整个类对象,包括基类部分。
如果有一个派生类对象的引用,就可以获取该对象基类部分的引用(使用类型转换运算符把该引用转换为基类类型)。类型转换运算符放置在对象引用的前面,由圆括号括起的要被转换成的类名组成。
将派生类对象强制转换为基类对象的作用是产生的变量只能访问基类的成员。
MyDerivedClass derived = new MyDerivedClass(); //创建一个对象
MyBaseClass mybc = (MyBaseClass)derived; //转换引用
关于上述两行代码:
- 第一行代码声明并初始化了变量derived,它包含一个MyDerivedClass类型对象的引用;
- 第二行代码声明了一个基类类型MyBaseClass的变量,并把derived中的引用转换为该类型,给出对象的基类部分的引用:
- 基类部分的引用被存储在变量mybc中,在赋值运算符的左边;
- 基类部分的引用“看不到”派生类对象的其余部分,因为它通过基类类型的引用“看”这个对象;
class MyBaseClass
{
public void Print()
{
Console.WriteLine("This is the base class");
}
}
class MyDerivedClass : MyBaseClass
{
public int var1;
new public void Print()
{
Console.WriteLine("This is the derived class");
}
}
class _6_UseRefOfBaseClass
{
static void Main()
{
MyDerivedClass derived = new MyDerivedClass();
MyBaseClass mybc = (MyBaseClass)derived;
derived.Print(); //从派生类部分调用Print
mybc.Print(); //从基类部分调用Print
//mybc.var1 = 5; //错误:基类引用无法访问派生类成员
}
}
执行结果:
1、虚方法和覆写方法
上一节可知,当使用基类引用访问派生类对象时,得到的是基类的成员。虚方法可以使基类的引用访问“升至”派生类内。
可以使用基类引用调用派生类的方法,只需满足下面的条件:
- 派生类的发方法和基类的方法有相同的签名和返回类型;
- 基类的方法使用virtual标注;
- 派生类的方法使用override标注;
class MyBaseClass //基类
{
virtual public void Print()
……
}
class MyDerivedClass : MyBaseClass
{
override public void Print()
……
}
注意与上一种情况(用new隐藏基类成员)相比在行为上的区别:
- 当使用基类引用(mybc)调用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 _6_1VirtualAndOverride
{
static void Main()
{
MyDerivedClass derived = new MyDerivedClass();
MyBaseClass mydc = (MyBaseClass)derived;
derived.Print();
mydc.Print();
}
}
执行结果:
其他关于virtual和override修饰符的重要信息如下:
- 覆写和被覆写的方法必须有相同的可访问性。例如,这种情况下是不可以的:被覆写的方法是private的,而覆写方法是public的;
- 不能覆写static方法或非虚方法;
- 方法、属性和索引器以及另一种成员类型——事件,都可以被声明为virtual和override;
2、覆写标记为override的方法
覆写方法可以在继承的任何层次出现。
- 当使用对象基类部分的引用调用一个被覆写的方法时,方法的调用被延派生层次上溯到执行,一直到标记为override的方法的最高派生版本;
- 如果在更高的派生级别有该方法的其他声明,但没有标记为override,那么它们不会被调用;
class MyBaseClass //基类
{
virtual public void Print()
{
Console.WiriteLine("This is the base class.");
}
}
class MyDerivedClass : MyBaseClass //派生类
{
override public void Print()
{
Console.WriteLine("This is the derived class.");
}
}
class SecondDerived : MyDerivedClass //最高派生类
{
……//在后面给出
}
上述代码展示了3个类,它们形成了一个继承层次:MyBaseClass、MyDerivedClass和SecondDerived。所有这3个类都包含名为Print的方法,并带有相同的签名。在MyBaseClass中Print被标记为virtual。在MyDerivedClass中,它被标记为override。在类SecondDerived中,可以使用override或new声明方法Print。让我们看一看在每种情况下将发生什么。
(1)情况1:使用override声明Print
若把SecondDerived的Print方法声明为override,那么它会覆盖方法的两个低派生级别的版本。
如上图,若一个基类的引用被用于调用Print,它会向上传递,一直到类SecondDerived中的实现。
class MyBaseClass
{
virtual public void Print()
{
Console.WriteLine("This is the base class");
}
}
class MyDerivedClass : MyBaseClass
{
public override void Print()
{
Console.WriteLine("This is the derived class");
}
}
class MySecondDerivedClass : MyBaseClass
{
public override void Print()
{
Console.WriteLine("This is the second derived class");
}
}
class _6_2OverrideStatementHigtestDerived
{
static void Main()
{
MySecondDerivedClass derived = new MySecondDerivedClass();
MyBaseClass mybc = (MyBaseClass)derived;
derived.Print();
mybc.Print();
}
}
执行结果:
关于上述代码:
- 两条语句中的第一条使用最高派生类MySecondDerivedClass的引用调用Print方法,这不是通过基类部分的引用的调用,所以它将会调用MySecondDerivedClass中实现的方法;
- 而第二条语句使用基类MyBaseClass的引用调用Print方法;
结论:
无论Print是通过派生类调用还是通过基类调用的,都会调用最高派生类中的方法。当通过基类调用时,调用沿着继承层次向上传递。
(2)情况2:使用new声明Print
相反,如果将MySecondDerivedClass中的Print方法声明为new:
class MyBaseClass
{
virtual public void Print()
{
Console.WriteLine("This is the base class");
}
}
class MyDerivedClass : MyBaseClass
{
public override void Print()
{
Console.WriteLine("This is the derived class");
}
}
class MySecondDerivedClass : MyBaseClass
{
public new void Print()
{
Console.WriteLine("This is the second derived class");
}
}
class _6_3NewStatementHighestDerived
{
static void Main()
{
MySecondDerivedClass derived = new MySecondDerivedClass();
MyBaseClass mybc = (MyBaseClass)derived;
derived.Print();
mybc.Print();
}
}
执行结果:
结论:
当通过MySecondDerivedClass的引用调用方法Print时,MySecondDerivedClass中的方法被执行。然而,当通过MyBaseClass的引用调用Print方法时,方法调用纸箱上传递了一级,到达类MyDerivedClass,在那里它被执行。两种情况的唯一不同是MySecondDerived中的方法使用修饰符override还是修饰符new声明。
3、覆盖其他成员类型
在属性、事件和索引器上virtual/override的用法也是一样的。
class MyBaseClass
{
private int myInt = 5;
virtual public int MyProperty
{
get { return myInt; }
}
}
class MyDerivedClass : MyBaseClass
{
private int myInt = 10;
public override int MyProperty
{
get { return myInt; }
}
}
class _6_4OverrideProp
{
static void Main()
{
MyDerivedClass derived = new MyDerivedClass();
MyBaseClass mybc = (MyBaseClass)derived;
Console.WriteLine($"derived.MyProperty = {derived.MyProperty}");
Console.WriteLine($"mybc.MyProperty = {mybc.MyProperty}");
}
}
执行结果:
七、构造函数的执行
派生类对象有一部分就是基类对象:
- 要创建对象的基类部分,需要隐式调用基类的某个构造函数;
- 继承层次链中的每个类在执行它自己的构造函数体之前执行它的基类构造函数;
class MyDerivedClass : MyBaseClass
{
MyDerivedClass() //构造函数调用基类构造函数MyBaseClass()
{
……
}
}
上述代码展示了类MyDerivedClass及其构造函数声明,当调用该构造函数时,它在执行自己的方法体之前会先调用无参数的构造函数MyBaseClass()。
构造的顺序如上图,创建一个实例的过程中,完成的第一件事是初始化对象的所有实例成员,在此之后,调用基类的构造函数,然后才执行该类自己的构造函数体。
警告:
强烈反对在构造函数中调用虚方法,在执行基类的构造函数时,基类的虚方法会调用派生类的覆写方法,但这是在执行派生类的构造函数方法体之前,因此调用会在派生类完全初始化之前传递到派生类。
1、构造函数初始化语句
默认情况下,在构造对象时,将调用基类的无参构造函数。但构造函数可以重载,所以基类可能有一个以上的构造函数。如果希望派生类使用一个指定的基类构造函数而不是无参数构造函数,必须在构造函数初始化语句中指定它。
有两种形式的构造函数初始化语句:
- 第一种形式使用关键字base并指明使用哪一个基类构造函数;
- 第二种形式使用关键字this并指明应该使用当前类的哪一个构造函数;
形式一:
基类构造函数初始化语句放在冒号后面,跟在类的构造函数声明的参数列表后面。构造函数初始化语句由关键字base和要调用的基类构造函数的参数列表组成。
public MyDerivedClass(int x,string s) : base(s, x) //构造函数初始化语句
{
……
}
- 构造函数初始化语句指明要使用有两个参数的基类构造函数,并且第一个参数是一个int,第二个参数是一个string;
- 基类参数列表中的参数必须在类型和顺序方面与已定的基类构造函数的参数列表相匹配;
当声明一个不带构造函数初始化语句的构造函数时,它实际上是带有base()构造函数初始化语句的简写形式。
上图所示,左右两种形式是语义等价的。
形式二:
另外一种形式的构造函数初始化语句可以让构造过程(实际上是编译器)使用当前类中其他的构造函数。
public MyClass(int x) : this(x, "Using Default String") //构造函数的初始化语句
{
……
}
上述代码所示的MyClass类包含带有一个参数的构造函数,但这个单参数的构造函数使用了同一个类中具有两个参数的构造函数,为第二个参数提供了一个默认值。
这种语法很有用的另一种情况是,一个类有好几个构造函数,并且它们都需要在对象构造的过程开始时执行一些公共的代码。对于这种情况,可以把公共代码提取出来作为一个构造函数,被其他所有的构造函数用作构造函数初始化语句。由于减少了重复的代码,实际上也是推荐的做法。
你可能会觉得还可以声明另外一个方法来执行这些公共的初始化,并让所有构造函数来调用这个方法。由于种种原因这不是一个好办法。
首先,编译器在知道方法是构造函数后能够做一些优化。其次,有些事情必须在构造函数中进行,在其他地方则不行。比如之前我们学到的readonly字段只可以在构造函数中初始化。如果尝试在其它方法(即使这个方法只被构造函数调用)中初始化一个readonly字段,会得到编译错误。不过要注意,这一限制仅适用于readonly字段,不适用于readonly属性。
回到公共构造函数,如果这个构造函数可以用作一个有效的构造函数,能够初始化类中所有需要初始化的东西,那么完全可以把它设置为public的构造函数。
但是如果它不能完全初始化一个对象怎么办?此时,必须禁止从类的外部调用构造函数,因为那样的话它只会初始化对象的一部分。要避免这个问题,可以把构造函数声明为private,而不是public,然后只让其他构造函数使用它。
class MyClass
{
readonly int firstVar;
readonly double secondVar;
public string UserName;
public int UserIdNumber;
private MyClass() //私有构造函数执行其他构造函数公用的初始化
{
firstVar = 20;
secondVar = 30.5;
}
public MyClass(string firstName) : this() //使用构造函数初始化语句
{
UserName = firstName;
UserIdNumber = -1;
}
public MyClass(int idNumber) : this() //使用构造函数初始化语句
{
UserName = "Anonymous";
UserIdNumber = idNumber;
}
}
2、类访问修饰符
类可以被系统中其他类看到并访问,可访问有时也称为可见,它们可以互换使用。
类的可访问性有两个级别:public和internal:
- 标记为public的类可以被系统内任何程序集中的代码访问,要使一个类对其他程序集可见,使用public访问修饰符;
- 标记为internal的类只能被它自己所在的程序集内的类看到(程序集既不是程序也不是DLL)
- 这是默认的可访问级别,所以,除非在类的声明中显式地指定修饰符public,否则程序集外部地代码不能访问该类;
- 可以使用internal访问修饰符显式地声明一个类为内部的;
八、程序集间的继承
C#允许从一个在不同的程序集内定义的基类来派生。
要从不同程序集中定义的基类派生类,必须具备以下条件:
- 基类必须被声明为public,这样才能从它所在的程序集外部访问它;
- 必须在Visual Studio工程中的References节点中添加对包含该基类的程序集的引用,可以在Solution Explorer中找到该标题;
要使引用其他程序集中的类和类型更容易,不使用它们的完全限定名称,可以在源文件的顶部放置一个using指令,并带上将要访问的 类或类型所在的命名空间。
说明:
增加对其他程序集的引用和增加using指令是两回事,增加对其他程序集的引用是告诉编译器所需的类型在哪定义,增加using指令 允许你引用其他的类而不必使用它们的完全限定名称。
using System;
namespace BaseClassNS //包含基类声明的命名空间
{
public class MyBaseClass //把该类声明为共有的,使其对外部可见
{
public void PrintMe()
{
Console.WriteLine("I am MyBaseClass");
}
}
}
上述代码创建了含有MyBaseClass类声明的程序集,该类有以下特征:
- 它声明在名为Assembly1.cs的源文件中,并位于BaseClassNS的命名空间内部;
- 它声明为public,这样就可以从其他程序集中访问它;
- 它含有一个单独的成员,即一个名为PrintMe的方法,它仅 打印一条简单的消息标识该类。
using BaseClassNS;
namespace UseBaseClass //包含基类声明的命名空间
{
class DrivedClass : MyBaseClass //在其他程序集中的基类
{
//空类体
}
class Program
{
static void Main()
{
DrivedClass mdc = new DrivedClass();
mdc.PrintMe();
}
}
}
上述代码在另一个程序集中,创建了包含DrivedClass类的声明,它继承在第一个程序集中声明的MyBaseClass,该源文件名为Assembly2.cs:
- DrivedClass的类体为空,但从MyBaseClass继承了方法PrintMe;
- Main创建了一个类型为DriveClass的对象并调用它继承的PrintMe方法;
执行结果:
九、成员访问修饰符
类的可访问性描述了类的可见性;成员的可访问性描述了类成员的可见性。
声明在类中的每个成员对系统的不同部分可见,这依赖于类声明中指派给他的访问修饰符。你已经看到private成员仅对同一类的其他成员可见,而public成员对程序集外部的类也可见。
在研究成员访问行的细节之前,首先阐述一些通用内容:
- 所有显式声明在类声明中的成员都是互相可见的,无论它们的访问性如何;
- 继承的成员不在类的声明中显式声明,所以,如你所见,继承的成员对派生类的成员可以是可见的,也可以是不可见的;
- 以下是五个成员访问级别的名称:
- public
- private
- protected
- internal
- protected internal
- 必须对每个成员指定成员访问级别,如果不指定某个陈观的访问级别,它的隐式访问级别为private;
- 成员的可访问性不能比它的类高,即,如果一个类的可访问性限于他所在的程序集,那么类的成员在程序集的外部也不可见,无论它们的访问修饰符是什么,public也不例外;
1、访问成员的区域
类通过成员的访问修饰符指明了哪些成员可以被其他类访问。下面的类中声明了5种访问级别的成员:
public class MyClass
{
public int Member1;
private int Member2;
protected int Member3;
internal int Member4;
protected internal int Member5;
……
}
另一个类(如类B)能否访问这些成员取决于该类的两个特征:
- 类B是否派生自MyClass类;
- 类B是否和MyClass类在同一集合;
上图所示,两个特征划分出4个集合,与MyClass类相比,其他类可以是下面任意一种:
- 在同一程序集且继承MyClass(右下);
- 在同一程序集但不继承MyClass(左下);
- 在不同的程序集且继承MyClass(右上);
- 在不同的程序集且不继承MyClass(左上);
这些特征用于定义五种访问级别。
2、公有成员的可访问性
public访问级别是限制最少的,所有的类,包括程序集内部的类和外部的类都可以自由地访问成员。
3、私有成员的可访问性
private访问级别是限制最严格的。
- private类成员只能被它自己的类的成员访问,它不能被其他的类访问,包括继承它的类;
- 然而,private成员能被嵌套在它的类中的类成员访问;
4、受保护成员的可访问性
protected访问级别如同private访问级别,但它允许派生自该类的类访问该成员。
注意:
即使程序集外部继承该类的类也能访问该成员。
5、内部成员的可访问性
标记为internal的成员对程序集内部的所有类可见,但对程序集外部的类不可见。
6、受保护内部成员的可访问性
标记为protected internal的成员对所有继承该类的类以及程序集内部的所有类可见。
注意:
允许访问的集合是protected修饰符允许访问的类的集合加上internal修饰符允许访问的类的集合,这是protected和internal的并集,不是交集。
7、成员访问修饰符小结
修饰符 | 含义 |
---|---|
private | 只在类的内部可访问 |
internal | 对该程序集内所有类可访问 |
protected | 对所有继承该类的类可访问 |
protected internal | 对所有继承该类或在该程序集内声明的类可访问 |
public | 对任何类可访问 |
上图阐述了5个成员访问修饰符的可访问级别。
同一程序集内的类 | 不同程序集内的类 | |||
---|---|---|---|---|
非派生 | 派生 | 非派生 | 派生 | |
private | × | × | × | × |
internal | √ | √ | × | × |
protected | × | √ | × | √ |
protected internal | √ | √ | × | √ |
public | √ | √ | √ | √ |
十、抽象成员
抽象成员是指设计为被覆写的函数成员,抽象成员有以下特征:
- 必须是一个 函数成员,也就是说,字段和常量不能作为抽象成员;
- 必须用abstract修饰符标记;
- 不能有实现代码块,抽象成员的代码用分号表示;
abstract public void PrintStuff(string s); //分号替换实现
abstract public int MyProperty
{
get; //分号替换实现
set; //分号替换实现
}
抽象成员可以只在抽象类中声明,总共有4种类型的成员可以声明为抽象的:
- 方法;
- 属性;
- 事件;
- 索引器;
关于抽象成员的其他重要事项:
- 尽管抽象成员必须在派生类中用相应的成员覆写,但不能把virtual修饰符附加到abstract修饰符;
- 类似于虚成员,派生类中抽象成员的实现必须指定override修饰符;
虚成员 | 抽象成员 | |
---|---|---|
关键字 | virtual | abstract |
实现体 | 有实现体 | 没有实现体 |
在派生类中被覆写 | 可以被覆写,使用override | 必须被覆写,使用override |
成员的类型 | 方法、属性、事件、索引器 | 方法、属性、事件、索引器 |
十一、抽象类
抽象类是指设计为被继承的类,抽象类只能被用作其他类的基类。
- 不能创建抽象类的实例;
- 抽象类使用abstract修饰符声明;
abstract class MyClass
{
……
}
- 抽象类可以包含抽象成员或普通的非抽象成员,抽象类的成员可以是抽象成员和普通带实现的成员的任意组合;
- 抽象类自己可以派生自另一个抽象类;
abstract class AbClass //抽象类
{
……
}
abstract class MyAbClass : AbClass //派生自抽象类的抽象类
{
……
}
- 任何派生自抽象类的类必须使用override关键字实现该类所有的抽象成员,除非派生类自己也是抽象类;
1、抽象类和抽象方法的示例
namespace Eight_ClassAndInherit
{
abstract class AbClass
{
public void IdentifyBase() //普通方法
{
Console.WriteLine("I am IdentifyBase");
}
abstract public void IdentifyDerived(); //抽象方法
}
class DevidedClass : AbClass //派生类
{
public override void IdentifyDerived() //抽象方法的实现
{
Console.WriteLine("I am IdentifyDerived");
}
}
class _11_1AbstractClassAndMethod
{
static void Main()
{
//AbClass a = new AbClass(); //错误,抽象类不能被实例化
//async.IdentifyBase();
DevidedClass b = new DevidedClass(); //实例化派生类
b.IdentifyBase(); //调用继承的方法
b.IdentifyDerived(); //调用“抽象”方法
}
}
}
执行结果:
2、抽象类的另一个例子
abstract class MyBase
{
public int SideLength = 10; //数据成员
const int TriangleSideCount = 3; //数据成员
abstract public void PrintStufff(string s); //抽象方法
abstract public int MyInt { get; set; } //抽象属性
public int PerimeterLength() //普通的非抽象方法
{
return TriangleSideCount * SideLength;
}
}
class MyClass : MyBase
{
public override void PrintStufff(string s) //覆盖抽象方法
{
Console.WriteLine(s);
}
private int _myInt;
public override int MyInt //覆盖抽象属性
{ get { return _myInt; } set { _myInt = value; } }
}
class _11_2AbstractClass
{
static void Main()
{
MyClass mc = new MyClass();
mc.PrintStufff("This is a string.");
mc.MyInt = 28;
Console.WriteLine(mc.MyInt);
Console.WriteLine($"Perimeter Length:{mc.PerimeterLength()}");
}
}
执行结果:
十二、密封类
抽象类必须用做基类,它不能像独立的类对象那样被实例化,密封类与它相反:
- 密封类只能被用作独立的类,它不能被用作基类;
- 密封类使用sealed修饰符标注;
sealed class MyClass
{
……
}
如上代码即为一个密封类,将它用作其他类的基类会产生编译错误。
十三、静态类
静态类中所有成员都是静态的,静态类用于存放不受实例数据影响的数据和函数。静态类的一个常见用途可能是创建一个包含一组数学方法和值的数学库。
关于静态类需要了解的重要事项如下:
- 类本身必须标记为static;
- 类的所有成员必须是静态的;
- 类可以有一个静态构造函数,但不能有实例构造函数,因为不能创建该类的实例;
- 静态类是隐式密封的,也就是说,不能继承静态类;
可以使用类名和成员名,像访问其他静态成员那样访问静态类的成员。从C#6.0开始,也可以通过使用using static指令来访问静态类的成员,而不必使用类名。
static public class MyMath
{
public static float PI = 3.1415926f;
public static bool IsOdd(int x)
{
return x % 2 == 1;
}
public static int Times2(int x)
{
return 2 * x;
}
}
class _13StaticClass
{
static void Main()
{
int val = 3;
Console.WriteLine("{0} is odd is {1}.", val, MyMath.IsOdd(val));
Console.WriteLine($"{val} * 2 = {MyMath.Times2(val)}");
}
}
执行结果:
十四、扩展方法
到此,你看到的每个方法都和声明它的类关联,扩展方法特性扩展了这个边界,允许编写的方法和声明它的类之外的类关联。
在实际开发中,扩展方法是一个特别有用的工具,事实上,几乎整个LINQ库都是通过扩展方法来实现的。
class MyData
{
private double D1; //字段
private double D2;
private double D3;
public MyData(double d1,double d2,double d3) //构造函数
{
D1 = d1;D2 = d2;D3 = d3;
}
public double Sum()
{
return D1 + D2 + D3;
}
}
上述代码定义了类MyData,这是一个非常有限的类,但假设它还含有另外一个方法会更有用,该方法返回3个数据的平均值。
使用已经了解的关于类的内容,有几种方法可以实现这个额外的功能:
- 如果你有源代码并可以修改这个类,你只需要为这个类增加这个新方法;
- 如果不能修改这个类(如这个类在一个第三方类库中),那么只要它不是密封的,你就能把它用作一个基类并在派生自它的类中实现这个额外的方法;
然而,如果不能访问代码,或该类是密封的,或有其它的设计原因使这些方法不适用,就不得不在另一个使用该类的公有可用成员的类中编写一个方法。
static class ExtendMyData
{
public static double Average(MyData md)
{
return md.Sum() / 3;
}
}
class _14_1ExtendMethod
{
static void Main()
{
MyData md = new MyData(3, 4, 5);
Console.WriteLine("Average:{0}", ExtendMyData.Average(md));
}
}
执行结果:
上述代码包含一个名为ExtendMyData的静态类,它含有一个名为Average的静态方法,该方法实现了额外的功能,注意该方法接受MyData的实例作为参数。
尽管这是一个非常好的解决方案,但如果能在类的实例自身上调用该方法,而不是创建另一个作用于它的类的实例(即创建另一个类操作该实例),将会更优雅。
ExtendMyData.Average(md); //静态调用形式
md.Average(); //实例调用形式
上述两行代码阐明了二者区别:
- 第一行使用刚展示的方法,在另一个类的实例上调用静态方法;
- 第二行展示了在对象自身上调用实例方法;
扩展方法允许你使用第二种形式,即使第一种形式可能是编写这种调用的正常方法。
通过对方法Average的声明做一个小小的改动,就可以使用实例调用形式。需要做的修改是在参数声明中的类型名前增加关键字this。
static class ExtendMyData //必须是一个静态类
{
public static double Average(this MyData md) //必须是共有的和静态的,关键字this+类型
{
……
}
}
如上述代码所示,把this关键字加到静态类的静态方法的第一个参数上,把该方法从类ExtendMyData的常规方法改变为类MyData的扩展方法,现在两种调用形式都可以使用。
扩展方法的重要要求如下:
- 声明扩展方法的类必须声明为static;
- 扩展方法本身必须声明为static;
- 扩展方法必须包含关键字this作为它的第一个参数类型,并在后面跟着它扩展的类的名称;
namespace Eight_ClassAndInherit
{
class MyData
{
private double D1; //字段
private double D2;
private double 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 _14_1ExtendMethod
{
static void Main()
{
MyData md = new MyData(3, 4, 5);
Console.WriteLine($"Sum: {md.Sum()}");
Console.WriteLine("Average:{0}", md.Average());
}
}
}
执行结果:
十五、命名约定
风格名称 | 描述 | 推荐使用 | 示例 |
---|---|---|---|
Pascal大小写 | 标识符中每个单词的首字母大写 | 用于类型名称和类中对外可见成员的名称,射界的名称包括:类、方法、命名空间、属性和公有字段 | CardDeck、Dealershand |
Camel大小写 | 标识符中每个单词的首字母大写,第一个单词除外 | 用于局部变量的名称和方法声明的形参名称 | totalCycleCount、randomSeedParam |
下划线加Camel大小写 | 以下划线开头的Camel大小的标识符 | 用于私有和受保护的字段 | _cycleCount、_selectedIndex |