如何在 Swift 中使 return 类型的函数通用

How to make return type of function generic in Swift

简介

在我的应用程序中,我有一个名为 "ElementData" 的 superclass 和 几个继承的子 classes来自它。

Each child class has its own validateModel() method which returns a different type, depending on the class - always in an Array.

换句话说:方法return每个子class的不同类型class。

例子

Class答:func validateModel() -> [String]

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

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

如您所见,只有 return 个值彼此不同。

编辑:validateModel() 方法示例:

Class答:

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
    }

Class乙:

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

        let filledValues = codes.filter {
            [=13=] != ""
        }

        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
    }

编辑:classes 的用途:

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

模型负责验证和returns - 取决于单元格 - 值数组告诉单元格哪些字段应该有红色边框(标记为无效)。

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

我想达到的目标

正如您想象的那样,在每个 child-class 中使用几乎相同的 validationMethods 是非常烦人的,因为有必要 downcast 每次我想在其中一个 class 上使用该方法时。

因此,我想保留 return 类型打开 即不要在父 class 中指定一个特定的作为子class 应该可以 return 任何类型。然后,我会将 validateModel() 方法移动到父 class 并覆盖其子 classes.

中的方法

我想到了 generics 的解决方案(如果可能的话)。

我试过的

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

class ElementData {

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

}

以及方法的调用:

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

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

"Generic parameter 'T' could not be inferred"

总结:

  • 我有一个 superclass "ElementData" 和几个 subclasses(classes 继承)
  • 每个 subclass 都有一个验证模型的方法 validateModel()
  • 只有 return 类型 的 validateModel() 方法在 subclasses differ -所以我想把这个方法放在父 class (ElementData) 里面,只是 override 它在 subclasses

这可能吗,如果可能,怎么办?

如有任何帮助,我们将不胜感激。

这是不可能的。

泛型的用途

假设你有这个功能:

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 导致类型信息丢失。尽管我们看到参数的类型和 return 值总是相同的,但我们还没有向类型系统表达这一点。这是泛型类型参数的完美 use-case。它允许我们表达参数类型和 return 值之间的关系。

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

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

哪些泛型不适合

回过头来看你的问题,你发现这里没有类型关系可以表达。

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

这不是通用的。

它是如何工作的?

假设您有一个 ElementData 类型的对象。该对象可以是 ElementDataClassAClassBClassC 的实例。鉴于所有这四种类型都是可能的,并且假设存在一些混合来做你想做的事,这段代码将如何工作?

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

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

此外,您的代码将违反 Liskov 替换原则。 ClassA 需要支持在需要 ElementData 的地方被替换。 ElementData.validateModel()可以做的事情之一是return一个Something。因此,ClassA.validateModel() 需要 return 一个 Something,或者一个子 class(奇怪的是,似乎只有继承关系起作用,而不是协议子类型关系。例如,returning Int 预期 Any 不起作用)。由于 ClassA.validateModel() returns Array<String>,并且 Array 不是 class(因此,不能有 superclass),因此有没有可用于使代码不违反 LSP 和编译的可能类型 Something

这是 LSP 的说明,以及 Covariance 如何在 return 类型的重写方法中工作,而不是在重写方法的参数类型中工作。

// 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
//      
//  }
}

一种解决方法

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

有两个工具可用:

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

两者都有advantages/disadvantages。协议迫使你走上 associated-type 痛苦的绝望之路,而 Classes 则不那么灵活(因为它们不能被枚举或结构子 classed)。在这种情况下,答案取决于您希望这段代码做什么。从根本上说,您正在尝试将此数据连接到 table 单元格。因此,为此制定一个协议:

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

现在,将您的方法更新为 return 这种类型:

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()
        )
    }
}

一种可能的解决方案是具有关联类型的协议。您必须在每个子类中将 return 类型指定为 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"] }
}

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