Perché Java / C # non può implementare RAII?


29

Domanda: Perché Java / C # non può implementare RAII?

Chiarimento: sono consapevole che il garbage collector non è deterministico. Pertanto, con le funzionalità della lingua corrente, non è possibile chiamare automaticamente il metodo Dispose () di un oggetto all'uscita dall'ambito. Ma si potrebbe aggiungere una tale caratteristica deterministica?

La mia comprensione:

Ritengo che un'implementazione di RAII debba soddisfare due requisiti:
1. La durata di una risorsa deve essere vincolata a un ambito.
2. Implicito. La liberazione della risorsa deve avvenire senza una dichiarazione esplicita da parte del programmatore. Analogo a un garbage collector che libera memoria senza un'istruzione esplicita. L '"implicazione" deve avvenire solo nel punto di utilizzo della classe. Il creatore della libreria di classi deve ovviamente implementare esplicitamente un metodo distruttore o Dispose ().

Java / C # soddisfa il punto 1. In C # una risorsa che implementa IDisposable può essere associata a un ambito "using":

void test()
{
    using(Resource r = new Resource())
    {
        r.foo();
    }//resource released on scope exit
}

Ciò non soddisfa il punto 2. Il programmatore deve esplicitamente legare l'oggetto a uno speciale ambito "utilizzo". I programmatori possono (e fare) dimenticare di legare esplicitamente la risorsa a un ambito, creando una perdita.

In effetti i blocchi "using" vengono convertiti in codice try-finally-dispose () dal compilatore. Ha la stessa natura esplicita del modello try-finally-dispose (). Senza un rilascio implicito, l'hook a un ambito è lo zucchero sintattico.

void test()
{
    //Programmer forgot (or was not aware of the need) to explicitly
    //bind Resource to a scope.
    Resource r = new Resource(); 
    r.foo();
}//resource leaked!!!

Penso che valga la pena creare una funzione di linguaggio in Java / C # che consenta oggetti speciali agganciati allo stack tramite un puntatore intelligente. La funzione ti consentirebbe di contrassegnare una classe come associata all'ambito, in modo che venga sempre creata con un gancio nello stack. Potrebbero esserci opzioni per diversi tipi di puntatori intelligenti.

class Resource - ScopeBound
{
    /* class details */

    void Dispose()
    {
        //free resource
    }
}

void test()
{
    //class Resource was flagged as ScopeBound so the tie to the stack is implicit.
    Resource r = new Resource(); //r is a smart-pointer
    r.foo();
}//resource released on scope exit.

Penso che l'implicazione sia "ne vale la pena". Proprio come l'implicazione della raccolta dei rifiuti vale "la pena". I blocchi espliciti che utilizzano sono rinfrescanti per gli occhi, ma non offrono alcun vantaggio semantico rispetto a try-finally-dispose ().

È impraticabile implementare tale funzionalità nei linguaggi Java / C #? Potrebbe essere introdotto senza rompere il vecchio codice?


3
Non è poco pratico, è impossibile . Lo standard C # non garantisce che i distruttori / Disposei vengano mai eseguiti, indipendentemente da come vengono attivati. L'aggiunta della distruzione implicita alla fine del campo di applicazione non aiuta.
Telastyn,

20
@Telastyn Huh? Ciò che lo standard C # dice ora non è rilevante, poiché stiamo discutendo di cambiare proprio quel documento. L'unico problema è se ciò sia pratico da fare, e per questo l'unico aspetto interessante dell'attuale mancanza di una garanzia sono le ragioni di questa mancanza di garanzia. Si noti che per usingl'esecuzione di Dispose è garantita (beh, lo sconto del processo muore improvvisamente senza che venga generata un'eccezione, a quel punto presumibilmente tutta la pulizia diventa discutibile).

4
duplicato di Gli sviluppatori di Java hanno abbandonato consapevolmente RAII? , sebbene la risposta accettata sia completamente errata. La risposta breve è che Java usa la semantica di riferimento (heap) piuttosto che la semantica di valore (stack) , quindi la finalizzazione deterministica non è molto utile / possibile. C # ha avere un valore-semantica ( struct), ma sono in genere evitato se non in casi molto particolari. Vedi anche .
BlueRaja - Danny Pflughoeft,

2
È un duplicato simile, non esatto.
Maniero,

3
blogs.msdn.com/b/oldnewthing/archive/2010/08/10/10048150.aspx è una pagina rilevante per questa domanda.
Maniero,

Risposte:


17

Una tale estensione linguistica sarebbe significativamente più complicata e invasiva di quanto sembri pensare. Non puoi semplicemente aggiungere

se la durata di una variabile di un tipo associato a stack termina, chiama Disposel'oggetto a cui fa riferimento

alla sezione pertinente delle specifiche della lingua ed essere fatto. Ignorerò il problema dei valori temporanei ( new Resource().doSomething()) che possono essere risolti con una formulazione leggermente più generale, questo non è il problema più grave. Ad esempio, questo codice sarebbe rotto (e questo genere di cose probabilmente diventa impossibile da fare in generale):

File openSavegame(string id) {
    string path = ... id ...;
    File f = new File(path);
    // do something, perhaps logging
    return f;
} // f goes out of scope, caller receives a closed file

Ora hai bisogno di costruttori di copie definiti dall'utente (o sposta costruttori) e inizia a invocarli ovunque. Non solo ciò comporta implicazioni in termini di prestazioni, ma rende queste cose in grado di valorizzare efficacemente i tipi, mentre quasi tutti gli altri oggetti sono tipi di riferimento. Nel caso di Java, questa è una deviazione radicale dal modo in cui funzionano gli oggetti. In C # meno (già ha structs, ma nessun costruttore di copie definito dall'utente per loro AFAIK), ma rende questi oggetti RAII ancora più speciali. In alternativa, una versione limitata di tipi lineari (cfr. Rust) può anche risolvere il problema, a costo di proibire l'aliasing incluso il passaggio di parametri (a meno che non si desideri introdurre una complessità ancora maggiore adottando riferimenti presi a prestito simili a Rust e un correttore di prestito).

Può essere fatto tecnicamente, ma si finisce con una categoria di cose che sono molto diverse da qualsiasi altra cosa nella lingua. Questa è quasi sempre una cattiva idea, con conseguenze per gli implementatori (più casi limite, più tempo / costo in ogni reparto) e per gli utenti (più concetti da imparare, più possibilità di bug). Non vale la comodità aggiunta.


Perché hai bisogno di costruttore copia / sposta? Il file mantiene un tipo di riferimento. In quella situazione f che è un puntatore viene copiato nel chiamante ed è responsabile di disporre la risorsa (il compilatore implicitamente metterebbe invece un modello try-finally-dispose nel chiamante)
Maniero

1
@bigown Se trattate ogni riferimento in Filequesto modo, nulla cambia e Disposenon viene mai chiamato. Se chiami sempre Dispose, non puoi fare nulla con oggetti usa e getta. O stai proponendo qualche schema per lo smaltimento a volte e talvolta no? In tal caso, descrivilo in dettaglio e ti dirò le situazioni in cui fallisce.

Non riesco a vedere quello che hai detto ora (non sto dicendo che ti sbagli). L'oggetto ha una risorsa, non il riferimento.
Maniero,

La mia comprensione, cambiando il tuo esempio in un semplice ritorno, è che il compilatore inserisce un tentativo appena prima dell'acquisizione della risorsa (riga 3 nel tuo esempio) e il blocco infine-dispose appena prima della fine dell'ambito (riga 6). Nessun problema qui, d'accordo? Torna al tuo esempio. Il compilatore vede un trasferimento, non può inserire try-finally qui ma il chiamante riceverà un oggetto File (puntatore a) e supponendo che il chiamante non stia trasferendo di nuovo questo oggetto, il compilatore inserirà il modello try-finally lì. In altre parole, ogni oggetto IDisposable non trasferito deve applicare il modello try-finally.
Maniero,

1
@bigown In altre parole, non chiamare Disposese fuoriesce un riferimento? L'analisi della fuga è un problema vecchio e difficile, che non funzionerà sempre senza ulteriori modifiche alla lingua. Quando un riferimento viene passato a un altro metodo (virtuale) ( something.EatFile(f);), dovrebbe f.Disposeessere chiamato alla fine dell'ambito? Se sì, interrompi i chiamanti che memorizzano fper un uso successivo. In caso contrario, si perde la risorsa se il chiamante non memorizza f. L'unico modo un po 'semplice per rimuovere questo è un sistema di tipo lineare, che (come ho già discusso più avanti nella mia risposta) introduce invece molte altre complicazioni.

26

La maggiore difficoltà nell'implementare qualcosa del genere per Java o C # sarebbe definire come funziona il trasferimento delle risorse. Avresti bisogno di un modo per estendere la durata della risorsa oltre lo scopo. Ritenere:

class IWrapAResource
{
    private readonly Resource resource;
    public IWrapAResource()
    {
        // Where Resource is scope bound
        Resource builder = new Resource(args, args, args);

        this.resource = builder;
    } // Uh oh, resource is destroyed
} // Crap, there's no scope for IWrapAResource we can bind to!

Quel che è peggio è che questo potrebbe non essere ovvio per l'implementatore di IWrapAResource:

class IWrapSomething<T>
{
    private readonly T resource; // What happens if T is Resource?
    public IWrapSomething(T input)
    {
        this.resource = input;
    }
}

Qualcosa di simile usingall'affermazione di C # è probabilmente il più vicino possibile alla semantica RAII senza ricorrere alle risorse di conteggio dei riferimenti o forzare la semantica dei valori ovunque come C o C ++. Poiché Java e C # hanno una condivisione implicita delle risorse gestite da un garbage collector, il minimo che un programmatore dovrebbe essere in grado di fare è scegliere l'ambito a cui è legata una risorsa, che è esattamente ciò che usinggià fa.


Supponendo che non sia necessario fare riferimento a una variabile dopo che è andata al di fuori dell'ambito (e non dovrebbe esserci davvero una tale necessità), sostengo che puoi ancora rendere un oggetto autosufficiente scrivendo un finalizzatore per esso . Il finalizzatore viene chiamato appena prima che l'oggetto venga garbage collection. Vedi msdn.microsoft.com/en-us/library/0s71x931.aspx
Robert Harvey,

8
@Robert: un programma scritto correttamente non può presumere che i finalizzatori vengano mai eseguiti. blogs.msdn.com/b/oldnewthing/archive/2010/08/09/10047586.aspx
Billy ONeal

1
Hm. Bene, probabilmente è per questo che hanno escogitato la usingdichiarazione.
Robert Harvey,

2
Esattamente. Questa è un'enorme fonte di bug per principianti in C ++ e sarebbe anche in Java / C #. Java / C # non elimina la possibilità di perdere il riferimento a una risorsa che sta per essere distrutta, ma rendendola esplicita e facoltativa ricordano al programmatore e gli danno una scelta consapevole di cosa fare.
Aleksandr Dubinsky,

1
@svick Non sta IWrapSomethinga smaltire T. Chiunque abbia creato Tdeve preoccuparsi di ciò, sia che si usi using, sia IDisposablese stesso, sia che abbia uno schema del ciclo di vita delle risorse ad hoc.
Aleksandr Dubinsky,

13

Il motivo per cui RAII non può funzionare in un linguaggio come C #, ma funziona in C ++, è perché in C ++ puoi decidere se un oggetto è veramente temporaneo (allocandolo in pila) o se è di lunga durata (con allocandolo sull'heap usando newe usando i puntatori).

Quindi, in C ++, puoi fare qualcosa del genere:

void f()
{
    Foo f1;
    Foo* f2 = new Foo();
    Foo::someStaticField = f2;

    // f1 is destroyed here, the object pointed to by f2 isn't
}

In C #, non è possibile distinguere tra i due casi, quindi il compilatore non avrebbe idea se finalizzare l'oggetto o meno.

Quello che potresti fare è introdurre un tipo di tipo di variabile locale speciale, che non puoi inserire nei campi ecc. * E che verrebbe automaticamente eliminato quando non rientra nell'ambito. Che è esattamente ciò che fa C ++ / CLI. In C ++ / CLI, scrivi il codice in questo modo:

void f()
{
    Foo f1;
    Foo^ f2 = gcnew Foo();
    Foo::someStaticField = f2;

    // f1 is disposed here, the object pointed to by f2 isn't
}

Questo si basa sostanzialmente sullo stesso IL del seguente C #:

void f()
{
    using (Foo f1 = new Foo())
    {
        Foo f2 = new Foo();
        Foo.someStaticField = f2;
    }
    // f1 is disposed here, the object pointed to by f2 isn't
}

Per concludere, se dovessi indovinare perché i progettisti di C # non hanno aggiunto RAII, è perché pensavano che non valesse la pena avere due diversi tipi di variabili locali, soprattutto perché in un linguaggio con GC, la finalizzazione deterministica non è utile che spesso.

* Non senza l'equivalente &dell'operatore, che è in C ++ / CLI %. Sebbene farlo sia "non sicuro", nel senso che dopo la fine del metodo, il campo farà riferimento a un oggetto disposto.


1
C # potrebbe banalmente fare RAII se permettesse distruttori per structtipi come fa D.
Jan Hudec,

6

Se ciò che ti disturba con i usingblocchi è la loro esplicitazione, forse possiamo fare un piccolo passo in avanti verso una minore esplicitazione, piuttosto che cambiare la specifica C # stessa. Considera questo codice:

public void ReadFile ()
{
  string filename = "myFile.dat";
  local Stream file = File.Open(filename);
  file.Read(blah blah blah);
}

Vedi la localparola chiave che ho aggiunto? Tutto ciò che fa è aggiungere un po 'più di zucchero sintattico, proprio come using, dicendo al compilatore di chiamare Disposeun finallyblocco alla fine dell'ambito della variabile. Questo è tutto. È totalmente equivalente a:

public void ReadFile ()
{
  string filename = "myFile.dat";
  using (Stream file = File.Open(filename))
  {
      file.Read(blah blah blah);
  }
}

ma con un ambito implicito, piuttosto che esplicito. È più semplice degli altri suggerimenti poiché non devo avere la classe definita come portata. Zucchero sintattico più pulito e implicito.

Potrebbero esserci problemi qui con ambiti difficili da risolvere, anche se non riesco a vederlo in questo momento e apprezzerei chiunque riesca a trovarlo.


1
@ mike30 ma spostarlo nella definizione del tipo porta esattamente ai problemi elencati da altri - cosa succede se si passa il puntatore a un metodo diverso o lo si restituisce dalla funzione? In questo modo l'ambito viene dichiarato nell'ambito, non altrove. Un tipo potrebbe essere usa e getta, ma non dipende da lui chiamare Dispose.
Avner Shahar-Kashtan,

3
@ mike30: Meh. Tutto ciò che fa questa sintassi è rimuovere le parentesi graffe e, per estensione, il controllo di scoping che forniscono.
Robert Harvey,

1
@RobertHarvey Esattamente. Sacrifica un po 'di flessibilità per un codice più pulito e meno nidificato. Se prendiamo il suggerimento di @ delnan e riutilizziamo la usingparola chiave, possiamo mantenere il comportamento esistente e utilizzarlo anche per i casi in cui non abbiamo bisogno dell'ambito specifico. Impostazione usingpredefinita senza parentesi graffa per l'ambito corrente.
Avner Shahar-Kashtan,

1
Non ho alcun problema con esercizi semi-pratici nella progettazione del linguaggio.
Avner Shahar-Kashtan,

1
@RobertHarvey. Sembra che tu abbia una propensione per tutto ciò che non è attualmente implementato in C #. Non avremmo generici, linq, using-blocks, tipi ipmlicit, ecc se fossimo soddisfatti di C # 1.0. Questa sintassi non risolve il problema dell'implicazione, ma è un buon zucchero da associare all'attuale ambito.
mike30,

1

Per un esempio di come RAII funziona in un linguaggio garbage collection, controlla la withparola chiave in Python . Invece di fare affidamento su oggetti distrutti in modo deterministico, ti consente di associare __enter__()e __exit__()metodi a un determinato ambito lessicale. Un esempio comune è:

with open('output.txt', 'w') as f:
    f.write('Hi there!')

Come con lo stile RAII di C ++, il file verrebbe chiuso all'uscita da quel blocco, non importa se si tratta di un'uscita "normale", di una break, immediata returno un'eccezione.

Si noti che la open()chiamata è la solita funzione di apertura del file. per farlo funzionare, l'oggetto file restituito include due metodi:

def __enter__(self):
  return self
def __exit__(self):
  self.close()

Questo è un linguaggio comune in Python: gli oggetti associati a una risorsa in genere includono questi due metodi.

Si noti che l'oggetto file potrebbe ancora rimanere allocato dopo la __exit__()chiamata, l'importante è che sia chiuso.


7
within Python è quasi esattamente come usingin C #, e come tale non in RAII per quanto riguarda questa domanda.

1
"With" di Python è una gestione delle risorse limitata dall'ambito ma manca l'implicazione di un puntatore intelligente. L'atto di dichiarare un puntatore come intelligente potrebbe essere considerato "esplicito", ma se il compilatore imponesse la smartness come parte del tipo di oggetti, si sposterebbe verso "implicito".
mike30,

AFAICT, il punto di RAII sta stabilendo una rigorosa definizione delle risorse. se sei interessato a essere fatto solo deallocando oggetti, allora no, le lingue raccolte con immondizia non possono farlo. se sei interessato a rilasciare costantemente risorse, questo è un modo per farlo (un altro è deferin lingua Go).
Javier,

1
In realtà, penso sia giusto dire che Java e C # favoriscono fortemente le costruzioni esplicite. Altrimenti, perché preoccuparsi di tutta la cerimonia inerente all'uso delle interfacce e dell'eredità?
Robert Harvey,

1
@delnan, Go ha interfacce "implicite".
Javier,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.