Le langage Rust a fêté ses six ans, et tout va très bien
Le 17 mai 2021 à 08h39
2 min
Logiciel
Logiciel
La version 1.0 du langage a été lancée le 15 mai 2015. Depuis, le chemin parcouru a modifié profondément les perspectives du projet, qui a maintenant sa propre fondation et le soutien de plusieurs multinationales, dont Amazon, Google et Microsoft.
Comme le résume l’équipe, le plus grand changement est l’utilisation de Rust en production, y compris dans des projets à visées commerciales. La promesse des performances du C dans un environnement plus accueillant et en bénéficiant d’avantages de langages managés comme le memory safe, attire toujours plus de développeurs.
Sur la dernière année écoulée, l’équipe rappelle les grandes étapes franchies : le passage à LLVM 12 dans Rust 1.52, le support Tier 1 de l’ARM 64 bits pour Linux et Tier 2 de l’ARM macOS et Windows, la stabilisation des génériques const pour les types primitive, l’arrivée du control flow pour const fn et l’extension des macros procédurales.
Le soutien de plus en plus fort des grosses entreprises accentue la gravité autour du langage. Amazon et Microsoft ont par exemple publié des outils pour faciliter l’utilisation de Rust au sein de Windows et AWS. Google permet aux développeurs d’utiliser Rust pour l’écriture et la compilation de composants bas niveau dans Android.
Octobre verra l’arrivée de l’édition 2021 du langage, qui introduira plusieurs changements pouvant rompre la compatibilité avec l’ancien code (chaque édition crée un roulement d’environ trois ans).
La plupart des apports se concentreront sur la facilité d’utilisation. Mara Bos, l’une des développeuses principales de Rust, a consacré un billet de blog aux améliorations prévues.
Le 17 mai 2021 à 08h39
Commentaires (40)
Vous devez être abonné pour pouvoir commenter.
Déjà abonné ? Se connecter
Abonnez-vousLe 17/05/2021 à 09h35
Quelqu’un pourrait m’expliquer l’intérêt que les variables soient constantes par défaut dans ce langage ? Dans ce que je programme, il y a rarement plus de constantes que de variables. Du coup je n’y vois qu’un allongement inutile de la syntaxe.
Le 17/05/2021 à 09h53
C’est une question de bonnes pratiques. Une variable est plus dangereuse qu’une constante.
C’est un peu comme sudo sur linux. Ça ne sert qu’a t’indiquer que ce que tu es en train de faire peu avoir de grosses conséquences.
En plus, dans les langages récents (j’inclus java 8 dedans), les variables ne sont pas souvent utiles au final.
Le 17/05/2021 à 10h01
Une variable (globale ou partagée) initialisée par un thread et modifiée par un autre peut avoir des conséquences inattendues sur le premier thread. Bon cet exemple est tordu car normalement on ne fait pas ça.
Le 17/05/2021 à 10h03
La sécurité.
Le 17/05/2021 à 10h25
Comme le dit le Rust book, on peut y trouver plusieurs raisons, mais la principale est la suivante: la sûreté. Je préfère un langage qui soit plus strict par défaut.
En bref, c’est juste une inversion de paradigme (par rapport au C++ par exemple) pour éviter les erreurs de réécriture non souhaités (source de beaucoup d’erreurs), pour garder le contrôle sur la vie des variables… Et le compilateur va te prévenir si tu utilises des variables mutables alors que t’en a pas besoin, et inversement, ça reste donc très simple à utiliser ça n’ajoute pas de complexité (au contraire).
Et au final après quelques temps à coder comme ça tu te rendra compte qu’assez peu de variables ont vraiment besoin d’être mutables.
Le 17/05/2021 à 13h12
Depuis quelque années une bonne pratique veut de faire de la programmation fonctionnelle, avec de l’immutabilité et donc un comportement en sortie parfaitement prédictible en sortie en fonction de ce que tu as en entrée, sans effet de bord.
Le 17/05/2021 à 10h15
Merci à tous pour vos réponses.
Ca dépend franchement de sa portée. Et il y a rarement besoin de déclarer des constantes au niveau local, le plus souvent les constantes utilisées à ce niveau sont déjà déclarées (en constante du coup) à un niveau plus global.
Euh, je suis pas sûr qu’on désigne la même chose avec le terme variable. Moi je penses surtout aux variables locales à une fonction, qui sont finalement les plus nombreuses avec la plus petite portée. Je vois pas bien comment on peut s’en passer, la moindre boucle for en a déjà une et n’est qu’un cas parmi d’autres, et ne parlons pas des fonctions où il y a un minimum d’algorithmique.
C’est un cas assez particulier que tu cites ici, mais sauf erreur, la syntaxe mut s’applique à toutes les variables, même les plus locales.
Oui, mais encore ?
Le 17/05/2021 à 10h26
C’est ce que je me disais au début mais au final a l’usage, je me suis rendu compte que moins de la moité de mes variable ont réellement besoin d’être modifiées. et au final le fait de devoir déclarer la mutabilité a l’usage ça évite pas mal d’erreur de manipulation.
C’est encore plus important en Rust en ce qui concerne les références. En effet ce qu’on peut faire en Rust avec les références est volontairement limité et c’est la clé de ce qui permet a Rust de garantir la sécurité mémoire. On ne peut avoir une seule référence mutable active a la fois sur une valeur, ça permet d’interdire les accès concurrent a une variable. A l’inverse un nombre illimité de référence peuvent accéder à une valeur, à condition de ne pas la modifier.
Le 17/05/2021 à 10h27
Le 17/05/2021 à 10h48
Encore merci pour vos réponses.
Quand je lis ça, je pense effectivement aux variables que l’ont crée pour condenser l’écriture du code qui vient derrière, genre éviter d’écrire partout truc.machin.bidule.chose et remplacer ça par une seule variable, qui n’existe d’ailleurs plus forcément une fois compilé. Ou celles destinées à seulement stocker la valeur de retour d’une fonction. Possible que j’en sous-estime leur nombre en effet.
Et oui on ne nomme pas une variable i, j’ai pour habitude de leur donner des noms explicites sans compter les caractères.
C’est un principe que je comprends mieux (vu que ça désigne souvent des ressources partagées) et qui ne devrait pas me poser de problème, j’utilises déjà beaucoup les références et pointeurs constants. Là comme ça j’ai juste un peu peur que ça pose des problèmes d’interopérabilité avec des binaires écrit dans d’autres langages (et donc avec d’autres paradigmes de programmation).
On peut toujours transformer un code itératif en code récursif, et inversement. Mais le choix d’utiliser l’un ou l’autre, et la clarté de la syntaxe finale, dépend grandement du problème posé. Je n’ai pas connaissance qu’une méthode soit universellement meilleure qu’une autre.
Le 17/05/2021 à 11h15
Non, ça ne dépend pas de sa portée, une variable est par définition plus dangereuse qu’une constante car son contenu peut changer et donc passer par des états anormaux (null pour ne citer que le plus connu, avec les fameux NullPointerException qui font toujours plaisirs).
C’est un peu comme les access modifiers. Rien ne t’empêche de tout mettre public et charge à celui qui utilise ce que tu as fait de pas faire de conneries.
On parle bien de la même chose. Comme l’a dit BarbossHack, dans biens des cas, les variables ne sont pas utiles.
En changeant un peu de paradigme, on s’en passe très facilement. Arrêter de faire des boucles for et utiliser les api de programmation fonctionnelle disponible dans plein de langages par exemple (.filter(), .map(), .reduce(), etc). Je suis bien conscient qu’il y a des cas où ce n’est pas possible de s’en passer mais souvent, on peut. En plus le code est plus clair.
Le 17/05/2021 à 11h16
Tout dépend du contexte. “i” peut très bien être le bon nom de la variable si tu codes une formule mathématique. Même chose pour x, y, z ou t qui peuvent avoir une signification univoque dans ce cadre…
Bref tout ça pour dire que le “bon” nom de la variable ne dépend pas de son nombre de caractères mais de sa signification, ce qui n’empêche pas eslint de m’enguirlander.
Dans l’autre sens, c’est le terme const n’est pas approprié, ça devrait être readonly ou final pour bien indiquer que ce n’est pas une constante “universelle” comme le serait pi, mais bien une valeur intermédiaire d’un calcul, qui ne doit plus changer. Sinon, hé bien on a des programmeurs qui se posent des questions existentielles, comme ici.
Ce qui n’empêche pas qu’il faut bien évidemment toujours écrire
for const i in 1...n
donc oui, les langages qui mettent ce “const” automatiquement sont une bénédiction.Le 17/05/2021 à 11h20
Je ne suis pas spécialiste de Rust, mais j’avais regardé un peu, et ça rappelait tout de même fortement OCaml, qui a ses racines dans le fonctionnel pur. Et en fonctionnel pur, justement, il n’y a pas de variable. Changer la valeur d’une variable est considéré comme un effet de bord, ce qui est totalement proscrit, le fonctionnel pur ne faisant que des évaluations de résultats de fonctions sans autre impact sur quoi que ce soit d’autre.
Le “problème” est que Rust mélange ce paradigme fonctionnel avec quelque-chose de plus “classique”, c’est à dire de l’impératif procédural à la C - ce n’est pas un reproche, OCaml le fait aussi, et d’autre langages issus du fonctionnel aussi, comme le Lisp, par exemple. Donc on peut écrire du C en Rust, mais les deux langages n’ont pas les même racines, et donc pas les mêmes façons de faire. Quelqu’un qui vient d’OCaml - ou pire, de Haskell, qui est du fonctionnel pur et dur - trouvera parfaitement normal d’avoir par défaut des “variables” qui n’en sont pas, et ça sera tout à fait adapté à la façon dont il écrit son code.
Ce que tu dis est d’ailleurs un peu inattendu: le fonctionnel pur est justement considéré comme particulièrement adapté aux problèmes algorithmiques, alors qu’il n’y a justement pas de variable au sens “classique”. Par contre, l’approche sera radicalement différente par rapport à ce qu’on pourrait faire en C.
Le 17/05/2021 à 11h22
J’avais effectivement oublié de préciser « dans la plupart des cas » mais ce n’est de toute façon pas le débat ici. Merci tout de même.
Le 17/05/2021 à 11h24
Et moi qui marche sur des oeufs pour éviter de démarrer une flame war entre partisans du fonctionnel pur et aficionados de l’impératif procédural dans les commentaires…
Le 17/05/2021 à 12h54
Déclarer ses intentions. Ne pas risquer d’optimiser une valeur qui est variable mais par l’extérieur.
Moi j’aime bien. Rust est un langage qui a été étudé en amont, méthodiquement. Pas un pet project sorti à l’arrache non fini comme Javascript et python.
Franchement, ça fait du bien. Rust apporte de vraies solutions aux problèmes réel que sont le multithreading et la sécu par défaut.
De mon expérience, très peu de gens sont capables d’appréhender correctement le multithreading par exemple. Les risque d’effet de bord sont rarement compris.
Si on prend les concepts qui ne sont même pas tous de l’optimisation que sont: le calcul réparti, la parallélisation, le multithreading, l’exécution différée par des lambdas ou des promesses, peu de gens voient rapidement poindre les problèmes et le débogage est suffisamment complexe (genre impossible à reproduire le cas de figure en débogage…)
Donc merci Rust de limiter tout cela!
Le 17/05/2021 à 14h43
En effet en programmant en Rust, je me suis rendu compte a que les variables qui me servent a stocker des résultats intermédiaires sont en fait bien plus courantes comparé aux variables qui ont réellement besoin de changer de valeur. De plus quand on prend l’habitude de faire du fonctionnel, ce que Rust ne force pas mais engage pas mal a faire, on se rend compte que l’on a pas besoin de tant de mutation que ça. En limitant les mutations on limite les points de risque, notamment quand on fait du concurrent.
Le code qui vient d’autres langage est considéré “unsafe” par défaut. Si tu sais que le code auquel tu fais appel respecte les règles que Rust garantit, tu peux simplement encapsuler l’appel dans un bloc unsafe ce qui signifie que tu déclares au compilateur que a l’intérieur du bloc tu assures personnellement que le code respecte les règles que le Rust garantit dans le code “safe”.
En effet, Rust pousse l’utilisation du fonctionnel, mais il n’est pas dans le fonctionnel pur, d’où le choix du mot clé mut supplémentaire pour permettre la mutabilité, mais pas par défaut.
C’est ce qui est bien avec le Rust, tu peux faire soit l’un, soit l’autre, mais généralement plutôt un mix des deux de manière assez propre.
Le 17/05/2021 à 15h54
OCaml a dû avoir un ancêtre fonctionnel pur, mais il autorise le mélange fonctionnel/impératif aussi. Par contre, c’est sûr que c’est moins immédiat ou “naturel” pour quelqu’un qui vient du C, vu qu’il faut faire des références explicites sur les valeurs, et qu’après il y a une syntaxe spécifique pour les manipuler. Et:
let x = ref 2 and y = 3 in x := !x * y + 4; Printf.printf "%d\n" !x
ça ne va pas forcément être très parlant pour quelqu’un qui vient du C (hint: x est “mutable”, pas y ).
Le 17/05/2021 à 16h08
C’est sûr que par définition, c’est plus dangereux, mais l’utilité n’est pas la même non plus, et on limite cette dangerosité justement en limitant leur portée. Les variables locales ne deviennent pas null toutes seules, puisque c’est local, on voir facilement où et quand elles peuvent être modifiées, à l’inverse des variables plus globales où c’est beaucoup plus dur et source d’erreurs.
Je ne trouve pas l’analogie correcte vu que je parle de variables locales, alors que les access que tu cites sont plutôt globaux dans le sens où n’importe qui qui a une référence peut les utiliser, c’est pour ça qu’on en limite l’accès, parce que la portée est plus grande.
On peut s’en passer, mais ça ne me paraît en rien être une obligation, ni même être meilleur, ni systématiquement plus clair, juste différent, un peu comme le récursif contre l’itératif, ou simplement les différents paradigmes de programmation. Ca dépend du problème posé, des traitements à faire à chaque itération, et de l’impact en performances des API que tu cites (qui peut exister ou pas et dépend de leur implémentation ainsi que de la puissance disponible). Mais j’imagine que c’est effectivement ce qui est encouragé ici.
Je ne cherche pas de guerre et je n’ai rien globalement contre d’autres types de programmation tant qu’on me les expose sans vouloir me les imposer (bon y a bien quelques cas particuliers qui me hérissent le poil mais pas ici). Je voulais juste la réponse à ma question et j’en ai eu plein de constructives.
Ce genre de chose ne peut arriver que pour des variables globales. En C on rajoute volatile pour éviter cette optimisation, mais c’est distinct d’une variable locale modifiable qui est optimisable vu qu’elle ne peut jamais être modifiée de l’extérieur.
On dirait bien oui, mais là tu risques de te faire insulter, surtout pour le deuxième. Quant au premier, on ne peut pas trop le lui reprocher vu l’époque et le contexte où il a été inventé, ainsi que l’utilisation envisagée à ce moment-là.
Faut que j’étudie tout ça (en Rust, dans d’autres langages je fais déjà mais c’est effectivement vite coton).
Je penses plutôt aux cas où tu récupères des références de structures de données depuis du code d’un autre langage, où ils peuvent ne pas s’être forcé sur l’architecture et avoir tout fourré dedans en vrac, et tu peux te retrouver à trimballer des structures d’un objet à un autre et à devoir y écrire depuis plusieurs endroits. Enfin c’est assez spécifique et j’ai pas assez étudié la question pour voir si c’est vraiment problématique ou s’il suffit juste au pire de les wrapper dans du Rust.
C’est sûrement ça qui m’a échappé et qui m’a fait poser ma question d’origine.
Le 17/05/2021 à 17h22
C’est justement là qu’est le truc. Le meilleur moyen de signaler quelles valeurs peuvent être modifié, c’est de rendre toutes les valeurs non-modifiables par défaut et d’ajouter un mot clef précis qui signifie “le code a été conçu avec le fait que cette valeur soit une modifiée”.
Donc avec cette pratique, tu te protèges toi même des erreurs de programmation et tu aides les autres à comprendre ton code.
Les comparaisons sont toujours un peu limitées pour argumenter, c’est sûr. Mais les cas d’usages sont un peu similaires : quand quelqu’un va utiliser ou repasser sur ton code, il faut qu’il comprenne le plus rapidement possible comment a été conçu ce dernier.
Donc l’effet est double : tu sécurises ton code contre tes propres erreurs (en te forçant à réfléchir à si oui ou non tel ou tel valeur doit être modifiée) et tu le rends plus explicite, plus auto-descriptif, ce qui aide les autres à comprendre ce que tu as fais.
On est bien d’accord sur le fond. A moins d’avoir des contraintes particulières, la clarté et l’homogénéité du code est plus importante que le reste (y compris les performances). D’autant qu’il y a autant de bonnes pratiques que de programmeurs.
Le 17/05/2021 à 18h16
Euh, une variable qui ne varie pas, ce n’est pas une variable mais une constante.
Le 17/05/2021 à 19h24
Serait-ce la journée de la diptérophilie ?
Le 18/05/2021 à 05h51
Ça reste une variable car tu peux avoir recours a la mutabilité intérieure (
Cell
). Et techniquement si tu es vraiment motivé pour faire n’importe quoi, tu peux la faire varier avec une vilaine combinaison de méthodes unsafe.En Rust, pour une vraie constante garantie d’être calculée a la compilation, il y a le mot clé
const
.Le 18/05/2021 à 06h31
Ca me rappelle le bon temps du C++ où tu pouvais avoir un pointeur constant vers un objet mutable ou un pointeur mutable vers un objet constant, ou les deux, selon que tu écrivais
const MyClass p1
ouMyClass const p2
Un pur bonheur.
Le 18/05/2021 à 06h26
Oui. Sujet suivant : l’accolade ouvrante doit-elle être sur une ligne à part, ou à la fin de la ligne précédente ?
Le 18/05/2021 à 06h39
Tous ces nouveaux langages, c’est bien, c’est beau, et ça n’a aucune pérennité.
Du code C/C++ écrit il y a 20 ans, ça compile encore aujourd’hui avec les derniers compilateurs. Alors que du code écrit dans ces nouveaux langages, 3 ans après, ben c’est devenu incompatible.
Le 18/05/2021 à 06h49
Rust a 6 ans et c’est toujours compatible, Il évolue a peu près a la même vitesse que le C++.
Le 18/05/2021 à 07h56
Là brève indique :
Octobre verra l’arrivée de l’édition 2021 du langage, qui introduira plusieurs changements pouvant rompre la compatibilité avec l’ancien code (chaque édition crée un roulement d’environ trois ans).
Donc, ça ne va pas durer.
6 ans, pour un langage informatique, ce n’est rien.
Le 18/05/2021 à 06h47
A la différence qu’en Rust, tu ne peux pas avoir de référence mutable à un objet non mutable sauf avec un pointeur ‘raw’ (que l’on utilise généralement que pour interagir avec le C) que tu ne pourras utiliser que dans un bloc unsafe.
Le 18/05/2021 à 08h11
Non, pas avec les passages de pointeurs. On peut accéder à une variable via un pointeur direct ou indirect dans une structure. Et avec des éléments de langage comme les lambdas + les outils de parallélisation, on peut aller dans des bugs sympatiques…
Le 18/05/2021 à 08h51
En fait, il y a déjà eu une édition en 2018, mais le système d’édition est conçu afin de garantir que l’on ne casse pas la compatibilité avec l’ancien code.
Le principe est que chaque crate (l’unité de package en Rust) déclare l’édition qu’elle utilise au compilateur qui sait donc quelle type d’analyse appliquer au code, mais au niveau de la représentation intermédiaire et de la génération du binaire, tout reste commun. Donc une crate qui utilise l’édition 2015 peut s’interfacer de manière totalement transparente avec une crate qui utilise l’édition 2018 et vice versa.
Le 18/05/2021 à 09h47
Tu sais que ce bon temps existe toujours, pour pas mal de gens. Et il ne me gêne pas le moins du monde.
Si c’est une structure passée en argument par pointeur, ce n’est pas une variable locale puisqu’elle vient de l’extérieur. Une variable locale c’est une variable déclarée à l’intérieur d’une fonction, elle est crée au début de la fonction et détruite à la fin, et aucun autre code que cette fonction ne peut y accéder à moins que la fonction ne passe son adresse à une autre fonction, chose qu’il ne faut faire que si on est sûr que la deuxième fonction ne va pas garder cette adresse après s’être terminée.
Le 18/05/2021 à 10h35
La seule chose qui me manque vraiment dans les langages que j’utilise maintenant, c’est l’héritage multiple (surtout l’héritage protected) qui était un mixin avant la lettre, et bien plus puissant. Il fallait faire gaffe, mais ça marchait super bien pour ce qu’on avait à faire.
[my life as a no-life]
Un jour, un gars “qui savait” m’a vivement conseillé de remplacer l’héritage multiple protected par de la composition. Je l’ai pris au mot et on a passé une demi-journée à ré-implémenter mon truc avec de la composition, sur un cas typique.
Quand on a eu fini, chaque classe (il y en avait une vingtaine) avait gagné 20 ou 30 lignes de code “boilerplate” qu’il fallait injecter à la main (ha oui, pas de préprocesseur non plus…), des fonctions avaient été dédoublées, etc.
Et puis je lui ai signalé que le code boilerplate qu’on a dû précautionneusement insérer à la main dans chaque classe faisait exactement ce que le compilateur C++ générait automatiquement dans la version avec héritage multiple virtuel.
Il est parti en faisant la tronche, je ne l’ai plus jamais revu. Peut-être qu’on n’aurait pas dû faire cet exercice devant tous les développeurs de ma boîte…
[/my life as a no-life]
Le 18/05/2021 à 11h20
Comme toujours, c’est la mesure et l’adéquation au problème posé qui détermine s’il est opportun ou non de recourir à une fonctionnalité, pas les conventions. J’utilise ces deux fonctionnalités avec parcimonie, quand ça apporte une amélioration sans nuire à la lisibilité, voire en l’améliorant. La seule fonctionnalité du C++ que je n’ai jamais utilisée, c’est le goto.
Se méfier des gens qui ont des principes ou solutions toutes faites à imposer, tout en écoutant quand-même ce qu’ils ont à dire car on peut ne pas tout savoir ou avoir pensé à tout. Si tu arrives à justifier tes choix sans obtenir en retour autre chose qu’un “d’habitude on fait pas comme ça” sans justification, c’est que ton choix est certainement valable. Et c’est pas valable que pour la programmation.
Le 18/05/2021 à 12h41
Exactement. Dans ce cas, l’héritage multiple se justifiait et était plus efficace que la composition. Mais on n’avait pas besoin de profiter de fonctionnalités come l’injection de dépendance dans ce cas précis. Certaines autres parties de la solution utilisaient de fait la composition et ça marchait super.
Et je dois vérifier, mais je crois qu’il y avait un goto quelque part dans ce code :-) Stricto sensu, il y avait une machine à états finis, est-ce qu’une transition peut être considérée un goto ?
Le 18/05/2021 à 13h02
Oui, parce que la variable locale est créée sur la pile, et qu’un fois revenue au niveau de l’appelant, une prochaine routine appelée risque de reprendre cet emplacement mémoire.
Malheureusement, on manipule souvent des types qui sont passés par référence dans la plupart des langages, s’ils sont mutables, c’est risqué.
Par ailleurs, certains langages permettent d’enfreindre cette règle très facilement (notamment avec la programmation par “promesses” ou avec des lambdas): les lignes de codes ne sont pas forcément exécutées dans l’ordre dans lequel on les voit.
Exemple en C# (je force le trait);
Résultat:
2
3
Marrant, non? En plus, le résultat sera vite différent si tu n’a qu’un seul CPU…
Maintenant imagine que dans un code, ces lambdas se balladent partout car la bibliothèque est faite ainsi… Et elle n’est pas mauvaise: simplement avec un paradigme de base issu d’un monde ancien, elle ouvre un peu la boite de Pandore.
A une époque, j’ai fait du fortran parallélisé (64 CPU en 2000). C’était propre, malgré le fait que c’était des ajouts dans le langage: au programmeur de dire avec un pragma s’il voulait que le compilo parallélise un FOR, et quelles étaient les variables constantes. Toute variable non déclarée constante dans la boucle était considérée comme une variable et donc protégée contre les accès concurrentiels.
Le 18/05/2021 à 17h38
Quand je dis goto, je veux dire écrire réellement goto dans le code. Les machines à états, je fais habituellement ça avec un switch plus ou moins gros dans une boucle infinie (ou presque), on pourrait l’assimiler à un goto. Après s’il y a un intérêt particulier à faire un goto et que ça ne rend pas le code illisible, y a qu’à en faire un, je dis juste que je n’ai jamais eu besoin d’en faire.
Donc une variable locale.
Donc une variable pas locale, et donc des règles d’utilisation différentes. D’ailleurs en C++ les références ou pointeurs sont très souvent passées en const pour éviter (une partie) des problèmes, mais personne ne fait l’effort de déclarer ses variables locales en const car l’intérêt est plutôt limité, d’où ma question au sujet du Rust.
Il faut toujours faire très gaffe avec les lambdas, se demander où elles peuvent finir, et quelles sont les variables capturées puisque la capture transforme des variables locales en non locales. Est-ce que c’est plus facile en Rust de ne pas se planter, je ne sais pas, c’est possible.
Le 18/05/2021 à 20h59
J’ai tenté de voir comment C# faisait pour ces captures: je n’ai pas compris. Il se comporte comme si la variable étaient maintenue à sa dernière valeur connue avant de quitter la routine, et les threads l’utilisent correctement APRES la sortie. Si je tente de l’écraser avec d’autres appels à des routines, elle n’est pas écrasée - donc la pile n’est pas réutilisée de façon simple comme en C.
L’exemple que j’avais mis est trivial, mais résume ce que j’ai du/je dois déboguer parfois mais avec des milliers de lignes entre les deux…
Avec Rust, il est plus difficile de se planter car on doit manuellement pointer une variable. Là vraiment le problème n’est pas d’avoir de lambdas ou des bibliothèques parallélisées, c’est de mélanger cela avec un vieux paradigme. Ca devient ahurissant pour tout le monde le nombre de choses à penser/apprendre/comprendre quand on programme. Perso j’ai commencé il y a longtemps et j’ai appris au fur et à mesure que ça devenait “courant”, je ne sais pas comment font les nouveaux programmeurs. Enfin, en vrai, si, je vois comment ils font: mal.
Mais je reconnais que je comprends bien mieux l’ADA maintenant qu’il y a 20ans quand on me l’a montré :)
Le 18/05/2021 à 23h45
En fait, plutôt que de chercher comment ça se comporte, je trouve plus simple de savoir comment c’est fait en réalité pour en mesurer directement les implications.
Quand le compilateur voit un lambda qui capture une variable locale, il remplace la déclaration de cette variable locale par une instance d’une classe anonyme dont la variable locale est un champ, et le lambda une méthode membre. Tous les accès hors lambda à la variable précédemment locale sont remplacés par un accès au champ de cette instance.
Ainsi, la déclaration
int a = 0;
devient (je mets des noms mais en vrai il n’y en a pas) :class Anonymous
{
int var = 0;
type_retour lambda(arguments) { code }
}
Anonymous a = new Anonymous();
Les accès hors lambda deviennent
a.var
, et l’affectation devienta.var = val
. Dans le lambda,var
est directement accessible.En appliquant les règles classiques de cycle de vie et de visibilité, on voit bien que la même instance de la variable précédemment locale est disponible à 2 endroits : dans le bloc où elle a été déclarée, et dans le lambda. Donc toute modification de sa valeur, que ce soit dans hors lambda (et ce même après la création du lambda), ou dans le lambda, aura un effet dans le code qui sera exécuté après et qui a accès à cette instance, qu’il soit hors lambda ou dans le lambda. On comprend mieux aussi le cycle de vie de cette instance, notamment pourquoi repasser plus tard dans ce bloc après en être sorti créera une nouvelle instance distincte de la première.
Il y a un cas un peu particulier à envisager, et qui d’ailleurs a fait l’objet d’une modification de comportement sur je sais plus quelle version : les variables déclarées entre les parenthèses des instructions
for
etforeach
. Au début, le compilateur les traitait comme si elles étaient en dehors du bloc qui suit lefor
. Mais du coup, ça ne marchait pas avec les lambdas déclarés à l’intérieur de la boucle, puisqu’alors une seule instance de la classe anonyme était créée hors du bloc, passée à tous les lambdas lors de l’itération, donc tous les lambdas se retrouvaient avec la valeur qu’avait la variable après la dernière itération.La solution était alors simplement de déclarer, avant le lambda, une variable locale en y affectant la valeur de la variable d’itération, et la donner au lambda. Ainsi, c’est la variable locale qui est remplacée par la classe anonyme et non la variable d’itération, et à chaque itération, une nouvelle instance de la classe anonyme est créée, et chaque lambda reçoit une valeur différente. Plus tard, ce comportement a été considéré comme contre-intuitif et le compilateur a été modifié pour considérer la variable d’itération comme étant déclarée au début du bloc au lieu de juste avant, ce qui a résolu le problème sans rupture de compatibilité.
Si tout ce que je dis n’est pas clair, un exemple ici. Comparer le code de la question avec celui de la 3ème réponse, et la 1ère réponse illustre la solution citée ci-dessus.
Et là j’ai pris l’exemple d’une variable locale capturée, mais il se passe la même chose si on capture une variable plus globale mais accessible (par exemple un membre de la classe parente), avec des effets bien plus dévastateurs vu la portée augmentée. Si on a quand-même besoin de le faire, la solution est la même que pour la boucle for, ajouter une variable locale pour que ce soit elle qui soit capturé et non la variable globale.
Je pense qu’ils font comme tu as fait, ils apprennent au fur et à mesure, car même pour les choses qui existent déjà, on ne s’essaie pas à tout d’un coup, et on se fait toujours avoir plus ou moins par une seule chose à la fois.
Ca c’est surtout du au fait que pas mal de langages dits “simples” (non, pas Rust) abstraient des concepts réels derrière des trucs plus ou moins intuitifs et magiques. Déjà ça n’encourage pas la rigueur, mais surtout, les choses réelles, même cachées, se produisent quand même, et selon la loi des abstractions qui fuient, il y a toujours des cas particuliers où soit ça merde, soit le concept réel fait que l’abstraction ne se comporte pas on le penserait. Et là, bonne chance à eux pour comprendre pourquoi s’ils n’ont pas un minimum d’idée de ce qui se passe derrière.
Le 19/05/2021 à 06h54
Excellente explication/démonstration, merci! Je n’avais pas trouvé d’article sur le comment du pourquoi. Et parfait sur l’exemple (le premier que j’ai connu) des for/foreach: ça a effectivement changé depuis C#4.
Cela confirme ce que je pense: ces mécanismes comme la capture ont été ajoutés et font partie d’un paradigme qui s’éloigne énormément du paradigme d’origine.
Le problème, c’est qu’il faut être au courant des 2, hors les tutos/vidéos/formation sont pour la plupart basés sur des exemples naïfs du monde des bisounours, mettant en avant un fonctionnement, pas l’amalgame des deux…
En fait, non: comme ils arrivent “formés” C#/CSS/JS/HTML mais ne connaissent pas bien ce qui concerne l’architecture/le déploiement, ils se cassent les dents sur le déploiement en même temps qu’ils découvrent les bugs de la vie réelle.
Et là je passe pour leur dire que RIEN en informatique n’est dispo tout le temps et qu’il faut revoir tout le code de gestion des erreurs (déjà tout un thème)
Je dirais qu’avant, on détricotait le pull et on avait le fil, là il y a tant de chose qui peuvent se passer en même temps, dans un autre ordre, bagotter, saturer… qu’il faut tout étudier tout de suite.
Pour moi la pléthore de langage reflète une chose: l’apparente complexité des langages et bibliothèques actuels. Donc quelqu’un “pond” un nouveau langage révolutionnaire, qui s’avère plein de limitations (les mêmes que les anciens langages à leur début) et 2 ans plus tard il a tout pour être compétitif et on le considère comme les “anciens” langages: trop complexe à maîtriser.
Rooooo! +++
Merci merci merci pour les échanges. Je pense toujours que Rust est une bonne idée (et que JS et python sont des pis-aller), que si autant de langages sortent c’est parce que les anciens langages ont du mal à s’adapter au monde du web et à la parallélisation (de même que les esprits).