web-dev-qa-db-fra.com

Compréhension et relation entre Box, ref et & et *

Je suis un peu confus quant au fonctionnement des pointeurs à Rust. Il y a ref, Box, &, *, Et je ne sais pas comment ils fonctionnent ensemble.

Voici comment je le comprends actuellement:

  1. Box n'est pas vraiment un pointeur - c'est un moyen d'allouer des données sur le tas et de passer autour de types non dimensionnés (traits en particulier) dans les arguments de fonction.
  2. ref est utilisé dans la correspondance de motifs pour emprunter quelque chose sur lequel vous correspondez, au lieu de le prendre. Par exemple,

    let thing: Option<i32> = Some(4);
    match thing {
        None => println!("none!"),
        Some(ref x) => println!("{}", x), // x is a borrowed thing
    }
    println!("{}", x + 1); // wouldn't work without the ref since the block would have taken ownership of the data
    
  3. & Est utilisé pour faire un emprunt (pointeur emprunté). Si j'ai une fonction fn foo(&self) alors je prends une référence à moi-même qui expirera après la fin de la fonction, laissant les données de l'appelant seules. Je peux également transmettre des données dont je souhaite conserver la propriété en faisant bar(&mydata).

  4. * Est utilisé pour créer un pointeur brut: par exemple, let y: i32 = 4; let x = &y as *const i32. Je comprends les pointeurs en C/C++ mais je ne sais pas comment cela fonctionne avec le système de type de Rust et comment ils peuvent être utilisés en toute sécurité. Je ne sais pas non plus quels sont les cas d'utilisation pour ce type de pointeur. De plus, le symbole * Peut être utilisé pour déréférencer des choses (quelles choses et pourquoi?).

Quelqu'un pourrait-il m'expliquer le 4ème type de pointeur et vérifier que ma compréhension des autres types est correcte? J'apprécierais également toute personne signalant des cas d'utilisation courants que je n'ai pas mentionnés.

46
zrneely

Tout d'abord, tous les éléments que vous avez répertoriés sont des choses vraiment différentes, même s'ils sont liés à des pointeurs. Box est un type de pointeur intelligent défini par la bibliothèque; ref est une syntaxe pour la correspondance de motifs; & Est un opérateur de référence, doublant comme un sigil dans les types de référence; * Est un opérateur de déréférence, doublant comme un sigil dans les types de pointeurs bruts. Voir ci-dessous pour plus d'explications.

Il existe quatre types de pointeurs de base dans Rust qui peuvent être divisés en deux groupes - références et pointeurs bruts:

&T        - immutable (shared) reference
&mut T    - mutable (exclusive) reference

*const T  - immutable raw pointer
*mut T    - mutable raw pointer

La différence entre les deux derniers est très mince, car l'un ou l'autre peut être converti en un autre sans aucune restriction, donc la distinction const/mut y sert principalement de peluche. Les pointeurs bruts peuvent être créés librement vers n'importe quoi, et ils peuvent également être créés à partir de rien à partir d'entiers, par exemple.

Naturellement, ce n'est pas le cas pour les références - les types de référence et leur interaction définissent l'une des principales caractéristiques de Rust: l'emprunt. Les références comportent de nombreuses restrictions sur la manière et le moment où elles peuvent être créées, comment elles peuvent être utilisées et comment elles interagissent les unes avec les autres. En retour, ils peuvent être utilisés sans blocs unsafe. Ce que l'emprunt est exactement et comment cela fonctionne est cependant hors de portée de cette réponse.

Les références et les pointeurs bruts peuvent être créés à l'aide de l'opérateur &:

let x: u32 = 12;

let ref1: &u32 = &x;
let raw1: *const u32 = &x;

let ref2: &mut u32 = &mut x;
let raw2: *mut u32 = &mut x;

Les références et les pointeurs bruts peuvent être déréférencés à l'aide de l'opérateur *, Bien que pour les pointeurs bruts, il nécessite un bloc unsafe:

*ref1; *ref2;

unsafe { *raw1; *raw2; }

L'opérateur de déréférencement est souvent omis, car un autre opérateur, l'opérateur "point" (c'est-à-dire .), Référence ou déréférence automatiquement son argument de gauche. Ainsi, par exemple, si nous avons ces définitions:

struct X { n: u32 };

impl X {
    fn method(&self) -> u32 { self.n }
}

puis, malgré que method() prend self par référence, self.n le déréférence automatiquement, vous n'aurez donc pas à taper (*self).n. La même chose se produit lorsque method() est appelée:

let x = X { n: 12 };
let n = x.method();

Ici, le compilateur fait automatiquement référence à x dans x.method(), vous n'aurez donc pas à écrire (&x).method().

L'avant-dernier morceau de code a également démontré la syntaxe spéciale &self. Cela signifie simplement self: &Self, Ou, plus précisément, self: &X Dans cet exemple. &mut self, *const self, *mut self Fonctionnent également.

Ainsi, les références sont le type de pointeur principal dans Rust et doivent être utilisées presque toujours. Les pointeurs bruts, qui n'ont pas de restrictions de références, doivent être utilisés dans du code de bas niveau implémentant de haut niveau abstractions (collections, pointeurs intelligents, etc.) et en FFI (interaction avec les bibliothèques C).

La rouille a également types de taille dynamique (ou non dimensionnés) . Ces types n'ont pas de taille définie statiquement connue et ne peuvent donc être utilisés que via un pointeur/référence. Cependant, seul un pointeur ne suffit pas - des informations supplémentaires sont nécessaires, par exemple, la longueur des tranches ou un pointeur vers une table de méthodes virtuelle pour les objets trait. Ces informations sont "intégrées" dans des pointeurs vers des types non dimensionnés, ce qui les rend "grosses".

Un gros pointeur est essentiellement une structure qui contient le pointeur réel vers l'élément de données et quelques informations supplémentaires (longueur pour les tranches, pointeur vers vtable pour les objets de trait). L'important ici est que Rust gère ces détails sur le contenu du pointeur de manière absolument transparente pour l'utilisateur - si vous passez des valeurs &[u32] Ou *mut SomeTrait Autour, les informations internes correspondantes seront être transmis automatiquement.

Box<T> Est l'un des pointeurs intelligents de la bibliothèque standard Rust. Il fournit un moyen d'allouer suffisamment de mémoire sur le tas pour stocker une valeur du type correspondant, puis il sert de poignée, un pointeur vers cette mémoire. Box<T> possède les données vers lesquelles il pointe; lorsqu'il est supprimé, la mémoire correspondante sur le tas est désallouée.

Une façon très utile de penser aux boîtes est de les considérer comme des valeurs régulières, mais avec une taille fixe. Autrement dit, Box<T> Équivaut à seulement T, sauf qu'il prend toujours un certain nombre d'octets qui correspondent à la taille du pointeur de votre machine. Nous disons que les boîtes (possédées) fournissent la sémantique des valeurs . En interne, ils sont implémentés à l'aide de pointeurs bruts, comme presque toute autre abstraction de haut niveau.

Boxes (en fait, cela est vrai pour presque tous les autres pointeurs intelligents, comme Rc) peuvent également être empruntés: vous pouvez obtenir un &T sur Box<T>. Cela peut se produire automatiquement avec l'opérateur . Ou vous pouvez le faire explicitement en déréférençant et en le référençant à nouveau:

let x: Box<u32> = Box::new(12);
let y: &u32 = &*x;

À cet égard, Boxes sont similaires aux pointeurs intégrés - vous pouvez utiliser l'opérateur de déréférence pour accéder à leur contenu. Cela est possible car l'opérateur de déréférencement dans Rust est surchargeable et il est surchargé pour la plupart (sinon tous) des types de pointeurs intelligents. Cela permet d'emprunter facilement le contenu de ces pointeurs.

Et, enfin, ref n'est qu'une syntaxe dans les modèles pour obtenir une variable du type de référence au lieu d'une valeur. Par exemple:

let x: u32 = 12;

let y = x;           // y: u32, a copy of x
let ref z = x;       // z: &u32, points to x
let ref mut zz = x;  // zz: &mut u32, points to x

Alors que l'exemple ci-dessus peut être réécrit avec des opérateurs de référence:

let z = &x;
let zz = &mut x;

(ce qui le rendrait également plus idiomatique), il y a des cas où refs sont indispensables, par exemple, quand on prend des références dans des variantes d'énumération:

let x: Option<Vec<u32>> = ...;

match x {
    Some(ref v) => ...
    None => ...
}

Dans l'exemple ci-dessus, x n'est emprunté que dans l'intégralité de l'instruction match, ce qui permet d'utiliser x après ce match. Si nous l'écrivons comme tel:

match x {
    Some(v) => ...
    None => ...
}

alors x sera consommé par ce match et deviendra inutilisable par la suite.

87
Vladimir Matveev

Box est logiquement un nouveau type autour d'un pointeur brut (*const T). Cependant, il alloue et désalloue ses données pendant la construction et la destruction, il n'a donc pas à emprunter de données à une autre source.

La même chose est vraie pour les autres types de pointeurs, comme Rc - un pointeur compté par référence. Ce sont des structures contenant des pointeurs bruts privés à partir desquels ils allouent et se désallouent.

Un pointeur brut a exactement la même disposition qu'un pointeur normal, il n'est donc pas compatible avec les pointeurs C dans plusieurs cas. Surtout, *const str et *const [T] sont de gros pointeurs , ce qui signifie qu'ils contiennent des informations supplémentaires sur la longueur de la valeur.

Cependant, les pointeurs bruts ne donnent absolument aucune garantie quant à leur validité. Par exemple, je peux faire en toute sécurité

123 as *const String

Ce pointeur n'est pas valide, car l'emplacement mémoire 123 ne pointe pas vers un String valide. Ainsi, lors du déréférencement d'un, un bloc unsafe est requis.

De plus, alors que les emprunteurs sont tenus de respecter certaines lois - à savoir que vous ne pouvez pas avoir plusieurs emprunts si l'un est mutable - les pointeurs bruts n'ont pas à respecter cela. Il y a d'autres lois, plus faibles, qui doivent être respectées, mais vous êtes moins susceptibles de les enfreindre.

Il n'y a pas de différence logique entre *mut et *const, bien qu'il soit peut-être nécessaire de les couler sur l'autre pour effectuer certaines opérations - la différence est documentaire.

9
Veedrac

Les références et les pointeurs bruts sont la même chose au niveau de l'implémentation. La différence du point de vue du programmeur est que les références sont sûres (en termes Rust), mais pas les pointeurs bruts.

Le vérificateur d'emprunt garantit que les références sont toujours valides (gestion de la durée de vie), que vous ne pouvez avoir qu'une seule référence mutable à la fois, etc.

Ce type de contrainte peut être trop strict pour de nombreux cas d'utilisation, donc les pointeurs bruts (qui n'ont pas de contraintes, comme en C/C++) sont utiles pour implémenter des structures de données de bas niveau, et en général des choses de bas niveau. Cependant, vous ne pouvez déréférencer que des pointeurs bruts ou effectuer des opérations sur eux à l'intérieur d'un bloc unsafe.

Les conteneurs de la bibliothèque standard sont implémentés à l'aide de pointeurs bruts, Box et Rc également.

Box et Rc sont ce que sont les pointeurs intelligents en C++, c'est-à-dire les wrappers autour des pointeurs bruts.

5
eulerdisk