继承
- 一个类可以继承另一个类,从而对原有类进行扩展和自定义
- 可以叫做父类和子类
- 继承的类让你可以重用被继承类的功能
- C#里,一个类只能继承于一个类,但是这个类却可以被多个类继承
public class Asset
{
public string Name;
}
public class Stock: Asset
{
public long SharesOwned;
}
...
Stock msft = new Stock { Name = "MSFT", SharesOwned = 1000}
Console.WriteLine(msft.Name); //MSFT
Console.WriteLine(msft.SharesOwned) // 1000
多态
- 引用是多态的,类型为x的变量可以引用其子类的对象
- 因为子类具有父类的全部功能特性,所以参数可以是子类
public static void Display (Asset asset)
{
System.Console.WriteLine(asset.Name)
}
Stock msft = new Stock { Name = "MSFT", SharesOwned = 1000};
Diaplay(msft);
- 反过来不行
引用转换
- 一个对象的引用可以隐式的转换到其父类的引用(向上转换)
- 想转换到子类的引用则需要显式转换 (向下转换)
- 引用转换: 创建了一个新的引用,它也指向同一个对象
向上转换
- 从子类的引用创建父类的引用
Stock msft = new Stock();
Asset a = msft;
- 变量a依然指向同一个Stock对象
Console.WriteLine(a == msft) //Ture
- 尽管变量a和msft指向同一个对象,但是a的可视范围更小一点
Console.WriteLine(a.Name); //OK
Console.WriteLine(a.SharedOwned); //Error
向下转换
- 从父类的引用创建出子类的引用
Stock msft = new Stock();
Asset a = msft; //Upcast
Stock s = (Stock)a; //DownCast
Console.WriteLine(s.SharesOwned); //
Console.WriteLine(s == a); // True
Console.WriteLine(s == msft); //True
- 和向上转换一样,只涉及到引用,底层的对象不会受影响
- 需要显式转换,因为可能会失败
as操作符
- as操作符会执行向下转换。如果转换失败,不会抛出异常,值会变为null
Asset a = new Asset();
Stock s = a as Stock; //s is null; no exception thrown
- as 操作符无法做自定义转换
is操作符
- is操作符会检验引用的转换是否成功。换句话说,判断对象是否派生于某个类(或者实现了某个接口)
- 通常用于向下转换前的验证
if(a is Stock)
Console.WriteLine( ((Stock)a).SharedOwned);
- 如果拆箱转换可以成功的话,那么使用is操作符的结果会是true
- c#7里, 在使用is操作符的时候,可以引入一个变量
if (a is Stock s)
Console.WriteLine(s.SharedOwned);
- 引入的变量可以立即消费
if (a is Stock s && s.SharedOwned > 100000)
Console.WriteLine('Wealthy");
else
s = new Stock(); //s is in scope
Console.WriteLine(s.SharedOwned;
Virtual 函数成员
- 标记为virtual的函数可以被子类重写,包括方法,属性,索引器,事件
public class Asset
{
public string Name;
public virtual decimal Liability => 0;
}
Override 重写
- 使用override 修饰符, 子类可以重写父类中标记为virtual的函数
public class Stock: Asset
{
public long SharesOwned;
}
public class House : Asset
{
public decimal Mortage;
public override decimal Liability => Mortage;
}
...
House mansion = new House { Name ="McMansion", Mortgage = 2500000};
Asset a = massion;
Console.WriteLine(mansion.Liability); //2500000
Console.WriteLine(a.Liability); //2500000
- Virtual方法和重写方法的签名、返回类型、可访问程度必须是一样的
- 重写方法使用base关键字可以调用父类的实现
注意:
在构造函数里调用virtual方法可能比较危险,因为编写子类的开发人员可能不知道他们在重写方法的时候,面对的是一个未完全初始化的对象
换句话说,重写的方法可能会访问依赖于还未被构造函数初始化的字段的属性或方法
抽象类和抽象成员
- 使用abstract声明的类是抽象类
- 抽象类不可以被实例化,只有其本身的子类才可以实例化
- 抽象类可以定义抽象成员
-抽象成员和virtual成员很像,但是不提供具体的实现。子类必须提供实现,除非子类也是抽象的
public abstract class Asset
{
//Note empty implementation
public abstract decimal NetValue {get;}
}
public class Stock: Asset
{
public long SharesOwned;
public decimal CurrentPrice;
//Override like a virtual method;
public override decimal NetValue => CurrentPrice * SharesOwned
}
隐藏被继承的成员
- 父类和子类可以定义相同的成员
public class A {public int Counter = 1;}
public class B: A { public int Counter = 2;}
class B中的Counter字段就隐藏了A里面的Couter字段(通常是偶然发生的)例如子类添加某个字段之后
编译器会发出警告
隐藏被继承的成员
按照如下规则进行解析:
- 编译时对A的引用会绑定到A.Counter
- 编译时对B的引用会绑定到B.Counter
class Program
{
static void Main()
{
A a = new A();
System.Console.WriteLine(a.Counter);//1
B b = new B();
System.Console.WriteLine(b.Counter); //2
A x = new B ();
System.Console.WriteLine(x.Counter); //1
}
}
- 如果像故意隐藏父类的成员,可以在子类的成员前面加上new修饰符
- 这里的new修饰符仅仅会抑制编译器的警告而已
public class A { public int Counter = 1; }
public class B: A {public new int Counter = 2;}
New vs Override
public class BaseClass
{
public virtual void Foo() { Console.WriteLine('Overrider.Foo");}
}
public class Overrider: BaseClass
{
public override void Foo() {Console.WriteLine("Overrider.Foo")}
}
public class Hider: BaseClass
{
public new void Foo() {Console.WriteLine("Hider.Foo");}
}
Overrider over = new Overrider();
BaseClass b1 = over;
over.Foo(); //Overrider.Foo
b1.Foo(); //Overrider.Foo
Hider h = new Hider();
BaseClass b2 = h;
h.Foo(); //Hider.Foo
b2.Foo(); //BaseClass.Foo
Sealed
- 针对重写的成员,可以使用sealed关键字把它密封起来,防止它被其他子类重写
public sealed override decimal Liability {get {return Mortgage;}}
- 也可以sealed类本身,就隐式的sealed所有的virtual函数了
Base 关键字
base和this略像,base主要用于:
- 从子类访问父类里被重写的函数
- 调用父类的构造函数
public class House: Asset
{
...
public override decimal Liability => base.Liabiliry + Mortgage;
}
- 这种写法可以保证,访问的一定是Asset的Liability, 无论该属性是被重写还是被隐藏了
构造函数和继承
- 子类必须声明自己的构造函数
- 从子类可访问父类的构造函数,但不是自动继承来的
- 子类必须重新定义它想要暴露的构造函数
public class Baseclass
{
public int x;
public Baseclass() {}
public Baseclass(int x) {this.x = x;}
}
public class Subclass: Baseclass {
public Subclass(int x): base(x) { }
}
class Program
{
static void Main(string[] args)
{
Subclass s = new Subclass(123);
}
}
- 调用父类的构造函数需要使用base关键字
- 父类的构造函数肯定会执行
隐式调用无参的父类构造函数
- 如果子类的构造函数里没有使用base关键字,那么父类的无参构造函数会被隐式的调用
public class Baseclass
{
public int x;
public Baseclass() {x = 1;}
}
public class Subclass: Baseclass
{
public Subclass() { Console.WriteLine(x); } // 1
}
- 如果父类没有无参构造函数,那么子类就必须在构造函数里使用base
构造函数和字段初始化顺序
对象被实例化时, 初始化动作按照如下顺序进行:
- 从子类到父类:
- 字段被初始化
- 父类构造函数的参数值被算出
- 从父类到子类
- 构造函数体被执行
public class B
{
int x = 1; //Executes 3rd
public B (int x)
{
... // Executes 4th
}
}
public class D:B
{
int y = 1; //Execites 1st
public D(int x) :base (x +1) //Executes 2nd
{
... //Executes 5th
}
}
重载和解析
static void Foo (Assst a) { }
static void Foo (House s) { }
- 重载方法被调用时,更具体的类型具有更高的优先级
House h = new House(...);
Foo(h); //Calls Foo(house)
- 调用哪个重载方法是在编译时就确定下来了
Asset a = new House(...);
Foo(a); //calls Foo(Asset)