欢迎光临散文网 会员登陆 & 注册

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

2021-10-31 01:04 作者:oyishyi  | 我要投稿

0. 为什么这次更新这么久

  • 上周六,周日,周一:电脑掉在出租车上,而我已经坐上回家的火车。

  • 并且,上周六到这周三:一个非常非常巨大的变故。

  • 最后,这周四到昨天发高烧, 38.8 度。

不过既然你们看到这一篇,说明之后会慢慢恢复更新速度了。我尽量不让变故影响到自己。

1. 继承

继承本身是个什么就不说了,实在是 OOP 太常用的概念了。 C# 只允许继承一个基类(base class)。不过具有传递性(transitive),B 继承 A,C 继承 B 这样。

1.1 不会继承下去的成员

以 B 继承 A 为例。A 中的一些成员不会被 B 继承。这些成员有:

  1. 静态构造函数。用于对静态数据进行初始化。

  2. 实例构造函数。也就是我们常说的构造函数。(虽然不继承,但可以调用)

  3. 终结器。和垃圾回收相关。

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 比较特殊:

  1. 类和成员必需同时加上 abstract 关键字才行。不然会报错。

  2. 成员不能有 implementation。这个很合理,既然是必定会被覆盖的,那么写 implementation 也没有意义。

  3. abstract 类并不代表方法也都要是 abstract 的。完全可以在 abstract 类里写 virtual 方法或者不能被覆盖的普通方法。abstract 加在 class 前面表示这个类是个抽象类不能被实例化,但可以在继承类里访问这些方法。

1.5 只有类和接口有继承的概念。

struct,enum,delegate 等是没有继承的概念的。

1.6 隐性继承

实际上,所有的类型都直接或间接继承自 Object。Object 的所有特性,所有类型都可以使用。
比如我们先定义一个空类:

public class SimpleClass { }

然后我们通过反射(以后再讲)查看这个类的成员可以看到其包含 9 个成员,其中 1 个是默认构造函数,另外 8 个则是从 Object 里继承而来:

  1. ToString 返回字符串表现形式,此例中返回类名:"SimpleClass"

  2. 接下来 3 个方法都是为了测试两个对象是否相等。一般这些方法测试的是两个变量是否引用相等。也就是说被比较的变量们必须指向同一个对象。

  3. GetHashCode 方法,会计算出一个值,用于 hashed collection

  4. GetType 方法,获取一个 Type Object,本例中为 SimpleClass 类型。和 ToString 不一样,这个返回的不是字符串,只不过用 Console.WriteLine 打印的时候会变成字符串罢了。

  5. Finalize 方法,用于垃圾回收。

  6. MemberwiseClone,会创建一个当前对象的浅克隆。

可以在类定义里 overrride 掉 ToString 方法来改写返回值。

2. 设计基类和继承类

由于 oop 的灵活性,以及 C# 提供了这么多关键字。导致设计成为了一个比较重要的一环。 比如现在就举一个例子,我们有一个 Publication 的基类,然后衍生出 Book,然后 Magazine 等。

2.1 设计思路

2.1.1 综观

  1. 我们要设计的地方有很多,比如说这个基类应该包含哪些成员,然后一些方法成员是否提供 implementation,以及这个基类是否应该作为一个 abstract 基类。

  • 非 abstract 方法的一个好处是可以复用代码。避免在多个继承类里写同样的代码,也可以避免很多 bugs 的产生。

2.1.2 继承关系层数

  1. oop 设计是很灵活的。比如说我们的例子。虽然我们确定了基类就是 Publication,但是之后我们既可以直接从 Publication 中衍生出 Magazine,也可以先衍生出 Periodical,再衍生下去。

  2. 我们的例子中,我们是 Publication->Book->Magazine 这种。

2.1.3 实例化是否 make sense

  1. 如果不 make sense,那么直接换成 abstract 类即可。

  2. 如果 make sense,那么就用构造函数来实例化。当然你会发现即使你不写构造函数也不会报错也可以实例化。那是因为编译器帮你生成了一个无参数构造函数(上一节已经讲过 .ctor 那个)

  3. 在我们的例子中,由于 Publication 实例化不 make sense,因此我们将其设为 abstract class。但是不包含 abstract method。 像这种无 abstract 方法的 abstract 类,一般是一个抽象概念,这个概念被一些具体类(后面的 Book 等)共享。

2.1.4 继承类里是否有部分成员需要覆盖基类方法

  1. 如果有的话,得用 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}"; }

  1. 两个构造函数:当参数数量不同的时候,使用不同的构造函数。可以看到第一个构造函数使用 :this 来调用第二个构造函数,第二个构造函数则是调用基类的构造函数。少参数的构造函数会去调用多参数的构造函数并提供默认值,这种方式叫做构造函数链。

  2. 不仅改写了 ToString,甚至还改写了 Equals。因为如果没有被 overrriden,Equal 测试的只是引用相等。改写 Equals 的同时应该改写 GetHashCode。GetHashCode 应该和 Equals 保持一致,本例因为比较的是 ISBN 号,因此 GetHashCode 也用 ISBN 的该方法。


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

分享到微博请遵守国家法律