
目录
一、类继承
二、访问继承的成员
三、所有类都派生自object类
四、屏蔽基类的成员
五、基类访问
六、使用基类的引用
1、虚方法和覆写方法
2、覆写标记为override的方法
(1)情况1:使用override声明Print
(2)情况2:使用new声明Print
3、覆盖其他成员类型
七、构造函数的执行
1、构造函数初始化语句
2、类访问修饰符
八、程序集间的继承
九、成员访问修饰符
1、访问成员的区域
2、公有成员的可访问性
3、私有成员的可访问性
4、受保护成员的可访问性
5、内部成员的可访问性
6、受保护内部成员的可访问性
7、成员访问修饰符小结
十、抽象成员
十一、抽象类
1、抽象类和抽象方法的示例
2、抽象类的另一个例子
十二、密封类
十三、静态类
十四、扩展方法
十五、命名约定
通过继承可以定义一个新类,新类纳入一个已经生命的类并进行扩展。
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。
虽然派生类不能删除它继承的任何成员,但可以用与基类成员名称相同的成员来屏蔽基类成员。这是继承的主要功能之一,非常实用。
例如,我们要继承包含某个特殊方法的基类,该特殊方法虽然适合声明它的类,但不一定适合派生类。在这种情况下,我们希望在派生类中声明新成员以屏蔽基类中的方法。在派生类中屏蔽基类成员的一些要点如下:
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; //转换引用
关于上述两行代码:
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; //错误:基类引用无法访问派生类成员
}
}
执行结果:
上一节可知,当使用基类引用访问派生类对象时,得到的是基类的成员。虚方法可以使基类的引用访问“升至”派生类内。
可以使用基类引用调用派生类的方法,只需满足下面的条件:
class MyBaseClass //基类
{
virtual public void Print()
……
}
class MyDerivedClass : MyBaseClass
{
override public void Print()
……
}
注意与上一种情况(用new隐藏基类成员)相比在行为上的区别:
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修饰符的重要信息如下:
覆写方法可以在继承的任何层次出现。
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。让我们看一看在每种情况下将发生什么。
若把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();
}
}
执行结果:
关于上述代码:
结论:
无论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声明。
在属性、事件和索引器上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()。
构造的顺序如上图,创建一个实例的过程中,完成的第一件事是初始化对象的所有实例成员,在此之后,调用基类的构造函数,然后才执行该类自己的构造函数体。
警告:
强烈反对在构造函数中调用虚方法,在执行基类的构造函数时,基类的虚方法会调用派生类的覆写方法,但这是在执行派生类的构造函数方法体之前,因此调用会在派生类完全初始化之前传递到派生类。
默认情况下,在构造对象时,将调用基类的无参构造函数。但构造函数可以重载,所以基类可能有一个以上的构造函数。如果希望派生类使用一个指定的基类构造函数而不是无参数构造函数,必须在构造函数初始化语句中指定它。
有两种形式的构造函数初始化语句:
形式一:
基类构造函数初始化语句放在冒号后面,跟在类的构造函数声明的参数列表后面。构造函数初始化语句由关键字base和要调用的基类构造函数的参数列表组成。
public MyDerivedClass(int x,string s) : base(s, x) //构造函数初始化语句
{
……
}
当声明一个不带构造函数初始化语句的构造函数时,它实际上是带有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;
}
}
类可以被系统中其他类看到并访问,可访问有时也称为可见,它们可以互换使用。
类的可访问性有两个级别:public和internal:
C#允许从一个在不同的程序集内定义的基类来派生。
要从不同程序集中定义的基类派生类,必须具备以下条件:
要使引用其他程序集中的类和类型更容易,不使用它们的完全限定名称,可以在源文件的顶部放置一个using指令,并带上将要访问的 类或类型所在的命名空间。
说明:
增加对其他程序集的引用和增加using指令是两回事,增加对其他程序集的引用是告诉编译器所需的类型在哪定义,增加using指令 允许你引用其他的类而不必使用它们的完全限定名称。
using System;
namespace BaseClassNS //包含基类声明的命名空间
{
public class MyBaseClass //把该类声明为共有的,使其对外部可见
{
public void PrintMe()
{
Console.WriteLine("I am MyBaseClass");
}
}
}
上述代码创建了含有MyBaseClass类声明的程序集,该类有以下特征:
using BaseClassNS;
namespace UseBaseClass //包含基类声明的命名空间
{
class DrivedClass : MyBaseClass //在其他程序集中的基类
{
//空类体
}
class Program
{
static void Main()
{
DrivedClass mdc = new DrivedClass();
mdc.PrintMe();
}
}
}
上述代码在另一个程序集中,创建了包含DrivedClass类的声明,它继承在第一个程序集中声明的MyBaseClass,该源文件名为Assembly2.cs:
执行结果:
类的可访问性描述了类的可见性;成员的可访问性描述了类成员的可见性。
声明在类中的每个成员对系统的不同部分可见,这依赖于类声明中指派给他的访问修饰符。你已经看到private成员仅对同一类的其他成员可见,而public成员对程序集外部的类也可见。
在研究成员访问行的细节之前,首先阐述一些通用内容:
类通过成员的访问修饰符指明了哪些成员可以被其他类访问。下面的类中声明了5种访问级别的成员:
public class MyClass
{
public int Member1;
private int Member2;
protected int Member3;
internal int Member4;
protected internal int Member5;
……
}
另一个类(如类B)能否访问这些成员取决于该类的两个特征:
上图所示,两个特征划分出4个集合,与MyClass类相比,其他类可以是下面任意一种:
这些特征用于定义五种访问级别。
public访问级别是限制最少的,所有的类,包括程序集内部的类和外部的类都可以自由地访问成员。
private访问级别是限制最严格的。
protected访问级别如同private访问级别,但它允许派生自该类的类访问该成员。
注意:
即使程序集外部继承该类的类也能访问该成员。
标记为internal的成员对程序集内部的所有类可见,但对程序集外部的类不可见。
标记为protected internal的成员对所有继承该类的类以及程序集内部的所有类可见。
注意:
允许访问的集合是protected修饰符允许访问的类的集合加上internal修饰符允许访问的类的集合,这是protected和internal的并集,不是交集。
| 修饰符 | 含义 |
|---|---|
| private | 只在类的内部可访问 |
| internal | 对该程序集内所有类可访问 |
| protected | 对所有继承该类的类可访问 |
| protected internal | 对所有继承该类或在该程序集内声明的类可访问 |
| public | 对任何类可访问 |
上图阐述了5个成员访问修饰符的可访问级别。
| 同一程序集内的类 | 不同程序集内的类 | |||
|---|---|---|---|---|
| 非派生 | 派生 | 非派生 | 派生 | |
| private | × | × | × | × |
| internal | √ | √ | × | × |
| protected | × | √ | × | √ |
| protected internal | √ | √ | × | √ |
| public | √ | √ | √ | √ |
抽象成员是指设计为被覆写的函数成员,抽象成员有以下特征:
abstract public void PrintStuff(string s); //分号替换实现
abstract public int MyProperty
{
get; //分号替换实现
set; //分号替换实现
}
抽象成员可以只在抽象类中声明,总共有4种类型的成员可以声明为抽象的:
关于抽象成员的其他重要事项:
| 虚成员 | 抽象成员 | |
|---|---|---|
| 关键字 | virtual | abstract |
| 实现体 | 有实现体 | 没有实现体 |
| 在派生类中被覆写 | 可以被覆写,使用override | 必须被覆写,使用override |
| 成员的类型 | 方法、属性、事件、索引器 | 方法、属性、事件、索引器 |
抽象类是指设计为被继承的类,抽象类只能被用作其他类的基类。
abstract class MyClass
{
……
}
abstract class AbClass //抽象类
{
……
}
abstract class MyAbClass : AbClass //派生自抽象类的抽象类
{
……
}
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(); //调用“抽象”方法
}
}
}
执行结果:
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 class MyClass
{
……
}
如上代码即为一个密封类,将它用作其他类的基类会产生编译错误。
静态类中所有成员都是静态的,静态类用于存放不受实例数据影响的数据和函数。静态类的一个常见用途可能是创建一个包含一组数学方法和值的数学库。
关于静态类需要了解的重要事项如下:
可以使用类名和成员名,像访问其他静态成员那样访问静态类的成员。从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的扩展方法,现在两种调用形式都可以使用。
扩展方法的重要要求如下:
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 |
上一篇 no route to host java.net.no
下一篇 如何让PowerShell invoke-restmethod 和 invoke-webrequest 忽略不工作的自签名证书