Swift: Extension on [?] to produce [?

2020-03-24 03:23发布

问题:

In Swift, I have a custom struct with this basic premise:

A wrapper struct that can contain any type that conforms to BinaryInteger such as Int, UInt8, Int16, etc.

protocol SomeTypeProtocol {
    associatedtype NumberType

    var value: NumberType { get set }
}

struct SomeType<T: BinaryInteger>: SomeTypeProtocol {
    typealias NumberType = T

    var value: NumberType
}

And an extension on Collection:

extension Collection where Element: SomeTypeProtocol {
    var values: [Element.NumberType] {
        return self.map { $0.value }
    }
}

For example, this works nicely:

let arr = [SomeType(value: 123), SomeType(value: 456)]

// this produces [123, 456] of type [Int] since literals are Int by default
arr.values

What I would like to do is the exact same thing, but for SomeType<T>?

let arr: [SomeType<Int>?] = [SomeType(value: 123), SomeType(value: 456)]

// this doesn't work, obviously
arr.values

// but what I want is this:
arr.values // to produce [Optional(123), Optional(456)]

I've tried numerous attempts to solve this and a fair amount of research, but I'm hoping any of the sage Swift veterans might shed some light on this.

This is what I envision it might look like, but this doesn't work:

extension Collection where Element == Optional<SomeType<T>> {
    var values: [T?] {
        return self.map { $0?.value }
    }
}

This is a clumsy way of achieving the goal without using generics, and it works:

extension Collection where Element == Optional<SomeType<Int>> {
    var values: [Int?] {
        return self.map { $0?.value }
    }
}

let arr: [SomeType<Int>?] = [SomeType(value: 123), SomeType(value: 456)]
arr.values // [Optional(123), Optional(456)]

But it requires manually writing extensions for every known type conforming to BinaryInteger, and will not automatically include possible future types adopting BinaryInteger without manually updating the code.

// "..." would contain the var values code from above, copy-and-pasted
extension Collection where Element == Optional<SomeType<Int>> { ... }
extension Collection where Element == Optional<SomeType<Int8>> { ... }
extension Collection where Element == Optional<SomeType<UInt8>> { ... }
extension Collection where Element == Optional<SomeType<Int16>> { ... }
extension Collection where Element == Optional<SomeType<UInt16>> { ... }
extension Collection where Element == Optional<SomeType<Int32>> { ... }
extension Collection where Element == Optional<SomeType<UInt32>> { ... }
extension Collection where Element == Optional<SomeType<Int64>> { ... }
extension Collection where Element == Optional<SomeType<UInt64>> { ... }

EDIT 2018-Jun-23:

Solution #1 - Fully Generic but Must be Func, Not Computed Property

Expanded on Ole's reply:

Pros: If values() becomes a func and not a computed property, this is an elegant solution.

Cons: No known way to implement this approach as computed properties, and Swift's Quick Help popup shows [T] and [T?] when inspecting values() in your code. ie: it just says func values<T>() -> [T] where T : BinaryInteger which isn't very informative or Swifty. However it still remains strongly typed of course.

extension Collection {
    func values<T>() -> [T] where Element == SomeType<T> {
        return map { $0.value }
    }

    func values<T>() -> [T?] where Element == SomeType<T>? {
        return map { $0?.value }
    }
}

Solution #2 - Optional Protocol Workaround

Expanded on Martin's reply:

Pros: Allows use of computed properties (cleaner for the end-user to access since it doesn't require func parens) and shows inferred type in Xcode's Quick Help popup.

Cons: Not as elegant from an internal code standpoint, as it requires a workaround. But not necessarily a drawback.

// Define OptionalType

protocol OptionalType {
    associatedtype Wrapped
    var asOptional: Wrapped? { get }
}

extension Optional: OptionalType {
    var asOptional: Wrapped? {
        return self
    }
}

// Extend Collection

extension Collection where Element: SomeTypeProtocol {
    var values: [Element.NumberType] {
        return self.map { $0.value }
    }
}

extension Collection where Element: OptionalType, Element.Wrapped: SomeTypeProtocol {
    var values: [Element.Wrapped.NumberType?] {
        return self.map { $0.asOptional?.value }
    }
}

回答1:

I don't know if there is a simpler solution now, but you can use the same “trick” as in How can I write a function that will unwrap a generic property in swift assuming it is an optional type? and Creating an extension to filter nils from an Array in Swift, the idea goes back to this Apple Forum Thread.

First define a protocol to which all optionals conform:

protocol OptionalType {
    associatedtype Wrapped
    var asOptional: Wrapped? { get }
}

extension Optional : OptionalType {  
    var asOptional: Wrapped? {  
        return self 
    }  
}  

Now the desired extension can be defined as

extension Collection where Element: OptionalType, Element.Wrapped: SomeTypeProtocol {
    var values: [Element.Wrapped.NumberType?] {
        return self.map( { $0.asOptional?.value })
    }
}

and that works as expected:

let arr = [SomeType(value: 123), nil, SomeType(value: 456)]
let v = arr.values

print(v) // [Optional(123), Optional(456)]
print(type(of: v)) // Array<Optional<Int>>


回答2:

Martin R's answer is a good solution. An alternative that doesn't require an extra marker protocol is this: write an unconstrained extension on Collection, and in that extension, define a generic function that's constrained to where Element == SomeType<T>?:

extension Collection {
    func values<T>() -> [T?] where Element == SomeType<T>? {
        return map( { $0?.value })
    }
}

This works:

let arr: [SomeType<Int>?] = [SomeType(value: 123), SomeType(value: 456)]
arr.values() // [Optional(123), Optional(456)]

You'll notice that I used a func instead of a computed property. I couldn't get the generic syntax right. Isn't this supposed to work?

extension Collection {
    // error: consecutive declarations on a line must be separated by ';'
    var values<T>: [T?] where Element == SomeType<T>? {
        return self.map( { $0?.value })
    }
}