•Inizializzazione membri classe |
Le classi di script sono dichiarate globalmente e forniscono un modo semplice per raggruppare proprietà e metodi in unità logiche. La sintassi delle classi è simile a quella di C++ e Java. |
Generalità
Con le classi lo script writer può dichiarare nuovi tipi di dati che contengono gruppi di proprietà e metodi per manipolarli.
Le classi di script sono tipi di riferimento, il che significa che è possibile mantenere più riferimenti o handle per la stessa istanza di oggetto. Le classi utilizzano una gestione automatica della memoria, per cui le istanze degli oggetti vengono distrutte solo quando l'ultimo riferimento all'istanza viene cancellato.
// The class declaration |
Una classe può implementare metodi specifici per sovraccaricare gli operatori. Questo può semplificare il modo in cui le istanze dell'oggetto vengono utilizzate nelle espressioni, in modo che non sia necessario nominare esplicitamente ogni funzione, ad esempio il metodo opAdd si traduce nell'operatore +.
Un'altra caratteristica utile è la possibilità di implementare gli accessi alle proprietà, che possono essere utilizzati sia per fornire proprietà virtuali, cioè che sembrano proprietà ma in realtà non lo sono, sia per implementare routine specifiche che devono essere eseguite ogni volta che si accede a una proprietà.
Una classe di script può anche ereditare da altre classi e implementare interfacce.
Costruttori classe
I costruttori di classe sono metodi specifici che verranno utilizzati per creare nuove istanze della classe. Non è obbligatorio per una classe dichiarare i costruttori, ma farlo può rendere più facile l'uso della classe, poiché non sarà necessario istanziare la classe e poi impostare manualmente le proprietà.
I costruttori sono dichiarati senza un tipo di ritorno e devono avere lo stesso nome della classe stessa. È possibile implementare più costruttori con diversi elenchi di parametri per diverse forme di inizializzazione.
class MyClass |
Il costruttore di copia è un costruttore specifico che il compilatore può utilizzare per creare codice più performante quando è necessario creare copie di un oggetto. Senza il costruttore di copia, il compilatore sarà costretto a istanziare prima la copia usando il costruttore predefinito e poi a copiare gli attributi con il metodo opAssign.
Un costruttore che accetta un singolo argomento può essere usato nelle conversioni di tipo. Per impostazione predefinita, il compilatore può utilizzarli per eseguire la conversione in modo implicito, se necessario. Se non lo si desidera, è possibile vietarlo aggiungendo il decoratore esplicito dopo il costruttore.
Un costruttore non può chiamare un altro costruttore. Se si desidera condividere le implementazioni nei costruttori, si deve utilizzare un metodo specifico per questo.
Il modo in cui i membri devono essere inizializzati può anche essere definito direttamente nella dichiarazione dei membri. In questo caso, l'espressione di inizializzazione verrà automaticamente compilata nel costruttore, senza che sia necessario scrivere nuovamente l'inizializzazione.
Costruttori generati automaticamente
In alcuni casi, il compilatore genera automaticamente un costruttore predefinito e un costruttore di copia.
Il costruttore predefinito viene generato automaticamente se non viene dichiarato esplicitamente un altro costruttore. Questo costruttore richiama semplicemente il costruttore predefinito per tutti i membri dell'oggetto e imposta tutti gli handle su null, a meno che i membri non abbiano inizializzazioni esplicite, nel qual caso queste vengono eseguite. Qualsiasi errore di compilazione nell'inizializzazione dei membri sarà segnalato come di consueto.
Il costruttore di copia viene generato automaticamente se non viene dichiarato esplicitamente un costruttore di copia. Questo costruttore cercherà di eseguire un costrutto di copia per ogni membro, oppure, se non è disponibile alcun costruttore di copia, eseguirà prima un costrutto predefinito seguito da un'assegnazione. Se viene riscontrato un errore di compilazione, ad esempio se un membro non può essere copiato, il costruttore di copia non verrà generato e il messaggio di errore verrà ignorato.
Se i costruttori generati automaticamente non sono desiderati, è possibile escluderli esplicitamente segnalandoli come cancellati.
class MyClass |
Inizializzazione membri classe
L'ordine in cui i membri della classe vengono inizializzati durante la costruzione di un oggetto diventa importante quando si utilizza l'ereditarietà o quando si definisce l'inizializzazione dei membri direttamente nella dichiarazione. Se si accede a un membro prima che sia stato inizializzato, lo script può causare un'eccezione di accesso a null handle, che interromperà l'esecuzione dello script.
Per una classe semplice, l'ordine in cui i membri vengono inizializzati è lo stesso in cui sono stati dichiarati. Se nella dichiarazione dei membri vengono fornite inizializzazioni esplicite, questi membri vengono inizializzati per ultimi.
// The order of this class will be: a, c, b, d |
Quando si usa l'ereditarietà, i membri della classe derivata senza inizializzazione esplicita saranno inizializzati prima di quelli della classe base, mentre i membri con inizializzazione esplicita saranno inizializzati dopo quelli della classe base.
// The order of this class will be: a, b |
Questo ordine di inizializzazione è stato scelto per evitare la maggior parte dei problemi legati all'accesso ai membri prima che siano stati inizializzati.
Tutti i membri sono inizializzati immediatamente all'inizio del costruttore definito, in modo che il resto del codice nel costruttore possa accedere ai membri senza problemi. Fa eccezione il caso in cui il costruttore inizializzi esplicitamente una classe base chiamando super(); in questo caso i membri con inizializzazione esplicita rimarranno non inizializzati fino a quando la classe base non sarà stata completamente costruita.
class Bar |
Si deve fare attenzione ai casi in cui un costruttore o l'inizializzazione di un membro chiamano i metodi della classe. Poiché i metodi della classe possono essere sovrascritti dalle classi derivate, è possibile che una classe base acceda involontariamente a un membro della classe derivata prima che sia stato inizializzato.
class Bar // This class will cause a null handle exception, because the Bar's constructor calls |
Distruttori classe
Normalmente non è necessario implementare il distruttore di classe, poiché lo script avanzato libererà per default tutte le risorse che l'oggetto detiene quando viene distrutto. Tuttavia, ci possono essere situazioni in cui è necessario eseguire una routine di pulizia più esplicita come parte della distruzione dell'oggetto.
Il distruttore viene dichiarato in modo simile al costruttore, tranne per il fatto che deve essere preceduto dal simbolo ~ (noto anche come operatore bitwise not).
class MyClass |
Si noti che lo script avanzato utilizza la gestione automatica della memoria con la garbage collection, quindi potrebbe non essere sempre facile prevedere quando il distruttore viene eseguito. Lo script avanzato chiamerà inoltre il distruttore una sola volta, anche se l'oggetto viene resuscitato aggiungendo un riferimento ad esso durante l'esecuzione del distruttore.
Non è possibile invocare direttamente il distruttore. Se si desidera invocare direttamente la pulizia, è necessario implementare un metodo pubblico per questo.
Metodi classe
I metodi di classe sono implementati allo stesso modo delle funzioni globali, con l'aggiunta che il metodo di classe può accedere alle proprietà dell'istanza di classe direttamente o tramite la parola chiave "this", nel caso in cui una variabile locale abbia lo stesso nome.
// The class declaration |
Metodi const
Le classi aggiungono un nuovo tipo di sovraccarico di funzione, ovvero il sovraccarico const. Quando si accede a un metodo di una classe tramite un riferimento o un handle di sola lettura, è possibile invocare solo i metodi contrassegnati come costanti. Quando il riferimento o l'handle è scrivibile, possono essere invocati entrambi i tipi, con la preferenza per la versione non const nel caso in cui entrambi corrispondano.
class CMyClass |
Ereditarietà e polimorfismo
Lo script avanzato supporta l'ereditarietà singola, in cui una classe derivata eredita le proprietà e i metodi della sua classe base. L'ereditarietà multipla non è supportata, ma il polimorfismo è supportato dall'implementazione di interfacce e il riutilizzo del codice è garantito dall'inclusione di classi mixin.
Tutti i metodi della classe sono virtuali, quindi non è necessario specificarlo manualmente. Quando una classe derivata sovrascrive un'implementazione, può estendere l'implementazione originale chiamando specificamente il metodo della classe base utilizzando l'operatore di risoluzione dell'ambito. Quando si implementa il costruttore di una classe derivata, il costruttore della classe base viene richiamato utilizzando la parola chiave super. Se nessuno dei costruttori della classe base viene chiamato manualmente, il compilatore inserirà automaticamente una chiamata al costruttore predefinito all'inizio. Il distruttore della classe base sarà sempre chiamato dopo quello della classe derivata, quindi non è necessario farlo manualmente.
// A derived class |
Una classe derivata da un'altra può essere implicitamente lanciata alla classe base. Lo stesso vale per le interfacce implementate da una classe. L'altra direzione richiede un cast esplicito, poiché non è noto in fase di compilazione se il cast è valido.
class A {} |
Controllo extra con final, abstract e override
Una classe può essere contrassegnata come "finale" per impedirne l'ereditarietà. Si tratta di una funzione opzionale, utilizzata soprattutto nei progetti più grandi, dove ci sono molte classi e può essere difficile controllare manualmente l'uso corretto di tutte le classi. È anche possibile contrassegnare come "finali" singoli metodi di una classe, nel qual caso è ancora possibile ereditare dalla classe, ma il metodo finale non può essere sovrascritto.
Un'altra parola chiave che può essere usata per contrassegnare una classe è "abstract". Le classi astratte non possono essere istanziate, ma possono essere derivate. Le classi astratte sono usate più frequentemente quando si vuole creare una famiglia di classi derivando da una classe base comune, ma non si vuole che la classe base sia istanziata da sola. Attualmente non è possibile contrassegnare i metodi come astratti, quindi tutti i metodi devono avere un'implementazione anche per le classi astratte.
// A final class that cannot be inherited from |
Quando si deriva una classe, è possibile indicare al compilatore che un metodo è destinato a sovrascrivere un metodo della classe base ereditata. Quando questo viene fatto e non c'è un metodo corrispondente nella classe base, il compilatore emette un errore, perché sa che qualcosa non è stato implementato nel modo previsto. Questo è particolarmente utile per individuare errori in progetti di grandi dimensioni, dove una classe base può essere modificata, ma le classi derivate sono state dimenticate.
class MyBase |
Membri protetti e privati classe
I membri della classe possono essere dichiarati come protetti o privati, per controllare da dove si può accedere ad essi. I membri protetti non possono essere accessibili dall'esterno della classe. I membri privati, inoltre, non sono accessibili alle classi derivate.
Questo può essere utile nei programmi di grandi dimensioni, quando si vogliono evitare errori di programmazione dovuti all'uso inappropriato di proprietà o metodi.
// A class with private members |
Sovraccarico operatori
È possibile definire cosa deve essere fatto quando un operatore viene utilizzato con una classe di script. Sebbene non sia necessario nella maggior parte degli script, può essere utile per migliorare la leggibilità del codice.
Questo si chiama sovraccarico dell'operatore e si ottiene implementando metodi specifici della classe. Il compilatore riconoscerà e utilizzerà questi
metodi quando compila espressioni che coinvolgono gli operatori sovraccaricati e la classe di script.
Operatori unari prefissi
op |
opfunc |
---|---|
- |
opNeg |
~ |
opCom |
++ |
opPreInc |
-- |
opPreDec |
Quando l'espressione op a viene compilata, il compilatore la riscrive come a.opfunc() e la compila al suo posto.
Operatori unari postfissi
op |
opfunc |
---|---|
++ |
opPostInc |
-- |
opPostDec |
Quando l'espressione a op viene compilata, il compilatore la riscrive come a.opfunc() e la compila al suo posto.
Operatori di confronto
op |
opfunc |
---|---|
== |
opEquals |
!= |
opEquals |
< |
opCmp |
<= |
opCmp |
> |
opCmp |
>= |
opCmp |
is |
opEquals |
!is |
opEquals |
L'espressione a == b viene riscritta come a.opEquals(b) e b.opEquals(a) e viene utilizzata la corrispondenza migliore. Il metodo != viene trattato in modo simile, tranne che per il fatto che il risultato viene negato. Il metodo opEquals deve essere implementato in modo da restituire un bool per essere considerato dal compilatore.
Gli operatori di confronto vengono riscritti come a.opCmp(b) op 0 e 0 op b.opCmp(a) e viene utilizzata la migliore corrispondenza. Il metodo opCmp deve essere implementato per restituire un int per essere considerato dal compilatore. Se l'argomento del metodo deve essere considerato più grande dell'oggetto, il metodo deve restituire un valore negativo. Se si suppone che siano uguali, il valore di ritorno deve essere 0.
Se viene effettuato un controllo di uguaglianza e il metodo opEquals non è disponibile, il compilatore cerca invece il metodo opCmp. Quindi, se il metodo opCmp è disponibile, non è necessario implementare il metodo opEquals, se non per motivi di ottimizzazione.
L'operatore di identità, is, si aspetta che opEquals prenda un handle, @, in modo che gli indirizzi possano essere confrontati per poter restituire se si tratta dello stesso oggetto, al contrario di due oggetti diversi che hanno lo stesso valore.
Operatori di assegnazione
op |
opfunc |
---|---|
= |
opAssign |
+= |
opAddAssign |
-= |
opSubAssign |
*= |
opMulAssign |
/= |
opDivAssign |
%= |
opModAssign |
**= |
opPowAssign |
&= |
opAndAssign |
|= |
opOrAssign |
^= |
opXorAssign |
<<= |
opShlAssign |
>>= |
opShrAssign |
>>>= |
opUShrAssign |
Le espressioni di assegnazione a op b vengono riscritte come a.opfunc(b) e poi viene utilizzato il metodo di corrispondenza migliore. Un operatore di assegnazione può essere implementato, ad esempio, in questo modo:
obj &opAssign(const obj &inout other) |
Operatore di assegnazione generato automaticamente
Il compilatore genera automaticamente un opAssign per copiare il contenuto di un'istanza dello stesso tipo, nel caso in cui non sia dichiarato esplicitamente un metodo opAssign con un singolo parametro. L'opAssign generato copierà semplicemente ogni membro.
Se l'opAssign generato automaticamente non è desiderato, è possibile escluderlo esplicitamente segnalandolo come cancellato.
class MyClass |
Operatori binari
op |
opfunc |
opfunc_r |
---|---|---|
+ |
opAdd |
opAdd_r |
- |
opSub |
opSub_r |
* |
opMul |
opMul_r |
/ |
opDiv |
opDiv_r |
% |
opMod |
opMod_r |
** |
opPow |
opPow_r |
& |
opAnd |
opAnd_r |
| |
opOr |
opOr_r |
^ |
opXor |
opXor_r |
<< |
opShl |
opShl_r |
>> |
opShr |
opShr_r |
>>> |
opUShr |
opUShr_r |
Le espressioni con operatori binari a op b verranno riscritte come a.opfunc(b) e b.opfunc_r(a) e verrà utilizzata la migliore corrispondenza.
Operatori indice
op |
opfunc |
---|---|
[] |
opIndex |
Quando l'espressione a[i] viene compilata, il compilatore la riscrive come a.opIndex(i) e la compila al suo posto. Sono supportati anche argomenti multipli tra le parentesi.
L'operatore indice può essere formato in modo simile agli accessi alle proprietà. L'accessore get dovrebbe essere chiamato get_opIndex e avere un parametro per l'indicizzazione. L'accessore set dovrebbe chiamarsi set_opIndex e avere due parametri, il primo per l'indicizzazione e il secondo per il nuovo valore.
class MyObj |
Quando l'espressione a[i] viene utilizzata per recuperare il valore, il compilatore la riscrive come a.get_opIndex(i). Quando l'espressione viene usata per impostare il valore, il compilatore la riscriverà come a.set_opIndex(i, expr).
Operatore functor
op |
opfunc |
---|---|
() |
opCall |
Quando viene compilata l'espressione expr(arglist) e expr valuta un oggetto, il compilatore la riscrive come expr.opCall(arglist) e la compila al suo posto.
Operatori di conversione di tipo
op |
opfunc |
---|---|
type(expr) |
constructor, opConv, opImplConv |
cast<type>(expr) |
opCast, opImplCast |
Quando viene compilata l'espressione type(expr) e type non ha un costruttore di conversione che accetta un argomento con il tipo dell'espressione, il compilatore cercherà di riscriverla come expr.opConv(). Il compilatore sceglierà quindi l'opConv che restituisce il tipo desiderato.
Per le conversioni implicite, il compilatore cercherà un costruttore di conversione del tipo di destinazione che accetti un argomento corrispondente e non sia contrassegnato come esplicito. Se non ne trova uno, proverà a chiamare l'opImplConv sul tipo sorgente che restituisce il tipo di destinazione.
class MyObj |
Questo dovrebbe essere usato solo per le conversioni di valori e non per i cast di riferimenti. Cioè, ci si aspetta che i metodi restituiscano una nuova istanza del valore con il nuovo tipo.
Nota: durante la compilazione delle espressioni booleane nelle condizioni, il compilatore non utilizzerà il bool opImplConv sui tipi di riferimento, anche se il metodo della classe è implementato. Questo perché è ambiguo se sia l'handle a essere verificato o l'oggetto vero e proprio.
Se si desidera un cast di riferimento, cioè un handle di tipo diverso per la stessa istanza di oggetto, è necessario implementare il metodo opCast. Il compilatore cercherà di riscrivere un'espressione cast<tipo>(expr) come expr.opCast() e sceglierà l'overload opCast che restituisce un handle del tipo desiderato. Anche in questo caso si può implementare l'opImplCast, se il cast di riferimento può essere eseguito implicitamente dal compilatore.
class MyObjA |
Un esempio in cui gli overload dell'operatore opCast/opImplCast sono utili è quando si estende un tipo senza ereditarlo direttamente.
Accessi alle proprietà
Nota: l'applicazione può disattivare facoltativamente il supporto per gli accessi alle proprietà, quindi è necessario verificare il manuale dell'applicazione per determinare se questo è supportato o meno.
Spesso, quando si lavora con le proprietà, è necessario assicurarsi che venga seguita una logica specifica quando si accede ad esse. Un esempio potrebbe essere quello di inviare sempre una notifica quando una proprietà viene modificata, oppure di calcolare il valore della proprietà a partire da altre proprietà. Implementando i metodi di accesso alle proprietà, questo può essere implementato dalla classe stessa, rendendo più semplice l'accesso alle proprietà.
In script avanzato gli accessi alle proprietà sono dichiarati con la seguente sintassi:
class MyObj |
Dietro le quinte, il compilatore lo trasforma in due metodi con il nome della proprietà e i prefissi get_ e set_, e con il decoratore di funzioni 'property'. Il seguente genera il codice equivalente ed è perfettamente valido:
class MyObj |
Se si implementano gli accessi alle proprietà scrivendo esplicitamente i due metodi, è necessario assicurarsi che il tipo di ritorno dell'accesso get e il tipo di parametro dell'accesso set corrispondano, altrimenti il compilatore non saprà quale sia il tipo corretto da utilizzare.
Per le interfacce, la prima alternativa è di solito il modo preferito di dichiarare gli accessi alle proprietà, in quanto è abbastanza breve e facile da leggere.
interface IProp |
Si può anche omettere l'accessor get o set. Se si tralascia l'accessore set, la proprietà sarà di sola lettura. Se si tralascia l'accessore get, la proprietà sarà di sola scrittura.
Gli accessi alle proprietà possono essere implementati anche per le proprietà globali, che seguono le stesse regole, tranne per il fatto che le funzioni sono globali.
Una volta dichiarati gli accessi alle proprietà, è possibile accedervi come alle proprietà ordinarie e il compilatore espanderà automaticamente le espressioni alle chiamate di funzione appropriate, set_ o get_, a seconda di come la proprietà viene utilizzata nell'espressione.
void Func() |
Si noti che, poiché gli accessi alle proprietà sono in realtà una coppia di metodi piuttosto che un accesso diretto al valore, si applicano alcune restrizioni su come possono essere utilizzati nelle espressioni che ispezionano e mutano nella stessa operazione. Gli assegnamenti composti possono essere utilizzati sugli accessi alle proprietà se l'oggetto proprietario è di tipo reference, ma non se l'oggetto proprietario è di tipo value. Questo perché il compilatore deve essere in grado di garantire che l'oggetto rimanga in vita tra le due chiamate all'accessor get e all'accessor set.
Gli operatori di incremento e decremento non sono attualmente supportati.
In questi casi, l'espressione deve essere espansa in modo che le operazioni di lettura e scrittura vengano eseguite separatamente, ad esempio l'operatore increment deve essere riscritto come segue:
a++; // will not work if a is a virtual property |
Accessi a proprietà indicizzati
Gli accessi a proprietà possono essere utilizzati per emulare una singola proprietà o un array di proprietà a cui si accede tramite l'operatore indice. Gli accessi a proprietà per l'accesso indicizzato funzionano come gli accessi a proprietà ordinari, tranne che per il fatto che accettano un argomento indice. L'accessore get deve prendere l'argomento indice come unico argomento, mentre l'accessore set deve prendere l'argomento indice come primo argomento e il nuovo valore come secondo argomento.
string firstString; |
Le assegnazioni composte attualmente non funzionano per le proprietà indicizzate.