0. 为什么这次更新这么久
- 上周六,周日,周一:电脑掉在出租车上,而我已经坐上回家的火车。
- 并且,上周六到这周三:一个非常非常巨大的变故。
- 最后,这周四到昨天发高烧, 38.8 度。
不过既然你们看到这一篇,说明之后会慢慢恢复更新速度了。我尽量不让变故影响到自己。
1. 继承
继承本身是个什么就不说了,实在是 OOP 太常用的概念了。 C# 只允许继承一个基类(base class)。不过具有传递性(transitive),B 继承 A,C 继承 B 这样。
1.1 不会继承下去的成员
以 B 继承 A 为例。A 中的一些成员不会被 B 继承。这些成员有:
- 静态构造函数。用于对静态数据进行初始化。
- 实例构造函数。也就是我们常说的构造函数。(虽然不继承,但可以调用)
- 终结器。和垃圾回收相关。
1.2 类成员的可访问性
这里我们只讨论继承的情况。
1.2.1 private 成员
即使是继承类,也无法访问基类的 private 成员。
但是也有例外,私有变量可以被嵌套继承类所访问。如下面代码中的 B 就可以访问,但 C 不行。
public class A
{
private int value = 3;
public class B : A
{
public int GetValue()
{
return this.value;
}
}
}
public class C : A
{
}
class Program
{
static void Main(string[] args)
{
A.B b = new A.B(); // 因为 B 在 A 里面,因此可以通过方法来访问。
C c = new C(); // 虽然 C 继承自 A,但无法访问 value
Console.WriteLine(b.GetValue()); // 可以获取 value
}
}
即使把 getValue 方法放到 C 里也无法访问。
话说我突然安意识到 C# 语法一直让我难以区分的一点,就是函数根本没有专有关键词,不存在什么 def,func 之类的东西。直接就是 int GetValue() ,前面加一个返回值就是函数了,把 int 换成 class 就是类。把括号去掉就变成变量。
1.2.2 protected 成员
相比 private,protected 可以被继承的类所访问了。不过实例代码所在的地方也会影响能否使用,如夏例子:
可以发现并不能访问 Method1(),因为实例化的地方在继承类之外,所以得通过注释里的代码间接访问:
1.2.3 internal 成员
像是上面的问题,在 internal 和 public 里就不会出现,可以直接通过 b.Method1() 访问。
可以被同一程序集中的继承类访问。在不同程序集里,继承类照样无法访问该成员。
1.2.4 public 成员
public 可以被继承的类访问。
1.3 override 和 virtual
如果想要在继承类里改写基类的某个成员,就需要在基类里给该成员加上 virtual 关键字,并给继承类加上 override 关键字。
如果没加上 virtual 关键字,override 会报错。
只加上 virtual,不加 overrride 则没有问题。因为只是给继承类可以覆盖的可能。
1.4 abstract
如果我们想要让继承类必需覆盖基类成员的话,则要用到 abstract 关键字。
abstract 比较特殊:
- 类和成员必需同时加上 abstract 关键字才行。不然会报错。
- 成员不能有 implementation。这个很合理,既然是必定会被覆盖的,那么写 implementation 也没有意义。
- abstract 类并不代表方法也都要是 abstract 的。完全可以在 abstract 类里写 virtual 方法或者不能被覆盖的普通方法。abstract 加在 class 前面表示这个类是个抽象类不能被实例化,但可以在继承类里访问这些方法。
1.5 只有类和接口有继承的概念。
struct,enum,delegate 等是没有继承的概念的。
1.6 隐性继承
实际上,所有的类型都直接或间接继承自 Object。Object 的所有特性,所有类型都可以使用。
比如我们先定义一个空类:
public class SimpleClass { }
然后我们通过反射(以后再讲)查看这个类的成员可以看到其包含 9 个成员,其中 1 个是默认构造函数,另外 8 个则是从 Object 里继承而来:
- ToString 返回字符串表现形式,此例中返回类名:"SimpleClass"
- 接下来 3 个方法都是为了测试两个对象是否相等。一般这些方法测试的是两个变量是否引用相等。也就是说被比较的变量们必须指向同一个对象。
- GetHashCode 方法,会计算出一个值,用于 hashed collection
- GetType 方法,获取一个 Type Object,本例中为 SimpleClass 类型。和 ToString 不一样,这个返回的不是字符串,只不过用 Console.WriteLine 打印的时候会变成字符串罢了。
- Finalize 方法,用于垃圾回收。
- MemberwiseClone,会创建一个当前对象的浅克隆。
可以在类定义里 overrride 掉 ToString 方法来改写返回值。
2. 设计基类和继承类
由于 oop 的灵活性,以及 C# 提供了这么多关键字。导致设计成为了一个比较重要的一环。 比如现在就举一个例子,我们有一个 Publication 的基类,然后衍生出 Book,然后 Magazine 等。
2.1 设计思路
2.1.1 综观
- 我们要设计的地方有很多,比如说这个基类应该包含哪些成员,然后一些方法成员是否提供 implementation,以及这个基类是否应该作为一个 abstract 基类。
- 非 abstract 方法的一个好处是可以复用代码。避免在多个继承类里写同样的代码,也可以避免很多 bugs 的产生。
2.1.2 继承关系层数
- oop 设计是很灵活的。比如说我们的例子。虽然我们确定了基类就是 Publication,但是之后我们既可以直接从 Publication 中衍生出 Magazine,也可以先衍生出 Periodical,再衍生下去。
- 我们的例子中,我们是 Publication->Book->Magazine 这种。
2.1.3 实例化是否 make sense
- 如果不 make sense,那么直接换成 abstract 类即可。
- 如果 make sense,那么就用构造函数来实例化。当然你会发现即使你不写构造函数也不会报错也可以实例化。那是因为编译器帮你生成了一个无参数构造函数(上一节已经讲过 .ctor 那个)
- 在我们的例子中,由于 Publication 实例化不 make sense,因此我们将其设为 abstract class。但是不包含 abstract method。 像这种无 abstract 方法的 abstract 类,一般是一个抽象概念,这个概念被一些具体类(后面的 Book 等)共享。
2.1.4 继承类里是否有部分成员需要覆盖基类方法
- 如果有的话,得用 virtual 和 override 配合。
2.1.5 某个继承类是否为层级的最后一级
任何继承类都可以作为其他类的基类。不过必要的时候,也可以加上 sealed 关键字表示该类为最后一层,无法被作为基类了。
2.2 例子-Publication
直接给代码:
using System;
public enum PublicationType { Misc, Book, Magazine, Article };
public abstract class Publication
{
private bool published = false;
private DateTime datePublished;
private int totalPages;
public Publication(string title, string publisher, PublicationType type)
{
if (String.IsNullOrWhiteSpace(publisher))
throw new ArgumentException("The publisher is required.");
Publisher = publisher;
if (String.IsNullOrWhiteSpace(title))
throw new ArgumentException("The title is required.");
Title = title;
Type = type;
}
public string Publisher { get; }
public string Title { get; }
public PublicationType Type { get; }
public string CopyrightName { get; private set; }
public int CopyrightDate { get; private set; }
public int Pages
{
get { return totalPages; }
set
{
if (value <= 0)
throw new ArgumentOutOfRangeException("The number of pages cannot be zero or negative.");
totalPages = value;
}
}
public string GetPublicationDate()
{
if (!published)
return "NYP";
else
return datePublished.ToString("d");
}
public void Publish(DateTime datePublished)
{
published = true;
this.datePublished = datePublished;
}
public void Copyright(string copyrightName, int copyrightDate)
{
if (String.IsNullOrWhiteSpace(copyrightName))
throw new ArgumentException("The name of the copyright holder is required.");
CopyrightName = copyrightName;
int currentYear = DateTime.Now.Year;
if (copyrightDate < currentYear - 10 || copyrightDate > currentYear + 2)
throw new ArgumentOutOfRangeException($"The copyright year must be between {currentYear - 10} and {currentYear + 1}");
CopyrightDate = copyrightDate;
}
public override string ToString() => Title;
}
- 明明是 abstract,为什么还有构造函数。当然可以有。只是无法用这个构造函数来实例化 Publication 实例罢了。但是可以在继承类里使用这个构造函数。这个在上一篇文章里也提到过。
2.3 例子 Book
using System;
public sealed class Book : Publication
{
public Book(string title, string author, string publisher) :
this(title, String.Empty, author, publisher)
{ }
public Book(string title, string isbn, string author, string publisher) : base(title, publisher, PublicationType.Book)
{
// isbn argument must be a 10- or 13-character numeric string without "-" characters.
// We could also determine whether the ISBN is valid by comparing its checksum digit
// with a computed checksum.
//
if (! String.IsNullOrEmpty(isbn)) {
// Determine if ISBN length is correct.
if (! (isbn.Length == 10 | isbn.Length == 13))
throw new ArgumentException("The ISBN must be a 10- or 13-character numeric string.");
ulong nISBN = 0;
if (! UInt64.TryParse(isbn, out nISBN))
throw new ArgumentException("The ISBN can consist of numeric characters only.");
}
ISBN = isbn;
Author = author;
}
public string ISBN { get; }
public string Author { get; }
public Decimal Price { get; private set; }
// A three-digit ISO currency symbol.
public string Currency { get; private set; }
// Returns the old price, and sets a new price.
public Decimal SetPrice(Decimal price, string currency)
{
if (price < 0)
throw new ArgumentOutOfRangeException("The price cannot be negative.");
Decimal oldValue = Price;
Price = price;
if (currency.Length != 3)
throw new ArgumentException("The ISO currency symbol is a 3-character string.");
Currency = currency;
return oldValue;
}
public override bool Equals(object obj)
{
Book book = obj as Book;
if (book == null)
return false;
else
return ISBN == book.ISBN;
}
public override int GetHashCode() => ISBN.GetHashCode();
public override string ToString() => $"{(String.IsNullOrEmpty(Author) ? "" : Author + ", ")}{Title}";
}
- 两个构造函数:当参数数量不同的时候,使用不同的构造函数。可以看到第一个构造函数使用 :this 来调用第二个构造函数,第二个构造函数则是调用基类的构造函数。少参数的构造函数会去调用多参数的构造函数并提供默认值,这种方式叫做构造函数链。
- 不仅改写了 ToString,甚至还改写了 Equals。因为如果没有被 overrriden,Equal 测试的只是引用相等。改写 Equals 的同时应该改写 GetHashCode。GetHashCode 应该和 Equals 保持一致,本例因为比较的是 ISBN 号,因此 GetHashCode 也用 ISBN 的该方法。