Быстрый родовой класс, наследование и ковариация - PullRequest
0 голосов
/ 31 августа 2018

Я столкнулся с проблемой использования обобщенного класса и наследования.

Краткое описание проблемы:

У меня есть базовый класс с именем BookPageDataSource и два унаследованных класса (ReadingBookPageDataSource и StarsBookPageDataSource) с разными реализациями.

Кроме того, у меня есть универсальный класс BookPageViewController, который содержит универсальный параметр этого источника данных и два унаследованных класса (ReadingBookPageViewController и StarsBookPageViewController) из этого класса.

Мне нужно написать метод, возвращаемый параметр которого BookPageViewController<DataSource>.

// Data Sources

class BookPageDataSource { }

class ReadingBookPageDataSource: BookPageDataSource { }

class StarsBookPageDataSource: BookPageDataSource { }

// Controllers

class BookPageViewController<DataSource: BookPageDataSource>: UIViewController {
    let dataSource: DataSource

    init(dataSource: DataSource) {
        self.dataSource = dataSource

        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        return nil
    }
}

final class ReadingBookPageViewController: BookPageViewController<ReadingBookPageDataSource> { }

final class StarsBookPageViewController: BookPageViewController<StarsBookPageDataSource> { }

// Communication

class Pager {
    func currentPageController<DataSource>(at index: Int) -> BookPageViewController<DataSource> {
        // for example

        if index == 0 {
            // How to remove the cast from the line below?
            return readingPageController() as! BookPageViewController<DataSource>
        }

        return starsPageController() as! BookPageViewController<DataSource>
    }

    private func readingPageController() -> ReadingBookPageViewController {
        return ReadingBookPageViewController(dataSource: ReadingBookPageDataSource())
    }

    private func starsPageController() -> StarsBookPageViewController {
        return StarsBookPageViewController(dataSource: StarsBookPageDataSource())
    }
}

Метод currentPageController всегда дает сбой, потому что DataSource всегда равен BookPageDataSource, а не ReadingBookPageDataSource или StarsBookPageDataSource.

1 Ответ

0 голосов
/ 07 сентября 2018

Концептуальное обсуждение

Ваша концепция архитектуры ошибочна, и это приводит к вашей проблеме.


Пример простого обобщенного примера

Вот очень простой пример универсальной функции, которая просто возвращает значение, которое вы ей даете:

func echo <T> (_ value: T) -> T { return value }

Поскольку эта функция является общей, существует неопределенность в отношении типа, который она использует. Что такое T? Swift является языком, безопасным для типов, что означает, что в конечном счете не допускается никакой двусмысленности относительно типа. Так почему же разрешена эта функция эха? Ответ заключается в том, что когда я на самом деле где-то использую эту функцию, неопределенность в отношении типа будет удалена. Например:

let myValue = echo(7)      // myValue is now of type Int and has the value 7

В действии с использованием этой обобщенной функции я устранил неоднозначность, передав ей Int, и поэтому компилятор не имеет никакой неопределенности относительно задействованных типов.


Ваша функция

func currentPageController <DataSource> (at index: Int) -> BookPageViewController<DataSource>

Ваша функция использует только общий параметр DataSource в возвращаемом типе, а не во входных данных - как компилятор должен выяснить, что такое DataSource? * Полагаю, именно так вы и представляли используя вашу функцию:

let pager = Pager()
let controller = pager.currentPageController(at: 0)

Но теперь, какой тип controller? Что вы можете ожидать от этого? Кажется, вы надеетесь, что controller примет правильный тип на основе значения , которое вы передаете (0), но это не так. Общий параметр определяется на основе типа ввода, а не значения ввода. Вы надеетесь, что передача 0 даст один тип возврата, в то время как 1 даст другой тип, но это запрещено в Swift. И 0, и 1 относятся к типу Int, а type - это все, что может иметь значение.

Как обычно бывает в Swift, это не язык / компилятор, который мешает вам что-то делать. Дело в том, что вы еще не сформулировали логически то, что вы хотите, и компилятор просто информирует вас о том, что то, что вы написали до сих пор, не имеет смысла.


Решения

Давайте перейдем к тому, чтобы дать вам решение.


Функциональность UIViewController

Предположительно есть что-то, для чего вы хотели бы использовать controller. Что вам на самом деле нужно? Если вы просто хотите вставить его в навигационный контроллер, вам не нужно, чтобы он был BookPageViewController. Вам нужно только, чтобы он был UIViewController, чтобы использовать эту функцию, поэтому ваша функция может стать такой:

func currentPageController (at index: Int) -> UIViewController {
    if index == 0 {
        return readingPageController()
    }
    return starsPageController()
}

И вы можете поместить контроллер, который он возвращает, в стек навигации.


Пользовательская функциональность (не универсальная)

Если, однако, вам нужно использовать некоторые функции, специфичные для BookPageViewController, то это зависит от того, что вы хотите сделать. Если есть метод BookPageViewController, подобный этому:

func doSomething (input: Int) -> String

, который не использует универсальный параметр DataSource, тогда, вероятно, вы захотите выделить эту функцию в свой собственный протокол / суперкласс, который не является универсальным. Например:

protocol DoesSomething {
  func doSomething (input: Int) -> String
}

и затем BookPageViewController соответствуют этому:

extension BookPageViewController: DoesSomething {
  func doSomething (input: Int) -> String {
    return "put your implementation here"
  }
}

Теперь типом возврата вашей функции может быть этот неуниверсальный протокол:

func currentPageController (at index: Int) -> DoesSomething {
    if index == 0 {
        return readingPageController()
    }
    return starsPageController()
}

и вы можете использовать его так:

let pager = Pager()
let controller = pager.currentPageController(at: 0)
let retrievedValue = controller.doSomething(input: 7)

Конечно, если тип возвращаемого значения больше не является UIViewController, тогда вы, вероятно, захотите рассмотреть переименование функции и связанных с ней переменных.


Пользовательские функции (общие)

Другой вариант заключается в том, что вы не можете выделить необходимую функциональность в неуниверсальный протокол / суперкласс, поскольку эта функция использует универсальный параметр DataSource. Базовый пример:

extension BookPageViewController {
  func setDataSource (_ newValue: DataSource) {
    self.dataSource = newValue
  }
}

Так что в этом случае вам действительно нужно, чтобы возвращаемый тип вашей функции был BookPageViewController<DataSource>. Чем ты занимаешься? Что ж, если вы действительно хотите использовать метод setDataSource(_:), определенный выше, то у вас должен быть объект DataSource, который вы планируете передать в качестве аргумента, верно? Если это так, то мы делаем успехи. Раньше у вас было только какое-то значение Int, которое вы передавали в вашу функцию, и проблема заключалась в том, что вы не могли указать с этим свой тип возвращаемого значения. Но если у вас уже есть значение BookPageDataSource, то, по крайней мере, логически возможно использовать его для специализации вашего функция.

Однако вы говорите, что хотите, просто использовать Int, чтобы получить контроллер с этим индексом, независимо от типа DataSource. Но если вас не волнует, что DataSource соответствует возвращенному BookPageViewController, то как вы можете ожидать установить DataSource в другое значение, используя метод setDataSource(_:)?

Видите ли, проблема решается сама собой. Единственная причина, по которой вам нужно, чтобы возвращаемый тип вашей функции был универсальным, заключается в том, что последующая функциональность, которую вам необходимо использовать, использует этот универсальный тип, но если это так, то у возвращаемого вами контроллера не может быть просто какой-либо старой DataSource (вы просто хотели, чтобы тот соответствовал индексу, который вы предоставляете) - вам нужно, чтобы он точно соответствовал типу DataSource, который вы планируете передать при его использовании, иначе вы дадите ему неправильный тип.

Итак, окончательный ответ на ваш вопрос заключается в том, что в том виде, в каком вы его представляли, нет возможности использовать функцию, которую вы пытались создать. Что очень круто в том, как Swift спроектирован, так это то, что компилятор на самом деле способен выявить этот логический недостаток и помешать вам создавать код, пока вы не переосмыслили его.


Сноска:

* Можно использовать универсальную функцию, которая использует универсальный параметр только в типе возвращаемого значения, а не во входных данных, но это вам здесь не поможет.

...