Swift泛型class、继承和协变

Swift generic class, inheritance and covariance

我面临使用泛型 class 和继承的问题。

问题简述:

我有一个名为 BookPageDataSource 的基础 class 和两个具有不同实现的继承 classes(ReadingBookPageDataSourceStarsBookPageDataSource)。

此外,我有一个通用的 class BookPageViewController,它包含此数据源的通用参数和两个继承的 classes(ReadingBookPageViewControllerStarsBookPageViewController) 从这个 class.

我需要写一个方法,return参数是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 总是崩溃,因为数据源总是等于 BookPageDataSource,而不是 ReadingBookPageDataSourceStarsBookPageDataSource

概念讨论

您的架构概念有缺陷,这导致了您的问题。


简单泛型示例

这是一个非常简单的泛型函数示例,它只是 return 您赋予它的值:

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

因为这个函数是通用的,所以它使用的类型不明确。什么是 T? Swift 是一种类型安全的语言,这意味着最终不允许任何关于类型的歧义。那么为什么允许使用这个 echo 函数呢?答案是当我在某处实际使用这个函数时,关于类型的歧义将被消除。例如:

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

使用这个泛型函数的过程中,我通过传递一个Int消除了歧义,因此编译器对所涉及的类型没有任何不确定性。


你的函数

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

您的函数仅在 return 类型中使用通用参数 DataSource,而不是在输入中 - 编译器应该如何确定 DataSource 是什么?* 我假设这就是您想象的使用函数的方式:

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

但是现在,controller的类型是什么?您期望用它做什么?您似乎希望 controller 会根据您传入的 (0) 采用正确的类型,但事实并非如此有用。通用参数是根据输入的 type 确定的,而不是根据输入的 value 确定的。您希望传入 0 会产生一种 return 类型,而 1 会产生另一种类型——但这在 Swift 中是被禁止的。 01 都是 Int 类型, 类型 才是最重要的。

与 Swift 通常的情况一样,并不是 language/compiler 阻止您做某事。这是你还没有逻辑地表达你想要的东西,编译器只是告诉你你到目前为止所写的东西没有意义。


解决方案

让我们继续为您提供解决方案。


UIViewController 功能

大概有一些你想要使用 controller 的东西。你真正需要的是什么?如果你只是想把它推到一个导航控制器上,那么你不需要它是一个 BookPageViewController。你只需要它是一个 UIViewController 就可以使用那个功能,所以你的函数可以变成这样:

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

并且您可以将它 return 的控制器推送到导航堆栈上。


自定义功能(非通用)

但是,如果您需要使用某些特定于 BookPageViewController 的功能,那么这取决于您想要做什么。如果 BookPageViewController 上有这样的方法:

func doSomething (input: Int) -> String

不使用通用参数 DataSource 那么您可能希望将该函数分离到它自己的 protocol/superclass 中,这不是通用的。例如:

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

然后让BookPageViewController符合它:

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

现在你的函数的 return 类型可以是这个非通用协议:

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)

当然,如果 return 类型不再是任何类型的 UIViewController,那么您可能需要考虑重命名函数和相关变量。


自定义功能(通用)

另一种选择是您无法将所需的功能分离到非通用 protocol/superclass 中,因为该功能使用了通用参数 DataSource。一个基本的例子是:

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

所以在这种情况下,您确实需要函数的 return 类型为 BookPageViewController<DataSource>。你做什么工作?那么,如果您真正想要的是使用上面定义的 setDataSource(_:) 方法,那么您必须有一个 DataSource 对象,您打算将其作为参数传入,对吧?如果是这种情况,那么我们正在取得进展。以前,您只有一些要传递给函数的 Int 值,问题是您无法用它指定通用 return 类型。但是,如果您已经有一个 BookPageDataSource 值,那么至少在逻辑上您可以使用它来专门化您的 功能。

但是,您所说的只是使用 Int 来获取该索引处的控制器,而不管 DataSource 类型是什么。但是,如果您不关心 returned BookPageViewControllerDataSource 是什么,那么您如何期望使用 [=43= 将其 DataSource 设置为其他内容] 方法?

你看,问题自己解决了。您需要 return 类型的函数是通用的唯一原因是如果您需要使用的后续功能使用该通用类型,但如果是这种情况,那么您返回的控制器不能有任何旧的 DataSource (你只想要与你提供的索引相对应的那个) - 你需要它具有你计划在使用时传入的 DataSource 类型,否则你给它错误的类型。

因此,您问题的最终答案是,按照您设想的方式,您尝试构建的函数没有任何用处。 Swift 的架构方式非常酷,因为编译器实际上能够找出逻辑缺陷并阻止您构建代码,直到您重新概念化它。


脚注:

* 可以有一个泛型函数只在 return 类型中使用泛型参数而不在输入中使用,但这无济于事你在这里。