类继承

通过继承可以定义一个新类,新类纳入一个已经声明的类并进行扩展。已存在的类称为基类(base class),新类称为派生类(derived class)。

  • 派生类扩展它的基类,因为它包含了基类的成员,还有派生类本身声明中的新增功能。
  • 派生类不能删除它所继承的任何成员。
1
2
3
4
class OtherClass : SomeClass
{
//...
}

访问继承的成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class SomeClass    //基类
{
public string Field1 = "base class field ";
public void Method1( string value ) {
Console.WriteLine($"Base class -- Method1: { value }");
}
}

class OtherClass: SomeClass { //派生类
public string Field2 = "derived class field";
public void Method2( string value ) {
Console.WriteLine($"Derived class -- Method2: { 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 ); //以派生字段为参数的派生方法
}
}
1
2
3
4
Base class -- Method1:    base class field
Base class -- Method1: derived class field
Derived class -- Method2: base class field
Derived class -- Method2: derived class field

所有类都派生自object类

除了特殊的类object,所有的类都是派生类,即使它们没有基类规格说明。类object是唯一的非派生类,因为它是继承层次结构的基础。

没有基类规格说明的类隐式地直接派生自类 object。不加基类规格说明只是指定 object 为基类的简写。这两种形式是语义等价的。
class SomeClass { ... } 与 class SomeClass : object { ... } 语义等价。

  • 一个类声明的基类规格说明中只能有一个单独的类。这称为单继承。
  • 基类和派生类是相对的术语。所有的类都是派生类,要么派生自object,要么派生自其他的类。所以,通常称一个类为派生类时,我们的意思是它直接派生自某类而不是object
1
2
3
4
5
6
7
8
9
10
class SomeClass
{ ... }

class OtherClass: SomeClass
{ ... }

class MyNewClass: OtherClass
{
//...
}

屏蔽基类的成员

虽然派生类不能删除它继承的任何成员,但可以用与基类成员名称相同的成员来屏蔽(mask)基类成员。这是继承的主要功能之一,非常实用。

  • 要屏蔽一个继承的数据成员,需要声明一个新的相同类型的成员,并使用相同的名称。
  • 通过在派生类中声明新的带有相同签名的函数成员,可以屏蔽继承的函数成员。请记住,签名由名称和参数列表组成不包括返回类型
  • 要让编译器知道你在故意屏蔽继承的成员,可使用new修饰符。否则,程序可以成功编译,但编译器会警告你隐藏了一个继承的成员。
  • 也可以屏蔽静态成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class SomeClass    //基类
{
public string Field1 = "SomeClass Field1";
public void Method1(string value)
{
Console.WriteLine($"SomeClass.Method1: { value }");
}
}

class OtherClass : SomeClass //派生类
{
new public string Field1 = "OtherClass Field1"; //屏蔽基类成员
new public void Method1(string value) //屏蔽基类成员
{
Console.WriteLine($"OtherClass.Method1: { value }");
}
}

class Program
{
static void Main()
{
OtherClass oc = new OtherClass(); //使用屏蔽成员
oc.Method1(oc.Field1); //使用屏蔽成员
}
}
1
OtherClass.Method1: OtherClass Field1

基类的访问

如果派生类必须访问被隐藏的继承成员,可以使用基类访问(base access)表达式。基类访问表达式由关键字 base 后面跟着一个点和成员的名称组成。Console.WriteLine("{0}", base.Field1);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class SomeClass {
public string Field1 = "Field1 -- In the base class";
}

class OtherClass : SomeClass {
public string Field1 = "Field1 -- In the derived class";
public void PrintField1()
{
Console.WriteLine(Field1); // 访问派生类
Console.WriteLine(base.Field1); // 访问基类
}
}

class Program {
static void Main()
{
OtherClass oc = new OtherClass();
oc.PrintField1();
}
}
1
2
Field1 -- In the derived class
Field1 -- In the base class

使用基类的引用

派生类的实例由基类的实例和派生类新增的成员组成。派生类的引用指向整个类对象,包括基类部分。
如果有一个派生类对象的引用,就可以获取该对象基类部分的引用(使用类型转换运算符把该引用转换为基类类型)。
类型转换运算符放置在对象引用的前面,由圆括号括起的要被转换成的类名组成。
将派生类对象强制转换为基类对象的作用是产生的变量只能访问基类的成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class MyBaseClass
{
public void Print()
{
Console.WriteLine("This is the base class.");
}
}

class MyDerivedClass : MyBaseClass
{
public int var1;

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();
}
}
1
2
This is the derived class.
This is the base class.

虚方法和覆写方法

虚方法可以使基类的引用访问“升至”派生类内。
可以使用基类引用调用派生类的方法,只需满足下面的条件:

  • 派生类的方法和基类的方法有相同的签名和返回类型。
  • 基类的方法使用 virtual 标注。
  • 派生类的方法使用 override 标注。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class MyBaseClass
{
virtual public void Print()
{
Console.WriteLine("This is the base class.");
}
}

class MyDerivedClass : MyBaseClass
{
override 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();
}
}
1
2
This is the derived class.
This is the derived class.
  • 覆写和被覆写的方法必须有相同的可访问性。如果被覆写的方法是 private 的,而覆写方法是 public 的,是不可以的。
  • 不能覆写 static 方法或非虚方法。
  • 方法、属性和索引器,以及另一种成员类型——事件,都可以被声明为 virtual 和 override

覆写标记为override的方法

覆写方法可以在继承的任何层次出现。

  • 当使用对象基类部分的引用调用一个被覆写的方法时,方法的调用被沿派生层次上溯执行,一直到标记为 override 的方法的最高派生(most-derived)版本。
  • 如果在更高的派生级别有该方法的其他声明,但没有被标记为 override,那么它们不会被调用。

公共部分(基类与派生类定义)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyBaseClass    // 基类
{
virtual public void Print()
{
Console.WriteLine("This is the base class.");
}
}

class MyDerivedClass : MyBaseClass // 派生类
{
override public void Print()
{
Console.WriteLine("This is the derived class.");
}
}

使用 override 的 SecondDerived时:
无论Print是通过派生类调用还是通过基类调用的,都会调用最高派生类中的方法。当通过基类调用时,调用沿着继承层次向上传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SecondDerived : MyDerivedClass
{
override public void Print() {
Console.WriteLine("This is the second derived class.");
}
}

class Program
{
static void Main()
{
SecondDerived derived = new SecondDerived(); // 使用 SecondDerived
MyBaseClass mybc = (MyBaseClass)derived; // 使用 MyBaseClass

derived.Print();
mybc.Print();
}
}
1
2
This is the second derived class.
This is the second derived class.

使用 new 的 SecondDerived时:
当通过SecondDerived的引用调用方法Print时,SecondDerived中的方法被执行。
当通过MyBaseClass的引用调用Print方法时,方法调用只向上传递了一级,到达类MyDerived

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class SecondDerived : MyDerivedClass
{
new public void Print()
{
Console.WriteLine("This is the second derived class.");
}
}

class Program
{
static void Main()
{
SecondDerived derived = new SecondDerived(); // 使用 SecondDerived
MyBaseClass mybc = (MyBaseClass)derived; // 使用 MyBaseClass

derived.Print();
mybc.Print();
}
}
1
2
This is the second derived class.
This is the derived class.

覆盖其他成员类型

在属性、事件以及索引器上virtual/override用法也是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class MyBaseClass
{
private int _myInt = 5;
virtual public int MyProperty
{
get { return _myInt; }
}
}

class MyDerivedClass : MyBaseClass
{
private int _myInt = 10;
override public 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 );
}
}
1
2
10
10

构造函数的执行

  • 要创建对象的基类部分,需要隐式调用基类的某个构造函数。
  • 继承层次链中的每个类在执行它自己的构造函数体之前执行它的基类构造函数。

创建一个实例的过程中,完成的第一件事是初始化对象的所有实例成员。在此之后,调用基类的构造函数,然后才执行该类自己的构造函数体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyDerivedClass : MyBaseClass
{
int MyField1 = 5; //1.成员初始化
int MyField2; // 成员初始化

public MyDerivedClass() //3.执行构造函数
{
// ...
}
}

class MyBaseClass
{
public MyBaseClass() //2.调用基类构造函数
{
// ...
}
}

构造函数初始化语句

默认情况下,在构造对象时,将调用基类的无参数构造函数。
但构造函数可以重载,所以基类可能有一个以上的构造函数。如果希望派生类使用一个指定的基类构造函数而不是无参数构造函数,必须在构造函数初始化语句中指定它。
有两种形式的构造函数初始化语句:

  • 第一种形式使用关键字 base 并指明使用哪个基类构造函数。
  • 第二种形式使用关键字 this 并指明应该使用当前类的哪一个构造函数。
1
2
3
4
public MyDerivedClass( int x, string s ) : base( s, x )
{
// ...
}
1
2
3
4
public MyClass(int x) : this(x, "Using Default String")
{
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class MyClass
{
readonly int firstVar;
readonly double secondVar;

public string UserName;
public int UserIdNumber;

private MyClass() //私有构造函数执行其他构造
{
//函数共用的初始化
firstVar = 20;
secondVar = 30.5;
}

public MyClass( string firstName ) : this() //使用构造函数初始化语句
{
UserName = firstName;
UserIdNumber = -1;
}

public MyClass( int idNumber ) : this() //使用构造函数初始化语句
{
UserName = "Anonymous";
UserIdNumber = idNumber;
}
}

类访问修饰符

类可以被系统中其他类看到并访问。可访问(accessible)有时也称为可见(visible)。类的可访问性有两个级别:publicinternal

  • 标记为public的类可以被系统内任何程序集中的代码访问。public class MyBaseClass { ... }
  • 标记为internal的类只能被它自己所在的程序集内的类看到。
    • 这是默认的可访问级别,所以,除非在类的声明中显式地指定修饰符public,否则程序集外部的代码不能访问该类。
    • 可以使用internal访问修饰符显式地声明一个类为内部的。internal class MyBaseClass {...}

程序集间的继承

C#允许从一个在不同的程序集内定义的基类来派生类。

  • 基类必须被声明为public,这样才能从它所在的程序集外部访问它。
  • 必须在Visual Studio工程中的References节点中添加对包含该基类的程序集的引用。可以在Solution Explorer中找到该标题。

要使引用其他程序集中的类和类型更容易,不使用它们的完全限定名称,可以在源文件的顶部放置一个using指令,并带上将要访问的类或类型所在的命名空间。

1
2
3
4
5
6
7
8
9
10
// Assembly1.cs
using System;
namespace BaseClassNS //包含基类声明的命名空间
{
public class MyBaseClass { //把该类声明为公有的,使它对程序集的外部可见
public void PrintMe() {
Console.WriteLine("I am MyBaseClass");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Assembly2.cs
using System;
using BaseClassNS; //包含基类声明的命名空间
namespace UsesBaseClass
{
class DerivedClass: MyBaseClass //其他程序集中的基类
{
// 空类体
}
class Program {
static void Main()
{
DerivedClass mdc = new DerivedClass();
mdc.PrintMe();
}
}
}
1
I am MyBaseClass

成员访问修饰符

  • 所有显式声明在类声明中的成员都是互相可见的,无论它们的访问性如何。
  • 继承的成员不在类的声明中显式声明,所以,继承的成员对派生类的成员可以是可见的,也可以是不可见的。
  • 以下是5个成员访问级别的名称:
    • public
    • private
    • protected
    • internal
    • protected internal
  • 必须对每个成员指定成员访问级别。如果不指定某个成员的访问级别,它的隐式访问级别为private
  • 成员的可访问性不能比它的类高。也就是说,如果一个类的可访问性限于它所在的程序集,那么类的成员在程序集的外部也不可见,无论它们的访问修饰符是什么,public也不例外。

访问成员的区域

1
2
3
4
5
6
7
8
9
public class MyClass
{
public int Member1;
private int Member2;
protected int Member3;
internal int Member4;
protected internal int Member5;
// ...
}

另一个类(如类B)能否访问这些成员取决于该类的两个特征:

  • 类B是否派生自 MyClass 类。
  • 类B是否和 MyClass 类在同一程序集。
    这两个特征划分出4个集合:
  • 在同一程序集且继承 MyClass(右下)。
  • 在同一程序集但不继承 MyClass(左下)。
  • 在不同的程序集且继承 MyClass(右上)。
  • 在不同的程序集且不继承 MyClass(左上)。
    这些特征用于定义 5 种访问级别。


![[csharp-10-oop-accessible.png]]

公有成员的可访问性

public 访问级别是限制最少的。
所有的类,包括程序集内部的类和外部的类都可以自由地访问成员。
要声明一个公有成员,使用 public 访问修饰符。

1
public int Member1;


![[csharp-10-oop-accessible1.png]]

私有成员的可访问性

private 访问级别是限制最严格的。

  • private 类成员只能被它自己的类的成员访问。它不能被其他的类访问,包括继承它的类
  • 然而,private 成员能被嵌套在它的类中的类成员访问(嵌套类)。


![[csharp-10-oop-accessible2.png]]

受保护成员的可访问性

protected 访问级别如同 private 访问级别,但它允许派生自该类的类访问该成员。即使程序集外部继承该类的类也能访问该成员。


![[csharp-10-oop-accessible3.png]]

内部成员的可访问性

标记为 internal 的成员对程序集内部的所有类可见,但对程序集外部的类不可见。


![[csharp-10-oop-accessible4.png]]

受保护内部成员的可访问性

标记为 protected internal 的成员对所有继承该类的类以及程序集内部的所有类可见。注意,允许访问的集合是 protected 修饰符允许访问的类的集合加上 internal 修饰符允许访问的类的集合。注意,这是 protected 和 internal 的并集,不是交集。


![[csharp-10-oop-accessible5.png]]

成员访问修饰符小结

成员的访问修饰符:

修饰符 含义
private 只在类的内部可访问
internal 对该程序集内所有类可访问
protected 对所有继承该类的类可访问
protected internal 对所有继承该类或在该程序集内声明的类可访问
public 对任何类可访问

成员的可访问性总结:

修饰符 同一程序集内的类(非派生) 同一程序集内的类(派生) 不同程序集内的类(非派生) 不同程序集内的类(派生)
private
internal
protected
protected internal
public

抽象成员

抽象成员是指设计为被覆写的函数成员。

  • 必须是一个函数成员。也就是说,字段和常量不能为抽象成员
  • 必须用 abstract 修饰符标记。
  • 不能有实现代码块。抽象成员的代码用分号表示。
1
2
3
4
5
6
abstract public void PrintStuff(string s);   //分号替换实现
abstract public int MyProperty //分号替换实现
{
get;
set;
}

一共有4种类型的成员可以声明为抽象的:

  • 方法
  • 属性
  • 事件
  • 索引器

尽管抽象成员必须在派生类中用相应的成员覆写,但不能把 virtual 修饰符附加到 abstract 修饰符。
类似于虚成员,派生类中抽象成员的实现必须指定 override 修饰符。

对比项 虚成员 抽象成员
关键字 virtual abstract
实现体 有实现体 没有实现体,被分号取代
在派生类中被覆写 能被覆写,使用 override 必须被覆写,使用 override
成员的类型 方法

属性

事件

索引器
方法

属性

事件

索引器

抽象类

抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。

  • 不能创建抽象类的实例。
  • 抽象类使用abstract修饰符声明。
1
abstract class MyClass { ... }
  • 抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带实现的成员的任意组合。
  • 抽象类自己可以派生自另一个抽象类。
  • 任何派生自抽象类的类必须使用override关键字实现该类所有的抽象成员,除非派生类自己也是抽象类。
1
2
3
4
5
6
7
8
abstract class AbClass //抽象类 
{
//...
}
abstract class MyAbClass : AbClass //派生自抽象类的抽象类
{
//...
}

抽象类和抽象方法的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
abstract class AbClass    //抽象类
{
public void IdentifyBase() //普通方法
{
Console.WriteLine("I am AbClass");
}

abstract public void IdentifyDerived(); //抽象方法
}

class DerivedClass : AbClass //派生类
{
override public void IdentifyDerived() //抽象方法的实现
{
Console.WriteLine("I am DerivedClass");
}
}

class Program
{
static void Main()
{
// AbClass a = new AbClass(); //错误,抽象类不能实例化
DerivedClass b = new DerivedClass(); //实例化派生类
b.IdentifyBase(); //调用继承的方法
b.IdentifyDerived(); //调用“抽象”方法
}
}
1
2
I am AbClass
I am DerivedClass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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);
}

private int _myInt;

public override int MyInt
{
get { return _myInt; }
set { _myInt = value; }
}
}

class Program
{
static void Main(string[] args)
{
MyClass mc = new MyClass();
mc.PrintStuff("This is a string.");
mc.MyInt = 28;
Console.WriteLine(mc.MyInt);
Console.WriteLine($"Perimeter Length: { mc.PerimeterLength() }");
}
}
1
2
3
This is a string.
28
Perimeter Length: 30

密封类

抽象类必须用作基类,它不能像独立的类对象那样被实例化。密封类与它相反。

  • 密封类只能被用作独立的类,它不能被用作基类。将它用作其他类的基类会产生编译错误。
  • 密封类使用 sealed 修饰符标注。
1
2
3
4
sealed class MyClass
{
// ...
}

静态类

静态类中所有成员都是静态的。静态类用于存放不受实例数据影响的数据和函数。
静态类的一个常见用途可能是创建一个包含一组数学方法和值的数学库。

  • 类本身必须标记为 static
  • 类的所有成员必须是静态的。
  • 类可以有一个静态构造函数,但不能有实例构造函数,因为不能创建该类的实例。
  • 静态类是隐式密封的,也就是说,不能继承静态类。
  • 可以使用类名和成员名,像访问其他静态成员那样访问静态类的成员。从C# 6.0开始,也可以通过使用 using static 指令来访问静态类的成员,而不必使用类名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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("{0} is odd is {1}.", val, MyMath.IsOdd(val));
Console.WriteLine($"{"val } * 2 = { MyMath.Times2(val) }.");
}
}
1
2
3 is odd is True.
3 * 2 = 6.

扩展方法

目前为止的每个方法都和声明它的类关联。扩展方法特性扩展了这个边界,允许编写的方法和声明它的类之外的类关联。
在现实世界的开发中,扩展方法是一个特别有用的工具。事实上,几乎整个LINQ库都是通过扩展方法来实现的。

  • 声明扩展方法的类必须声明为static
  • 扩展方法本身必须声明为static
  • 扩展方法必须包含关键字this作为它的第一个参数类型,并在后面跟着它扩展的类的名称。

    ![[csharp-10-oop-kuozhan-func.png]]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
namespace ExtensionMethods
{
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; }
}

static class ExtendMyData
{
public static double Average(this MyData md) //声明为静态的
{
return md.Sum() / 3;
}
}
}

class Program
{
static void Main()
{
MyData md = new MyData(3, 4, 5);
Console.WriteLine($"Sum: { md.Sum() }");
Console.WriteLine("Average: {0}", md.Average());
}
}
1
2
Sum: 12
Average: 4

命名约定

风格名称 描述 推荐使用 示例
Pascal 大小写 标识符中每个单词的首字母大写 用于类型名称和类中对外可见成员的名称。涉及的名称包括:类、方法、命名空间、属性和公有字段 CardDeck, Dealershand
Camel 大小写 标识符中每个单词的首字母大写,第一个单词除外 用于局部变量的名称和方法声明的形参名称 totalCycleCount, randomSeedParam
下划线加 Camel 大小写 以下划线开头的 Camel 大小写的标识符 用于私有和受保护的字段 _cycleCount, _selectedIndex