Come fanno le lingue a garantire "l'immutabilità interiore"?

1

In C #, posso dichiarare un campo immutabile su una classe usando readonly . Funziona per i tipi di valore, ma se è un tipo di riferimento, non c'è nulla che mi impedisca di chiamare metodi sull'oggetto per cambiare il suo stato interno.

L'ordinamento C # risolve questo problema fornendo tipi concreti e interfacce che semplicemente non presentano un modo per modificare lo stato.

Lingue come F # e Rust impediscono questo tipo di cambiamento (e sembrano farlo nella lingua, piuttosto che nel livello del tipo). Quali approcci vengono utilizzati per raggiungere questo obiettivo? Gli sviluppatori contrassegnano ogni metodo / funzione ecc. Come mutating e questo viene raccolto dal compilatore? Se questo è il caso, come funzionerebbe per i tipi definiti dall'utente? In alternativa, il compilatore traccia in qualche modo l'intero percorso che l'esecuzione della funzione potrebbe richiedere e cerca dove potrebbe cambiare i valori in memoria?

    
posta richzilla 16.05.2018 - 16:07
fonte

1 risposta

2

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 #.

    
risposta data 16.05.2018 - 17:09
fonte

Leggi altre domande sui tag