L'idée de Rust est qu'à la compilation, on ne peut pas avoir de problème de concurrence, de fuite mémoire, de segmentation.
Mais en cherchant comment faire certaines opérations sur internet, je me retrouve avec du code contenant le mot unsafe. Je me suis refusé à l'utiliser mais pourquoi faire un langage qui se veut sûr et qui permet de faire des choses unsafe ?
Dans un certain nombre de situations, qu'on le veuille ou pas, il est absolument nécessaire de faire des choses qui ne peuvent pas être vérifiées à la compilation.
Par exemple :
Appeler une fonction dans une bibliothèque C (i.e., faire de la FFI) Le compilateur ne peut pas vérifier qu'elle est bien appelée ni que l'interaction avec le modèle de gestion mémoire de la bibliothèque est correcte.
Sauter un bound check (vérification que i est entre 0 et t.len() quand on fait t[i]) dans une boucle appelée des millions de fois, quand on est sûr que l'indice est bien dans les bornes.
Faire des appels à l'allocateur de mémoire et travailler avec ce qu'il donne, pour implémenter une structure de données.
unsafe est l'une des fonctionnalités les plus intéressantes du langage. Ça permet d'écrire quand même toutes ces choses en Rust, plutôt que dans des langages qui font très peu de vérifications comme le C, et quand on veut auditer la sécurité du code, il suffit de se concentrer sur les blocs unsafe plutôt que de regarder absolument tout. Par ailleurs, il y a des outils pour vérifier à l'exécution que le unsafe ne déclenche pas d'undefined behavior (Miri) et il y a des projets de recherche pour créer des outils de preuve formelle de sûreté de code unsafe (RustBelt).
Sans unsafe, la bibliothèque standard std ne pourrait pas être elle-même écrite en Rust. Le noyau Linux n'aurait pas pu incorporer du code Rust. Et tu ne pourrais pas utiliser dans ton code la bibliothèque C fuse.
La syntaxe des durées de vie est complexe et rend le code peu lisible. Là aussi, je trouve dommage que Rust ne soit pas capable de gérer ces durées de vie tout seul.
Je ne trouve pas la syntaxe complexe. Les paramètres de lifetime sont déclarés exactement de la même manière que les paramètres de type, en mettant juste un ' devant.
« Gérer les durées de vie tout seul »... difficile de savoir ce que tu entends par là. Si tu veux dire inférer les lifetimes dans les signatures, c'est tout à fait possible, de même que l'inférence de types — en fait il n'y a pas de distinction, c'est la même chose que l'inférence de types. Et le compilateur le fait bel et bien à l'intérieur d'une fonction. Mais il y a un choix de la part des concepteurs du langage d'exiger toujours que le type d'une fonction soit déclaré explicitement, pour qu'il soit facile de comprendre comment utiliser une fonction sans avoir à lire son code.
Cela dit, l'élision des lifetimes allège très souvent les déclarations (par exemple fn id(x: &str) -> &str { x } au lieu de fn<'a> id(x: &'a str) -> &'a str { x }, les règles sont ici).
De la même manière, j'ai l'impression que Rust nous oblige souvent à utiliser le clonage d'objet. Parfois j'aurais préféré ne pas cloner l'objet mais faire une référence (pour des questions de performances), mais là on se retrouve à gérer des durées de vie. Parfois je n'ai pas trouvé comment éviter le clonage simplement.
Il faut un tout petit peu d'expérience, mais on trouve rarement des cas où le compilateur force vraiment un clonage non-nécessaire.
Je n'y ai passé que quelques minutes, mais en parcourant les appels clone(), j'en ai déjà trouvé un certain nombre qui peuvent s'éviter simplement :
# Gestion mémoire etc.
Posté par jeanas (site web personnel, Mastodon) . En réponse au journal PullRequest d'une application en Rust. Évalué à 10.
Dans un certain nombre de situations, qu'on le veuille ou pas, il est absolument nécessaire de faire des choses qui ne peuvent pas être vérifiées à la compilation.
Par exemple :
iest entre 0 ett.len()quand on faitt[i]) dans une boucle appelée des millions de fois, quand on est sûr que l'indice est bien dans les bornes.unsafeest l'une des fonctionnalités les plus intéressantes du langage. Ça permet d'écrire quand même toutes ces choses en Rust, plutôt que dans des langages qui font très peu de vérifications comme le C, et quand on veut auditer la sécurité du code, il suffit de se concentrer sur les blocsunsafeplutôt que de regarder absolument tout. Par ailleurs, il y a des outils pour vérifier à l'exécution que leunsafene déclenche pas d'undefined behavior (Miri) et il y a des projets de recherche pour créer des outils de preuve formelle de sûreté de codeunsafe(RustBelt).Sans
unsafe, la bibliothèque standardstdne pourrait pas être elle-même écrite en Rust. Le noyau Linux n'aurait pas pu incorporer du code Rust. Et tu ne pourrais pas utiliser dans ton code la bibliothèque Cfuse.Je ne trouve pas la syntaxe complexe. Les paramètres de lifetime sont déclarés exactement de la même manière que les paramètres de type, en mettant juste un
'devant.« Gérer les durées de vie tout seul »... difficile de savoir ce que tu entends par là. Si tu veux dire inférer les lifetimes dans les signatures, c'est tout à fait possible, de même que l'inférence de types — en fait il n'y a pas de distinction, c'est la même chose que l'inférence de types. Et le compilateur le fait bel et bien à l'intérieur d'une fonction. Mais il y a un choix de la part des concepteurs du langage d'exiger toujours que le type d'une fonction soit déclaré explicitement, pour qu'il soit facile de comprendre comment utiliser une fonction sans avoir à lire son code.
Cela dit, l'élision des lifetimes allège très souvent les déclarations (par exemple
fn id(x: &str) -> &str { x }au lieu defn<'a> id(x: &'a str) -> &'a str { x }, les règles sont ici).Il faut un tout petit peu d'expérience, mais on trouve rarement des cas où le compilateur force vraiment un clonage non-nécessaire.
Je n'y ai passé que quelques minutes, mais en parcourant les appels
clone(), j'en ai déjà trouvé un certain nombre qui peuvent s'éviter simplement :