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

28

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

Chiarimento: Sono consapevole che il garbage collector non è deterministico. Pertanto, con le funzionalità linguistiche correnti, non è possibile chiamare automaticamente il metodo Dispose () di un oggetto all'uscita dell'ambito. Ma potrebbe essere aggiunta una caratteristica così deterministica?

Comprensione:

Ritengo che un'implementazione di RAII debba soddisfare due requisiti:
1. La durata di vita di una risorsa deve essere associata 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 '"implicità" deve solo accadere al punto di utilizzo della classe. Il creatore della biblioteca di classi deve ovviamente implementare esplicitamente un metodo destructor o Dispose ().

Punto di soddisfazione Java / C # 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
}

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

Infatti i blocchi "using" vengono convertiti nel codice try-finally-dispose () dal compilatore. Ha la stessa natura esplicita del modello try-finally-dispose (). Senza una versione implicita, l'hook di 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 funzionalità linguistica in Java / C # che consenta oggetti speciali che sono agganciati allo stack tramite uno smart-pointer. La funzione consentirebbe di contrassegnare una classe come bound-bound, in modo che venga sempre creata con un hook allo 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'implicità sia "ne vale la pena". Proprio come l'implicità della raccolta dei rifiuti è "ne vale la pena". L'uso esplicito di blocchi è rinfrescante per gli occhi, ma non offre alcun vantaggio semantico rispetto a try-finally-dispose ().

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

    
posta mike30 30.10.2013 - 16:51
fonte

5 risposte

16

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

if the life-time of a variable of a stack-bound type ends, call Dispose on the object it refers to

nella sezione pertinente delle specifiche della lingua e fatti. Ignorerò il problema dei valori temporanei ( new Resource().doSomething() ) che possono essere risolti con un testo leggermente più generale, non è questo il problema più serio. 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 copia definiti dall'utente (o sposta i costruttori) e inizia a invocarli ovunque. Non solo ciò comporta implicazioni di prestazioni, ma rende anche queste cose valide ai tipi, mentre quasi tutti gli altri oggetti sono tipi di riferimento. Nel caso di Java, questa è una radicale deviazione dal modo in cui gli oggetti funzionano. In C # meno così (ha già struct s, ma nessun costruttore di copie definito dall'utente per loro AFAIK), ma rende ancora più speciali questi oggetti RAII. In alternativa, una versione limitata di tipi lineari (cfr Rust) può anche risolvere il problema, al costo di proibire l'aliasing incluso il passaggio dei parametri (a meno che non si voglia introdurre ancora più complessità adottando riferimenti presi in prestito da Rust e un controllore del 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 / costi in ogni reparto) e utenti (più concetti da apprendere, più possibilità di bug). Non ne vale la pena.

    
risposta data 30.10.2013 - 17:56
fonte
26

La più grande difficoltà nell'implementare qualcosa come questa per Java o C # sarebbe la definizione di come funziona il trasferimento delle risorse. Avresti bisogno di un modo per estendere la vita della risorsa oltre l'ambito. Prendere in considerazione:

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 come l'affermazione using di C # è probabilmente vicina quanto alla semantica di RAII senza ricorrere alle risorse di conteggio dei riferimenti o alla forzatura della 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 è vincolata una risorsa, che è esattamente ciò che using già fa.

    
risposta data 30.10.2013 - 17:46
fonte
12

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 nello stack) o se è lungo vissuto (allocandolo sull'heap usando new e usando i puntatori).

Quindi, in C ++, puoi fare qualcosa di simile:

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 puoi 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 speciale tipo di variabile locale, che non puoi inserire nei campi, ecc. * e che verrebbe automaticamente eliminato quando esce dal campo di applicazione. Che è esattamente ciò che fa C ++ / CLI. In C ++ / CLI, scrivi codice come questo:

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

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

Questo compila sostanzialmente lo 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 avere due diversi tipi di variabili locali non ne valesse la pena, soprattutto perché in una lingua con GC, la finalizzazione deterministica è non utile spesso.

* Non senza l'equivalente dell'operatore & , che in C ++ / CLI è % . Anche se farlo è "non sicuro" nel senso che, una volta terminato il metodo, il campo farà riferimento a un oggetto disposto.

    
risposta data 30.10.2013 - 17:52
fonte
5

Se ciò che ti infastidisce con using di blocchi è la loro chiarezza, forse possiamo fare un piccolo passo in avanti verso una minore chiarezza, 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 parola chiave local che ho aggiunto? Tutto ciò che fa è aggiungere un po 'più zucchero sintattico, proprio come using , dicendo al compilatore di chiamare Dispose in un blocco finally 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 dato che non devo avere la classe definita come bound-bound. Semplicemente più pulito, più implicito di zucchero sintattico.

Potrebbero esserci problemi con obiettivi difficili da risolvere, anche se non riesco a vederlo adesso, e sarei grato a chiunque possa trovarlo.

    
risposta data 30.10.2013 - 20:21
fonte
1

Per un esempio di come RAII funziona in una lingua raccolta in un cestino, controlla with parola chiave in Python . Invece di fare affidamento su oggetti distrutti in modo deterministico, è possibile associare i metodi __enter__() e __exit__() a un dato 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 quando si esce da quel blocco, indipendentemente dal fatto che si tratti di un'uscita "normale", un break , un'immediata return o un'eccezione.

Si noti che la chiamata open() è la solita funzione di apertura file. per fare in modo che funzioni, l'oggetto file restituito include due metodi:

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

Questo è un idioma 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 chiamata di __exit__() , l'importante è che sia chiuso.

    
risposta data 30.10.2013 - 18:59
fonte

Leggi altre domande sui tag