Guard & Defer en Swift

Swift 2.0 a introduit 2 nouvelles structures de control qui ont pour objectif de simplifier et rationaliser le code que nous écrivons : guard et defer. Tandis que le premier rend le code plus linéaire par nature, le deuxième permet de reporter l’exécution de son contenu. Dans ce tutoriel nous allons voir comment utiliser ces 2 nouvelles structures et dans quels cas ils sont utile pour améliorer la lisibilité et la compréhension du code.

Guard

Swift 1.2 avait réglé le problème de l’imbrication des if-let en introduisant la syntaxe du chainage multiple des optionnels. Avec Swift 2.0, guard supprime complètement le problème.

Cette nouvelle structure de control nécessite l’exécution à sortir du bloc de code courant si la condition n’est pas satisfaite. Ce qui est intéressant avec guard c’est que toutes liaisons optionnelles (Optional Binding) créer dans la condition sont disponibles dans l’ensemble du bloc de code courant :

for imageName in imageNames {
  guard let image = UIImage(named: imageName) else {
    continue
  }

  // Vous pouvez utiliser image
}

Le bloc de code après le else doit obligatoirement quitter le contexte courant en utilisant soit return pour quitter une méthode, continue ou break a l’intérieur d’une boucle, ou une fonction @noreturn comme fatalError().

Maintenant regardons un avant/après sur la manière dont guard peut améliorer notre code et empêcher certaines erreurs. Comme example nous allons créer une méthode qui va récupérer des informations sur liste d’utilisateur que l’on passe en paramètre. Cette méthode peut émettre des erreurs si il n’y a aucune connection de disponible, si l’on est pas connecté au service distant ou si tout simplement on ne lui passe pas d’utilisateur à chercher. Voici l’example sans utiliser de guard :

func chercherClients(clients: [Client]?) throws {
  if !accessible {
    throw APIError.APIErrorUnreachable
  }

  if !connecté {
    throw APIError.APIErrorNotConnected
  }

  if let clients = clients where clients.count > 0 {
    print(clients)
  }
  else {
    throw APIError.APIErrorNoCustomers
  }
}

Maintenant regardons ce que l’utilisation de guard apporte :

func chercherClients(clients: [Client]?) throws {
  guard !accessible else { throw APIError.APIErrorUnreachable }

  guard !connecté else { throw APIError.APIErrorNotConnected }

  guard let clients = clients where clients.count > 0 else {
    throw APIError.APIErrorNoCustomers
  }

  print(clients)
}

Beaucoup mieux. Maintenant chaque erreur est traiter dès qu’elle a été vérifier et nous savons en voyons le mot clé guard que le code peut potentiellement s’arrêter à cet endroit. De plus on évite ici le dernier if-let qui imbrique notre code et le rend donc moins lisible.

Defer

Entre la structure du guard et celle du throw pour la gestion des erreurs, Swift 2.0 semble encourager un certain style de retourner/quitter le plus tôt possible contrairement aux structures if imbriqués. Cependant retourner le plus tôt possible engendre de nouvelle problématique comme le fait qu’une fois des resources initialisé (et en utilisation) nous devons les nettoyer avant de sortir de la méthode.

Le nouveau mot clé defer fournit un moyen simple et sûre de répondre a ce problème en déclarant un bloc de code qui sera exécuter seulement quand l’exécution quitte le bloc de code courant. Prenons un example d’une fonction qui ouvre un fichier pour l’afficher :

func afficherFichier(nomDuFichier: String) throws {
  if let fichier = ouvrirFichier(nomDuFichier) {
    while let ligne = try fichier.ligneSuivante() {
      print(ligne)
    }

    fermerFichier(fichier)
  }
}

La problématique ici, c’est que si une erreur est levée durant l’exécution de la méthode ligneSuivante la méthode fermerFichier ne sera jamais fermé (ou alors il faut traiter le cas dans la méthode appelant afficherFichier).

Voici comment defer et guard viennent à la rescousse :

func afficherFichier(nomDuFichier: String) throws {
  guard let fichier = ouvrirFichier(nomDuFichier) else { return }

  defer {
    fermerFichier(fichier)
  }

  while let ligne = try fichier.ligneSuivante() {
    print(ligne)
  }
}

La méthode fermerFichier va être exécuter uniquement une fois que l’on aura fini de lire toute les lignes. Même si une erreur survient pendant la lecture des lignes le bloc de code dans defer sera dans tout les cas appelé. On est certain alors que le fichier sera correctement fermé. Cela nous permet d’éviter certains effet de bord et de rendre ainsi notre code plus sûre.

Pour finir, une chose importante à connaitre sur defer. Les blocs de code defer sont exécutés dans le sens inverse de leur déclaration. Par example :

func testDefer() {
  defer { print("A") }
  defer { print("B") }
  defer { print("C") }
}
// C, B, A
testDefer()

Cette ordre inverse est un détail essentiel, car il permet d’assurer que tout ce qui était à la portée du bloc lors de sa création le sera toujours lorsque le bloc est exécuté.

Conclusion

Un grand pouvoir implique de grandes responsabilités, il faut peser les avantages de chaque option du langage par rapport à ses coûts. Une nouvelle déclaration comme guard conduit à un code plus linéaire, un programme plus lisible et tend à être appliquer aussi largement que possible. De même, defer résout un défi de taille, mais nous oblige à garder une trace de sa déclaration comme il peut être hors de porté de la vue; et donc il faut l’utiliser avec réserve.

Comme d’habitude si vous avez des commentaires, n’hésitez pas.

1 Etoile2 Etoiles3 Etoiles4 Etoiles5 Etoiles (1 votes, average: 5,00 out of 5)
Loading...
Share on FacebookShare on Google+Tweet about this on TwitterShare on LinkedInPin on PinterestShare on RedditDigg this

2 commentaires

Time limit is exhausted. Please reload CAPTCHA.

  1. Webbdoger · 8 février 2016

    Très bonne explication =) continue je passerais régulièrement voir les nouveaux articles =)

    • yannickl · 8 février 2016

      Merci beaucoup pour ces encouragements, je compte bien continuer.