类继承
可以使用一个已经存在的类作为新类的基础,这个已经存在的类称为基类,新类称为派生类,派生类成员组成为:本身声明中的成员和基类的成员。派生类不能删除它所继承的任何成员。
要声明一个派生类,需要在类名后加入基类规格说明。基类规格说明是由冒号和基类名称组成。
访问继承的成员
继承的成员可以被访问,就像它们是派生类自己声明的一样。
namespace InheritanceExample
{
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("Derive class -- Method2:{0}", value);
}
}
class Program
{
static void Main()
{
OtherClass oc = new OtherClass();
oc.Method1(oc.Field1);
oc.Method1(oc.Field2);
oc.Method2(oc.Field1);
oc.Method2(oc.Field2);
}
}
}
所有类都派生自object类
除了特殊的类object,所有的类都是派生类,即使它们没有基类规格说明。类object是唯一的非派生类,因为它是继承层次结构的基础。
需要注意的是:一个类声明的基类规格说明中只能有一个单独的类,这称为单继承。虽然类只能直接继承一个基类,但继承的层次没有限制。
屏蔽基类的成员
虽然派生类不能删除它继承的任何成员,但可以用与基类成员名称相同的成员来屏蔽基类成员。这是继承的基本功能之一。在派生类中屏蔽基类成员的一些要点如下:
- 要屏蔽一个继承的数据成员,需要声明一个新的相同类型的成员,并使用相同的名称。
- 要屏蔽一个继承的函数成员,需要声明新的带有相同签名的函数成员,签名即由名称和参数列表组成,不包含返回类型。
- 要让编译器知道在故意屏蔽继承的成员,使用
new
修饰符。 - 也可以屏蔽静态成员。
如果派生类必须完全的访问被隐藏的继承类成员,可以使用基类访问表达式来访问,所谓基类表达式就是由关键字new后面跟着一个点和数据成员名字组成。
namespace InheritanceExample
{
class SomeClass
{
public string Field1 = "base class field";
public void Method1(string value)
{
Console.WriteLine("Base class-- Method1: {0}", value);
}
}
class OtherClass : SomeClass
{
new public string Field1 = "derived class field";//屏蔽基类字段成员
new public string Method1(string value)//不要求返回值类型一致
{
Console.WriteLine("Derive class -- Method1:{0}", value);
Console.WriteLine("using base--Field1,{0}", base.Field1);//类内使用base来访问被隐藏的继承成员
base.Method1( Field1);
return value;
}
}
class Program
{
static void Main()
{
OtherClass oc = new OtherClass();
oc.Method1(oc.Field1);
}
}
}
基类访问
如果派生类必须完全地访问被隐藏的继承成员,可以使用基类访问表达式访问隐藏的继承成员。基类访问表达式由关键字base后跟一个点和成员的名称组成。
namespace InheritanceExample
{
class SomeClass
{
public string Field1 = "base class field";
public void Method1(string value)
{
Console.WriteLine("Base class-- Method1: {0}", value);
}
}
class OtherClass : SomeClass
{
new public string Field1 = "derived class field";
new public string Method1(string value)//不要求返回值类型一致
{
Console.WriteLine("Derive class -- Method1:{0}", value);
Console.WriteLine("using base--Field1,{0}", base.Field1);//类内使用base来访问被隐藏的继承成员
base.Method1(Field1);
return value;
}
public void PrintField1()
{
Console.WriteLine(Field1);
Console.WriteLine(base.Field1);
base.Method1("called by PrintField1()");//用base 调用基类方法
}
}
class Program
{
static void Main()
{
OtherClass oc = new OtherClass();
oc.PrintField1();
}
}
}
可以看到可以在派生类内使用base来调用基类的成员(字段与方法)。
使用基类的引用
派生类的实例由基类的实例加上派生类新增的成员组成。派生类的引用指向整个类对象,包括基类部分。
如果有一个派生类对象的引用,就可以获取该对象基类部分的引用(使用类型转换运算符把该引用转换为基类类型)。
namespace InheritanceExample
{
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()
{
MyDerivedClass derived = new MyDerivedClass();
MyBaseClass mybc = (MyBaseClass)derived; //转换为基类,该对象仅能“看到”基类的方法。
derived.Print();
mybc.Print();
}
}
}
可以看到派生类屏蔽了基类的Print
方法,故派生类实例对象调用Print
方法,调用的是派生类的方法,而基类引用对象调用print
方法,调用的是基类的Print
方法。
如果对派生类增加新的方法Print
,则基类引用不能调用该方法,再次印证了“基类引用只能‘看到’基类的成员。
namespace InheritanceExample
{
class MyBaseClass
{
public void Print()
{
Console.WriteLine("This is the base class.");
}
public int Field = 20;
}
class MyDerivedClass : MyBaseClass
{
new public void Print()
{
Console.WriteLine("This is the derived class.");
}
public void Print1()
{
Console.WriteLine("This is derive class Method Print1");
}
}
class Program
{
static void Main()
{
MyDerivedClass derived = new MyDerivedClass();
MyBaseClass mybc = (MyBaseClass)derived; //转换为基类,该对象仅能“看到”基类的方法。
derived.Print();
mybc.Print();
derived.Print1();
//mybc.Print1();
Console.WriteLine("mybc.Field:{0}", mybc.Field);
}
}
}
虚方法与覆写方法
当使用基类引用访问派生类对象时,得到的是基类的成员,也只能访问和调用基类成员。虚方法可以使得基类引用访问”升至“派生类内。
可以使基类引用调用派生类的方法,需要满足下面的条件:
- 派生类的方法和基类的方法有相同的签名和返回类型,并且具有相同的可访问性。
- 基类的方法使用
virtual
标注 - 派生类的方法使用
override
标注
namespace InheritanceExample
{
class MyBaseClass
{
virtual public void Print()
{
Console.WriteLine("This is the base class");
}
}
class MyDerivedClass : MyBaseClass
{
public override void Print()
{
Console.WriteLine("This is called in MyDerivedClass");
base.Print();
Console.WriteLine("This is the derived class");
}
}
class Program
{
static void Main()
{
MyDerivedClass derived = new MyDerivedClass();
MyBaseClass mybc = (MyBaseClass)derived;
derived.Print();
mybc.Print();
}
}
}
需要注意的是:不能覆写static方法或者非虚方法,
覆写标记为override的方法
覆写方法可以在继承的任何层次出现。
- 当使用对象基类引用调用一个覆写方法时,方法的调用被沿派生层次上溯执行,一直到标记为override的方法的最高派生版本。
namespace InheritanceExample
{
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 SecondDerived : MyDerivedClass
{
public override void Print()
{
Console.WriteLine("This is the second derived class");
}
}
class Program
{
static void Main()
{
SecondDerived derived = new SecondDerived();
MyBaseClass mybc = (MyBaseClass)derived;
derived.Print();
mybc.Print();
}
}
}
可以看到基类的引用mybc
在调用Print
方法时,直接上溯到了最高派生类的Print
方法,因为最高派生类的Print
有override标记。
- 如果在更高的派生级别有该方法的其他声明,但没有被标记为override,那么它们不会被调用。
namespace InheritanceExample
{
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 SecondDerived : MyDerivedClass
{
new public void Print()
{
Console.WriteLine("This is the second derived class");
}
}
class Program
{
static void Main()
{
SecondDerived derived = new SecondDerived();
MyBaseClass mybc = (MyBaseClass)derived;
derived.Print();
mybc.Print();
}
}
}
取消最高派生类的Print
方法的override标记,加以new标记进行屏蔽,发现基类的引用调用Print
方法,只能调用中间层次的派生类(MyDerived)的Print
方法。
覆盖其他成员类型
在属性以及索引器上的覆盖与方法的覆盖一样。
namespace ClassInheritance
{
class MyBaseClass
{
public int _myInt = 5;
virtual public int MyProperty
{
get { return _myInt; }
}
}
class MyDerivedClass : MyBaseClass
{
public int _myInt = 10;
public override int MyProperty
{
get { return _myInt; }
}
}
class Program
{
static void Main()
{
MyDerivedClass derived = new MyDerivedClass();
MyBaseClass mybc = (MyBaseClass)derived;
Console.WriteLine(derived.MyProperty);
Console.WriteLine(mybc.MyProperty);
}
}
}
虚方法与覆盖只适用于函数成员,没有字段的”虚方法“与”覆写方法“
构造函数的执行
要创建对象的基类部分,需要隐式调用基类的某个构造函数作为创建实例过程的一部分,继承层次链中的每个类在执行它自己的构造函数之前执行它的基类构造函数。
构造函数初始化语句
默认情况下,在构造对象时,将调用基类的无参数构造函数。但构造函数可以重载,所以基类可能有一个以上的构造函数。如果希望派生类使用一个指定的基类构造函数而不是无参数构造函数,必须在构造函数初始化语句中指定它。
有两种形式的构造函数初始化语句。
- 第一种是使用base并指明使用哪一个基类构造函数。
- 第二种是使用this并指明应使用当前类哪一个构造函数。
第一种比如:
public MyDerivedClass(int x, string s):base(s,x) //构造函数初始化语句。
{
...
}
如上面代码片段,构造函数初始化指明了要使用两个参数的基类构造函数,并且第一个参数是一个string,第二个参数是一个int。
在基类参数列表中的参数必须在类型和顺序方面与已定的基类构造函数的参数列表相匹配。
第二种比如:
public MyClass(int x):this(x,"Using default string") //构造函数初始化语句
{
...
}
这种形式的构造函数初始化语句可以让构造过程使用当前类的其他构造函数,比如上面代码片段,MyClass类包含一个参数的构造函数,但这个单参数函数构造函数使用了同一个类中具有两个参数的构造函数,为第二个参数提供了一个默认值。
这种语法很有用的一种情况是:一个类有好几个构造函数,并且它们都需要在对象构造的过程开始时执行一些公共的代码。对于这种情况,可以把公共的代码提取出来作为一个构造函数,被其他所有的构造函数初始化语句使用。可能我们会认为,还可以声明另外一个方法来进行这些公共的初始化,让所有的构造函数来调用这个方法,但这个方法不是一个好的办法,因为首先,编译器知道方法是构造函数后会进行一些优化。其次有的时候一些事情必须在构造函数中进行,在其他地方则不行。比如readonly字段只能在构造函数中初始化。
回到公共构造函数,如果这个构造函数可以初始化类中所有需要初始化的东西,并且可以独立作为一个有效的函数,那么完全可以把它设置为public的构造函数,但是如果它不能完全初始化一个对象,怎么办?此时,必须禁止从类的外部调用构造函数,因为那样的话,它只会初始化对象的一部分。要避免这个问题,可以把构造函数声明为private,而不是public,然后让其他构造函数使用它,比如:
namespace Constructor
{
class MyClass
{
readonly int firstVar;
readonly double secondVar;
public string UserName;
public int UserID;
private MyClass()
{
firstVar = 20;
secondVar = 30.5;
Console.WriteLine("Calling constructor with no parameters");
}
public MyClass(string firstName) : this()
{
UserName = firstName;
UserID = -1;
}
public MyClass(int idNum) : this()
{
UserName = "Anonymous";
UserID = idNum;
}
}
class Program
{
static void Main()
{
MyClass mc1 = new MyClass("JohnYang");
MyClass mc2 = new MyClass(0);
Console.WriteLine("mc1.Id is {0}", mc1.UserID);
Console.WriteLine("mc2.UserName is {0}", mc2.UserName);
}
}
}
类访问修饰符
类可以被系统中其他类看到并访问.
类的可访问性有两个级别:public
和internal
.默认是internal
,不允许是其他类型.
- 标记为public的类可以被系统内任何程序集中的代码访问.要使一个类对其他程序集可见,使用
public
访问修饰符,如下:
public class Myclass{
...
}
- 标记为
internal
的类只能被它自己所在的程序集内的类看到,这是默认的可访问级别,所以除非在类的声明中显式地指定修饰符pulic,程序集外部的代码不能访问该类.
比如:创建类库SuperLib.cs
,并写下如下代码:
其中,SquareWidget
类不声明为public.
//SuperLib.cs
using System;
namespace SuperLib
{
class SquareWidget
{
public double SideLength = 0;
internal int Test = 10;
public int Test1 = 11;
public double Area
{
get { return SideLength * SideLength; }
}
}
}
然后在测试代码test.cs
中引用上述类库,然后写入以下测试代码:
using System;
using SuperLib;
namespace Constructor
{
class WidgetsProgram
{
static void Main()
{
SquareWidget sq = new SquareWidget();
sq.SideLength = 5;
Console.WriteLine($"{sq.Area}");
Console.WriteLine($"{sq.Test1}");
Console.WriteLine($"{sq.Test}");
Console.ReadLine();
}
}
}
然后,将类库代码中对类SquareWidget
添加public
声明:
using System;
namespace SuperLib
{
public class SquareWidget
{
public double SideLength = 0;
internal int Test = 10;
public int Test1 = 11;
public double Area
{
get { return SideLength * SideLength; }
}
}
}
可以发现可以在测试代码中顺利的引用访问类库中的SquareWidget
类.
补充:Visual studio 对C#类库生成dll及对其引用
1.新建一个项目,选择"类库"‘用于创建C#类库dll项目’.
2.将Class1.cs改名自己要创建的文件名,并填入代码。
3.生成解决方案,(ctrl+shift+B);在工程文件的bin\debug目录里看到dll,文件扩展名是dll。
4.新建一个C#控制台程序;
5.点击 “项目” 然后 “添加引用”,从"浏览(B)"路径找到刚刚生成的Dll文件并添加.
6.写入测试代码,在开头加上using dll项目名
语句
程序集间的继承
C#允许从一个在不同的程序集内定义的基类来派生类。
要从不同程序集中定义基类的派生类,必须满足:基类必须被声明为public,这样才能从它所在的程序集外部访问它。
仍然利用上述SuperLib.cs
,在另一个程序的源文件中引入该类库,然后对SquaredWidget
进行继承:
// assembly.cs
using System;
using SuperLib;
namespace Constructor
{
class SquareWidgetInheritance : SquareWidget //继承SuperLib中的SquareWidget类
{
}
class WidgetsProgram
{
static void Main()
{
SquareWidgetInheritance sq = new SquareWidgetInheritance(); //创建SquareWidgetInheritance实例
sq.SideLength = 5;
Console.WriteLine($"{sq.Area}");
Console.WriteLine($"{sq.Test1}");
//Console.WriteLine($"{sq.Test}");
Console.ReadLine();
}
}
}
成员访问修饰符
对于类的可访问性,只有两种修饰符:internal,public。而类的可访问性有5种:public, private, internal, protected, protected internal。
对于成员访问性的细节学习之前,有一些通用性的内容需要阐述:
- 所有显式声明在类声明种的成员都是互相可见的,无论它们的访问性如何。
- 继承的成员不在类的声明种显式声明
- 必须对每个成员指定成员访问级别。如果不指定某个成员的访问级别,它的隐式访问级别为private。
- 成员不能比它的类有更高的可访问性。也就是说,如果一个类的可访问性限于它所在的程序集,那么类的成员个体也不能从程序集的外部看到,无论它们的访问修饰符是什么,Public也不例外。
访问成员的区域
下面的类种声明了5种不同访问级别的成员:
public class MyClass
{
public int Member1;
privatte int Member2;
protected int Member3;
internal int Member4;
protected internal int Member5;
}
另一个类B能否访问这些成员取决于以下两个特征:
- 类B是否派生自MyClass
- 类B是否和MyClass在同一程序集
这两个特征划分出4个集合,与MyClass类相比,其他类可以是下面任意一种。
公有成员的可访问性
public访问级别是限制最少的,所有的类,包括程序集内部的类和外部的类都可以自由地访问成员。
私有成员的可访问性
私有成员的级别是限制最严格的。
- private 类成员只能被它自己的类的成员访问。它不能被其他的类访问,包括继承它的类。
- 然而,Private成员能被嵌套在它的类中的类成员访问。
受保护的可访问性
protetcted访问级别如同private访问级别,除了一点,它允许派生自该类的类访问该成员。需要注意的是,即使程序外部继承该类的类也可以访问该成员。
内部成员的可访问性
标记为internal的成员对程序集内的所有类可见,但对程序集外的类不可见。
受保护内部成员的可访问性
标记为protected internal的成员对所有继承该类的类以及所有程序集内部的类可见。注意这是protected 和internal的并集,不是交集。
成员访问修饰符小结
抽象成员
抽象成员只能在抽象类中声明,抽象成员有以下的特征:
- 必须是函数成员,也就是说字段和常量不能是抽象成员;
- 必须用abstract修饰符标记
- 不能有实现代码块,抽象成员的代码用分号表示。
- 一共有四种类型的成员可以被声明为抽象的:(1)方法(2)属性(3)事件(4)索引
- 尽管抽象成员必须在派生类中用相应的成员覆写,但不能把virtual修饰符附加到abstract修饰符
- 类似虚成员,抽象成员的实现必须指定override修饰符
抽象类
抽象类只能被用作其他类的基类,不能创建抽象类的实例,抽象类使用abstract修饰符声明。抽象类的成员可以包含抽象成员,或普通的非抽象成员。
抽象类可以自己派生自另一个抽象类。任何派生自抽象类的类必须使用override关键字实现该类所有的抽象成员,除非派生类也是抽象类.
namespace AbstractExample
{
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);
}
//public override int MyInt { get; set; } //没有实现MyInt属性
}
class Program
{
static void Main()
{
MyClass mc = new MyClass();
mc.PrintStuff("This is a string...");
}
}
}
由于没有实现MyInt
属性,结果抛出错误。
namespace AbstractExample
{
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);
}
public override int MyInt { get; set; }
}
class Program
{
static void Main()
{
MyClass mc = new MyClass();
mc.PrintStuff("This is a string...");
Console.WriteLine($"Perimeter length:{mc.PerimeterLength()}");
}
}
}
密封类
密封类与抽象类正好相反,抽象类不能有实例,只能被继承,而密封类不能被继承,只能被实例。用sealed修饰符标注。
静态类
静态类中所有成员都是静态的。静态类用于存放不受实例数据影响的数据和函数,常见用途是创建一个包含一组数学方法和值的数学库。
关于静态类需要注意的是:
- 类本身必须被标记为static
- 类的所有成员必须是静态的
- 类可以有一个静态构造函数,但不能有实例构造函数,不能创建该类的实例
- 静态类是隐式密封的,也就是 说静态类不可以被继承
namespace StaticClassExample
{
static public class MyMath
{
public static float PI = 3.14f;
public static bool IsOdd(int x)
{
return x % 2 == 1;
}
public static int Times2(int x)
{
return 2 * x;
}
}
class Program
{
static void Main()
{
int val = 3;
Console.WriteLine($"{val} is odd is {MyMath.IsOdd(val)}");
Console.WriteLine($"{val}*2={MyMath.Times2(val)}");
}
}
}
扩展方法
扩展方法允许编写的方法和声明它的类之外的类关联。
扩展方法必须声明在静态类中,本身必须被声明为static,必须包括关键字this和它所扩展的类的名称,作为第一个参数的类型。
namespace StaticClassExample
{
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;
}
}
//由于上述类是sealed 类,只能被实例化,不能被继承,派生,那么为增强该类的功能,如果我们拿不到它的源码,无法进行
//直接添加方法,那么可以尝试新建立一个静态类,然后以MyData类的实例为参数,增加新功能的函数成员,然后用
//静态类方法调用即可,但现在有更为较好的方法来解决,那就是扩展方法,它可以让实例调用我们增加的方法,而不是用
//静态类的类方法来调用。
static class ExtendClass
{
public static double Average(this MyData md)
{
return md.Sum() / 3;
}
}
class Program
{
static void Main()
{
MyData md = new MyData(4, 5, 6);
Console.WriteLine($"Sum:{md.Sum()}");
Console.WriteLine($"Average:{md.Average()}");//可以使用扩展方法,使得实例可以直接调用我们新创建的函数。
}
{
}
}
}