如何在Swift中使函数的返回类型成为通用类型?

4

简介

在我的应用程序中,我有一个名为“ElementData”的超类以及多个继承自它的子类。

每个子类都有自己的validateModel()方法,该方法返回不同类型的值,具体取决于类-始终以数组形式返回。

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

例子

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

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

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

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

编辑:validateModel()方法的示例:

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

Class 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都有自己的类。由于集合视图的重用行为,必须将输入的值存储在模型中。

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

这有时可以是枚举、整数或字符串。

我想要实现的目标

正如您所想象的那样,在每个子类中拥有几乎相同的validationMethods非常烦人,因为每次我想在其中一个类上使用该方法时都需要进行downcast。

因此,我想保持返回类型开放,即不在父类中指定特定的类型,因为子类应该能够返回任何类型。然后,我将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,但我仍然需要将每个对象强制转换以访问方法并知道它返回什么,对吧?顺便说一下:请查看编辑。 - linus_hologram
1
到目前为止,我在你的方法中没有看到任何共同点,也不确定为什么你会将这些方法称为相等。 - Sulthan
@Sulthan 在每个类的方法内部的代码是不同的,但基本功能只在返回类型上有所不同。现在我必须根据我的单元格类型进行切换,以便知道它是哪个elementDataClass。如果我将validateModel方法放在顶级类中,我可以在不强制转换的情况下调用它的每个DataElementClass。我希望现在更清楚了 :) - linus_hologram
旁注:对我来说,返回一个数组听起来不像是模型验证。要么重命名validateModel(),要么更改其行为。 - Alexander
显示剩余2条评论
2个回答

9

这是不可能的。

泛型的作用

假设您有以下函数:

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 的对象。该对象可以是 ElementDataClassAClassB 或者 ClassC 的实例。考虑到这四种类型都是可能的,并且假设存在某种方法来实现你想要的功能,那么这段代码将如何工作呢?

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<String>,而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. 协议符合(符合协议的类型是它们所符合的协议的子类型)

两者都有优缺点。协议会迫使你走上困难的道路,因为它们涉及到关联类型,而类则不太灵活(因为它们不能被枚举或结构体继承)。在这种情况下,答案取决于你想让这段代码做什么。从根本上说,你正在尝试将这些数据连接到一个表格单元格。因此,创建一个针对此目的的协议:

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 我正在处理这个问题,但是我需要一些时间来有效地解释。 - Alexander
嗨,亚历山大,关于我的项目中的类结构,我又有一个问题。如果我开另一个聊天室,你觉得可以吗? - linus_hologram
当然,提到我的名字,我会收到通知。 - Alexander
我已经做了,但如果您没有收到通知,这是链接:https://chat.stackoverflow.com/rooms/199467/room-for-linus-hologram-and-alexander - linus_hologram
显示剩余4条评论

5

一种可能的解决方案是使用关联类型的协议。您需要在每个子类中将返回类型指定为 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"] }
}

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

enter image description here


谢谢您的回答。但是有没有办法将validateModel()方法移动到父类中?子类只需要覆盖它即可。因为我实际上遇到的问题是,我想避免向下转换一个类以便访问validateModel()方法。 - linus_hologram
不行,不能使用异构返回类型。 - vadian
没事了!我还没有尝试过,但我认为你的解决方案可能可行。 - linus_hologram
我该怎么做? - linus_hologram
1
我不知道你想要实现什么。我的答案是用相同的签名但不同的返回类型来实现一个方法的方式。 - vadian
显示剩余4条评论

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