从零开始独立游戏开发学习笔记(十六)--Unity学习笔记(五)--微软C#指南(二)

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 的该方法。