5

It's a very common idiom to continue a loop if some condition fails on an element.

Say we want to do something to all subviews of a certain type (and, for some reason, don't want to duck type things). Ideally, we would write:

for view in self.subviews as [NSView] { // cast required in beta 6
    if (let specificView = view as? SpecificView) == nil { // <- Error here
        continue
    }

    // Do things at a sensible indentation level
}

The above code fails with 'Pattern variable binding cannot appear in an expression', as in this question.

However, this seems like such a common pattern that there has to be a way to do it in Swift. Am I missing something?


EDIT: Now that I think about it, this appears to fall afoul of the scoping rules for if let statements, which only scope the variable to the inner block.

With that in mind, I'd like to broaden the question a little: how do people apply this pattern generally in Swift?

Community
  • 1
  • 1
sapi
  • 9,944
  • 8
  • 41
  • 71
  • What happens if you remove `as [NSView]`? Or try `as? [NSView]` – idmean Aug 25 '14 at 12:41
  • @wumm - that line is fine, the error is on the next. I'll edit the question. (The cast is required because `self.subviews` is `[AnyObject?]` for some reason...) – sapi Aug 25 '14 at 12:43
  • Ahh. Why are you comparing to `nil`? There is no need to do that, remove that. – idmean Aug 25 '14 at 12:44
  • @wumm - it is. What I'm trying to do is have a single-line if statement which will execute if the downcast *fails*, so that I don't have my indentation rushing off to the right. – sapi Aug 25 '14 at 12:45
  • So you are trying something similar as in this question http://stackoverflow.com/questions/25484718/early-return-golden-path-in-swift ? – Martin R Aug 25 '14 at 12:48
  • @MartinR - exactly, good find. Pity there doesn't seem to be a neat solution, though :( – sapi Aug 25 '14 at 12:55

1 Answers1

10

This is a somewhat common pattern, but it is not a good pattern. I've seen it inject bugs into ObjC projects. It assumes too much about the view hierarchy, and winds up being fragile when that changes (such as when someone injects an extra view you weren't expecting in order to manage rotations; true story). The better pattern is to maintain a property that points to your SpecificView (or views) that you want to track. Downcasting in general is something to be avoided, not optimized.

That said, it is not a terrible pattern, and sometimes it is a very useful pattern. So how might you handle it?

let specifics = self.subviews
  .filter { $0 is SpecificView }
  .map { $0 as SpecificView }

for view in specifics { ... }

That's kind of a common pattern, so maybe we can genericize it?

extension Array {
  func filterByClass<T>(c: T.Type) -> [T] {
    return self.filter { $0 is T }.map { $0 as T }
  }
}

for view in self.subviews.filterByClass(SpecificView) { ... }

That said, I think this approach should be avoided wherever possible rather than excessively simplified.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • In this particular example, I agree it's a bit silly. This just seemed like an easy-to-understand case where continuation upon failed downcasting might make some kind of sense. That said, the block pattern should generalise well enough, at the cost of a bit of clarity (well, at the cost of a few lines). – sapi Aug 25 '14 at 13:16
  • Oh, and I'm curious about one thing with your generic method: what is the difference between saying `c: T.Type` cf `c: T`? Thanks! – sapi Aug 25 '14 at 13:26
  • 1
    `c: T` would be "a parameter of type T". `c: T.Type` is "a parameter that gives the type of T." With `T.Type`, you can pass the *class* `SpecificView` as the parameter. – Rob Napier Aug 25 '14 at 13:40