【问题标题】:Swift generic class, inheritance and covarianceSwift 泛型类、继承和协变
【发布时间】:2018-09-07 10:19:30
【问题描述】:

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

问题的简要描述:

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

另外,我有一个泛型类BookPageViewController,它包含这个数据源的泛型参数和从这个类继承的两个类(ReadingBookPageViewControllerStarsBookPageViewController)。

我需要写一个返回参数为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,而不是ReadingBookPageDataSourceStarsBookPageDataSource

【问题讨论】:

  • 也许你需要类型擦除。
  • 你真的需要BookPageDataSource 来上课吗?您是否考虑过它只是一个协议的情况?这样你就可以在 VC 的 init 方法中进行依赖注入。更重要的是,您将不需要 VC 通用 where 子句。
  • @dvp.petrov ReadingBookPageDataSourceStarsBookPageDataSource 有很多相同的方法,所以我在BookPageDataSource 的基础上编写了这些方法。如果我将BookPageDataSource 更改为协议,我需要将扩展​​添加到协议中,所有存储的变量都应该使用objc_associated_objects 方法创建。我认为这很糟糕......
  • 我同意 dvp.petrov 但你也可以在你的 ViewController 中使用 associatedtype 这可能是一个数据源
  • @Sweeper 你能解释一下如何在当前示例中执行此操作吗?

标签: ios swift generics covariance


【解决方案1】:

概念讨论

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


简单的泛型示例

这是一个非常简单的泛型函数示例,它只返回你给它的值:

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) 采用正确的类型,但这不是它的工作方式。泛型参数是根据输入的 type 而不是输入的 value 确定的。您希望传入0 会产生一种返回类型,而1 会产生另一种返回类型——但这在Swift 中是被禁止的。 01 都是 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&lt;DataSource&gt;。你做什么工作?好吧,如果您真正想要的是使用上面定义的setDataSource(_:) 方法,那么您必须有一个您计划作为参数传入的DataSource 对象,对吧?如果是这种情况,那么我们正在取得进展。以前,您只有一些 Int 值,您将其传递给您的函数,问题是您无法使用它指定您的通用返回类型。但是,如果您已经有一个 BookPageDataSource 值,那么您至少在逻辑上可以使用它来专门化您的 功能。

但是,您所说的只是使用Int 来获取该索引处的控制器,而不管DataSource 类型是什么。但是,如果您不关心返回的BookPageViewController 中的DataSource 是什么,那么您怎么能期望使用setDataSource(_:) 方法将其DataSource 设置为其他值呢?

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

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


脚注:

* 可以有一个泛型函数,它只在返回类型中使用泛型参数而不在输入中使用,但这在这里对你没有帮助。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多