Ray Toal Ray Toal - 11 months ago 44
Swift Question

Why don't shorthand argument names work in this Swift closure?

Here's a Swift function that takes in two ints and a three-arg function, and calls the passed-in function.

func p(x:Int, _ y:Int, _ f: (Int, Int, Int) -> ()) {
f(x, y, 0)

I can call this just fine using both trailing closure syntax and shorthand argument names, no problem:

> p(1, 2) {print($0 + $1 + $2)}


That worked as expected. But in the Foundation library, there is a string method called
defined as follows:

func enumerateSubstringsInRange(
_ range: Range<Index>,
options opts: NSStringEnumerationOptions,
_ body: (substring: String?,
substringRange: Range<Index>,
enclosingRange: Range<Index>,
inout Bool) -> ())

Okay, that's easy enough: the function takes three arguments, the last of which is four-argument function. Just like my first example! Or so I thought....

I can use this function with the trailing closure syntax, but I cannot use shorthand argument names! I have no idea why. This is what I tried:

let s = "a b c"
"a b c".enumerateSubstringsInRange(s.characters.indices, options: .ByWords) {(w,_,_,_) in print(w!)}


All good; I just wanted to print out the matched words, one at a time. That worked when I specified by closure as ``{(w,,,_) in print(w!)}`. HOWEVER, when I try to write the closure with shorthand argument syntax, disaster:

> "a b c".enumerateSubstringsInRange(s.characters.indices, options: .ByWords) {print($0!)}
repl.swift:9:86: error: cannot force unwrap value of non-optional type '(substring: String?, substringRange: Range<Index>, enclosingRange: Range<Index>, inout Bool)' (aka '(substring: Optional<String>, substringRange: Range<String.CharacterView.Index>, enclosingRange: Range<String.CharacterView.Index>, inout Bool)')

So what did I do wrong?! The error message seems to say that closure argument
is the whole tuple of args. And indeed, when I tried that, that sure seems to be the case!

>"a b c".enumerateSubstringsInRange(s.characters.indices, options: .ByWords) {print($0.0!)}


So I'm terribly confused. Why in the first case (my function
, are the arguments understood to be
, etc., but in the second case, all the arguments are rolled up into a tuple? Or are they? FWIW, I found the signature of enumerateSubstringsInRange here.

Answer Source

It depends on the number of parameters.

For example,

func test( closure: (Int,Int,Int) -> Void ){
    // do something

To make test works as you expect, you must specify $2 ( 3rd argument ). The compiler will infer to the values inside tuple, otherwise it will infer to the tuple itself.

If you don't specify $number that match the number of parameters. For example, only specify $1, will make compile error.

// work as expected ( infer to int )

// not work ( infer to tuple )

// not work ( cannot infer and compile error )

There is a question relate to this question. Why is the shorthand argument name $0 returning a tuple of all parameters?