Come includere un argomento extra in una chiamata su una chiamata di chiusura in Swift per iOS

Dipende da come viene chiamata la chiusura e da quanta stenografia/inferenza viene usata.

Skip to the end for main answer to this, what follows first is a fairly deep dive into the subtle interplays of type inference, shorthand, closure expressions, trailing closures and a few others.

A closure is just a block of code which is usually defined in braces.

  1. print("hello") 

Se si digita quanto sopra in un parco giochi, si ottiene l'errore "Closure expression is unused". Questo ci dice due cose, primo, che si tratta effettivamente di una chiusura, e secondo, che non viene usata per niente.

Questo è in realtà un grande indizio, perché come e dove esprimiamo le chiusure è strettamente legato a come e dove le usiamo.

Per quanto detto sopra, di solito la vediamo espressa come una funzione. A Function can be thought of as a special case closure that is given a name which we will call when we want to use it.

  1. func greet() { 
  2. print("hello") 
  3.  
  4. greet() 
  5. // prints "hello" 

So here, the braces define the block of code, while func assigns a name, parameter list and a return type to that block. Chiamiamo la funzione usando il suo nome e il blocco viene poi eseguito.

Ora potreste essere sorpresi di vedere il tipo di ritorno menzionato qui perché non è stato specificato alcun tipo di ritorno e la funzione non finisce nemmeno con una dichiarazione di ritorno.

Questa è la stenografia di Swift e l'inferenza dei tipi in azione. Entrambi giocano un grande ruolo in Swift, e specialmente nelle chiusure. Both of these are great for the experienced programmer because they can write code much more concisely.

However, beginners typically learn a lot about a language by reading through other people’s code, and both shorthand and type inference refer to inferred code that isn’t actually there to be read! So when learning, it’s actually more useful to peel back the shorthand and inference and be more explicit about what is happening.

It’s a feature of Swift that blocks without a return value will implicitly return a Void. So the above function is actually shorthand for:

  1. func greet() -> Void { 
  2. print("hello") 
  3. return Void() 
  4.  
  5. greet() 
  6. // prints "hello" 

As a function, it has a return type of type Void, but as a block it has type of:

  1. () -> Void 

This means it takes no parameter and returns a Void. As a named function it also has a signature composed of its name and type, so:

  1. greet() -> Void 

Note, I’m continuing to use Void intentionally here, but you won't usually see it written like that. You will usually find it either omitted or written as (), so the block type is:

  1. () -> () 

However, I think that extra parens can sometimes impair readability, especially while learning, so I’ll stick with Void in these examples to help emphasise the context.

Now let’s introduce parameters into the mix:

  1. func greet(user: String) -> Void { 
  2. print("hello (user)") 
  3. return Void() 
  4.  
  5. greet("Andy") 
  6. // fails with missing argument label error 

Here, by introducing the parameter, I have inadvertently fallen foul of shorthand again. The specification for a function parameter actually takes two names per parameter:

  1. func name(argumentLabel parameterName: paramType) 

The parameterName: paramType are the parameter’s name and Type. This is what I specified with:

  1. greet(user: String) 

Because I didn't specify an argument label, I was writing shorthand for using the same name for both the argument label and the parameter name, kind of like I’d actually written:

  1. greet(user user: String) 

So in order to use this function I will need to specify the user: argument label when calling greet.

  1. func greet(user: String) -> Void { 
  2. print("hello (user)") 
  3. return Void() 
  4.  
  5. greet(user: "Andy") 
  6. // prints "hello Andy" 

I could use a different name for the argument label

  1. func greet(username user: String) -> Void { 
  2. print("hello (user)") 
  3. return Void() 
  4.  
  5. greet(username: "Andy") 
  6. // prints "hello Andy" 

Or I could decide that the label is superfluous and eliminate it.

  1. func greet(_ user: String) -> Void { 
  2. print("hello (user)") 
  3. return Void() 
  4.  
  5. greet("Andy") 
  6. // prints "hello Andy" 

So even for simple functions shorthand and type inference plays a role in how you express and call the function.

With other closures, the syntax and execution call can vary much more significantly so it’s even more important to keep track of what is actually happening.

Let’s start simple and just replace the function with a regular closure expression assigned to a constant.

  1. let greet = {(_ user: String) -> Void in 
  2. print("hello (user)") 
  3. return Void() 
  4.  
  5. greet("Andy") 
  6. // prints "hello Andy" 

You can see just how similar to a function this is, but even so, there are some extra important aspects to be aware of.

Most importantly, look how the parameter list and return type specification have migrated into the start of the block. To separate this spec from the main body we use the ‘in’ keyword.

Another difference is that although the parameter definition looks the same, we can no longer use argument labels for closures so the “_” isn’t needed here, in other words, we should write it like this:

  1. let greet = {(user: String) -> Void in 
  2. print("hello (user)") 
  3. return Void() 
  4.  
  5. greet("Andy") 
  6. // prints "hello Andy" 

To pass additional parameters into the closure, we simply specify multiple parameters in the closure, and pass multiple arguments when we call for execution:

  1. let greet = {(user: String, isAM: Bool) -> Void in 
  2. print("good (isAM ? "morning" : "afternoon") (user)") 
  3. return Void() 
  4.  
  5. greet("Andy", true) 
  6. // prints "good morning Andy" 
  7.  
  8. greet("Andy", false) 
  9. // prints "good afternoon Andy" 

It’s also worth noting that type inference is also happening in this definition. The constant called greet is a reference to a closure which has a type of:

  1. (String, Bool) -> Void 
  2.  
  3. // aka (String, Bool) -> () 

Swift has inferred this type from the definition of the closure, so we don’t need to specify it, but in the spirit of unrolling all the shortcuts, lets add it in:

  1. let greet: (String, Bool) -> Void = {(user: String, isAM: Bool) -> Void in 
  2. print("good (isAM ? "morning" : "afternoon") (user)") 
  3. return Void() 
  4.  
  5. greet("Andy", true) 
  6. // prints "good morning Andy" 
  7.  
  8. greet("Andy", false) 
  9. // prints "good afternoon Andy" 

Type inference works the other way around too. Because we have specified the type of greet, Swift knows the parameters and return value of the closure.

This means these can be omitted from the closure, but in doing so the parameter names will be lost. The body still has access to the argument(s) but they are now anonymous and so are given the names $0 for the first, $1 for the second, and so on.

  1. let greet: (String, Bool) -> Void = { 
  2. print("good ($1 ? "morning" : "afternoon") ($0)") 
  3. return Void() 
  4.  
  5. greet("Andy", true) 
  6. // prints "good morning Andy" 
  7.  
  8. greet("Andy", false) 
  9. // prints "good afternoon Andy" 

Now obviously this would not be a good solution in this case because without the parameter names we have lost some code readability, but anonymous arguments do play an important role in exploiting shorthand, and in particular when we don’t call the closure ourselves.

To demonstrate, we’ll adapt an example from the Swift language guide:

  1. let names = ["Chris", "Alice", "Bob", "Arnold"] 
  2.  
  3. let reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in 
  4. return s1 > s2 
  5. }) 
  6.  
  7. print(reversedNames) 
  8. // prints ["Chris", "Bob", "Arnold", "Alice"] 

.sorted is an instance method that works on an array and returns a copy of the array in sorted order.

It can optionally take a parameter with the argument label ‘by:’ which can be used to pass a closure to determines how the array elements are to be sorted.

Rather than dive right in to what it does, instead lets first see the shorthand version

  1. let reversedNames = names.sorted(by: >) 
  2.  
  3. print(reversedNames) 
  4. // prints ["Chris", "Bob", "Arnold", "Alice"] 

Through type inference, shorthand and something called operator methods, this:

  1. (by: { (s1: String, s2: String) -> Bool in return s1 > s2 }) 

becomes this:

  1. (by: >) 

Let’s break it down:

The last (only) expression ‘s1 > s2’ returns a boolean so the explicit return can be dropped:

  1. (by: { (s1: String, s2: String) -> Bool in s1 > s2 }) 

Type inference can infer the type as:

  1. (String, String) -> Bool 

so that can be dropped:

  1. (by: {s1, s2 in s1 > s2 }) 

anonymous arguments can be used:

  1. (by: { $0 > $1 }) 

Now if we wanted to stop there, there is a little syntactic sugar that can be applied when the closure is the last argument, it can be move outside the parens (and the argument label can be dropped), this is called trailing closure syntax.:

  1. () { $0 > $1 } 

This is how you will often see this type of closure used, but in our case there is a different feature we can use which is Operator Methods.

For Strings, the ‘>’ (greater than) operator is defined as a method whose type is also:

  1. (String, String) -> Bool 

So in this instance the ‘>’ method operator happens to match our closure so we can simply pass the ‘>’ operator, hence:

  1. let reversedNames = names.sorted(by: >) 
  2.  
  3. print(reversedNames) 
  4. // prints ["Chris", "Bob", "Arnold", "Alice"] 

One last aspect to consider is when we want the closure to have access to other values.

When we both created and called the closure ourself, we could simply pass it any values we chose as parameters/arguments. Ma quando la chiusura viene chiamata da altro codice, come il metodo .sorted, gli argomenti sono forniti dal chiamante, quindi non abbiamo la libertà di aggiungere ulteriori argomenti. Invece, abbiamo dei valori di cattura.

Questo è in realtà molto semplice e viene fatto facendo riferimento ad un valore attualmente in-scope da usare. Here’s a somewhat contrived but hopefully easy to follow example.

  1. let reversed = false 
  2. let sortedNames = names.sorted(by: ) { reversed ? $0 > $1 : $0 < $1 } 
  3. print(sortedNames) 
  4. // prints ["Alice", "Arnold", "Bob", "Chris"] 

Here, we have introduced a reversed constant and a reference to it is being captured in the closure and then used to decide whether or not to sort reversed.

Questo potrebbe non essere immediatamente ovvio e sono sicuro che qualcuno potrebbe contestare se conta come cattura, ma in questo contesto è inutile preoccuparsi se il valore è catturato o semplicemente nello scope. The important part is the availability of the value for the closure.

To demonstrate ‘real’ capture, we need a slightly more complex example:

Rather than sort the name directly into a named copy, we instead create a ‘name sorter’ by enclosing the sort into its own closure:

  1. var reversed = false 
  2. let sorter = { names.sorted(by: ) { reversed ? $0 > $1 : $0 < $1 }} 
  3. print(sorter()) 
  4. // prints ["Alice", "Arnold", "Bob", "Chris"] 
  5. reversed = true 
  6. print(sorter()) 
  7. // prints ["Chris", "Bob", "Arnold", "Alice"] 

Now, the sort is done within its own closure which can be called when we want the results.

Now we can show that we are really capturing the value of reversed by making it copy by value, which would mean when we change the value of reversed, the sorter would continue to use its own copy and not be affected by the change.

Here’s the first attempt:

  1. var reversed = false 
  2. let sorter = { names.sorted(by: ) { [reversed] in reversed ? $0 > $1 : $0 < $1 }} 
  3. print(sorter()) 
  4. // prints ["Alice", "Arnold", "Bob", "Chris"] 
  5. reversed = true 
  6. print(sorter()) 
  7. // prints ["Chris", "Bob", "Arnold", "Alice"] 

Here we told the inner closure (that gets passed to .sorted(by:)) to capture reversed as a copy, we did this by adding it to a capture list by putting it inside [] (square brackets). Questo significa anche che dobbiamo reintrodurre 'in' per separarlo dal corpo.

Tuttavia, non ha avuto il risultato desiderato. L'errore è sottile.

Questa chiusura interna viene creata quando viene chiamato .sorted. Per come stanno le cose, ciò significa che invertito sarà qualunque sia il suo valore attuale. In altre parole, viene catturato due volte, una volta sulla prima chiamata a .sorted quando il valore è falso, e di nuovo sulla seconda chiamata a .sorted, quando il valore è vero. Hence, capturing in this closure has no impact.

Here’s how we actually capture a copy:

  1. var reversed = false 
  2. let sorter = { [reversed] in names.sorted(by: ) { reversed ? $0 > $1 : $0 < $1 }} 
  3. print(sorter()) 
  4. // prints ["Alice", "Arnold", "Bob", "Chris"] 
  5. reversed = true 
  6. print(sorter()) 
  7. // prints ["Alice", "Arnold", "Bob", "Chris"] 

Here we capture reversed in the outer closure, the one that gets assign to sorter.

This closure is created when it is assigned to sorter and so it captures a copy of reversed current value, which is false.

This copied value is now part of the sorter closure and that’s the value used whenever the closure is executed. Non importa dove cambiamo il valore dell'originale invertito, la chiusura verrà sempre eseguita con il suo valore copiato impostato a false.