继承
一、派生
1.基类型和派生类型之间的转型
从派生类型转基类型时为隐式转型,不需要特殊的运算符,转换总会成功,不会引发异常。
从基类型转派生类型需要执行一次显示转型,可能会失败。
2.访问修饰符
3.扩展方法
4.单一继承
5.密封类(sealed)
简单来说就是不允许派生,string类就是密封类。
二、重写
1.基类的重写
virtual修饰符
C#默认都是非虚,Java默认都是虚,C#不允许隐式重写,override关键字是必须的。
重载一个成员,会造成“运行时”调用最底层或者说是派生得最远的实现。
public class Person { public virtual string Name { get; set; } } public class Contact : Person { public override string Name { get { return FirstName + " " + LastName; } set { string[] names = value.Split(' '); //未处理异常 FirstName = names[0]; LastName = names[1]; } } public string FirstName { get; set; } public string LastName { get; set; } } Contact contact; Person person; contact = new Contact(); person = contact; person.Name = "Inigo Montoya"; Console.WriteLine("{0} {1}", contact.FirstName, contact.LastName);
输出结果:Inigo Montoya
注意:创建类是必须小心选择在什么时候允许重写一个方法,因为不能控制在派生类中的实现。虚方法中不应包含关键性代码,因为如果派生类重写这个方法,那么它永远得不到调用。
下面代码包含一个virtual Run 方法,我们在调用Run()时,假如粗心的认为关键的Start和Stop方法无论如何都会被调用,就会出现问题。
public void Start() { //... } public void Stop() { //... } public virtual void Run() { Start(); Stop(); }
别的人在重写Run方法时,完全可能选择不调用关键的Start和Stop方法。为了强制调用Start和Stop方法,代码如下
public void Start() { //... } public void Stop() { //... } public void Run() { Start(); InternalRun(); Stop(); } protected virtual void InternalRun() { //... }
防止了用户错误的调用InternalRun方法,以为它是protected的。
另外,将Run声明为public,可以保证Start和Stop可以被正确的调用。
用户可以在派生类中重写virtual InternalRun修改默认实现。
不要在构造器中调用会影响所构造对象的任何虚方法。原因是假如这个虚方法在当前要实例化的类型的派生类型中进行了重写,就会调用重写的实现。但在继承的层次结构中,字段尚未完全初始化。所以,调用虚方法将导致无法预测的行为。
2.new 修饰符
如果重写方法没有使用override关键字,编译器会报一条警告消息。
一个明显的解决方案是使用override修饰符(假定基类型成员是virtual的)。还可以使用new修饰符。
我们来看如下场景:
1.程序员A定义Person类,包含FirstName和LastName属性
2.程序员B从Person派生出Contact类,并添加了额外的属性Name。除此之外,他还定义了一个Program类,Main方法会实例化一个Contact,对Name赋值,并输出姓名。
3.不久,程序员A也添加了Name属性,但他不是将取值方法实现成FirstName + " " + LastName,而是实现成FirstName + "," + LastName。除此之外,没有将属性Name定义为virtual,而且在DisPlay方法中使用了该属性。
public class Person { public string FirstName { get; set; } public string LastName { get; set; } public string Name { get { return FirstName + "," + LastName; } set { string[] names = value.Split(','); //未处理异常 FirstName = names[0]; LastName = names[1]; } } public static void DisPlayName(Person person) { Console.WriteLine(person.Name); } } public class Contact : Person { public string Name { get { return FirstName + " " + LastName; } set { string[] names = value.Split(' '); //未处理异常 FirstName = names[0]; LastName = names[1]; } } }
Person.Name不是virtual,所以A希望DisPlay方法总是使用Person实现,即便你传给他的数据类型是Person的派生类型Contact。然而,B希望在变量数据类型是Contact的任何情况下都使用Contact.Name(B不知道Person.Name,因为最开始Person类没有Name属性)。
为了允许添加Person.Name,同时不破坏A和B预期的行为,我们不能假定Name是virtual的,此外,由于C#要求重写成员时显示地使用override修饰符,所以必须使用其他某种形式的语法,保证基类中添加一个成员后,不会造成派生类编译错误。
这时我们使用new修饰符,它在基类中隐藏了派生类重新声明的成员。
在这种情况下,不是调用派生得最远的成员。基类的成员会自动搜索继承链,找到使用了new修饰符的那个成员之前的成员,然后调用该成员。如果继承链中只包含两个类,则使用基类成员,就像派生类没有重写那个成员(如果派生类实现重新声明了基类成员)。
如果即没有指定override也没有指定new,编译器会默认为new以维持版本的安全性。
3.sealed修饰符
类似与一个类使用sealed使用sealed修饰符,从而禁止从该类继承,virtual成员也可以密封。禁止子类重写声明为virtual的基类成员。
public class A { public virtual void Method() { } } public class B : A { public override sealed void Method() { } } public class C : B { //Error “CSharpDemo.C.Method()”: 继承成员“CSharpDemo.B.Method()”是 sealed,无法进行重写 public override void Method() { } }
4.base成员
5.构造器
实例化一个派生类是,“运行时”先调用基类的构造器,以避免基类构造器被绕过。
为避免因为缺少可供访问的默认构造器而造成错误,我们需要在派生类构造器的头部显示指定要运行哪一个基类的构造器。
三、抽象类 abstract
很多时候,类的实例没有意义,只有作为基类,从其派生出来的一系列数据类型之间共享默认的方法实现才显得有意义,这时类就应该被设计为抽象类。
仅供派生、无法实例化。
通常,抽象类中的大部分功能是没有实现的。一个类要想从抽象类派生,它必须为抽象基类中的抽象方法提供实现。
四、System.Object
所有类都从System.Object派生。
五、is ,as运算符
is运算符的优点是,它可以验证一个数据项是否属于特定的类型。as像一次转型那样,尝试将对象转换为一个特定的数据类型。
as转型失败会将null值赋给目标对象。
注意:is运算符不只是检查数据能成功转型为特定的数据类型(如string),还会检查数据底层对象本身是否真的是一个string。
一个类可能实现了向string的强制转换,所以能转型为一个string,但是为那个对象使用is运算符会返回false。
public class MyClass { public static explicit operator string(MyClass myClass) { return myClass.ToString(); } public override string ToString() { return "String"; } } class Program { static void Main(string[] args) { object o; string s = "text"; MyClass myClass = new MyClass(); o = myClass; Trace.Assert(!(myClass is string)); Trace.Assert((string)myClass == "String"); o = s; Trace.Assert((string)o == "String"); Trace.Assert(!(o is string)); } }