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

550 阅读7分钟

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 可以被继承的类所访问了。不过实例代码所在的地方也会影响能否使用,如夏例子: image.png 可以发现并不能访问 Method1(),因为实例化的地方在继承类之外,所以得通过注释里的代码间接访问:

image.png

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 前面表示这个类是个抽象类不能被实例化,但可以在继承类里访问这些方法。 image.png

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

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

1.6 隐性继承

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

public class SimpleClass { }

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

  1. ToString 返回字符串表现形式,此例中返回类名:"SimpleClass"
  2. 接下来 3 个方法都是为了测试两个对象是否相等。一般这些方法测试的是两个变量是否引用相等。也就是说被比较的变量们必须指向同一个对象。
  3. GetHashCode 方法,会计算出一个值,用于 hashed collection
  4. GetType 方法,获取一个 Type Object,本例中为 SimpleClass 类型。和 ToString 不一样,这个返回的不是字符串,只不过用 Console.WriteLine 打印的时候会变成字符串罢了。
  5. Finalize 方法,用于垃圾回收。
  6. MemberwiseClone,会创建一个当前对象的浅克隆。 image.png

可以在类定义里 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 的该方法。