Proprietà delegate a Kotlin

1. Introduzione

Il linguaggio di programmazione Kotlin ha il supporto nativo per le proprietà della classe.

Le proprietà sono solitamente supportate direttamente dai campi corrispondenti, ma non è sempre necessario che sia così - purché siano correttamente esposte al mondo esterno, possono comunque essere considerate proprietà.

Ciò può essere ottenuto gestendolo in getter e setter o sfruttando il potere dei delegati.

2. Cosa sono le proprietà delegate?

In poche parole, le proprietà delegate non sono supportate da un campo di classe e delegano il recupero e l'impostazione a un'altra parte di codice. Ciò consente di astrarre le funzionalità delegate e condividerle tra più proprietà simili, ad esempio memorizzare i valori delle proprietà in una mappa invece di campi separati.

Le proprietà delegate vengono utilizzate dichiarando la proprietà e il delegato che utilizza. La parola chiave by indica che la proprietà è controllata dal delegato fornito anziché dal proprio campo.

Per esempio:

class DelegateExample(map: MutableMap) { var name: String by map }

Questo utilizza il fatto che un MutableMap è esso stesso un delegato, consentendo di trattare le sue chiavi come proprietà.

3. Proprietà delegate standard

La libreria standard di Kotlin viene fornita con una serie di delegati standard pronti per essere utilizzati.

Abbiamo già visto un esempio di utilizzo di una MutableMap per supportare una proprietà mutabile. Allo stesso modo, puoi eseguire il backup di una proprietà immutabile utilizzando una mappa , consentendo l'accesso ai singoli campi come proprietà, ma non modificandoli mai.

Il delegato pigro consente di calcolare il valore di una proprietà solo al primo accesso e quindi di memorizzarlo nella cache. Questo può essere utile per proprietà che potrebbero essere costose da calcolare e che potrebbero non essere necessarie, ad esempio, caricate da un database:

class DatabaseBackedUser(userId: String) { val name: String by lazy { queryForValue("SELECT name FROM users WHERE userId = :userId", mapOf("userId" to userId) } }

Il delegato osservabile consente l'attivazione di un lambda ogni volta che il valore della proprietà cambia , ad esempio consentendo le notifiche di modifica o l'aggiornamento di altre proprietà correlate:

class ObservedProperty { var name: String by Delegates.observable("") { prop, old, new -> println("Old value: $old, New value: $new") } }

A partire da Kotlin 1.4, è anche possibile delegare direttamente a un'altra proprietà. Ad esempio, se stiamo rinominando una proprietà in una classe API, potremmo lasciare quella vecchia e delegare semplicemente a quella nuova:

class RenamedProperty { var newName: String = "" @Deprecated("Use newName instead") var name: String by this::newName }

Qui, ogni volta che accediamo alla proprietà name , stiamo effettivamente utilizzando la proprietà newName .

4. Creazione dei delegati

Ci saranno volte in cui vorresti scrivere ai tuoi delegati, piuttosto che usare quelli già esistenti. Ciò si basa sulla scrittura di una classe che estende una delle due interfacce: ReadOnlyProperty o ReadWriteProperty.

Entrambe queste interfacce definiscono un metodo chiamato getValue , che viene utilizzato per fornire il valore corrente della proprietà delegata quando viene letta. Questo richiede due argomenti e restituisce il valore della proprietà:

  • thisRef - un riferimento alla classe in cui si trova la proprietà
  • proprietà - una descrizione riflessiva della proprietà da delegare

L' interfaccia ReadWriteProperty definisce inoltre un metodo chiamato setValue che viene utilizzato per aggiornare il valore corrente della proprietà quando viene scritta. Questo richiede tre argomenti e non ha valore di ritorno:

  • thisRef - Un riferimento alla classe in cui si trova la proprietà
  • proprietà - Una descrizione riflessiva della proprietà da delegare
  • value - Il nuovo valore della proprietà

A partire da Kotlin 1.4, l' interfaccia ReadWriteProperty estende effettivamente ReadOnlyProperty. Questo ci consente di scrivere una singola classe delegato che implementa ReadWriteProperty e di usarla per i campi di sola lettura all'interno del nostro codice. In precedenza, avremmo dovuto scrivere due delegati diversi: uno per i campi di sola lettura e un altro per i campi modificabili.

Ad esempio, scriviamo un delegato che funzioni sempre per quanto riguarda una connessione al database anziché i campi locali:

class DatabaseDelegate(readQuery: String, writeQuery: String, id: Any) : ReadWriteDelegate { fun getValue(thisRef: R, property: KProperty): T { return queryForValue(readQuery, mapOf("id" to id)) } fun setValue(thisRef: R, property: KProperty, value: T) { update(writeQuery, mapOf("id" to id, "value" to value)) } }

Ciò dipende da due funzioni di primo livello per accedere al database:

  • queryForValue : richiede un po 'di SQL e alcune associazioni e restituisce il primo valore
  • aggiornamento : richiede un po 'di SQL e alcuni collegamenti e lo tratta come un'istruzione UPDATE

Possiamo quindi usarlo come qualsiasi delegato ordinario e fare in modo che la nostra classe venga automaticamente supportata dal database:

class DatabaseUser(userId: String) { var name: String by DatabaseDelegate( "SELECT name FROM users WHERE userId = :id", "UPDATE users SET name = :value WHERE userId = :id", userId) var email: String by DatabaseDelegate( "SELECT email FROM users WHERE userId = :id", "UPDATE users SET email = :value WHERE userId = :id", userId) }

5. Delegare la creazione del delegato

Un'altra nuova funzionalità che abbiamo in Kotlin 1.4 è la possibilità di delegare la creazione delle nostre classi delegate a un'altra classe. Funziona implementando l' interfaccia PropertyDelegateProvider , che dispone di un unico metodo per creare un'istanza di qualcosa da usare come delegato effettivo.

Possiamo usarlo per eseguire del codice attorno alla creazione del delegato da usare, ad esempio per registrare ciò che sta accadendo. Possiamo anche usarlo per selezionare dinamicamente il delegato che useremo in base alla proprietà per cui viene utilizzato. Ad esempio, potremmo avere un delegato diverso se la proprietà è nullable:

class DatabaseDelegateProvider(readQuery: String, writeQuery: String, id: Any) : PropertyDelegateProvider
     
       { override operator fun provideDelegate(thisRef: T, prop: KProperty): ReadWriteDelegate { if (prop.returnType.isMarkedNullable) { return NullableDatabaseDelegate(readQuery, writeQuery, id) } else { return NonNullDatabaseDelegate(readQuery, writeQuery, id) } } }
     

Questo ci consente di scrivere codice più semplice in ogni delegato perché deve concentrarsi solo su casi più mirati. In quanto sopra, sappiamo che NonNullDatabaseDelegate verrà utilizzato solo su proprietà che non possono avere un valore nullo , quindi non abbiamo bisogno di alcuna logica aggiuntiva per gestirlo.

6. Riepilogo

La delega delle proprietà è una tecnica potente, che consente di scrivere codice che assume il controllo di altre proprietà e aiuta questa logica a essere facilmente condivisa tra classi diverse. Ciò consente una logica robusta e riutilizzabile che sembra e si sente come un normale accesso alla proprietà.

Un esempio completamente funzionante per questo articolo può essere trovato su GitHub.