Breve introduzione su come gestire le risorse con la keyword using
{ ← function scope const getResource = () => { return { [Symbol.dispose]: () => { console.log('Resource picked') } } } using resource = getResource(); } // 'Resource picked' logged to console
In questo caso, abbiamo una funzione che “finge” di ottenere una risorsa da un database, attraverso un nuovo simbolo integrato, chiamato Symbol.dispose (In TS tipizzato con Disposable).
Proposta nel TC39, gruppo di sviluppatori che lavorano per evolvere la definizione di ECMAScript (standard per i linguaggi di scripting, tra cui JS, Python etc.) la keyword using ha superato lo Stage 1: Proposal lo Stage 2: Draft e ha raggiunto lo Stage 3: Candidate Proposal, questo significa che in seguito a feedback e perfezionamenti da parte della community sarà pronta per essere rilasciata – tanto che Typescript l'ha già aggiunta in anticipo! In seguito a questa fase, verrebbero effettuati cambiamenti solo per situazioni critiche. Già presente in altri linguaggi come il with statement in Python, sarà estremamente utile per la gestione delle risorse come il trattamento dei file, connessioni di database e altro.
function processFile(path: string) { const file = fs.openSync(path, “w+”); // use file.. if (condition) { // some actions return // early return } try { // other actions } finally { Close file and delete. fs.closeSync(file); fs.unlinkSync(path); } }
Immaginiamo di dover aprire un file ed eseguire delle azioni. Può sembrare semplice, ma in un real-case scenario possiamo facilmente perderci nella complessità di if-else statements e clean-up e non chiudere un file può facilmente portare a dei bachi inaspettati.
class TempFile implements Disposable { #path: string; #handle: number; constructor (path: string) { this.#path = path; this.#handle = fs.openSync(path, “w+”); } // altri metodi [Symbol.dispose]() { // chiude i file e li elimina } }
A questo punto possiamo chiamare i metodi.
function processFile(path: string) { const file = new TempFile(path); try { // smth } finally { file[Symbol.dispose](); } }
In questo modo, Javascript può costruire funzionalità su questo metodo ben definito; ed è qui che using entra in gioco: vediamo la differenza con la funzione di prima.
function processFile(path: string) { using file = new TempFile(path); // use file if (condition) { // actions return } }
Come si può vedere, non ci sono blocchi try/finally o clean-ups. La keyword using effettuerà la pulizia alla fine dello scope in cui è contenuta, o appena prima di un early return o throw error.
function openFile(id: string) { console.log(`+ ${id}`) return { [Symbole.dispose]() { console.log(`- ${id}`); } } } function openF() { using a = openFile(“a”) using b = openFile(“b”) { using c = openFile(“c”) using d = openFile(“d”) } using e = openFile(“e”) return using f = openFile(“f”) } openF();
Il risultato sarà:
// + a // + b // + c // + d // - c // - d // + e // - e // - b // - a // Non raggiunge “f”
Notiamo come appena una variabile non è più accessibile ed è fuori dal suo scope, viene rimossa con la “cleanup” function; mentre se dovessimo inserire un throw new Error() nel bel mezzo del codice, l'output sarebbe il seguente:
// + a // + b // + c throw new Error() → cleanup automatico // - c // - b // - a
Fino ad ora in questi esempi abbiamo utilizzato solamente metodi sincroni, ma realisticamente quando dobbiamo effettuiamo operazioni con delle risorse, non possiamo aspettare la fine di questi per andare avanti ed è per questo che è stato aggiunto il Symbol.asyncDispose accompagnato dal await using (tipizzato AsyncDisposable).
Async function doSomething() { await new Promise(resolve => setTimeout(resolve, 500)); } function consoleLog(id: string) { console.log(`+ ${id}`); return { async [Symbol.asyncDispose]() { console.log(`- “asnyc” ${id}`); await doSomething(); } } } async function openFile() { await using a = consoleLog(“a”); await using b = consoleLog(“b”); { await using c = consoleLog(“c”); await using d = consoleLog(“d”); } await using e = consoleLog(“e”); return await using f = consoleLog(“f”); } // + a // + b // + c // + d // async – d // async – c // + e // async – e // async – b // async – a // Non raggiunge mai “f”
La principale differenza tra le due implementazioni sta nel fatto che la seconda è progettata per gestire disposizioni asincrone, utilizzando AsyncDisposable e await per attendere la disposizione degli oggetti. La funzione doSomething() viene chiamata quando si esegue la disposizione asincrona dell'oggetto restituito da consoleLog(); quindi, quando questa viene eseguita per un oggetto creato utilizzando la seconda versione di consoleLog, verrà visualizzato il messaggio “+ async {id}” e verrà atteso il completamento della funzione doSomething (ritardo simulato di mezzo secondo).
Tutto questo potrebbe risultare complesso ma è importante sottolineare che Javascript è iniziato come un linguaggio di scripting utilitario; il suo creatore non lo aveva mai inteso come un linguaggio di programmazione completo e così lo è stato per anni, dunque sono state accumulate funzionalità su ciò che è ancora una base alquanto instabile. E' interessante però vedere come i linguaggi si influenzino tra di loro: JS vuole tenere il passo con delle funzionalità che altri linguaggi hanno avuto da anni e viceversa ( vedi l'implementazione di #import in C++) ed è probabile che vengano fatti assestamenti in futuro e più informazioni e discussioni a riguardo sono disponibili sulla seguente repo:
https://github.com/microsoft/TypeScript/pull/54505