Swift 2.0 est sorti en version finale le 17 Septembre 2015. Mais puisque le blog Itelios n’existait pas à cette époque, pourquoi ne pas en parler maintenant ?

Allez, c’est parti pour une session rattrappage !

Let’s go !

La gestion des erreurs

Parmi les nouveautés majeures de Swift, il y a la gestion des erreurs. Fini l’utilisation des NSError herités de l’Objective-C et leur passage par référence 😱.

Swift propose une syntaxe qui est bien plus propre et plus à même de convenir aux développeurs venant du monde Java ou .Net : do...catch.

Oui, en Swift on ne parle pas de try...catch. Ne vous inquiétez pas, la syntaxe est quasi la même, vous noterez néanmoins après lecture de cette section que le try en Swift existe, qu’il est essentiel mais mais n’a le même usage que dans d’autre langage.

Les erreurs ErrorType

Dorénavant, on ne parlera plus de NSError mais de ErrorType. Swift impose que toute erreur lancée (comprendre throwed) dans le code doit hériter du protocole ErrorType.

Ce protocole, qui est vide de contenu public, permet à Swift d’identifier qu’il s’agit d’une erreur et non d’autre chose. Pour définir des erreurs particulières, la syntaxe la plus simple est l’utilisation d’une enum :

enum CustomErrorType : ErrorType {
    case ValueNotAllowed
}

Dans cet exemple, nous avons défini un type d’erreur nommé CustomErrorType qui peut avoir comme valeur ValueNotAllowed. Pour lever une erreur, il suffit d’utiliser l’opérateur throw.

throw CustomErrorType.ValueNotAllowed

La syntaxe est assez simple.

La syntaxe do...catch

Pour qu’une méthode puisse être incluse dans un do...catch, celle-ci doit être marquée comme leveuse d’erreur. Cela se fait via l’opérateur throws que l’on place en fin de définition de méthode, avant la définition des retours.

func withoutReturnValue() throws { 
	// ... throw error here
}

func withReturnValue() throws -> AnyObject { 
	// ... throw error here
}

Prenons le cas d’une méthode qui prend en paramètre un objet et le retourne si celui-ci est toléré.

func s<T where T:IntegerType>(v: T) throws -> T {
    guard 0...100 ~= v else { throw CustomErrorType.ValueNotAllowed }
    return v
}

On aurait pu faire plus simple, mais le choix du générique ici permettrait d’étendre la fonction s à d’autres types d’objets aisément.

Si on appelle la méthode s faire attention à la gestion des erreurs, la compilation échouera.

/swift-execution/code-tmp.swift:10:1: error: call can throw but is not marked with 'try'
s(10)
^~~~~

Pardon ? L’appel à la fonction n’est pas marqué par l’opérateur try ?

Et oui… comme je le disais, la gestion des erreurs se fait via l’utilisation des clauses do...catch… mais pas que ! Chaque méthode pouvant lever une erreur doit en plus être précédé du mot-clef try.

do {
    try print(s(10))
} catch {
    print(error)
}

Swift autorise l’utilisation d’une clause catch générique. Mais on peut aussi détailler chaque clause catch pour certaines erreurs.

do {
    try print(s(10))
} catch CustomErrorType.ValueNotAllowed {
    print("This value is not allowed")
}

Simplifier le test via l’approche optional

Swift a un atout majeur par rapport à certains langages: les types optionnels.

Un procédé similaire existe dans la gestion d’erreur : le try optionnel. Cela permet de simplifier la lecture dans certaines situations.

try? print(s(10))		/// print "10"
try? print(s(1000))		/// print nothing

Si on assigne à une variable le résultat d’un try optionnel, on aura une variable optionnelle :

let x = try? s(1000)
print(x.dynamicType) 	/// print "Optional<Int>"

L’écriture du même code, encapsulé dans un do...catch aura pour effet de stopper l’exécution du bloc de code do si un try échoue :

do {
    let x = try s(1000)
    print(x.dynamicType) 	/// print nothing, this code is never reached !
} catch { }

Outrepasser la gestion d’erreur

Swift permet tout de même de se passer de la gestion d’erreur en exécutant un try forcé: try!.

try! print(s(10))		/// print "10"

Par contre, comme toute utilisation du forcing en Swift, une mauvaise manipulation entraîne un crash systématique.

try! print(s(1000))		/// print "fatal error: 'try!' expression unexpectedly raised an error: main.CustomErrorType.ValueNotAllowed"

Retrouvez tous ces exemples dans la Sandbox Swift d’IBM

La mécanique des guard & defer

Je ne pense pas avoir déjà vu ce genre de mécanisme dans aucun autre langage. À mon avis il s’agit ici d’une différence majeure entre Swift et ses concurrents.

defer

Vous l’avez sûrement constaté, la clause do...catch a un défaut par rapport à ce qui existe dans d’autres langages: il n’y a point de finally.

FAUX !

En fait, Apple a choisi une approche différente. Plutôt que de proposer un bloc de code qui sera exécuté en fin de do...catch quelle que soit l’issue de celui-ci, ils ont preféré proposer une fonctionnalité qui permet d’exécuter un bloc de code en fin de… scope !

Prenons l’exemple de l’ouverture d’un fichier.

func readFile() {
    print("Opening file descriptor")   
    defer {
        print("Closing file descriptor")
    }
    print("Reading file content")
}

L’ouverture d’un fichier dans de nombreux langages nécessite la création d’un pointeur de fichier qui doit être explicitement fermé en fin d’utilisation, sous peine de fuite mémoire. On nomme ces pointeurs plus communément des file descriptor.

Cette problématique est bien connue des développeurs bas niveau qui manipulent l’allocation mémoire manuellement.

L’ouverture d’un pointeur et la lecture du contenu peuvent prendre du temps à être écrits. Et il s’agit là de la logique même voulue dans notre code. Fermer un pointeur en fin de méthode, AVANT de retourner le résultat, n’est pas forcément pratique.

Swift a donc pensé à ce problème là et propose de pouvoir écrire les codes dédiés au nettoyage des ressources à n’importe quel endroit du code, mais qui s’exécuteront au moment ou nous retournons notre résultat.

L’exécution du code prouve bien que le code dans defer est appelé en fin de scope.

Opening file descriptor
Reading file content
Closing file descriptor

De plus, Swift permet d’écrire plusieurs blocs defer au sein d’une même méthode et ceux-ci seront appelés séquentiellement, en mode LiFo (Last-In, First-Out).

func multiDefered() {
    defer { print("Get your woman on the floor") }
    defer { print("Four") }
    defer { print("Three") }
    defer { print("Two") }
    defer { print("One") }
    
    print("Coolio sing this in 1995 :")
}

multiDefered()

// print :
// > 	Coolio sing this in 1995 :
// >	One
// >	Two
// >	Three
// >	Four
// >	Get your woman on the floor

guard

Si defer permet de s’assurer qu’un bloc de code sera toujours exécuté lors de la clôture du scope, guard permet de s’assurer de certaines conditions avant de continuer dans le code.

Il s’avère très utile dans le cadre de l’utilisation de paramètres optionels, lorsqu’on veut s’assurer que ceux-ci sont bien définis avant de continuer l’exécution du code.

func plus(i: Int?, _ j: Int?) throws -> Int {
    enum Error: ErrorType { case BadImplementation }
    
    guard let i = i, let j = j else { throw Error.BadImplementation }
    
    return i + j
}

Si on exécute le code successivement avec un des paramètres vide puis avec les deux paramètres définis, nous observons bien que guard remplit son rôle.

var a = 1
var b: Int?
try? print(plus(a, b))		// print nothing, an exception is thrown so plus(a, b) returns nil here

b = 10
try? print(plus(a, b))		// print "11"

L’utilisation de l’opérateur guard est très pratique pour éviter les conditions à répétition dans le code if ... else if else ...

Retrouvez tous ces exemples dans la Sandbox Swift d’IBM

Les énumérations récursives

Les énumerations récursives, dison le tout de suite, ce n’est pas quelque chose que vous allez utiliser tous les jours.

Il s’agit d’un mécanisme permettant à une énumeration de se référencer elle-même dans les valeurs associées à ses possibilités. L’exemple fourni par Apple est très parlant.

Prenons une énumération dédiée aux opérations arithmétiques basiques.

indirect enum ArithmeticExpression {
    case Number(Int)
    case Addition(ArithmeticExpression, ArithmeticExpression)
    case Multiplication(ArithmeticExpression, ArithmeticExpression)
    case Substraction(ArithmeticExpression, ArithmeticExpression)
    case Division(ArithmeticExpression, ArithmeticExpression)
}

Ici, un nombre sera toujours un entier et il existe quatre opérations arithmétiques possibles.

La résolution de toute expression arithmétique se réduit à un nombre. On peut donc réaliser une méthode d’évaluation simplement en utilisant une programmation récursive.

func evaluate(expression: ArithmeticExpression) -> Int {
    switch expression {
    case .Number(let value):
        return value
    case .Addition(let left, let right):
        return evaluate(left) + evaluate(right)
    case .Multiplication(let left, let right):
        return evaluate(left) * evaluate(right)
    case .Division(let left, let right):
        return evaluate(left) / evaluate(right)
    case .Substraction(let left, let right):
        return evaluate(left) - evaluate(right)
    }
}

Cette méthode va évaluer les expressions arithmétiques passées en paramètre pour determiner le nombre final.

let five = ArithmeticExpression.Number(5)
let ten = ArithmeticExpression.Number(10)
let tenPlusFive = ArithmeticExpression.Addition(ten, five)
let tenMinusFive = ArithmeticExpression.Substraction(ten, five)
let tenByFive = ArithmeticExpression.Multiplication(ten, five)
let tenDividedByFive = ArithmeticExpression.Division(ten, five)

let allAdditioned = ArithmeticExpression.Addition(ArithmeticExpression.Addition(ArithmeticExpression.Addition(tenPlusFive, tenMinusFive), tenByFive), tenDividedByFive)

print(evaluate(five))					// print "5"
print(evaluate(ten))					// print "10"
print(evaluate(tenByFive))				// print "50"
print(evaluate(tenDividedByFive))		// print "2"
print(evaluate(tenPlusFive))			// print "15"
print(evaluate(tenMinusFive))			// print "5"
print(evaluate(allAdditioned))		// print "72"

Ce genre de cas bien spécifique ne vous servira pas tous les jours. Mais la connaissance de cette syntaxe est toujours bonne à prendre le jour où vous en aurez besoin !

Retrouvez tous ces exemples dans la Sandbox Swift d’IBM

La vérification de disponibilité dans le code

Avec l’évolution rapide du SDK d’iOS, les développeurs ont très vite été confrontés aux problématiques de disponibilité d’une API d’une version à une autre.

Les développeurs Objective-C n’avaient pas d’autre moyen que de faire la vérification dans le code à l’exécution en vérifiant qu’une classe existait ou qu’une instance de classe répondait à un sélecteur de méthode.

Avec Swift 2.0, Apple a décidé de gérer cela d’une manière plus propre, et surtout, à la compilation.

Pour cela, il faut utiliser l’opérateur #available en combinaison d’une clause if ou guard pour gérer les différentes situations qui peuvent se présenter.

Prenons l’exemple du framework CoreLocation qui a vu son fonctionnement changer avec l’arrivée d’iOS 9.

let manager = CLLocationManager()
if #available(iOS 9.0, *) {
	manager.allowsBackgroundLocationUpdates = true
} else {
	// inform user that some feature wasn't available in his device
}

L’opérateur #available prend en paramètre une liste de plateformes supportées avec le numéro de version associé (optionnel). Il existe trois types de plateformes possibles : iOS, OSX et watchOS. Néanmoins, il est fort probable que tvOS fasse partie des possibilités rapidement.

La liste doit toujours se terminer par le caractère *.

La programmation par protocol

De multiples paradigmes de programmation existent: programmation objet par classe, programmation objet par prototype, fonctionnelle, impérative, déclarative, etc.

Swift a la capacité, comme beaucoup d’autres langages, de travailler avec plusieurs paradigmes : orienté objet, fonctionnel et impératif.

Avec la version 2.0, Apple propose un nouveau paradigme: orienté protocole.

Les protocoles peuvent désormais être étendus comme des classes, et proposent une alternative de programmation similaire à ce qu’on pourrait faire dans d’autres langages avec des classes abstraites.

On peut donc dorénavant définir des implémentations par défaut de certaines méthodes depuis une extension de protocole sans jamais connaître réellement la classe ou la structure qui implémentera ces protocoles

Pour mieux illustrer ce principe, prenons un exemple parlant: les êtres vivants et leurs capacités physiques.

protocol Animal {    
    var canWalk: Bool { get }
    var canRun: Bool { get }
    var canFly: Bool { get }
}

Toute implémentation du protocole Animal doit implémenter trois propriétés : canWalk, canRun et canFly.

Par défaut, je souhaiterais que les propriétés physiques des animaux soient inactives. Nous pouvons donc d’ores et déjà définir une extension proposant une implémentation par défaut.

extension Animal {
    var canWalk: Bool { return false }
    var canRun: Bool { return false }
    var canFly: Bool { return false }

    func printCapacities() {
        print("\(self.dynamicType) \(canWalk ? "can" : "cannot") walk \(canFly ? "can" : "cannot") fly \(canRun ? "can" : "cannot") run")
    }    
}

Je me suis permis aussi d’ajouter une fonction qui va afficher les capacités d’un animal.

Si nous définissons une structure héritant du protocole Animal, nous obtiendrons bien un animal qui n’a aucune capacité.

struct Worm : Animal {}
print(Worm().printCapacities())		/// print "Worm cannot walk cannot fly cannot run"

A présent, nous allons définir trois nouveaux protocoles dédiés aux changements des capacités initiales d’un animal :

protocol Walker {}
protocol Runner {}
protocol Flyer {}

Par définition, ces protocoles ne font strictement rien. Il faut donc les étendre pour qu’ils adoptent un comportement par défaut agissant sur Animal.

extension Animal where Self:Walker {
    var canWalk: Bool { return true }
}
extension Animal where Self:Flyer {
    var canFly: Bool { return true }
}
extension Animal where Self:Runner {
    var canRun: Bool { return true }
}

Comme vous pouvez le constater, ici nous n’étendons pas directement les protocoles, mais nous étendons le protocole Animal pour que ses capacités par défaut soient différentes si celui-ci possède une caractéristique particulière, représentée par un protocole ici.

Ainsi, prenons l’exemple de trois animaux différents, au hasard: l’oiseau, le canard et l’être humain.

struct Bird: Animal, Flyer { }
struct Duck: Animal, Flyer, Walker, Runner { }
struct Human: Animal, Walker, Runner { }

Un appel à la méthode printCapacities() permet de bien se rendre compte de l’héritage des comportements par défaut.

Bird().printCapacities()		/// print "Bird cannot walk can fly cannot run"
Duck().printCapacities()		/// print "Duck can walk can fly can run"
Human().printCapacities()		/// print "Human can walk cannot fly can run"

Comme vous pouvez le constater, les capacités par défaut du protocole Animal ont été modifiées par les protocoles modifiant les capacités de celui-ci.

Retrouvez tous ces exemples dans la Sandbox Swift d’IBM

Bonus: les @autoclosure

Petit bonus qui ne servira pour ainsi dire quasiment jamais: les autoclosure.

Plutôt qu’une nouveauté importante, il s’agit d’un paramètre supplémentaire utilisable dans la définition d’une méthode. Celle-ci permet de simplifier le passage d’une closure en paramètre d’une méthode dans certaines situations.

Prenons par exemple, un groupe de personnes et une fonction qui prend en paramètre une closure fournissant des personnes (au hasard, celles du groupe défini auparavant) :

var SemiCroustillants = ["Gwendal", "Marc", "Fabien", "Fred", "Vincent", "Nicolas", "Florent"]

func presentYou(peopleProvider : () -> String) {
    print("Bonjour, je m'appelle \(peopleProvider()).")
}

Si l’on veut boucler aléatoirement pour que chaque personne se présente, nous devons donc passer une closure en argument de la méthode presentYou.

for index in 1...SemiCroustillants.count  {
    let random = Int(rand()) % SemiCroustillants.count
    presentYou({ SemiCroustillants.removeAtIndex(random) })
}

// print :
// > 	Bonjour, je m'appelle Fred.
// > 	Bonjour, je m'appelle Nicolas.
// > 	Bonjour, je m'appelle Vincent.
// > 	Bonjour, je m'appelle Fabien.
// > 	Bonjour, je m'appelle Florent.
// > 	Bonjour, je m'appelle Gwendal.
// > 	Bonjour, je m'appelle Marc.

Ici, notre closure ne fait qu’appeler une méthode qui retourne elle-même une chaîne de caractères. Il est donc assez ridicule de devoir passer par une closure ici, mais la définition de la méthode ne nous permet pas d’utiliser un string directement.

Changeons la méthode presentYou pour modifier le paramètre comme ayant la capacité @autoclosure.

func presentYou(@autoclosure peopleProvider : () -> String) {
    print("Bonjour, je m'appelle \(peopleProvider()).")
}

Nous pouvons donc à présent simplifier l’écriture de notre boucle.

for index in 1...SemiCroustillants.count  {
    let random = Int(rand()) % SemiCroustillants.count
    presentYou(SemiCroustillants.removeAtIndex(random))
}

Ici, Swift sait qu’il doit encapsuler l’appel à SemiCroustillant.removeAtIndex(random) dans une closure lors de l’entrée dans la méthode. Cela simplifie l’écriture.

Attention, cela peut aussi mener à des confusions dans la lecture du code.

Retrouvez tous ces exemples dans la Sandbox Swift d’IBM


Voilà, bien entendu, cette liste n’est pas exhaustive et aborde principalement les grands changements de Swift 2.0 vis à vis de Swift 1.2 (hormis le bonus sur l’autoclosure qui a été proposé dans Swift 2.1).

Pour découvrir l’ensemble des nouveautés, je vous invite à consulter la liste des changements effectués sur la documentation Swift. Celle-ci est totalement exhaustive et bien documentée (pour tous ceux qui n’ont pas peur de la langue de Shakespeare)