简体   繁体   English

Swift实现通用功能

[英]Swift Implementation generic function

I'm a developer on Java and I'm trying to write in Swift the same solution that I have in Java code. 我是Java开发人员,因此尝试在Swift中编写与Java代码相同的解决方案。

Is it possible to do this on Swift? 是否可以在Swift上执行此操作?

Example Java: 示例Java:

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

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

Example Swift: Swift示例:

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

How it would be the implementation? 实施情况如何?

Thanks!!! 谢谢!!!

What appears to be a simple question is actually the tip of a rather large and unpleasant iceberg… 看来一个简单的问题实际上是一个相当大且令人不愉快的冰山的一角……

I'm going to start by giving you what is probably the real solution to your problem: 首先,我将为您提供可能是您的问题的真正解决方案:

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

}

Above, I've translated your Java Converter interface into a Swift class instead of a Swift protocol . 上面,我已经将Java Converter接口转换为Swift class而不是Swift protocol That's probably what you want. 那可能就是你想要的。

Now I'll explain why. 现在,我将解释原因。

A programmer coming from Java to Swift might think that a Swift protocol is the equivalent of a Java interface. 从Java到Swift的程序员可能会认为Swift协议等效于Java接口。 So you might write this: 所以你可以这样写:

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

Okay, now you can create a converter and convert something: 好的,现在您可以创建一个转换器并进行以下转换:

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

But you're going to run into a problem as soon as you want to write a function that takes a Converter as an argument: 但是,一旦要编写一个将Converter作为参数的函数,就会遇到问题:

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

“Well, duh,” you say, “you forgot the type arguments!” But no, I didn't. “恩,嗯,”您说,“您忘记了类型参数!”但是不,我没有。 Swift doesn't allow explicit type arguments after a protocol name: Swift不允许在协议名称后使用显式类型参数:

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

I don't want to get into why you can't do this. 我不想弄明白为什么你不能这样做。 Just accept that a Swift protocol is not generally equivalent to a Java interface. 只需接受Swift协议通常不等同于Java接口即可。

A Swift protocol with no associated types and no mention of Self is, generally, equivalent to a non-generic Java interface. 没有关联类型且没有提及Self的Swift协议通常等效于非泛型Java接口。 But a Swift protocol with associated types (or that mentions Self ) is not really equivalent to any Java construct. 但是带有关联类型(或提及Self )的Swift协议实际上并不等同于任何Java构造。

When discussing this problem, we often use the acronym “PAT”, which stands for “Protocol with Associated Types” (and includes protocols that mention Self ). 在讨论此问题时,我们经常使用首字母缩写词“ PAT”,它代表“具有关联类型的协议”(并包括提及Self协议)。 A PAT doesn't define a type that you can use as a function argument, return value, or property value. PAT并未定义您可以用作函数参数,返回值或属性值的类型。 There's not much you can do with a PAT: 使用PAT可以做的事情不多:

  • You can define a subprotocol. 您可以定义一个子协议。 For example, Equatable is a PAT because it defines the == operator to take two arguments of type Self . 例如, Equatable是PAT,因为它定义==运算符以接受Self类型的两个参数。 Hashable is a subprotocol of Equatable . HashableHashable的子Equatable

  • You can use a PAT as a type constraint. 您可以将PAT用作类型约束。 For example, Set is a generic type. 例如, Set是一个通用类型。 Set 's type parameter is named Element . Set的类型参数名为Element Set constrains its Element to be Hashable . SetElement约束为Hashable

So you can't write a function that takes a plain Converter as an argument. 因此,您不能编写将纯Converter用作参数的函数。 But you can write a function that takes any implementation of Converter as an argument, by making the function generic: 但是您可以通过使该函数通用来编写一个将Converter任何实现作为参数的函数:

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

That compiles just fine. 这样编译就可以了。 But sometimes it's inconvenient to make your function generic. 但是有时候使函数通用是很不方便的。

And there's another problem that this doesn't solve. 还有另一个问题无法解决。 You might want a container that holds various Converter s from Car to CarDTO . 您可能需要一个容器,用于容纳从CarCarDTO各种Converter That is, you might want something like this: 也就是说,您可能想要这样的东西:

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

We can't fix this by making converters generic, like we did with the useConverter function. 我们无法像使用useConverter函数那样通过使converters通用来解决此问题。

What you end up needing is a “type-erased wrapper”. 您最终需要的是“类型擦除的包装器”。 Note that “type-erased” here has a different meaning that Java's “type erasure”. 请注意,此处的“类型擦除”与Java的“类型擦除”具有不同的含义。 In fact it's almost the opposite of Java's type erasure. 实际上,这几乎与Java的类型擦除相反。 Let me explain. 让我解释。

If you look in the Swift standard library, you'll find types whose names start with Any , like AnyCollection . 如果您查看Swift标准库,则会发现名称以Any开头的类型,例如AnyCollection These are mostly “type-erased wrappers” for PATs. 这些大多是PAT的“类型擦除包装”。 An AnyCollection conforms to Collection (which is a PAT), and wraps any type that conforms to Collection . AnyCollection符合Collection (它是PAT),并包装符合Collection任何类型。 For example: 例如:

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)

Note that we have to explicitly wrap our other collections in AnyCollection . 注意,我们必须将其他集合显式包装在AnyCollection The wrapping is not automatic. 包装不是自动的。

Here's why I say this is almost the opposite of Java's type erasure: 这就是为什么我说这几乎与Java的类型擦除相反:

  • Java preserves the generic type but erases the type parameter. Java保留通用类型,但删除type参数。 A java.util.ArrayList<Car> in your source code turns into a java.util.ArrayList<_> at runtime, and a java.util.ArrayList<Truck> also becomes a java.util.ArrayList<_> at runtime. 源代码java.util.ArrayList<Car>在运行时变为java.util.ArrayList<_> ,并且java.util.ArrayList<Truck> java.util.ArrayList<_>在运行时也变为java.util.ArrayList<_> In both cases, we preserve the container type ( ArrayList ) but erase the element type ( Car or Truck ). 在这两种情况下,我们都保留容器类型( ArrayList ),但删除元素类型( CarTruck )。

  • The Swift type-erasing wrapper erases the generic type but preserves the type parameter. Swift类型擦除包装器将擦除通用类型,但保留type参数。 We turn an Array<Car> into an AnyCollection<Car> . 我们将Array<Car>变成AnyCollection<Car> We also turn a Dictionary<String, Car>.Values into an AnyCollection<Car> . 我们还将Dictionary<String, Car>.Values转换为AnyCollection<Car> In both cases, we lose the original container type ( Array or Dictionary.Values ) but preserve the element type ( Car ). 在这两种情况下,我们都会丢失原始容器类型( ArrayDictionary.Values ),但保留元素类型( Car )。

So anyway, for your Converter type, one solution to storing Converter s in a container is to write an AnyConverter type-erased wrapper. 因此,无论如何,对于您的Converter类型,一种将Converter存储在容器中的解决方案是编写一个AnyConverter类型擦除的包装器。 For example: 例如:

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
}

(There are multiple ways to implement type-erased wrappers. That is just one way.) (实现类型擦除的包装器有多种方法。这只是一种方法。)

You can then use AnyConverter in property types and function arguments, like this: 然后,您可以在属性类型和函数参数中使用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))")
    }
}

But now you should ask: what's the point? 但是现在您应该问:有什么意义? Why bother making Converter a protocol at all, if I'm going to have to use a type-erased wrapper? 如果我必须使用类型擦除的包装器,为什么还要烦恼将Converter变成一个协议呢? Why not just use a base class to define the interface, with subclasses implementing it? 为什么不只使用基类来定义接口,而子类来实现它呢? Or a struct with some closures provided at initialization (like the AnyConverter example above)? 还是在初始化时提供了一些闭包的结构(例如上面的AnyConverter示例)?

Sometimes, there's not a good reason to use a protocol, and it's more sensible to just use a class hierarchy or a struct. 有时,没有充分的理由使用协议,而仅使用类层次结构或结构更为明智。 So you should take a good look at how you're implementing and using your Converter type and see if a non-protocol approach is simpler. 因此,您应该仔细看一下如何实现和使用Converter类型,并查看非协议方法是否更简单。 If it is, try a design like I showed at the top of this answer: a base class defining the interface, and subclasses implementing it. 如果是这样,请尝试像我在该答案顶部所示的设计:定义接口的基类,以及实现该接口的子类。

The Swift equivalent to your Java code looks like this. 与您的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()
    }
}

Explanation 说明

The equivalent to a generic Java interface in Swift, would be a protocol with associatedtype s. 与Swift中的通用Java接口等效的是带有associatedtype s的协议。

protocol Converter {
    associatedtype Input
    associatedtype Output
}

To create an implementation of that protocol, the implementation must specify which types the associated types maps to, using typealias . 要创建该协议的实现,实现必须使用typealias指定关联类型映射到的类型。

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

Type Erasure 类型擦除

If you try to use to this approach, you may run into the issue of trying to store an instance of your generic protocol in a variable or property, in which case you will get the compiler error: 如果尝试使用此方法,则可能会遇到尝试将通用协议的实例存储在变量或属性中的问题,在这种情况下,您会收到编译器错误:

protocol 'Converter' can only be used as a generic constraint because it has Self or associated type requirements 协议“转换器”只能用作通用约束,因为它具有“自身”或关联的类型要求

The way to solve this issue in Swift, is by using type erasure , where you create a new implementation of your generic protocol, that itself is a generic type (struct or class), and uses a constructor accepting a generic argument, matching your protocol, like so: 在Swift中解决此问题的方法是使用类型erasure ,在其中您创建通用协议的新实现,该协议本身就是通用类型(结构或类),并使用接受通用参数的构造函数来匹配您的协议,就像这样:

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

This is usually accompanied by an extension function on the generic protocol, that performs the type erasure by creating an instance of AnyConverter<Input, Output> using self , like so: 这通常伴随通用协议上的扩展功能,该扩展功能通过使用self创建AnyConverter<Input, Output>的实例来执行类型擦除:

extension Converter {

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

Using type erasure, you can now create code that accepts a generic Converter (by using AnyConverter<Input, Output> ), that maps Car to CarDTO : 使用类型擦除,您现在可以创建接受通用Converter (通过使用AnyConverter<Input, Output> )的代码,该代码将Car映射到CarDTO

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

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM