Search code examples
swiftdowncast

Is it possible to convert instance of a class to an instance of a subclass?


I've recently came across a case where it would be very convenient to convert an instance of a class to a subclass, while the instance has been created within the parent class. But I've never seen such thing. So is there a way to do something like:

class Foo {
  var name: String
}

class Bar: Foo {
  var friendName: String
}

let foo = Foo(name: "Alice")
foo.toBar(friendName: "Bob")
// foo now of type Bar, as if I'd done
// foo = Bar(name: "Alice", friendName: "Bob")

If that's not possible, is there some reasons this would be impossible from a design perspective?

===edit=== description of a use case where it could make sense

Let say there's two views representing what correspond to the same database record for a book, on is a just a preview of the book and another is a more complex view. Models could be:

protocol BookMetaDelegate {
  func onReadStatusUpdate()
}

/// describe a book
class BookMeta {
  var delegate: BookMetaDelegate?
  private var _hasBeenRead: Bool
  var hasBeenRead: Bool {
    get {
      return _hasBeenRead
    }
    set {
      guard newValue != _hasBeenRead else { return }
      _hasBeenRead = newValue
      delegate?.onReadStatusUpdate()
    }
  }
  var title: String
}

/// contains all the content of a book
class Book: BookMeta {
  var content: BookContent
  var lastPageRead: Int

  /// some logic that only makes sense in a Book instance
  func getLastPageRead() {
    return content.getPage(lastPageRead)
  }
}

and views could look like:

class BookPreview: UIView, BookMetaDelegate {
  var book: BookMeta
  init(book: BookMeta) {
    book.delegate = self
  }
  func onReadStatusUpdate() {
    print("read status has changed! UI should update")
  }
}

class BookView: UIView {
  var book: Book
  init(book: Book) {
    book.hasBeenRead = true
  }
}

Then things could happen like

fetch(bookMetaWithId: 123).then { bookMeta in // bookMeta is of type BookMeta
  let preview = BookPreview(book: bookMeta)
  ...

  fetch(contentOf: bookMeta).then { content, lastPageRead in
    bookMeta.asBook(content: content, lastPageRead: lastPageRead)
    let bookView = BookView(book: bookMeta) // doing so will change the hasBeenRead flag and message the instance's delegate, ie the preview
    ...
  }
}

Solution

  • Thinking more about it, it sounds like that if such thing was possible, it'd break things like:

    class Foo {
      var name: String
    }
    
    class Bar: Foo {
      var friendName: String
    }
    
    class Bla: Foo {
      var surname: String
    }
    
    func something(foo: Foo) {
      foo.toBla(surname: "Will")
    }
    
    let bar = Bar(name: "Alice", friendName: "Bob")
    something(foo: bar) // what does that do ??? is bar a Bla now ?
    

    so that'd be a good reason for making such casting impossible.