T Swift的替代方法

4
在我的测验应用程序中,我初始化测验,提供类在提供问题之前不知道问题的格式(尽管它们受到QuestionProtocol的约束):
public protocol QuestionProtocol {
    init?(fields: [String] )
    var description: String {get}
    var question: String {get}
    var solution: String {get}
    var explainAnswer: String {get}
    var answered: Int {get set}
    var qa: String {get}
    var qb: String {get}
    var qc: String {get}
    var qd: String {get}
}

我可以通过一个方法来初始化测验并轻松返回它们,该方法的签名如下:

public func initializeQuizzes<T: QuestionProtocol>(with type: T.Type, withCompletionHandler completion: ((Result<[Quiz<T>], Error>) -> Void)?) 

然而,提供这些测验是很昂贵的(需要API调用或SQL检索),因此我想将这些测验存储起来,并从具有签名的合适函数中单独检索它们。

public func getNextQFromSet<T: QuestionProtocol>(with type: T.Type) -> (question: T, answers: [String])?

我遇到的问题是存储类型为T的这些问题。
它们与一个Quiz对象相关联:
public class Quiz<T> {
    private let questions : [T]
    private let name : String

    init(name: String, questions: [T]) {
        self.name = name
        self.questions = questions
    }

    public func getQuestions() -> [T] {
        return questions
    }

    func getName() -> String {
        return name
    }
}

因此,我能够将它们存储为符合问题协议的测验题

private var quizzes = [Quiz<QuestionProtocol>]()

但是这样我就失去了想要存储在问题中的额外信息。

我可以存储任何类型的数据,但我认为这是不良做法。

private var anyquizzes = [Quiz<Any>]()

理想情况下,我想要存储T,即:

Quiz<T>

但在Swift中似乎不可能。因为这些类在Pod中,无法了解Question的内部工作方式,因此在运行时提供它们,因此使用泛型并且在存储这些问题方面存在困难。

我想不出改进应用程序设计(更具体地说是Pod)的方法 - 我希望仅初始化一次测验,然后运行像getNextQFromSet()这样的函数以检索相关问题 - 这显然取决于我在运行时不知道问题类型(我不知道问题类型)。

为了清晰起见,这是Pod的链接: https://github.com/stevencurtis/QuizManager

如何在不知道类型的情况下存储包含这些问题的数组?


你能给我们展示一个有关于那些“额外信息”的真实问题的例子吗? - rraphael
@rraphael 我建议你查看 GitHub 链接,其中包含完整的背景信息和完整的问题类(我在此重复链接:https://github.com/stevencurtis/QuizManager)。总体而言,额外的信息只是问题中的字段,但它们与问题不相关,因为可以生成一般化的 stackoverflow 帖子,涉及将通用数据存储在数组中,而不是在此测验上下文中。 - WishIHadThreeGuns
1
如果我只保留“存储泛型数据”的上下文,那么不,这是不可能的,你必须使用Any。而且泛型无法解决你的问题,因为它们不是在运行时解析的(正如你所认为的那样),而是在编译时解析的。如果你想要在运行时解析某些东西,你必须使用Any。一个真实问题的例子可能并不相关,但是像你所说的,在Swift中过度依赖运行时几乎是一种不好的做法。我只是试图理解你想要通过这个实现什么。 - rraphael
我不相信泛型是在运行时解决的(这是一个关于如何解决现实世界问题的问题,却有一种奇怪而令人不悦的推断)。我正在尝试找到一个好的解决方案来解决这个问题——我有一个对象数组(Any),并且在运行时我知道类型(来自API的用户)。使用Any是一个相当糟糕的解决方案,但它确实可以工作,并在此作为示例包含在内。 - WishIHadThreeGuns
我已经查看了GitHub存储库。是否有可能更改模型,仅在需要从数据库解码测验时使用泛型,否则使用“QuestionProtocol”?如果不行,为什么? - Qbyte
显示剩余4条评论
4个回答

2
如何在不知道类型的情况下存储包含这些问题的数组?
据我所知,你不能。正如 rraphael 在他的评论中指出的那样,泛型在运行时无法解析。此外,Swift 中的 Arrays 设计用于保存单一类型:
具体来说,您使用 Array 类型来保存一个类型的元素,即数组的 Element 类型。
所以无论你做什么,你都会有一个 Any 或者可能是 QuestionProtocol 数组,但没有比这更动态的了:类型将在编译时解析。
您可以重新设计您的 QuestionProtocol 来适应您的需求,但由于缺乏关于不同类型问题的任何信息,因此很难帮助您更多,因为这是一个架构问题。

不同的问题只是您想要显示的内容。您可能有一个包含多个答案的问题格式,这由类(在GitHub链接中)处理,我们使用相同的类来执行此处理。这个通用类接受这个格式作为目标格式进行输出,但不幸的是,我们希望将其存储在我们的类中。将其存储为Any,甚至QuestionProtocol都不太令人满意,因为当我们返回它们时,我们需要将它们强制转换为我们请求的类型(通过存储所请求的类型,或者只返回基类)。 - WishIHadThreeGuns
如果你的问题可能有多个答案,你的 QuestionProtocol 可以有一个答案数组(你可能还想为你的答案创建一个类型)。 - AnderCover
这就是总是要求具体示例的问题所在。是的,我知道数组。但问题是关于拥有多种类型的问题(例如答案数量,另一个可能是图像的存在)。我知道如何存储图像 - 问题是关于使用泛型来存储未知类型的问题(具体而言) - 但更一般地说,这是关于存储一组类型而不使用 Any。 - WishIHadThreeGuns
是的,我的回答仍然不变,你必须找到另一种方法。所以如果你已经知道替代方案,就去试试吧 ;) - AnderCover
如果我理解正确,您的解决方案是存储Any或者使用QuestionProtocol。这些都在问题中提到了(悬赏要求“不得存储Any”),所以我知道我写的替代方案。非常感谢,我会另寻他法。 - WishIHadThreeGuns

2
简而言之,我认为删除QuestionProtocol并用普通数据结构struct Question替换它是有意义的。
在我阐述观点之前,我想指出,尽管我看了这个pod,但我仍然不知道所有的要求,所以我可能是错的。
让我们试着从设计的角度来看待问题,而不是编程语言的角度。
为什么要有QuestionProtocol?它可以被替换成对象吗?为什么这些属性应该是多态的?当然,实现细节应该被隐藏,但隐藏数据不是关于协议或附加函数层,而是关于抽象。
现在让我们将QuestionProtocol转换为Question对象,并考虑一个抽象。如果有一个真正的抽象,那么就应该有一个对象来隐藏数据(细节)并公开操作该数据的函数。但是在Question对象中没有函数,这意味着没有真正的抽象。
最后,这意味着Question实体很可能是一个带有公共属性的纯数据结构,并且可以定义为struct Question
现在有了这个Question结构,您可以将测验定义为Quiz<Question>并用它来保存和检索数据。
此外,我认为有两件事值得指出,这可以简化并潜在地改进设计和实现:
1.为什么SQLiteManager知道一些关于具体问题(取决于QuestionProtocol)的信息?我认为引入一些通用的DBObject或至少是普通的字典[String:Any]是有意义的,SQLiteManager将知道如何处理然后插入。然后,Repository可以在某个组合级别上将Question数据结构转换为DBObject并将其传递给SQLiteManager
2.在使用泛型时,在大多数情况下,不需要定义额外的type:T.Type参数。一旦定义了泛型,您可以将其用作[T]T.init等。如果仍然需要元类型(T.Type),则可以通过T.self获取。
希望这有所帮助!
这里有一个使用TDD和模块化设计创建的优秀Quiz应用的例子:Quiz App。同时,还有一个视频系列逐步解释了设计和创建过程。原始答案被翻译成“最初的回答”。

感谢您提供的额外建议。我已经从SQLiteManager中删除了任何具体类,但选择保留T通过协议绑定,以允许访问该协议提供可见性的特定字段。 - WishIHadThreeGuns
不用谢!抱歉,我不理解你关于协议的观点。但是,正如我之前所说,你很可能不需要协议。拥有一个简单的问题数据传输对象(DTO)就可以让你的POD暴露给消费者。祝你好运! - Alex D.

0

你可以使用带有关联值的枚举来描述类型。例如:

struct QuestionTypeA { }
struct QuestionTypeB { }
struct QuestionTypeC { }

enum Question {
    case typeA(question: QuestionTypeA)
    case typeB(question: QuestionTypeB)
    case typeC(question: QuestionTypeC)
}

然后:

public class Quiz {
    private let questions : Question
    private let name : String
    ...

并存储一个不带泛型的Quiz数组

private var anyquizzes = [Quiz]()

0

使用这两种类型,您将无法将Quiz<T>Quiz<U>存储在同一个数组中。它们只是不同的类型。

如果您有一个Array<QuizProtocol>,您可以在switch-case语句中匹配已知的类型:

var quizzes: [QuizProtocol] = ...

for quiz in quizzes {
    switch quiz {
       case let someQuiz as SomeQuiz:
           ...
       case let someOtherQuiz as SomeOtherQuiz:
           ...
       default: 
           ... // couldn't cast to any known type; do some fallback logic
       ....
    }
}

其中 SomeQuiz 和 SomeOtherQuiz 符合 QuizProtocol(严格来说,你可以匹配任何类型)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接