Swift where condition to check if a property is im

2019-07-04 02:57发布

问题:

I just found another way to make a great use of protocols and protocol extensions in Swift by extending the Optional protocol to add a function so I can provide default values.

I wrote a blog post about this here: https://janthielemann.de/random-stuff/providing-default-values-optional-string-empty-optional-string-swift-3-1/

The gist of the post is that I needed a clean and easy way to provide default values for optional String which are nil or empty. To do this, I created a Emptyable protocol end extended the Optional protocol like so:

protocol Emptyable {
    var isEmpty: Bool { get }
}

extension Optional where Wrapped: Emptyable {
    func orWhenNilOrEmpty<T: Emptyable>(_ defaultValue: T) -> T {
        switch(self) {
        case .none:
            return defaultValue
        case .some(let value) where value.isEmpty:
            return defaultValue
        case .some(let value):
            return value as! T
        }
    }
}

extension String: Emptyable {}

Now the question is: Is there a way I can get rid of the Emptyable protocol and instead have a conditional check whether or not a property or function is implemented by the generic type so that I automatically get orWhenNilOrEmpty() for each and every type which has isEmpty?

UPDATE

As suggested by Paulo, the T generic is actually not needed and I created a operator for even quicker access and more convenient usage (at least I think so. Feel free to correct me, I'm always happy to learn new things and improve myself).

I call it the "not empty nil coalescing" operator (who can come up with a better names? I feel like I suck at naming things :/ ). Hopefully some day it helps somebody:

protocol Emptyable {
    var isEmpty: Bool { get }
}

infix operator ???: NilCoalescingPrecedence

extension Optional where Wrapped: Emptyable {
    func orWhenNilOrEmpty(_ defaultValue: Wrapped) -> Wrapped {
        switch(self) {
        case .none:
            return defaultValue
        case .some(let value) where value.isEmpty:
            return defaultValue
        case .some(let value):
            return value
        }
    }

    static func ???(left: Wrapped?, right: Wrapped) -> Wrapped {
        return left.orWhenNilOrEmpty(right)
    }
}

extension String: Emptyable {}

extension Array: Emptyable {}

extension MyStruct: Emptyable {
    let text: String
    let number: Int
    var isEmpty: Bool { return text.isEmpty && number == 0 }
    init(text: String, number: Int) {
        self.text = text
        self.number = number
    }
}

let mandatoryNotEmptyString = optionalOrEmptyString ??? "Default Value"
let mandatoryNotEmptyStruct = optionalOrEmptyStruct ??? MyStruct(name: "Hello World", number: 1)

回答1:

No, you cannot query if an object or value has a certain property as a constraint on an extension without using a protocol. That would require reflection in a way that is currently not implemented in Swift. Also, an isEmpty property could have different meanings for different types, so testing for the existence of a method or property instead of a protocol could lead to unexpected behaviour.

You could just write

if let unwrappedString = optionalString, !unwrappedString.isEmpty {
    // Do stuff
} else {
    // Use default value
}

No protocol or extension required and very readable.

In Swift 4, which is coming out this fall, String will conform to BidirectionalCollection, which inherits from Collection. The Collection protocol provides an isEmpty property, so your extension could be

extension Optional where Wrapped: Collection {
     // ...
}

But even then you should consider to set empty strings to nil when storing them in the first place, because you now have two states (nil and empty) which seem to represent the exact same thing.