Swift实现通用功能

时间:2018-11-07 21:34:53

标签: swift generics

我是Java开发人员,我试图用Swift编写与Java代码相同的解决方案。

是否可以在Swift上执行此操作?

Java示例:

public interface Converter<S,T> {
    T convert(S in)
}

public class CarConverterToDTO implements Converter<Car, CarDTO> {
      @Override
      public CarDTO convert(Car in) {
      .....
      }
}

示例Swift:

protocol Converter {
   func convert<IN, OUT>(in: IN) -> OUT
}

实施情况如何?

谢谢!

2 个答案:

答案 0 :(得分:2)

看似简单的问题实际上是相当大且令人不快的冰山一角……

首先,我将为您提供解决问题的真正方法:

class Converter<Input, Output> {
    func convert(_ input: Input) -> Output {
        fatalError("subclass responsibility")
    }
}

struct Car { }
struct CarDTO { }

class DTOCarConverter: Converter<Car, CarDTO> {

    override func convert(_ input: Car) -> CarDTO {
        return CarDTO()
    }

}

上面,我已经将您的Java Converter接口转换为Swift class而不是Swift protocol。那可能就是你想要的。

现在我将解释原因。

从Java到Swift的程序员可能会认为Swift协议等效于Java接口。所以你可以这样写:

protocol Converter {
    associatedtype Input
    associatedtype Output

    func convert(_ input: Input) -> Output
}

struct Car { }
struct CarDTO { }

class /* or struct */ DTOCarConverter: Converter {
    func convert(_ input: Car) -> CarDTO {
        return CarDTO()
    }
}

好的,现在您可以创建一个转换器并进行转换:

let converter = DTOCarConverter()
let car = Car()
let dto = converter.convert(car)

但是,当您要编写一个以Converter作为参数的函数时,就会遇到问题:

func useConverter(_ converter: Converter) { }
//                             ^
// error: protocol 'Converter' can only be used as a generic constraint because it has Self or associated type requirements

“好吧,嗯,”您说,“您忘记了类型自变量!”但不,我没有。 Swift不允许在协议名称后使用显式类型参数:

func useConverter(_ converter: Converter<Car, CarDTO>) { }
//                             ^        ~~~~~~~~~~~~~
// error: cannot specialize non-generic type 'Converter'

我不想陷入为什么的境地。只需接受Swift协议与Java接口通常不等效即可。

没有关联类型且没有提及Self的Swift协议通常等效于非通用Java接口。但是具有关联类型(或提及Self)的Swift协议实际上并不等同于任何Java构造。

在讨论此问题时,我们经常使用首字母缩写词“ PAT”,代表“具有关联类型的协议”(并包括提及Self的协议)。 PAT并未定义您可以用作函数参数,返回值或属性值的类型。 PAT可以做很多事情:

  • 您可以定义一个子协议。例如,Equatable是PAT,因为它定义了==运算符以接受两个Self类型的参数。 HashableEquatable的子协议。

  • 您可以将PAT用作类型约束。例如,Set是通用类型。 Set的类型参数命名为ElementSet将其Element约束为Hashable

因此,您不能编写以纯Converter作为参数的函数。但是您可以编写一个函数,该函数将Converter的任何实现作为参数,方法是使该函数通用:

func useConverter<MyConverter: Converter>(_ converter: MyConverter)
    where MyConverter.Input == Car, MyConverter.Output == CarDTO
{ }

这样编译就可以了。但是有时使您的函数通用是不便的。

还有另一个无法解决的问题。您可能需要一个容器,该容器可容纳ConverterCar之间的各种CarDTO。也就是说,您可能想要这样的东西:

var converters: [Converter<Car, CarDTO>] = []
//               ^        ~~~~~~~~~~~~~
// error: cannot specialize non-generic type 'Converter'

我们无法通过使converters通用来解决此问题,就像我们对useConverter函数所做的那样。

您最终需要的是“类型擦除的包装器”。请注意,此处的“类型擦除”与Java的“类型擦除”具有不同的含义。实际上,这几乎与Java的类型擦除相反。让我解释一下。

如果您查看Swift标准库,则会发现名称以Any开头的类型,例如AnyCollection。这些大多是PAT的“类型擦除包装”。 AnyCollection符合Collection(是PAT),并包装符合Collection的任何类型。例如:

var carArray = Array<Car>()
let carDictionary = Dictionary<String, Car>()
let carValues = carDictionary.values
// carValues has type Dictionary<String, Car>.Values, which is not an array but conforms to Collection

// This doesn't compile:
carArray = carValues
//         ^~~~~~~~~
// error: cannot assign value of type 'Dictionary<String, Car>.Values' to type '[Car]'

// But we can wrap both carArray and carValues in AnyCollection:
var anyCars: AnyCollection<Car> = AnyCollection(carArray)
anyCars = AnyCollection(carValues)

请注意,我们必须将其他集合显式包装在AnyCollection中。包装不是自动的。

这就是为什么我说这几乎与Java的类型擦除相反:

  • Java保留通用类型,但删除type参数。源代码中的java.util.ArrayList<Car>在运行时变成java.util.ArrayList<_>,而java.util.ArrayList<Truck>在运行时也变成java.util.ArrayList<_>。在这两种情况下,我们都保留容器类型(ArrayList),但删除元素类型(CarTruck)。

  • Swift类型擦除包装器将擦除通用类型,但保留type参数。我们将Array<Car>变成AnyCollection<Car>。我们还将Dictionary<String, Car>.Values变成AnyCollection<Car>。在这两种情况下,我们都会丢失原始容器类型(ArrayDictionary.Values),但保留元素类型(Car)。

因此,无论如何,对于您的Converter类型,将Converter存储在容器中的一种解决方案是编写一个AnyConverter类型擦除的包装器。例如:

struct AnyConverter<Input, Output>: Converter {
    init<Wrapped: Converter>(_ wrapped: Wrapped) where Wrapped.Input == Input, Wrapped.Output == Output {
        self.convertFunction = { wrapped.convert($0) }
    }

    func convert(_ input: Input) -> Output { return convertFunction(input) }

    private let convertFunction: (Input) -> Output
}

(实现类型擦除的包装器有多种方法。这只是一种方法。)

然后可以在属性类型和函数参数中使用AnyConverter,如下所示:

var converters: [AnyConverter<Car, CarDTO>] = [AnyConverter(converter)]

func useConverters(_ converters: [AnyConverter<Car, CarDTO>]) {
    let car = Car()
    for c in converters {
        print("dto = \(c.convert(car))")
    }
}

但是现在您应该问:有什么意义?如果我将不得不使用类型擦除的包装器,为什么还要麻烦Converter做一个协议呢?为什么不只使用基类来定义接口,而子类来实现它呢?还是在初始化时提供了一些闭包的结构(例如上面的AnyConverter示例)?

有时候,没有充分的理由使用协议,而仅使用类层次结构或结构更为明智。因此,您应该仔细研究如何实现和使用Converter类型,并查看非协议方法是否更简单。如果是这样,请尝试像我在该答案顶部所示的设计:定义接口的基类,以及实现该接口的子类。

答案 1 :(得分:0)

与Java代码等效的Swift如下所示。

protocol Converter {
    associatedtype Input
    associatedtype Output

    func convert(input: Input) -> Output
}

class CarConverterToDTO: Converter {
    typealias Input = Car
    typealias Output = CarDTO

    func convert(input: Car) -> CarDTO {
        return CarDTO()
    }
}

说明

等效于Swift中的通用Java接口,将是带有associatedtype s的协议。

protocol Converter {
    associatedtype Input
    associatedtype Output
}

要创建该协议的实现,实现必须使用typealias指定关联类型映射到的类型。

class CarConverterToDTO: Converter {
    typealias Input = Car
    typealias Output = CarDTO
}

擦除类型

如果尝试使用此方法,则可能会遇到尝试将通用协议的实例存储在变量或属性中的问题,在这种情况下,您会收到编译器错误:

  

协议“转换器”只能用作通用约束,因为它具有“自我”或相关联的类型要求

在Swift中解决此问题的方法是使用类型擦除,您可以在其中创建通用协议的新实现,该协议本身就是通用类型(结构或类),并使用接受与您的协议相匹配的通用参数的构造函数,如下所示:

struct AnyConverter<Input, Output>: Converter {

    // We don't need to specify type aliases for associated types, when the type
    // itself has generic parameters, whose name matches the associated types.

    /// A reference to the `convert(input:)` method of a converter.
    private let _convert: (Input) -> Output

    init<C>(_ converter: C) where C: Converter, C.Input == Input, C.Output == Output {
        self._convert = converter.convert(input:)
    }

    func convert(input: Input) -> Output {
        return self._convert(input)
    }
}

这通常伴随通用协议上的扩展功能,该功能通过使用AnyConverter<Input, Output>创建self的实例来执行类型擦除,如下所示:

extension Converter {

    func asConverter() -> AnyConverter<Input, Output> {
        return AnyConverter(self)
    }
}

使用类型擦除,您现在可以创建接受通用Converter的代码(通过使用AnyConverter<Input, Output>),该代码将Car映射到CarDTO

let car: Car = ...
let converter: AnyConverter<Car, CarDTO> = ...

let dto: CarDTO = converter.convert(input: car)