重写
重写的概念已经很熟了,就像上文列举的,为了扩展而准备,需要注意以下几点:
- C#支持重写实例方法和属性、不支持字段和任何静态成员重写
- C#重写的成员必须显示添加virtual关键字,C#默认方法非虚
- 如果派生类要重写方法,必须显式的使用override关键字!
- 重写后的override修饰的方法还是虚方法,还能被继承重写。
还有一个重要的修饰符就是new,可以在派生类显式的隐藏从基类继承的成员! 说白了new的一个重要作用是,如果派生类使用了和基类相同的方法签名,不管这个方法是虚方法还是实例方法,都会隐藏基类的方法而用自己的实现。
脆弱的基类:new修饰符
还有一点值得注意:虽然运行时调用派生的最远的虚成员,但如果遇到new就会变为,new之前的派生的最远的虚成员
public class Program { public class BaseClass { public void DisplayName() { Console.WriteLine("BaseClass"); } } public class DerivedClass : BaseClass { // Compiler WARNING: DisplayName() hides inherited // member. Use the new keyword if hiding was intended public virtual void DisplayName() { Console.WriteLine("DerivedClass"); } } public class SubDerivedClass : DerivedClass { public override void DisplayName() { Console.WriteLine("SubDerivedClass"); } } public class SuperSubDerivedClass : SubDerivedClass { public new void DisplayName() { Console.WriteLine("SuperSubDerivedClass"); } } public static void Main() { SuperSubDerivedClass superSubDerivedClass = new SuperSubDerivedClass(); SubDerivedClass subDerivedClass = superSubDerivedClass; DerivedClass derivedClass = superSubDerivedClass; BaseClass baseClass = superSubDerivedClass; superSubDerivedClass.DisplayName(); //SuperSubDerivedClass subDerivedClass.DisplayName(); //SubDerivedClass derivedClass.DisplayName();//SubDerivedClass baseClass.DisplayName();//BaseClass } }
这里最远的是SuperSubDerivedClass ,因为派生链最终指向了 SuperSubDerivedClass superSubDerivedClass= new SuperSubDerivedClass()
产生的实例对象,如果为:
using System; public class Program { public class BaseClass { public virtual void DisplayName() { Console.WriteLine("BaseClass"); } } public class DerivedClass : BaseClass { // Compiler WARNING: DisplayName() hides inherited // member. Use the new keyword if hiding wasb intended public override void DisplayName() { Console.WriteLine("DerivedClass"); } } public class SubDerivedClass : DerivedClass { public override void DisplayName() { Console.WriteLine("SubDerivedClass"); } } public class SuperSubDerivedClass : SubDerivedClass { public new void DisplayName() { Console.WriteLine("SuperSubDerivedClass"); } } public static void Main() { SuperSubDerivedClass superSubDerivedClass = new SuperSubDerivedClass(); DerivedClass derivedClass1 = new DerivedClass(); SubDerivedClass subDerivedClass = superSubDerivedClass; DerivedClass derivedClass = subDerivedClass; BaseClass baseClass = derivedClass1; superSubDerivedClass.DisplayName(); //SuperSubDerivedClass subDerivedClass.DisplayName(); //SubDerivedClass derivedClass.DisplayName();//SubDerivedClass baseClass.DisplayName();//DerivedClass } } }
baseClass.DisplayName()
, 这里的最远就是DerivedClass里new出来的实例。
密封方法,禁止重写
和密封类一样,也有密封方法,就是子类继承实现后,不允许孙类继续继承,sealed和override是紧密配合使用的!
namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter07.Listing07_15 { class A { public virtual void Method() { } } class B : A { public override sealed void Method() { } } class C : B { // ERROR: Cannot override sealed members //public override void Method() //{ //} } }
base修饰符
和this一样,base可以用于调用基类的成员,显式调用基类的实现,这个时候不用实现最远的派生,就是自己的方法实现(即使这个方法是虚方法)。
有了base修饰符,就能在代用派生类构造函数前确定要调用哪一个基类的构造函数。
public class PdaItem { public PdaItem(string name) { Name = name; } // ... public string Name { get; set; } } public class Contact : PdaItem { public Contact(string name) : base(name) { Name = name; } public new string Name { get; set; } // ... }
抽象类
抽象类说白了,就是仅供派生的类,无法实例化,与之相反的是,不抽象可直接实例化的是具体类。
public abstract class PdaItem { public PdaItem(string name) { Name = name; } public virtual string Name { get; set; } //虚成员已被基类实现,可以派生类被重写实现 public abstract string GetSummary(); //抽象方法未被基类实现,必须被派生类实现! }
虚成员已被基类实现,可以派生类被重写实现,抽象成员未被基类实现,必须被派生类实现!并且**抽象成员(未被实现的方法或属性)**自动为虚。
System.Object
在万类始祖提供的方法里,
模式匹配
这部分内容主要聊聊两个操作符:is和as
is操作符
is作用是检查一个对象是否兼容于其他指定的类型,并返回一个Bool值,如果一个对象是某个类型或是其父类型的话就返回为true,否则的话就会返回为false,永远不会抛出异常,如果对象引用为null,那么is操作符总是返回为false,因为没有对象可以检查其类型。
public static void Save(object data) { //is操作符的一般用法是向下显式转型之前,使用is来确定实例对象是否兼容于对应类型? if (data is string) //如果data为null也返回false { string text = (string)data; if (text.Length > 0) { data = Encrypt(text); // ... } } }
即使这个对象是类型的子类型,转型也会失败哦,易错点,记住了(虽然编译的时候不会报错):
从C#7.0开始,is增加了模式匹配:
public static void Save(object data) { if (data is string text && text.Length > 0) { data = Encrypt(text); // ... } else if (data is null) { throw new ArgumentNullException(nameof(data)); } // ... Console.WriteLine(data); }
data is string text
,如果data是string类型,则返回true,同时把值赋给text,这样就不需要两步操作了。主要用途就是判断一个数据项是否属于特定类型。
switch对模式匹配的支持
之前的博客里简单聊过从C#7.0开始,支持模式匹配,但没有详细介绍过:
static public void Eject(Storage storage) { switch (storage) { case null: // The location of case null doesn't matter throw new ArgumentNullException(nameof(storage)); // ** Causes compile error because case statments below // ** are unreachable // case Storage tempStorage: // throw new Exception(); // break; case UsbKey usbKey when usbKey.IsPluggedIn: usbKey.Unload(); Console.WriteLine("USB Drive Unloaded!"); break; case Dvd dvd when dvd.IsInserted: dvd.Eject(); Console.WriteLine("DVD Ejected!"); break; case Dvd dvd when !dvd.IsInserted: throw new ArgumentException("There was no DVD present."); case HardDrive hardDrive: throw new InvalidOperationException(); default: // The location of case default doesn't matter throw new ArgumentException(nameof(storage)); } }
有以下几点需要注意:
- 和基本switch语句不同,模式匹配case子句不限于有常量值的类型(string,int,long,enum等)。相反,任何类型都可使用。
- 模式匹配case标签在类型后声明一个变量:
case HardDrive hardDrive:
该变量的作用域限于当前switch小节(始于case标签,中间是一个或多个语句,结束于跳转语句。) - 模式匹配case标签支持条件表达式,允许对条件进行额外筛选,例如:
case UsbKey usbKey when usbKey.IsPluggedIn
- 模式匹配switch小节的顺序变得重要。为基类写一个case标签,且不添加任何条件表达式(例如只写case Storage storage:),后面为派生类写的switch块都不会执行而且不写条件表达式,后边如果有case语句,编译器会报错。但如果基类的case标签写了条件表达式(编译时解析不了),之后的派生类标签都会被屏蔽
- 为null写的switch小节可在任意位置,它解析为true的条件总是具有唯一性。(和基本switch语句一样,default标签位置还是随意。)
- 允许针对相同类型写多个模式匹配case标签,前提是其中最多一个没有条件表达式,而且要想有条件表达式的执行,这个没有的要放到最后
- 常量switch小节可以和模式匹配switch小节混合使用。当然优先考虑简化
- 模式匹配switch小节仍然需要跳转语句。
- case子句不允许使用可空类型(例如int?)。改为使用非可空版本。这是因为空值会匹配case null,永远不会匹配针对可空类型的case子句
暂时不需要太深入的去了解,在实践中验证吧。
as操作符
比起is操作符,as操作符的不同是:直接进行转型,转型成功则直接使用该类型的引用,如果转型失败返回null。
static object Print(IDocument document) { if(document != null) { // Print document... } else { } return null; } static void Main() { object data = new object(); // ... Print(data as Document); }
as有个缺点就是不能判断基础类型,也就是不能用在值类型里:AS是引用类型类型的转换或者装箱转换,不能用与值类型的转换。如果是值类型只能结合is来强制转换。
本篇博客详细介绍了继承这一特性,顺便梳理了下几个操作符的使用,对转型操作有了更深入的理解。