blexin

Sviluppo
Consulenza e
Formazione IT


Blog

Evolvi la tua azienda

Vediamo come eseguire conversioni fra tipi non compatibili in C# con gli operatori implicit ed explicit

Conversioni di tipi custom in C#

Martedì 25 Giugno 2019

Uno sviluppatore di codice professionale deve avere delle basi solide. La realtà quotidiana del lavoro può portarci lontano da quanto abbiamo studiato così come l’abitudine a spingerci a ripetere sempre gli stessi errori. Un ripasso delle basi è sempre un utile esercizio che non può che farci del bene! Sfruttiamo, quindi, questo spazio per parlare del nostro linguaggio di programmazione preferito.

C# utilizza una tipizzazione statica in compile-time. Dopo aver dichiarato una variabile, questa non può essere assegnata ad un valore di altro tipo a meno che quest'ultimo non sia convertibile in modo implicito nel tipo della variabile stessa.

Avremo quindi i seguenti tipi di conversione:

  • Implicita: per la quale non abbiamo bisogno di nessuna sintassi particolare e non ci sarà perdita di dati
  • Esplicita: quando esiste la possibilità che ci sia perdita di dati (per esempio se proviamo a convertire una variabile di un determinato tipo che ha un valore superiore al valore massimo memorizzabile nel tipo della variabile ospite), la conversione è detta narrowing. il compilatore ci obbliga (pena la mancata compilazione) ad eseguire una conversione esplicita detta cast. Un cast si esegue specificando, tra parentesi tonde, il tipo di dato prima della variabile o del valore da convertire. Stiamo così informando il compilatore che siamo consapevoli della possibile perdita di informazioni

Per i tipi numerici, quando c'è la certezza che non ci sia perdita informazioni perché il tipo della variabile ospite potrà sicuramente contenere il valore della variabile da convertire, la conversione è detta widening

int numInt = 1000;
long numLong = i;

Per i tipi di riferimento esiste sempre una conversione implicita da una classe ad una classe base diretta o indiretta o alle interfacce implementate.

Dipendente dip = new Dipendente();
Persona pers = dip;
long numLong = 2000;
int numInt = (int)numLong;

Nota: Questa assegnazione compila correttamente, ma potrebbe generare un’eccezione (OverflowException) in fase di runtime se il valore del tipo da convertire (numLong) non rientra nell'intervallo del tipo della variabile di destinazione (numInt).

La stessa cosa accade per i tipi di riferimento. Se vogliamo convertire una variabile del tipo della classe base ad un tipo della classe derivata, abbiamo bisogno di effettuare un cast esplicito.

Persona pers = new Persona();
Dipendente dip = (Dipendente)pers;

Nota: Questa assegnazione compila correttamente, ma potrebbe generare un eccezione (InvalidCastException) in fase di runtime se l'oggetto sulla destra di quest'ultima (pers) non è realmente del tipo specificato col cast (Dipendente).

E se la conversione dovesse avvenire tra tipi che non sono compatibili tra loro? In questo caso possono venirci in aiuto metodi di classi helper. Se volessimo per esempio convertire una stringa in un numero, potremmo utilizzare i metodi Parse o TryParse.

string testo = "100";
int numero = int.Parse(testo);

Se volessimo convertire un array di byte in un Int64, potremmo usare la classe BitConverter e il suo metodo ToInt64(byte[] value, int startIndex):

byte[] bytes = {3,5,2,8};
Int64 valoreInt64 = BitConverter.ToInt64( bytes, 0 );

Come viene invece trattata la conversione tra classi definite da noi? Anche in questo caso, potremmo avere la necessità di assegnare ad una variabile di un dato tipo una di tipo diverso.

C# consente di creare dei metodi statici che utilizzano appositi operatori (implicit ed explicit) all'interno delle loro dichiarazioni e di specificare in questo modo conversioni all'interno di una classe permettendoci di convertire quest'ultima da e/o in un altra classe. Il tutto senza che ci siano fra loro relazioni di ereditarietà o un’interfaccia che le accomuni. Non sarebbe quindi possibile, in teoria, nessun tipo di conversione.

Vediamo un esempio pratico per chiarirci un po’ le idee. Questo codice converte in modo implicito dal tipo NuovoProdotto a quello VecchioProdotto:

class VecchioProdotto
{
   public static implicit operator VecchioProdotto(NuovoProdotto np)
   
       VecchioProdotto vp = new VecchioProdotto();
       vp.nomeProdotto = np.nome;
       vp.descrizioneProdotto = np.descrizione;
       vp.matricola = np.codiceIdentificativo;
       return vp;
   
}

In questo metodo statico, che sarà utilizzato automaticamente in fase di conversione, abbiamo mappato le proprietà della classe VecchioProdotto su quelle della classe NuovoProdotto. Possiamo quindi ora scrivere

VecchioProdotto vp = np;

In questo altro caso, invece, vediamo una funzione di conversione esplicita (che necessita di un cast) che converte dal tipo VecchioProdotto a quello NuovoProdotto:

class NuovoProdotto //Avremmo potuto indifferentemente inserire la funzione all'interno della classe VecchioProdotto
{
   public static explicit operator NuovoProdotto(VecchioProdotto vp)
   
       NuovoProdotto np = new NuovoProdotto();
       np.nome = vp.nomeProdotto;
       np.descrizione = vp.descrizioneProdotto;
       np.codiceIdentificativo = vp.matricola + vp.idTipologia;
       return np;
   
}

In questo caso scriveremo:

NuovoProdotto npr = (NuovoProdotto)vp;

Le conversioni di tipo definite dall'utente possono avvenire oltre che tra tipi custom, come abbiamo già visto, anche fra un tipo base e uno custom non compatibili. Non è possibile però definire di nuovo una conversione (implicita o esplicita) già esistente.

Si può dichiarare una funzione di conversione con gli operatori implicit ed explicit da un tipo di origine ad uno di destinazione seguendo le seguenti regole:

  • Né l'origine né la destinazione devono essere di un tipo interfaccia o di tipo object.
  • L'origine o la destinazione sono una classe o uno struct all'interno delle quali avviene la dichiarazione dell'operatore
  • Nel caso di conversione tra due classi, la dichiarazione può avvenire indifferentemente nella classe origine o in quella destinazione.
  • L'origine e la destinazione non devono essere classi base o derivate l'una dell'altra.
  • La funzione deve essere public e static, non deve esserci tipo di ritorno e il nome deve coincidere col tipo della destinazione.

In definitiva, se una conversione definita dall'utente può dar luogo a eccezioni o perdita di dati, allora deve essere definita con l'operatore explicit.

Le conversioni definite dall'utente con l'operatore implicit, devono essere progettate in modo che non generino mai eccezioni e che non ci sia perdita di informazioni.

Spero vi torni utile.

Alla prossima!

Autore

C#

Servizi

Evolvi la tua azienda