美文网首页我爱编程
高级类(三)

高级类(三)

作者: 小橘子成长记 | 来源:发表于2018-05-28 10:34 被阅读58次

    何时以及为什么要子类化。

    本章介绍了类的继承,以及子类化相关的众多编程技术。

    但是你可能会问,“什么时候我应该子类化?”

    对于这个问题,很少有正确或错误的答案。辩证的看点这个问题可以帮助你为任何特殊情况做出最佳决策。

    以Student学生和StudentAthlete学生运动员类为例,你可以简单地把StudentAthlete学生运动员的所有特征都放在Student学生身上:

    class Student: Person {
      var grades: [Grade]
      var sports: [Sport]
      // original code
    }
    

    这可以解决所有需要的用例。一个不做运动的学生只会有一个空的sports数组,你可以避免一些额外的复杂的子类化。

    单一职责

    在软件开发中,单一职责是说,声明的任何类都应该有一个单独职责。在学生和学生运动员中,学生有学生的责任,运动员有运动员的责任,学生的责任不应该封装到运动员里,运动员的也不应该封装到学生里。

    强类型

    使用Swift的类型系统,子类化创建了一个附加类型。你可以基于学生运动员而不是普通学生的对象声明属性或行为:

    class Team {
      var players: [StudentAthlete] = []
      var isEligible: Bool {
        for player in players {
          if !player.isEligible {
            return false
          } 
        }
        return true
      } 
    }
    

    一个队有运动员,他们是学生运动员。如果你试图将一个普通的学生对象添加到players数组中,类型系统将不允许。这很有用,因为编译器可以帮助你执行系统的逻辑和需求。

    共享基类

    你可以通过具有互斥行为的类,多次子类化一个共享基类:

    // A button that can be pressed.
    class Button {
      func press() {}
    }
    // An image that can be rendered on a button
    class Image {}
    // A button that is composed entirely of an image.
    class ImageButton: Button {
      var image: Image
      init(image: Image) {
        self.image = image
      }
    }
    
    // A button that renders as text.
    class TextButton: Button {
      var text: String
      init(text: String) {
        self.text = text
      }
    }
    

    在本例中,你可以想象许多Button子类只共享按下的事件。因此当按下按钮时,Button子类必须实现自己的行为。ImageButton和TextButton类有完全不同的机制来呈现按钮的外观。

    你可以在这里看到,在Button类中存储图像和文本——更不用说可能出现的任何其他类型的按钮,还会有其他的样式。因此按钮与媒体行为有关,只有处理按钮的实际外观的子类才是有意义的。

    可扩展性

    有时,如果你要扩展你的代码不拥有的行为,那么你就必须进行子类化。在上面的示例中,可能Button是你正在使用的框架的一部分,并且你不可能修改或扩展源代码以满足你的需要。

    在这种情况下,子类化Button可以添加自定义子类,并使用这种类型按钮对象来使用Button。

    同一性

    最后,重要的是要理解类和类层次结构模型的对象是什么。如果你的目标是在类型之间共享行为(对象可以做什么),那么通常你应该更喜欢协议而不是子类化。

    理解类的生命周期

    在前一章中,你了解到对象是在内存中创建的,它们存储在堆中。堆上的对象不会被自动销毁,因为堆只是一个巨大的内存池。如果没有调用堆栈的实用程序,就没有自动的方法来知道一个内存将不再被使用。

    在Swift中,决定何时清理堆上未使用的对象的机制称为引用计数。简而言之,每个对象都有一个引用计数,每一个常量或变量对该对象的引用都会递增,并且每次删除引用时都会递减。

    注意:在其他书籍和在线资源中,你可能会看到引用计数被称为“保留计数”。他们指的是同一件事!

    当引用计数达到0时,这意味着该对象现在被放弃,因为系统中没有任何引用。当这种情况发生时,Swift将清理对象。

    这里展示了一个对象的引用计数的变化。注意,在这个示例中只创建了一个实际对象;一个对象有很多引用。

    var someone = Person(firstName: "Johnny", lastName: "Appleseed")
    // Person object has a reference count of 1 (someone variable)
    var anotherSomeone: Person? = someone
    // Reference count 2 (someone, anotherSomeone)
    var lotsOfPeople = [someone, someone, anotherSomeone, someone]
    // Reference count 6 (someone, anotherSomeone, 4 references in
    lotsOfPeople)
    anotherSomeone = nil
    // Reference count 5 (someone, 4 references in lotsOfPeople)
    lotsOfPeople = []
    // Reference count 1 (someone)
    someone = Person(firstName: "Johnny", lastName: "Appleseed")
    // Reference count 0 for the original Person object!
    // Variable someone now references a new object
    

    在本例中,你不需要自己做任何工作来增加或减少对象的引用计数。这是因为Swift具有自动引用计数或ARC的特性。

    虽然一些较旧的语言要求你在代码中增加和减少引用计数,但是Swift编译器在编译时自动添加这些调用。

    注意:如果你使用像C这样的低级语言,那么你需要手动释放内存。像Java和c#这样的高级语言使用了称为垃圾收集的东西。在这种情况下,运行时,将在清理不再使用的对象之前,搜索进程引用的对象。垃圾收集虽然比ARC更强大,但它带来的内存利用率低和性能成本高,所以苹果公司决定不接受移动设备或通用系统语言。

    Deinitialization

    当一个对象的引用计数达到0时,Swift将对象从内存中移除,并将其标记为空闲内存。

    deinitializer是一个特殊的方法,它在对象的引用计数达到0时运行,但是在Swift将对象从内存中移除之前。

    修改类Person如下:

    class Person {
      // original code
      deinit {
        print("\(firstName) \(lastName) is being removed
              from memory!")
      }
    }
    

    很像init是类初始化中的一种特殊方法,deinit是一个处理初始化的特殊方法。与init不同,deinit不是必需的,并且会被Swift自动调用。你也不需要覆盖它或在其中调用super。Swift将确保调用每个类的deinitializer。

    如果你添加这个deinitializer,在运行前一个示例后的调试区域中,你将看到Johnny Appleseed正在被从内存中删除的消息!

    你在deinitializer中所做的事情取决于你。通常,你将使用它来清理其他资源,将状态保存到磁盘,或者在对象超出范围时执行你可能需要的任何其他逻辑。

    循环引用和弱引用

    由于Swift的类依赖于引用计数来将它们从内存中删除,所以理解retain cycle的概念非常重要。

    添加一个代表同学的字段—例如,一个实验室伙伴—和一个deinitializer方法,这样的学生:

    class Student: Person {
      var partner: Student?
      // original code
      deinit {
        print("\(firstName) is being deallocated!")
      }
    }
    var alice: Student? = Student(firstName: "Alice",
                                  lastName: "Appleseed")
    var bob: Student? = Student(firstName: "Bob",
                                lastName: "Appleseed")
    alice?.partner = bob
    bob?.partner = Alice
    

    现在假设alice和bob都辍学了:

    alice = nil 
    bob = nil
    

    如果你在你的playground上运行这个,你会注意到,Swift不调用deinit。这是为什么呢?

    Alice和Bob各自有一个引用,所以引用计数永远不会达到零!更糟糕的是,通过给alice和bob分配nil,就没有对初始对象的引用了。这是一个循环引用的经典案例,它导致了一个被称为内存泄漏的软件缺陷。

    在内存泄漏的情况下,即使它的实际生命周期已经结束,内存也不会释放出来。循环引用是内存泄漏最常见的原因。

    幸运的是,有一种方法,学生对象可以引用另一个学生而不容易循环引用,这是通过使用弱引用。

    class Student: Person {
      weak var partner: Student?
      // original code
    }
    

    这个简单的修改将伙伴变量标记为弱,这意味着该变量中的引用不会参与引用计数。当引用不弱时,它被称为强引用,这是Swift的默认值。弱引用必须声明为可选类型,以便当它们引用的对象被释放时,它会自动变为nil。

    关键点

    •类继承是类最重要的特性之一,它支持多态性。
    •Swift类使用两阶段初始化作为安全措施,确保在使用所有存储属性之前都进行了初始化。
    •子类化是一个强大的工具,但是知道什么时候子类化是很好的。当你想要扩展一个对象,并且可以从子类和超类之间的“is-a”关系中受益,但是要注意继承的状态和深层的类层次结构。
    •类实例有它们自己的生命周期,它们由它们的引用计数控制。
    •自动引用计数,或ARC,称为自动处理引用计数,但重要的是要注意循环引用。

    相关文章

      网友评论

        本文标题:高级类(三)

        本文链接:https://www.haomeiwen.com/subject/mlswjftx.html