C, C ++ e Rust hanno un concetto di const
(Rust: mut
) tipi. Quelli sono un qualificatore di tipo. Cioè puoi avere int
e qualificarlo come const int
. Un valore const non può essere mutato.
Questo diventa potente quando consideriamo puntatori o riferimenti a valori const. Se ho un int*
(C), int&
(C ++), &mut i64
(Rust), posso cambiare il valore puntato. Se ho un const int*
, const int&
, &i64
, non posso modificare quel valore.
Una funzione dichiara come accetta i suoi argomenti: per valore, per puntatore / riferimento, o per puntatore const / riferimento. Questo determina quali operazioni sono possibili all'interno della funzione:
// C, C++
int function_a(int* x);
int function_b(const int* x);
// C++
auto function_a(int& x) -> int;
auto function_b(const int& x) -> int;
// Rust
fn function_a(x: &mut i64) -> i64;
fn function_b(x: &i64) -> i64;
vale a dire. non la funzione è contrassegnata come const o mutante, ma i suoi argomenti come costanti o mutevoli . Se passato per valore, non fa alcuna differenza se quel valore è const o mutabile, poiché la funzione opera sulla sua singola copia, e la differenza non è visibile esternamente.
C ++ e Rust supportano anche la sintassi del metodo. Qui, la sintassi è leggermente diversa. In particolare, in C ++ il puntatore this
è un argomento implicito. Quindi il qualificatore const si trova all'esterno dell'elenco degli argomenti:
auto method_a() -> int;
auto method_b() const -> int;
fn method_a(&mut self) -> i64;
fn method_b(&self) -> i64;
Quindi il compilatore non deve tracciare l'intero grafo delle chiamate di funzioni per vedere se un valore può essere mutato o può essere const. Invece, le firme delle funzioni codificano le informazioni necessarie nel sistema di tipi. Quindi, per ogni funzione controllata, il compilatore deve solo vedere quali funzioni, metodi e campi sono accessibili. Se provo a eseguire un'operazione non const con un valore const, si tratta di un errore di tipo.
Aliasing
Come descritto qui, questo impedisce solo la mutazione attraverso quel riferimento. Potrebbero esistere altri riferimenti non const allo stesso valore. Per esempio. considera questa funzione C:
int foo(int* mutable, const int* constant) {
*mutable = 42;
return *constant;
}
Quale valore viene restituito? Se i puntatori puntano a oggetti diversi, restituirà il valore puntato da constant
. Ma potrebbero anche puntare allo stesso oggetto (un alias ): int x; int result = foo(&x, &x)
. Ora, la funzione restituirà 42
. In Rust, questo è impedito al controllore del prestito. Il sistema di tipo dimostra che per ogni oggetto esiste al massimo un riferimento mutabile o un numero qualsiasi di riferimenti costanti. Non ci può mai essere un riferimento mutabile e costante allo stesso oggetto allo stesso tempo. Tuttavia, questo funziona solo a causa di un sistema di tipo molto più vincolante rispetto al C ++.
Mutevolezza degli interni
Ci sono modi per implementare la mutabilità interna, in modo che alcuni campi possano ancora essere modificati anche se fanno parte di un valore const. In C e C ++, è possibile sovvertire il sistema di tipi mediante fusione. C ++ ti permette di annotare campi (non tipi!) Come mutable
. Rust supporta Cell
e RefCell
astrazioni nella sua libreria standard con effetto simile.
Confronto con la semantica di riferimento
Nota anche che per questi linguaggi ci sono solo tipi di valore (che possono essere qualificati come const o mut). Per ottenere la semantica di riferimento, si utilizza in modo esplicito un puntatore o un riferimento. Questo è in netto contrasto con C # o Java, dove readonly
e final
si applicano a variabili , ma non ai valori - e il valore a cui fa riferimento una variabile può ancora essere mutato se è un tipo di riferimento.
Non sono sicuro di come F # specifichi l'immutabilità, ma come linguaggio CLR non può differire significativamente dalla semantica C #.