【问题标题】:How to make return type of function generic in Swift如何在 Swift 中使函数的返回类型通用
【发布时间】:2019-11-26 17:14:33
【问题描述】:

简介

在我的应用中,我有一个名为“ElementData”的超类几个继承自它的子类。

每个子类都有自己的 validateModel() 方法,该方法返回 不同的类型,取决于类 - 总是在一个数组中。

换句话说:该方法在每个子类中返回不同的类型。

示例

A类:func validateModel() -> [String]

B类:func validateModel() -> [Int]

C类:func validateModel() -> [MyCustomEnum]

如您所见,只有返回值彼此不同。

编辑:validateModel() 方法示例:

A 类:

func validateModel() -> [DefaultElementFields]{ // DefaultElementFields is an enum with the different view types of my collection view

        var checkResult: [DefaultElementFields] = []

        if name == "" {
            checkResult.append(.Name)
        }

        if Int(rewardedPoints) == nil {
            checkResult.append(.Points)
        }

        if description == "" {
            checkResult.append(.Description)
        }

        if selectedImage == nil {
            checkResult.append(.Image)
        }

        return checkResult
    }

B 类:

func validateModel() -> [Int] { // returns the index of the text field which is wrong
        var checkResult: [Int] = []

        let filledValues = codes.filter {
            $0 != ""
        }

        if filledValues.count == 0 { // if no values have been entered, all fields should be marked red.
            checkResult.append(-1)
            return checkResult
        }


        for (i, code) in codes.enumerated() {
            if code != "" && (code.count < 3 || code.count > 10 || code.rangeOfCharacter(from: NSCharacterSet.alphanumerics.inverted) != nil){ // code must have more than 3 and less than 11 characters. No symbols are allowed.
                checkResult.append(i)
            }
        }



        return checkResult
    }

编辑:这些课程的用途:

这些类基本上存储数据用户输入到集合视图单元格,例如文本、数字或日期。每个 CollectionViewCellType 都有自己的类。由于集合视图的重用行为,有必要将输入的值存储在模型中。

模型负责验证并根据单元格返回一个值数组,告诉单元格哪些字段应该有红色边框(是标记为无效)。

这有时可以是 Enum、Int 或 String。

我想要达到的目标

如您所想,在每个 子类 中拥有几乎相同的验证方法是很烦人的,因为每次我想使用方法之一。

因此,我想保持 返回类型打开,即不要在父类中指定特定的类型,因为子类应该能够返回任何类型。然后,我会将 validateModel() 方法移到父类中,并在其子类中覆盖该方法。

我想到了一个使用 泛型 的解决方案(如果可能的话)。

我尝试了什么

这是我对整个事情的通用方法:

class ElementData {

    func validateModel<T>() -> [T] {
        return [1] as! [T] // just a test return
    }

}

以及方法的调用:

dataObject.validateModel() // dataObject inherits from ElementData -> has access to validateModel()

不幸的是,它不起作用,我收到以下错误:

“无法推断通用参数'T'”

总结:

  • 我有一个超类“ElementData”和几个子类(继承的类)
  • 每个子类都有一个方法 validateModel(),在该方法中验证模型
  • 只有 返回类型 子类中的 validateModel() 方法不同 - 所以我想将方法​​放在父类 (ElementData) 中,并且只是 覆盖子类

这可能吗,如果可以,怎么做?

任何帮助将不胜感激。

【问题讨论】:

  • 请向我们展示您的验证方法的至少两种实现。有两种可能性。要么使用协议而不是超类,要么使整个类通用。
  • 要推断泛型类型,您必须执行let array: [Int] = dataObject.validateModel() 之类的操作来使用返回值推断类型。
  • @Sulthan 但是我仍然必须放弃每个对象才能访问该方法并知道它返回什么,对吧?顺便说一句:见编辑
  • 到目前为止,我在您的方法中没有看到任何共同点,我不确定您为什么还要平等地调用这些方法。
  • @Sulthan 每个类的方法中的代码都不同-但基本函数仅在返回类型上有所不同。现在我必须打开单元格的类型才能知道它是哪个 elementDataClass。如果我在顶级类中有 validateModel 方法,我可以在每个 DataElementClass 上调用它而不强制转换它。我希望现在更清楚:)

标签: arrays swift class generic-programming


【解决方案1】:

这是不可能的。

什么是泛型

假设你有这个功能:

func identity(_ value: Any) -> Any {
    return value
}

它实际上不起作用:

let i = 5
assert(identity(i) == i) // ❌ binary operator '==' cannot be applied to operands of type 'Any' and 'Int'

Any 导致类型信息丢失。尽管我们看到参数的类型和返回值总是相同的,但我们还没有向类型系统表达这一点。这是泛型类型参数的完美用例。它允许我们表达参数类型和返回值之间的关系。

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

let i = 5
assert(identity(i) == i) // ✅

什么泛型不适合

回顾你的问题,你会发现这里没有类型关系可以表达。

  • ClassA.validateModel() 总是返回 [String]
  • ClassB.validateModel() 总是返回 [Int]
  • ClassC.validateModel() 总是返回 [MyCustomEnum]

这不是通用的。

它是如何工作的?

假设您有一个ElementData 类型的对象。该对象可以是ElementDataClassAClassBClassC 的实例。鉴于所有这四种类型都是可能的,并且假设存在一些混合物可以满足您的需求,那么这段代码将如何工作?

let elementData = someElementData()
let validatedModel = elementData.validateModel() // ? What type is `someValue` supposed to be?

由于我们(也不是编译器)知道elementData 的值将是什么具体类型(我们只知道它是ElementData 或其子类之一),编译器应该如何确定validatedModel ?

此外,您的代码将违反 Liskov 替换原则。 ClassA 需要支持在预期 ElementData 的地方被替换。 ElementData.validateModel()可以做的一件事是返回一个Something。因此,ClassA.validateModel() 需要返回 Something 或子类(奇怪的是,似乎只有继承关系有效,协议子类型关系无效。例如,在预期 Any 的地方返回 Int 不起作用)。由于ClassA.validateModel() 返回Array&lt;String&gt;,并且Array 不是类(因此,不能有超类),所以没有可能的类型Something 可用于使代码不违反LSP 和编译。

这里是 LSP 的说明,以及协方差如何在被覆盖方法的返回类型中起作用,而不是在被覆盖方法的参数类型中起作用。

// https://www.mikeash.com/pyblog/friday-qa-2015-11-20-covariance-and-contravariance.html

class Animal {}
class Cat: Animal {}
    
class Person {
    func purchaseAnimal() -> Animal {
        return Animal()
    }
}

class CrazyCatLady: Person {
    // Totally legal. `Person` has to be able to return an `Animal`.
    // A `Cat` is an animal, so returning a `Cat` where an `Animal` is required is totally valid
    override func purchaseAnimal() -> Cat {
        return Cat()
    }

//  This method definition wouldn't be legal, because it violates the Liskov Substitution Principle (LSP).
//  A `CrazyCatLady` needs to be able to stand in anywhere a `Person` can be used. One of the things a
//  `Person` can do is to `pet(animal: Animal)`. But a `CrazyCatLady` can't, because she can only pet cats.
//
//  If this were allowed to compile, this could would be undefined behaviour:
//
//      let person: Person = getAPerson()
//      let animal: Animal = getAnAnimal()
//      person.pet(animal)
//
//  override func pet(animal: Cat) { // ❌ method does not override any method from its superclass
//      
//  }
}

解决方案的一种方法

首先,我们需要确定这些返回类型之间的共同点。如果我们能做到这一点,那么编译器就可以回答“someModel 应该是什么类型?”这个问题。以上。

有两种工具可用:

  1. 类继承(子类是其超类的子类型)
  2. 协议一致性(协议一致性类型是它们所遵循的协议的子类型)

两者都有优点/缺点。协议迫使您走上痛苦的绝望之路,即associated-type,而类则不那么灵活(因为它们不能被枚举或结构子类化)。在这种情况下,答案在于您希望这段代码做什么。从根本上说,您试图将这些数据连接到表格单元格。所以为此制定一个协议:

protocol CellViewDataSource {
    func populate(cellView: UICellView) {
        // adjust the cell as necessary.
    }
} 

现在,更新您的方法以返回此类型:

class ElementData {
    func validateModel() -> CellViewDataSource {
        fatalError()
    }
}

class ClassA {
    func validateModel() -> CellViewDataSource {
        fatalError()
    }
}

要实现这些方法,您必须扩展 Array 以符合 CellViewDataSource。然而,这是一个非常可怕的想法。我建议您创建一个新类型(可能是 struct)来存储您需要的数据。

struct ModelA {
    let name: String
    let points: Int
    let description: String
    let image: UIImage
}

extension ModelA: CellViewDataSource {
    func populate(cellView: UICellView) {
        // Populate the cell view with my `name`, `points`, `description` and `image`.
    }
}

class ElementData {
    func validateModel() -> CellViewDataSource {
        fatalError("Abstract method.")
    }
}

class ClassA {
    func validateModel() -> CellViewDataSource {
        return ModelA(
            name: "Bob Smith",
            points: 123,
            description: "A dummy model.",
            image: someImage()
        )
    }
}

【讨论】:

  • 非常感谢您的回答。为了避免打开我的单元格类型然后向下转换数据对象,您是否有任何其他方法可以遵循?我有点迷茫,因为我不知道如何改进我的课程/模型......
  • @linus_hologram 我正在研究它,但我需要一段时间才能有效地解释
  • @linus_hologram 立即查看
  • 感谢您的详细解释。那么,populate 方法为单元格返回一个完成的 uiview 吗?还是我误会了什么?
  • 在我的示例中,TableViewDelegate 将一个单元格出列并传入模型,以便模型可以填充其数据(通过修改单元格对象,该单元格对象通过引用传递)。在这种情况下返回对象实际上不会实现任何目标。但细节并不是太重要。您只需要委派填充目标视图(表格单元格或其他)的责任。
【解决方案2】:

一个可能的解决方案是具有相关类型的协议。您必须在每个子类中将返回类型指定为typealias

protocol Validatable {
    associatedtype ReturnType
    func validateModel() -> [ReturnType]
}

class ElementData {}

class SubClassA : ElementData, Validatable {
    typealias ReturnType = Int

    func validateModel() -> [Int] { return [12] }

}

class SubClassB : ElementData, Validatable {
    typealias ReturnType = String

    func validateModel() -> [String] { return ["Foo"] }
}

现在编译器知道所有子类的不同返回类型

【讨论】:

  • 感谢您的回答。但是有没有办法将 validateModel() 方法移到父类中?而子类只覆盖它?因为我遇到的实际问题是我想避免向下转换一个类以访问 validateModel()?
  • 不,不是异类返回类型。
  • 没关系!我还没有尝试过,但我认为你的解决方案可以工作。
  • 如果我试图将我的类降级为 Validatable,我会收到以下错误:“Protocol 'Validatable' 只能用作通用约束,因为它具有 Self 或关联的类型要求”。尝试使用该函数时出现的另一个错误:“成员 'validateModel' 不能用于协议类型 'Validatable' 的值;请改用通用约束”如何访问我在 Validatable 中指定的函数协议?
  • 我不知道你要完成什么。我的答案是实现一个签名相同但返回类型不同的方法
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-14
  • 2019-05-09
  • 2020-08-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多