文档资料

继承

  • 一个类可以继承另一个类,从而对原有类进行扩展和自定义。
  • 可以叫做子类和父类。
  • 继承的类让你可以重用被继承类的功能。
  • C# 里一个类只能继承于另一个类,但是这个类却可以被多个类继承。换句话说,只能有一个亲爹(父类),多个干爹(接口),可以有多个儿子(子类)
public class Asset
{
	public string Name;
}

public class Stock : Asset
{
	public long SharesOwned;
}

public class House : Asset
{
	public decimal Mortgage;
}

Stock msft = new Stock { Name="MSFT", SharesOwned=1000 };

Console.WriteLine(msft.Name);	// MSFT
Console.WriteLine(msft.SharesOwned);	// 1000

House mansion = new House { Name="Mansion", Mortgage=250000 };

Console.WriteLine(mansion.Name);	// Mansion
Console.WriteLine(mansion.Mortgage);	// 250000

构造函数和继承

  • 子类必须声明自己的构造函数。
  • 从子类可访问父类的构造函数,但不是自动继承的。
  • 子类必须重新定义他想要暴露的构造函数。
  • 调用父类的构造函数需要使用 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;	// Executes 1st
	public D(int x) : base(x+1)	// Executes 2nd
	{
		...	// Executes 5th
	}
}

多态

  • 多态就是使得你能够用一种统一的方式来处理一组各具个性却同属一族的不同个体的机制。
  • 引用是多态的,类型为 x 的变量可以引用其子类的对象。
  • 子类具有附类的全部功能特性,所以参数可以是子类。
public static void Display(Asset asset)
{
	Console.WriteLine(asset.Name);
}

static void Main(){
	var msft = new Stock();
	var mansion = new House();

	Display(msft);	// Ok
	Display(mansion);	// Ok
}
  • 反过来不行,因为子类可能具有父类没有的成员。
static void Main()
{
	Display(new Asset());	// Compile-time error
}

public static void Display(House house)	// Will not accept Child
{
	Console.WriteLine(house.Mortgage);
}

引用转换

  • 一个对象的引用可以隐式的转换到其父类的引用(向上转换)。
  • 想转换到子类的引用,则需要显示转换(向下转换)。
  • 引用转换:创建了一个新的引用,它也指向同一个对象。

向上转换

  • 从子类的引用创建父类的引用。
Stock msft = new Stock();
Asset a = msft;	// Upcast
  • 变量 parent 依然指向同一个 Child 对象(child 也指向它)。
Console.WriteLine(a == msft);	// True
  • 尽管变量 parentchild 指向同一个对象,但是 parent 的可视范围更小一些。
Console.WriteLine(a.Name);	// Ok
Console.WriteLine(a.SharesOwned);	// Error: ChildProp undefined

向下转换

  • 从父类的引用创建出子类的引用。
Stock msft = new Stock();
Asset a = msft;	// Upcast	
Stock s = (Stock)a;	// Downcast
Console.WriteLine(s.SharesOwned);// <No error>
Console.WriteLine(s == a);	// True
Console.WriteLine(s == msft);	// True
  • 和向上转换一样,只涉及到引用,底层的对象不会受影响。
  • 需要显示转换,因为可能会失败。
House h = new House();
Asset a = h;	// Upcast always succeeds
Stock s = (Stock)a;	// Downcast fails: a is not a Stcok
  • 如如果向下转换失败,那么会抛出 InvalidCastException (属于运行时类型检查)。

as 操作符

  • as 操作符会直行,向下转换,如果转换失败,不会抛出异常,只会变成 null
Asset a = new Asset();
Stock s = a as Stock;	// c is null, no exception thrown
  • as 操作符无法做自定义转换。
int x = 3 as long;	// Compile-time error

is 操作符

  • is 操作符会检验应用的转换是否成功,换句话说,判断对象是否派生于某个类,或者实现了某个接口。
  • 通常用于向下转换前的验证。
if (a is Stock)
	Console.WriteLine(((Stock)a).SharesOwned);
  • 如果拆箱转换可以成功的话,那么使用 is 操作符的结果会是 true

模式变量

  • C#7 里,在使用 is 操作符的时候,可以引入一个变量。
if (a is Stock s)
	Console.WriteLine(s.SharesOwned);
  • 引用的变量可以立即使用。
if (a is Stock s && s.SharesOwned > 100000)
	Console.WriteLine("Wealthy");
else
	s = new Stock();	// c is in scope

Console.WriteLine(s.SharesOwned);	// Still in scope

Virtual 函数成员

  • 虚拟成员的作用是实现多态性。
  • 当调用一个对象的函数时,系统会直接去检查这个对象声明定义的类,即声明类,看所调用的函数是否为虚函数。
  • 如果不是虚函数,那么它就直接执行该函数。而如果有 virtual 关键字,也就是一个虚函数,那么这个时候它就不会立刻执行该函数了,而是转去检查对象的实例类。
  • 在这个实例类里,他会检查这个实例类的定义中是否有重新实现该虚函数(通过 override 关键字),如果有,则马上执行该实例类中的这个重新实现的函数。而如果没有,系统会不停地往上找实例类的父类,并对父类重复刚才在实例类里的检查,直到找到第一个重载了该虚函数的父类为止,然后执行该父类里重载后的函数。
  • 标记为 virtual 的函数,可以被子类重写,包括方法、属性、索引器、事件。
public class Asset
{
	public string Name;
	public virtual decimal Libility => 0;
}
  • 使用 override 修饰符子类,可以重写父类的 virtual 函数。
public class Stock : Asset
{
	public long SharesOwned;
}

public class House : Asset
{
	public decimal Mortage;
	public override decimal Liability => Mortgage;
}

House mansion = new House {Name="McMansion", Mortgage=250000};
Asset a = mansion;
Console.WriteLine(mansion.Liability);	// 250000
Console.WriteLine(a.Liability);	// 250000
  • virtual 方法和重写方法的签名、返回类型、可访问程度必须是一样的。
  • 重写方法里使用 base 关键字可以调用父类的实现。
  • 在构在构造函在构在构造函数里调用 virtual 方法可能比较危险,因为编写子类的开发人员可能不知道,他们在重写方法的时候面对的是一个未完全初始化的对象。
  • 换句话说,重写的方法可能会访问依赖于还未被构造函数初始化的字段的属性或方法。

隐藏被继承的成员

  • 父类和子类可以定义相同的成员,但这不能实现多态。
public class A { public int Counter = 1; }
public class B : A { public int Counter = 2; }
  • B 类中的 Counter 字段就隐藏了 A 里面的 Counter 字段(通常是偶然发生的)。例如子类添加某个字段之后,父类也添加了相同的一个字段。
  • 编译器会发出警告。
  • 按照如下规则进行解析:
    • 编译时对 A 的引用会绑定到 A.Counter
    • 编译时对 B 的引用会绑定到 B.Counter
A a = new A();
Console.WriteLine(a.Counter);	// 1
A b = new B();
Console.WriteLine(b.Counter);	// 2
A x = new B();
Console.WriteLine(x.Counter);	// 1
  • 如果希望派生类具有与基类中的成员同名的成员,则可以使用 new 关键字隐藏基类成员。 new 关键字放置在要 替换的类成员的返回类型之前。
public class BaseClass
{
	public virtual void Foo()
	{
		Console.WriteLine("BaseClass.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 函数了。

抽象类和抽象成员

  • 使用 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;
}

抽象方法和虚拟方法的区别

  • 对于抽象方法,子类必须实现它。
    • 抽象方法中无法声明方法体
  • 对于虚拟方法,子类可以重写也可以不重写。
    • 虚拟方法必须实现方法体
  • 因此,两者约束不同