2015-06-19 4 views
9

Ho un tratto con due funzioni associate:Perché è necessario il limite `Dimensione 'necessario in questo tratto?

trait WithConstructor: Sized { 
    fn new_with_param(param: usize) -> Self; 

    fn new() -> Self { 
     Self::new_with_param(0) 
    } 
} 

Perché l'implementazione predefinita del secondo metodo (new()) mi costringono a mettere il Sized legata al tipo? Penso che sia dovuto alla manipolazione del puntatore dello stack, ma non ne sono sicuro.

Se il compilatore deve conoscere la dimensione per allocare memoria nello stack, perché il seguente esempio non richiede Sized per T?

struct SimpleStruct<T> { 
    field: T, 
} 

fn main() { 
    let s = SimpleStruct { field: 0u32 }; 
} 

risposta

21

Come probabilmente già sapete, i tipi in Rust possono essere dimensionati e non dimensionati. I tipi non standard, come suggerisce il loro nome, non hanno una dimensione necessaria per memorizzare valori di questo tipo che sono noti al compilatore. Ad esempio, [u32] è una matrice non numerata di u32 s; poiché il numero di elementi non è specificato da nessuna parte, il compilatore non ne conosce la dimensione. Un altro esempio è un nudo tipo di oggetto tratto, ad esempio, Display, quando viene utilizzato direttamente come Tipo:

let x: Display = ...; 

In questo caso, il compilatore non sa quale tipo viene effettivamente utilizzato qui, si cancella, quindi non conosce la dimensione dei valori di questi tipi. La riga sopra non è valida - non è possibile creare una variabile locale senza conoscere la sua dimensione (per allocare abbastanza byte nello stack) e si non può passare il valore di un tipo non salvato in una funzione come argomento o restituirlo da uno.

I tipi non registrati possono essere utilizzati tramite un puntatore, tuttavia, che può contenere informazioni aggiuntive: la lunghezza dei dati disponibili per le porzioni (&[u32]) o un puntatore a un tavolo virtuale (Box<SomeTrait>). Poiché i puntatori hanno sempre una dimensione fissa e nota, possono essere memorizzati in variabili locali e passati o restituiti da funzioni.

Dato qualsiasi tipo di calcestruzzo si può sempre dire se è dimensionato o non dimensionato. Con i generici, tuttavia, sorge una domanda: alcuni parametri di tipo sono dimensionati o no?

fn generic_fn<T>(x: T) -> T { ... } 

Se T è non calibrati, allora tale definizione di funzione non è corretto, in quanto non è possibile passare i valori non calibrati in giro direttamente. Se è dimensionato, allora tutto è OK.

In Rust tutti i parametri di tipo generico vengono ridimensionati di default ovunque, nelle funzioni, nelle strutture e nei tratti.Hanno un limite implicito Sized; Sized è un tratto per la marcatura tipi di dimensioni:

fn generic_fn<T: Sized>(x: T) -> T { ... } 

Questo perché nella stragrande maggioranza delle volte si desidera che i parametri generici per essere di dimensioni. A volte, tuttavia, che ci si vuole scegliere di sizedness, e questo può essere fatto con ?Sized vincolati:

fn generic_fn<T: ?Sized>(x: &T) -> u32 { ... } 

Ora generic_fn può essere chiamato come generic_fn("abcde"), e T vengono istanziati con str che è non calibrati, ma va bene - questa funzione accetta un riferimento a T, quindi non succede niente di male.

Tuttavia, c'è un altro posto in cui la questione della grandezza è importante. Tratti a Rust sono sempre implementate per un certo tipo:

trait A { 
    fn do_something(&self); 
} 

struct X; 
impl A for X { 
    fn do_something(&self) {} 
} 

Tuttavia, questo è necessaria solo per scopi di comodità e praticità. E 'possibile definire i tratti di prendere sempre un parametro di tipo e di non specificare il tipo di tratto è implementato per:

// this is not actual Rust but some Rust-like language 

trait A<T> { 
    fn do_something(t: &T); 
} 

struct X; 
impl A<X> { 
    fn do_something(t: &X) {} 
} 

Ecco come classi di tipo Haskell lavoro, e, in effetti, è così che i tratti sono effettivamente implementate in Ruggine a un livello inferiore.

Ogni tratto in Rust ha un parametro di tipo implicito, denominato Self, che indica il tipo per cui questo tratto è implementato. È sempre disponibile nel corpo del carattere:

trait A { 
    fn do_something(t: &Self); 
} 

Ecco dove entra in gioco la questione della grandezza. Il parametro Self è dimensionato?

Si scopre che no, Self non è dimensionato per impostazione predefinita in Rust. Ogni tratto ha un numero implicito ?Sized associato a Self. Uno dei motivi per cui questo è necessario perché ci sono molti tratti che possono essere implementati per tipi non ancora registrati e funzionano ancora. Ad esempio, qualsiasi tratto che contiene solo metodi che accettano e restituiscono solo Self per riferimento possono essere implementati per i tipi non registrati. Puoi leggere ulteriori informazioni sulla motivazione in RFC 546.

La dimensione non è un problema quando si definisce solo la firma del tratto e i suoi metodi. Perché non esiste un codice effettivo in queste definizioni, il compilatore non può assumere nulla. Tuttavia, quando inizi a scrivere codice generico che utilizza questa caratteristica, che include i metodi predefiniti perché prendono un parametro implicito Self, devi prendere in considerazione la grandezza. Poiché Self non è dimensionato per impostazione predefinita, i metodi di tratto predefiniti non possono restituire Self in base al valore o prenderlo come parametro in base al valore. Di conseguenza, vi sia bisogno di specificare che Self deve essere dimensionato in modo predefinito:

trait A: Sized { ... } 

o è possibile specificare che un metodo può essere chiamato solo se Self è di dimensioni:

trait WithConstructor { 
    fn new_with_param(param: usize) -> Self; 

    fn new() -> Self 
    where 
     Self: Sized, 
    { 
     Self::new_with_param(0) 
    } 
} 
+0

Grazie per una risposta così completa. Non sapevo che "il predefinito è Sized ma Self non è" parte. Questa è la ragione principale per cui sono rimasto perplesso. – eulerdisk

4

Vediamo cosa succederebbe se lo faceste con un tipo non standardizzato.

new()sposta il risultato del metodo new_with_param(_) per il chiamante. Ma a meno che il tipo non sia ridimensionato, quanti byte dovrebbero essere spostati? Semplicemente non possiamo sapere. Ecco perché spostare la semantica richiede i tipi Sized.

Nota: i vari Box sono stati progettati per offrire servizi di runtime esattamente per questo problema.

+2

Perché non si lamenta a proposito di 'new_with_param' però? Richiede inoltre di riservare la giusta quantità di spazio nello stack del suo chiamante. –

+0

Quindi la mia idea era corretta, ma allora perché 'Size' non è richiesto nelle strutture generiche ?? Ho aggiornato la domanda. – eulerdisk

+0

@Matthieu M.'New_with_param' è solo una definizione del metodo tratto, non un'implementazione. – llogiq