Tipi fondamentali .NET - Mondadori Informatica
Transcript
Tipi fondamentali .NET - Mondadori Informatica
Capitolo 1 Tipi fondamentali .NET In questo capitolo: Il tipo System.Object ...................................................................................... 1 Tipi stringa .................................................................................................. 7 Tipi numerici .............................................................................................. 31 Il tipo DateTime ........................................................................................... 38 Valori enum ................................................................................................ 47 Il Microsoft .NET Framework espone centinaia di classi differenti per svolgere attività come l’apertura dei file, il parsing XML e l’aggiornamento dei database, ma è più di una semplice collezione di oggetti utili. È un albero di oggetti ben strutturato che fornisce anche oggetti per memorizzare valori, come numeri, date e stringhe. Tutto nel .NET Framework è una classe, e in vetta alla gerarchia a oggetti spicca la classe System.Object. Per evitare righe troppo lunghe, gli esempi di codice in questo capitolo assumono che le seguenti istruzioni using siano utilizzate all’inizio di ciascun file sorgente: using using using using using using using using using System; System.Diagnostics; System.Globalization; System.IO; System.Runtime.InteropServices; System.Security; System.Text; System.Threading; System.Windows.Forms; Il tipo System.Object Tutte le classi derivano, direttamente o indirettamente, da System.Object, il che significa che si può sempre assegnare un oggetto a una variabile System.Object senza mai ottenere, facendolo, un errore di compilazione o a runtime: // Il tipo object in C# è sinonimo di System.Object. object o = new AnyOtherType(); Per inciso, si noti che le interfacce sono le uniche entità nel .NET Framework che non derivano da System.Object. 1 2 Programmare Microsoft Visual C# 2005 Metodi Public e Protected Poiché le classi .NET derivano da System.Object (si veda la Figura 1-1), espongono tutte i quattro metodi di istanza che espone System.Object, cioè • Equals Un metodo overridable che verifica se l’oggetto corrente ha lo stesso valore dell’oggetto passato come argomento. Restituisce true quando due riferimenti ad oggetti puntano alla stessa istanza di oggetto, ma molte classi ridefiniscono questo metodo per implementare una forma differente di uguaglianza. Ad esempio, le classi numeriche ridefiniscono questo metodo in modo che restituisca true se gli oggetti confrontati hanno lo stesso valore numerico. • GetHashCode Un metodo overridable che restituisce un codice hash dell’oggetto. Questo metodo viene utilizzato quando l’oggetto è una chiave di collection e tavole hash. In teoria, il codice hash dovrebbe essere univoco per una determinata istanza d’oggetto in modo che si possa controllare che due oggetti siano “uguali” confrontandone il codice hash. Tuttavia, implementare una funzione hash che fornisce valori univoci è raramente possibile, e oggetti differenti potrebbero restituire lo stesso codice hash, pertanto non si deve mai desumere che due istanze con lo stesso codice hash siano uguali, qualsiasi cosa possa significare “uguale” per quel tipo specifico. Una classe può ridefinire questo metodo per implementare un differente algoritmo di hashing per migliorare le prestazioni quando i suoi oggetti vengono utilizzati come chiavi nelle collection. Una classe che ridefinisce il metodo Equals dovrebbe sempre ridefinire anche il metodo GetHashCode, in modo che due oggetti che sono considerati uguali restituiscano anche lo stesso codice hash. • GetType Un metodo che restituisce un valore che identifica il tipo dell’oggetto. Il valore restituito viene tipicamente utilizzato nelle operazioni di reflection, come spiegato nel Capitolo 9. • ToString Un metodo ridefinibile che restituisce il nome completo della classe, ad esempio MyNamespace.MyClass. Tuttavia, gran parte delle classi ridefiniscono questo metodo in modo che restituisca una stringa che descriva meglio il valore dell’oggetto. Ad esempio, i tipi di base come int, double e string ridefiniscono questo metodo per restituire il valore numerico, o stringa, dell’oggetto. Il metodo ToString viene implicitamente invocato quando si passa un oggetto ai metodi Console.Write e Debug.Write. È interessante notare, che ToString è “consapevole” della nazionalità (in senso di culture). Ad esempio, se applicato a un tipo numerico utilizza una virgola come separatore decimale se la nazionalità corrente lo richiede. Anche la classe System.Object espone due metodi statici: • Equals Un membro statico che accetta due argomenti oggetto e restituisce true se possono essere considerati uguali. È simile, e spesso è utilizzato al suo posto, all’omonimo metodo di istanza, che fallirebbe se fosse invocato su una variabile di tipo reference che è null. • ReferenceEquals Un metodo static che accetta due argomenti oggetto e restituisce true se referenziano la stessa istanza; pertanto, corrisponde all’operatore == di Capitolo 1 Tipi fondamentali .NET Microsoft Visual C# applicato ai riferimenti a oggetti. Questo metodo è simile al metodo Equals eccetto che le classi derivate non possono ridefinirlo. Object Array Attribute Console Delegate MulticastDelegate AccessException Environment ArgumentException EventArgs SystemException ArithmeticException Exception URIFormatException InvalidCastException GC ApplicationException NullReferenceException .... MarshalByRefObject Math OperatingSystem Random String Boolean TimeZone Char ValueType DateTime Version Double WeakReference Int16 Byte Currency Decimal Enum Int32 Int64 IntPtr ParamArray Single TimeSpan Guid Figura 1-1 Le classi più importanti del namespace System La classe System.Object espone anche due metodi protected. Poiché tutto nel .NET Framework deriva direttamente o indirettamente da System.Object, tutte le classi che si scrivono possono invocare i seguenti metodi della relativa classe base e ridefinirli: • MemberwiseClone Un metodo che restituisce un nuovo oggetto dello stesso tipo e inizializza i campi e le proprietà del nuovo oggetto in modo che il nuovo oggetto possa essere considerato una copia (un clone) dell’oggetto corrente. 3 4 Programmare Microsoft Visual C# 2005 • Finalize Un metodo overridable che il .NET Framework invoca quando l’oggetto è sottoposto a garbage collection. (Per ulteriori informazioni su questo metodo, si veda il paragrafo “Metodi Finalize e Dispose” nel Capitolo 2, “Durata dell’oggetto”). La gerarchia della classe System comprende tutti gli oggetti più comuni e utili del .NET Framework, compresi tutti i tipi di base. Le classi più importanti sono presentate nella Figura 1-1. Tipi value e tipi reference Gran parte dei tipi di dati di base (numeri, date e via dicendo) della gerarchia .NET derivano da System.ValueType e quindi hanno un comportamento comune. Ad esempio, System.ValueType ridefinisce il metodo Equals e ridefinisce l’uguaglianza in modo che due riferimenti a oggetto vengono considerati uguali se hanno lo stesso valore (che è il modo in cui di solito confrontiamo numeri e date), piuttosto che se puntano alla stessa istanza. Inoltre, tutte le classi derivanti da System.ValueType ridefiniscono il metodo GetHashCode in modo che il codice hash venga creato tenendo conto dei campi dell’oggetto. Le classi che derivano da System.ValueType sono comunemente riferite come tipi value, per distinguerle da altre classi, che vengono collegialmente denominate tipi reference. Tutti i tipi numerici e Enum sono tipi value, essendi tipi che operano su dati. La documentazione .NET utilizza il termine tipo per esprimere il significato di tipi value e tipi reference. In questo libro ho seguito questa convenzione e riservo il termine classe solo per i tipi reference. C# impedisce di ereditare esplicitamente da System.ValueType. Il solo modo per creare un tipo value è creando un blocco struct: pstruct Position { double X; double Y; // Aggiungere qui altri campi, proprietà, metodi e interfacce. … } In generale, i tipi value sono più efficienti dei tipi reference poiché i dati non vengono allocati nello heap managed e perciò non sono soggetti a garbage collection. Più precisamente, un tipo value dichiarato come variabile locale in una procedura viene allocato sullo stack; quando la procedura termina, il valore viene semplicemente scartato senza comportare alcun lavoro extra per il garbage collector. (Il metodo distruttore non è valido nelle strutture). Tuttavia, questa descrizione non è strettamente accurata se la struttura comprende un membro di un tipo reference. Capitolo 1 Tipi fondamentali .NET Si consideri questa nuova versione del tipo Position: struct Position { double X; double Y; string Description; // String è un tipo reference. … } Il garbage collector deve recuperare la memoria utilizzata per il membro stringa Description quando la struttura viene distrutta. In altri termini, i tipi value sono notevolmente più veloci dei tipi reference solo se non espongono membri di un tipo reference. Molti articoli tecnici spiegano che i tipi value vengono allocati sullo stack e che non occupano memoria nello heap managed, ma questa descrizione non è corretta al cento percento: se un tipo value viene utilizzato nella definizione di un tipo reference, occupa spazio nello heap, come in questo caso: public class Square { // Questi due membri vanno nello heap, nello slot allocato per l’oggetto Square. double Side; Position Position; // Il puntatore alla stringa Name viene allocato nello slot dell’oggetto Square, // mentre i relativi caratteri vanno in un *altro* slot dello heap. string Name; } Altri fattori possono influenzare la scelta tra un tipo value e un tipo reference. I tipi value sono implicitamente sealed; pertanto si deve utilizzare una struttura se i propri oggetti si comportano come tipo primitivo e non devono ereditare comportamenti speciali da altri tipi, e altri tipi non devono derivare da esso. Inoltre, le Structure non possono essere astratte e non possono contenere metodi virtuali, diversi da quelli ereditati da System.Object. Un dettaglio che potrebbe confondere molti sviluppatori è che la classe String è un tipo reference, non un tipo value, come mostrato nella Figura 1-1. Si può facilmente dimostrare questo punto assegnando una variabile string a un’altra variabile e poi testando se entrambe le variabili puntano allo stesso oggetto: string s1 = “ABCD”; string s2 = s1; // Dimostra che entrambe le variabili puntano allo stesso oggetto. Console.WriteLine(string.ReferenceEquals(s1, s2)); // => True Anche gli array .NET sono tipi reference, e l’assegnazione di un array a una variabile Array copia solo il riferimento all’oggetto, non il contenuto dell’array. La classe Array espone il metodo Clone per permettere di creare una copia shallow degli elementi. (Una copia shallow è una operazione di copia che crea una copia di un oggetto ma non degli oggetti figli). 5 6 Programmare Microsoft Visual C# 2005 Boxing e unboxing Anche se si è principalmente interessati alle prestazioni, non sempre si deve optare per i tipi value, poiché talvolta i tipi reference sono più veloci. Ad esempio, un’assegnazione tra tipi value richiede la copia di ogni campo dell’oggetto, mentre l’assegnazione di un valore reference a una variabile richiede solo la copia dell’indirizzo dell’oggetto (4 byte nelle versioni Windows a 32 bit). Quando si passa un tipo value a un metodo che si aspetta un argomento object si verifica un tipo differente di degradazione delle prestazioni, poiché in questo caso il valore deve essere sottoposto a boxing. Il boxing di un valore significa che il compilatore crea un copia del valore nello heap managed e assegna l’indirizzo di questa copia a una variabile object o a un argomento object in modo che il tipo possa poi essere utilizzato come tipo reference. Un valore sottoposto al boxing (il cosiddetto valore boxed) non mantiene un link al valore originale, il che significa che si può modificare uno dei due senza influenzare l’altro. Se questo valore boxed viene assegnato in seguito a una variabile del tipo originale (ossia di tipo value), l’oggetto è detto unboxed e i dati vengono copiati dallo heap managed alla memoria allocata per la variabile (ad esempio, sullo stack se è una variabile locale). Non sorprendentemente, le operazioni di boxing e unboxing consumano tempo di CPU e infine richiedono che sia recuperata della memoria durante una garbage collection. In sintesi: se si eseguono molte assegnazioni o si eseguono frequentemente operazioni che producono una sequenza di boxing e unboxing, l’implementazione di un tipo reference potrebbe essere una scelta più saggia. Il più delle volte, il boxing si verifica in modo trasparente, dove è richiesto un esplicito cast o come operatore per riconvertire da object a un tipo value. Si può determinare se una invocazione causa una operazione di boxing osservando la dichiarazione del metodo con l’object browser o nella documentazione della classe. Se il metodo accetta un argomento del tipo che si sta passando, non si verifica alcun boxing; se accetta un generico argomento object, l’argomento verrà sottoposto al boxing. Quando si creano i propri metodi, si può considerare di comprendere delle variazioni in overload che accettano argomenti di tipi differenti ma anche una procedura generica che accetta un argomento object. In genere, non ha senso confrontare le prestazioni di un metodo che utilizza il boxing con un metodo simile che non utilizza il boxing. Un benchmark informale mostra che un ciclo serrato che invoca una funzione che richiede un’operazione di boxing può essere fino a 30 volte più lento di un ciclo che non utilizza il boxing. Tuttavia, si deve ripetere il ciclo 10 milioni di volte per osservare una differenza significativa in termini assoluti, pertanto in pratica bisogna preoccuparsi del boxing solo nelle sezioni di codice critiche in termini di prestazioni. Talvolta si può utilizzare il boxing senza saperlo. In primo luogo, si esegue implicitamente il boxing di una Structure se si invoca uno dei metodi virtuali che la Structure eredita da System.Object: ad esempio, ToString. In secondo luogo, si esegue implicitamente il boxing di una Structure se si invoca un metodo di un’interfaccia che la struttura espone. Capitolo 1 Tipi fondamentali .NET Essere consapevoli del boxing permette di ottimizzare il proprio codice in modi non alla portata del compilatore C#. Ad esempio, si consideri il seguente codice: object res; for (i = 1; i <= 1000; i++) { for (j = 1; j <= 100000; j++) { // GetObject accetta due argomenti Object, e perciò causa // il boxing sia di i sia di j. res = GetObject2(i, j); } } Il valore della variabile i non cambia nel ciclo interno, tuttavia il codice che il compilatore C# produce esegue il boxing di questo valore ogni volta che viene invocato il metodo GetObject2. Eseguendo esplicitamente il boxing del valore, si può raddoppiare la velocità di questo codice: for (i = 1; i <= 1000; i++) { // Conserva il tipo value in una variabile oggetto. object o = i; for (j = 1; j <= 100000; j++) { res = GetObject2(o, j); // Solo j è boxed. } } Tipi stringa C# supporta il tipo di dato string, che è mappato alla classe System.String. Siccome System.String è un tipo potente, si possono manipolare le stringhe per mezzo dei molti metodi che il tipo espone. Innanzitutto, la classe String espone molti metodi costruttori in overload, pertanto si può creare una stringa in diversi modi: ad esempio, come sequenza di N caratteri identici. // Una sequenza di <N> caratteri string s = new string(‘A’, 10); // => AAAAAAAAAA Proprietà e metodi Le sole proprietà della classe String sono Length e Chars. La prima restituisce il numero di caratteri nella stringa; la seconda è l’indexer del tipo String e restituisce il carattere presente nella posizione determinata dall’indice specificato (che parte da zero): string s = “ABCDEFGHIJ”; Console.WriteLine(s.Length); // => 10 // Index parte da zero. Console.WriteLine(s[3]); // => D 7 8 Programmare Microsoft Visual C# 2005 Si può sfruttare la potenza della classe String invocandone uno dei molti metodi. Ad esempio, si osservi quanto è semplice e leggibile l’operazione di inserimento di una sottostringa: s = s.Insert(3, “1234”); // => ABC1234DEFGHIJ Ecco un ulteriore esempio di compattezza che si può ottenere utilizzando i metodi String. Si supponga di voler eliminare i caratteri spazio e tab dall’inizio di una stringa. Con C# basta caricare i caratteri da eliminare in un array di Chars e passare l’array alla funzione TrimStart char[] cArr = new char[]{‘ ‘, ‘\t’}; s = s.TrimStart(cArr); (Si può utilizzare lo stesso pattern con le funzioni TrimEnd e Trim). In molti casi, i metodi String possono produrre prestazioni migliori poiché si può indicare più precisamente cosa si intende fare. Ad esempio, ecco come si può determinare se una stringa inizia o termina con una determinata sequenza di caratteri: // Controlla se la stringa inizia con “abc” e termina con “xyz.” if ( s.StartsWith(“abc”) && s.EndsWith(“xyz”) ) { ok = true; } Si può iterare su tutti i caratteri di una stringa per mezzo di un ciclo foreach: string s = “ABCDE”; foreach ( char c in s ) { Console.Write(c + “.”); // => A.B.C.D.E. } Ecco un dettaglio interessante: il compilatore C# traduce automaticamente il ciclo foreach nel seguente ciclo ben più efficiente: // Ecco come C# compila effettivamente un ciclo foreach su una stringa. for ( int index = 0; index < s.Length; index++ ) { char c = s[index]; … } Confronto e ricerca di stringhe Molti metodi del tipo String permettono di confrontare stringhe o di ricercare una sottostringa all’interno della stringa corrente. Nella versione 2.0 del .NET Framework il tipo String espone degli overload della versione di istanza e della versione statica del metodo Equals per accettare un ulteriore tipo enum StringComparison che specifica la nazionalità che deve essere utilizzata per il confronto e se il confronto è case-sensitive. In generale si Capitolo 1 Tipi fondamentali .NET raccomanda di utilizzare la versione statica di questo metodo, poiché funziona bene anche se una o entrambe le stringhe da confrontare sono null: // Confronta due stringhe in modalità case-sensitive, utilizzando valori Unicode. bool match = string.Equals(s1, s2); // Confronta due stringhe in modalità case-insensitive, utilizzando la nazionalità corrente. match = string.Equals(s1, s2, StringComparison.CurrentCultureIgnoreCase); // Confronta due stringhe in modalità case-sensitive, utilizzando la nazionalità invariante. match = string.Equals(s1, s2, StringComparison.InvariantCulture); // Confronta i valori numerici Unicode di tutti i caratteri delle due stringhe. match = string.Equals(s1, s2, StringComparison.Ordinal); (Si legga più oltre per ulteriori informazioni sulle nazionalità e sul tipo CultureInfo). In generale si devono utilizzare, se possibile, i valori enumerati Ordinal e OrdinalIgnoreCase, poiché sono più efficienti. Ad esempio, questi valori possono essere più adatti per confrontare percorsi di file, chiavi del registry e tag XML e HTML. I valori InvariantCulture e InvariantCultureIgnoreCase sono presumibilmente i meno utili e dovrebbero essere utilizzati nei rari casi in cui si confrontano stringhe linguisticamente significative ma che non hanno un significato per la cultura; l’utilizzo di questi valori assicura che i confronti portino allo stesso risultato su tutte le macchine, indipendentemente dalla nazionalità del thread corrente. Se si deve rilevare se una stringa è minore o maggiore di un’altra stringa si deve utilizzare il metodo static Compare, che può anche confrontare sottostringhe, sia in modalità case-sensitive sia case-insensitive. Il valore di ritorno è -1, 0 o 1 in base a se la prima stringa è minore, uguale o maggiore della seconda stringa: // Confronta due stringhe in modalità case-sensitive, utilizzando la cultura corrente. int res = string.Compare(s1, s2); if ( res < 0 ) { Console.WriteLine(“s1 < s2”); } else if ( res > 0 ) { Console.WriteLine(“s1 > s2”); } else { Console.WriteLine(“s1 = s2”); } // Confronta i primi 10 caratteri di due stringhe in modalità case-insensitive. // (Il secondo e il quarto argomento sono l’indice del primo char da confrontare). res = string.Compare(s1, 0, s2, 0, 10, true); Nel .NET Framework 2.0 si può anche passare un valore enum StringComparison per specificare se si vuole utilizzare la nazionalità invariante, la nazionalità corrente o il valore numerico Unicode dei singoli caratteri, e se si vuole che il confronto sia in modalità case-insensitive: 9 10 Programmare Microsoft Visual C# 2005 // Confronta due stringhe utilizzando la cultura locale in modalità case-insensitive. res = string.Compare(s1, s2, StringComparison.CurrentCultureIgnoreCase); // Confronta due sottostringhe utilizzando la cultura invariante in modalità case-sensitive. res = string.Compare(s1, 0, s2, 0, 10, StringComparison.InvariantCulture); // Confronta due stringhe in base al codice numerico dei relativi caratteri. res = string.Compare(s1, s2, StringComparison.Ordinal); Il tipo StringComparer (anch’esso nuovo del .NET Framework 2.0) offre una tecnica alternativa per effettuare i confronti fra stringhe. Le proprietà statiche di questo tipo restituiscono un oggetto IComparer in grado di eseguire un tipo specifico di confronto su stringhe. Ad esempio, ecco come si può eseguire un confronto case-insensitive in base alla cultura corrente: // Confronta due stringhe utilizzando la cultura locale in modalità case-insensitive. res = StringComparer.CurrentCultureIgnoreCase.Compare(s1, s2); // Confronta due stringhe utilizzando la cultura invariante in modalità case-sensitive. res = StringComparer.InvariantCulture.Compare(s1, s2); L’oggetto StringComparer ha l’ulteriore vantaggio di poterlo passare ai metodi che accettano un argomento IComparer, tra cui il metodo Array.Sort, ma ha anche alcune pecche; ad esempio, non si può utilizzare il metodo StringComparer.Compare per confrontare sottostringhe. Il tipo String espone anche il metodo di istanza CompareTo, che confronta la stringa corrente con l’argomento passato e restituisce -1, 0 o 1, esattamente come fa il metodo statico Compare. Tuttavia, il metodo CompareTo non offre nessuna delle opzioni del metodo Compare e perciò andrebbe evitato a meno che non si voglia realmente confrontare utilizzando le regole della cultura corrente. Inoltre, essendo un metodo di istanza si deve sempre controllare se l’istanza non è null prima di invocare il metodo: // Questo statement genera un’eccezione se s1 è null. if ( s1.CompareTo(s2) > 0 ) { Console.WriteLine(“s1 > s2”); } Quando si confronta solo il valore numerico Unicode dei singoli caratteri si possono risparmiare alcuni cicli di CPU utilizzando il metodo static CompareOrdinal; in generale, tuttavia, si deve utilizzare questo metodo solo per testare l’uguaglianza, poiché ha senso raramente decidere se una stringa è maggiore o minore di un’altra stringa in base ai valori numerici Unicode: if ( string.CompareOrdinal(s1, s2) == 0 ) { Console.WriteLine(“s1 = s2”); } Capitolo 1 Tipi fondamentali .NET Una variabile stringa .NET viene considerata null se è nulla, e vuota se punta a una stringa di zero caratteri. Nelle versioni precedenti di C# si dovevano testare queste due condizioni separatamente, ma .NET 2.0 introduce il pratico metodo static IsNullOrEmpty: // Questi due statement sono equivalenti, ma solo il secondo funziona in .NET 2.0. string s = “ABCDE”; if ( s == null || s.Length == 0 ) { Console.WriteLine(“Empty string”); } if ( string.IsNullOrEmpty(s) ) { Console.WriteLine(“Empty string”); } Il modo più semplice per controllare se una stringa appare all’interno di un’altra è per mezzo del metodo Contains, anche’esso nuovo di .NET 2.0: // Il metodo Contains funziona solo in modalità case-sensitive. s = “ABCDEFGHI ABCDEF”; bool found = s.Contains(“BCD”); // => True found = s.Contains(“bcd”); // => False Si può rilevare la posizione effettiva di una sottostringa all’interno di una stringa per mezzo dei metodi IndexOf e LastIndexOf, che restituiscono, rispettivamente, l’indice della prima e dell’ultima occorrenza di una sottostringa o –1 se la ricerca fallisce: int pos = s.IndexOf(“CDE”); // => 2 pos = s.LastIndexOf(“CDE”); // => 12 // Entrambi IndexOf e LastIndexOf sono case-sensitive per default… pos = s.IndexOf(“cde”); // => -1 // …ma espongono un overload che può specificare la modalità case-insensitive. pos = s.LastIndexOf(“cde”, StringComparison.CurrentCultureIgnoreCase); // => 12 I metodi StartsWith e EndsWith permettono di controllare rapidamente se una stringa inizia o termina con una determinata sottostringa. Per default, questi metodi eseguono un confronto case-sensitive utilizzando la cultura corrente: match = s.StartsWith(“ABC”); // => True match = s.EndsWith(“def”); // => False Nel .NET Framework 2.0 questi due metodi sono stati espansi per supportare un argomento StringComparison e possono accettare un oggetto CultureInfo come terzo argomento, in modo che si può specificare la nazionalità che deve essere utilizzata nel confrontare i caratteri: // Entrambi questi statement assegnano true alla variabile. match = s.StartsWith(“abc”, StringComparison.CurrentCultureIgnoreCase); match = s.EndsWith(“CDE”, true, CultureInfo.InvariantCulture); 11 12 Programmare Microsoft Visual C# 2005 I metodi IndexOfAny e LastIndexOfAny restituiscono, rispettivamente, la prima e l’ultima occorrenza di un carattere tra quelli dell’array specificato. Entrambi questi metodi possono accettare un indice di partenza opzionale come secondo argomento e un conteggio di caratteri da esaminare come terzo argomento: char[] chars = new char[]{‘D’, ‘F’, ‘I’}; pos = s.IndexOfAny(chars); // => 3 pos = s.LastIndexOfAny(chars); // => 15 pos = s.IndexOfAny(chars, 6); // => 8 pos = s.IndexOfAny(chars, 6, 2); // => -1 Modifica e estrazione di stringhe Il modo più semplice per creare una nuova stringa da una stringa esistente è per mezzo del metodo Substring, che estrae una sottostringa a partire da un determinato indice e con il numero specificato di caratteri: string s = “ABCDEFGHI ABCDEF”; // Estrae la sottostringa dopo l’11° carattere. string result = s.Substring(10); // => ABCDEF // Estrae 4 caratteri dopo l’11° carattere. result = s.Substring(10, 4); // => ABCD // Estrae gli ultimi 4 caratteri. result = s.Substring(s.Length - 4); // => CDEF Il metodo Insert restituisce la nuova stringa creata inserendo una sottostringa nella posizione specificata dall’indice, mentre il metodo Remove rimuove un determinato numero di caratteri, partendo dall’indice specificato: result = s.Insert(4, “-123-”); // => ABCD-123-EFGHI ABCDEF result = s.Remove(4, 3); // => ABCDHI ABCDEF In .NET 2.0 è stata definita una versione in overload del metodo Remove che accetta solo l’indice di partenza; questa nuova versione si può utilizzare come surrogato della “classica” funzione Left: // Estrae i primi 4 caratteri. result = s.Remove(4); // => ABCD Ho già fatto riferimento al metodo TrimStart in un paragrafo precedente. Questo metodo, utilizzato con i metodi TrimEnd e Trim, permette di scartare spazi e altri caratteri che si trovano all’inizio, alla fine o ad entrambi gli estremi di una stringa. Per default questi metodi rimuovono i caratteri di spaziatura (ossia, spazi, tab e newline), ma si può passare uno o più argomenti per specificare quali caratteri devono essere rimossi: string result result result result t = = = = = “ 001234.560 “; t.Trim(); // => “001234.560” t.TrimStart(‘ ‘, ‘0’); // => “1234.560 “ t.TrimEnd(‘ ‘, ‘0’); // => “ 001234.56” t.Trim(‘ ‘, ‘0’); // => “1234.56” Capitolo 1 Tipi fondamentali .NET L’operazione opposta alla rimozione è il riempimento. Si può popolare una stringa con un determinato carattere, a partire da sinistra o da destra, portando la stringa alla lunghezza specificata, per mezzo dei metodi PadLeft e PadRight. Il motivo più ovvio per utilizzare questi metodi è allineare stringhe e numeri: // Allinea a destra un numero in un campo di 8 caratteri. int number = 1234; result = number.ToString().PadLeft(8); // => “ 1234” Come suggeriscono i nomi, i metodi ToLower e ToUpper restituiscono una nuova stringa ottenuta convertendo tutti i caratteri di una stringa in minuscolo o in maiuscolo. La versione 2.0 del .NET Framework fornisce anche i nuovi metodi ToLowerInvariant e ToUpperInvariant, che convertono una stringa utilizzando le regole di grafia della cultura invariante: // Converte la stringa s in minuscolo, utilizzando le regole della cultura corrente. result = s.ToLower(); // Converte la stringa s in maiuscolo, utilizzando le regole della cultura invariante. result = s.ToUpperInvariant(); I principi guida di Microsoft suggeriscono di utilizzare il metodo ToUpperInvariant nel preparare una stringa normalizzata per un confronto in modalità case-insensitive, poiché è questo il modo in cui il metodo Compare opera internamente quando si utilizza StringComparison.InvariantCultureIgnoreCase. Per quanto possa sembrare strano, in certe culture convertire due stringhe in maiuscolo per poi confrontarle può produrre un risultato differente da ciò che si riceve se si convertono in minuscolo prima del confronto. Il metodo Replace sostituisce tutte le occorrenze di un singolo carattere o di una sottostringa con un ulteriore carattere o sottostringa. Queste ricerche vengono eseguite in modalità case-sensitive: string k = “ABCDEFGHI ABCDEF”; result = k.Replace(“BCDE”, “--”); // => A--FGHI A--F Lavorare con array String e Char Alcuni metodi accettano o restituiscono un array di elementi String o Char. Ad esempio, il metodo ToCharArray restituisce un array che contiene tutti i caratteri della stringa. Tipicamente, si utilizza questo metodo quando processare i caratteri separatamente è più efficiente che estrarli dalla stringa. Ad esempio, consideriamo il problema di verificare se due stringhe contengono gli stessi caratteri, indipendentemente dall’ordine (in altri termini, se una stringa è l’anagramma dell’altra). string s1 = “file”; string s2 = “life”; // Trasforma entrambe le stringhe in un array di caratteri. char[] chars1 = s1.ToCharArray(); char[] chars2 = s2.ToCharArray(); // Ordina entrambi gli array. Array.Sort(chars1); Array.Sort(chars2); 13 14 Programmare Microsoft Visual C# 2005 // Crea due nuove stringhe dagli array ordinati, e le confronta. string sorted1 = new string(chars1); string sorted2 = new string(chars2); // Le confronta. (Se necessario, si possono utilizzare i confronti case-insensitive.) bool match = (string.Compare(sorted1, sorted2) == 0); // => True Si può suddividere una stringa in un array di sottostringhe con il metodo Split, che accetta un elenco di separatori e un numero massimo opzionale di elementi nell’array risultato. Questo metodo è stato migliorato in .NET 2.0 e ora ha la capacità di processare separatori di qualsiasi lunghezza e di eliminare facoltativamente gli elementi vuoti dall’array risultante: string x = “Hey, Visual C# Rocks!”; string[] arr = x.Split(‘ ‘, ‘,’, ‘.’); // Il risultato contiene le parole “Hey”, “”, “Visual”, “C#”, “Rocks!” int numOfWords = arr.Length; // => 5 // Come prima, ma non più di 100 elementi, e elimina gli elementi vuoti. char[] separators = {‘ ‘, ‘,’, ‘.’}; string[] arr2 = x.Split(separators, 100, StringSplitOptions.RemoveEmptyEntries); // Il risultato contiene le parole “Hey”, “Visual”, “C#”, “Rocks!” numOfWords = arr2.Length; // => 4 La nuova capacità di utilizzare stringhe multi-carattere come separatore è abbastanza utile quando si devono recuperare le singole righe di una stringa che contiene coppie di caratteri Carriage Return-Line Feed: // Conta il numero di righe non vuote in un file di testo. string[] crlfs = {“\r\n”}; string[] lines = File.ReadAllText(“data.txt”).Split(crlfs, StringSplitOptions.None); int numOfLines = lines.Length; Con il metodo static Join si può eseguire l’operazione opposta, cioè concatenare tutti gli elementi di un array di stringhe. Questo metodo accetta opzionalmente l’indice iniziale e il numero massimo di elementi da considerare. // (Continua l’esempio precedente…) // Riassembla la stringa aggiungendo coppie CR-LF, ma salta il primo elemento dell’array. string newText = string.Join(“\r\n”, lines, 1, lines.Length - 1); I metodi assenti Nonostante il gran numero di metodi esposti dal tipo String, a volte si deve scrivere una propria funzione per eseguire dei compiti ricorrenti. Ad esempio, non vi è alcun metodo predefinito simile a PadLeft o PadRight ma in grado di centrare una stringa in un campo di una determinata dimensione. Ecco il codice che esegue questo compito: public static string PadCenter(string s, int width, char padChar, bool truncate) { int diff = width - s.Length; if ( diff == 0 || (diff < 0 && !(truncate)) ) { // Restituisce la stringa così com’è. return s; } Capitolo 1 Tipi fondamentali .NET else if (diff < 0) { // Tronca la stringa. return s.Substring(0, width); } else { // Metà dei caratteri extra vanno a sinistra, i rimanenti vanno a destra. return s.PadLeft(width - diff / 2, padChar).PadRight(width, padChar); } } Per curiosità, ecco un metodo che inverte l’ordine dei caratteri in una stringa. Ecco come rimediare con un metodo custom che permette anche di invertire solo una parte della stringa: public static string StringReverse(string s, int startIndex, int count) { char[] chars = s.ToCharArray(); if (count < 0) { count = s.Length - startIndex; } Array.Reverse(chars, startIndex, count); return new string(chars); } Ecco un metodo migliore che permette di duplicare una stringa di qualsiasi lunghezza, basato sul metodo CopyTo: public static string StringDuplicate(string s, int count) { // Prepara un array di caratteri di una determinata lunghezza. char[] chars = new char[s.Length * count - 1 + 1]; // Copia la stringa nell’array più volte. for (int i = 0; i <= count - 1; i++) { s.CopyTo(0, chars, i * s.Length, s.Length); } return new string(chars); } O si può utilizzare la seguente dichiarazione/istruzione di una sola riga, che costruisce una stringa di spazi la cui lunghezza è uguale al numero di ripetizioni, e poi sostituisce ciascuno spazio con la stringa da ripetere: // Ripete la stringa Text un numero di volte pari a Count. string dupstring = new string(‘ ‘, Count).Replace(“ “, Text); Si può utilizzare il metodo IndexOf in un ciclo per contare il numero di occorrenze di una sottostringa: public static int CountSubstrings(string source, string search) { int count = -1; int index = -1; do 15 16 Programmare Microsoft Visual C# 2005 { count += 1; index = source.IndexOf(search, index + 1); } while ( index >= 0 ); return count; } Si può anche calcolare il numero di occorrenze di una sottostringa con la seguente tecnica, che è più concisa ma leggermente meno efficiente del metodo precedente poiché crea due stringhe temporanee: count = source.Replace(search, search + “*”).Length - source.Length; Ottimizzazioni di stringhe Un dettaglio importante da tenere a mente è che un oggetto String è immutabile: una volta creata una stringa, il contenuto non può mai cambiare. Infatti, tutti i metodi visti finora non modificano la stringa originale; piuttosto, restituiscono un ulteriore oggetto String che si può o meno assegnare alla stessa variabile String. Comprendere questo dettaglio permette di evitare un errore comune di programmazione: string s = “abcde”; // Si *potrebbe* credere s.ToUpper(); // …ma così non è poiché Console.WriteLine(s); // // Ecco il modo corretto s = s.ToUpper(); che lo statement successivo modifichi la stringa… il risultato non è stato riassegnato a s. => abcde per invocare i metodi stringa. Se si assegna il risultato alla stessa variabile, la stringa originale diventa irraggiungibile dalla parte dell’applicazione (a meno che non vi siano altre variabili che puntano ad essa) e infine verrà sottoposta alla garbage collection. Poiché i valori stringa sono immutabili, il compilatore può ottimizzare il codice risultante in modi altrimenti non possibili. Ad esempio, si consideri questo frammento di codice: string s1 = “1234” + “5678”; string s2 = “12345678”; Console.WriteLine(object.ReferenceEquals(s1, s2)); // => True Il compilatore calcola l’operatore di concatenazione (+) alla compilazione e si rende conto che entrambe le variabili contengono gli stessi caratteri, pertanto può allocare un solo blocco di memoria per la stringa e far sì che le due variabili puntino ad esso. Poiché la stringa è immutabile, dietro le quinte viene creato un nuovo oggetto non appena si tenta di modificare i caratteri di questa stringa: A causa di questo comportamento, non è mai realmente necessario invocare il metodo Clone per creare esplicitamente una copia di String. Basta utilizzare semplicemente la stringa come si farebbe normalmente, e il compilatore crea una copia in automatico se e quando è necessario. Capitolo 1 Tipi fondamentali .NET Il CLR può ottimizzare la gestione delle stringhe mantenendo un pool interno di valori stringa per ciascuna applicazione .NET, noto come pool interno. Se il valore da assegnare a una variabile stringa coincide con una delle stringhe già presenti nel pool interno, non viene occupata alcuna memoria ulteriore e la variabile riceve l’indirizzo del valore stringa nel pool. Come si è visto prima, il compilatore è in grado di utilizzare il pool interno per ottimizzare l’inizializzazione della stringa e far sì che due variabili stringa puntino allo stesso oggetto String in memoria. Questo passo di ottimizzazione non viene eseguito a runtime, tuttavia, poiché la ricerca nel pool consuma tempo e in gran parte dei casi fallirebbe, aggiungendo un inutile overhead all’applicazione senza comportare alcun vantaggio. // Dimostra che nessuna ottimizzazione viene eseguita a runtime. s1 = “1234”; s1 += “5678”; s2 = “12345678”; // Queste due variabili puntano a differenti oggetti String. Console.WriteLine(object.ReferenceEquals(s1, s2)); // => False Si può ottimizzare la gestione delle stringhe utilizzando il metodo shared Intern. Questo metodo ricerca un valore stringa nel pool interno e restituisce un riferimento all’elemento del pool che contiene il valore se il valore è già presente nel pool. Se la ricerca fallisce, la stringa viene aggiunta al pool e viene restituito un riferimento ad essa. Si noti come si può ottimizzare “manualmente” il frammento di codice precedente utilizzando il metodo String.Intern: s1 = “ABCD”; s1 += “EFGH”; // Sposta S1 nel pool interno. s1 = String.Intern(s1); // Assegna a S2 una costante stringa (che sappiamo essere nel pool). s2 = “ABCDEFGH”; // Queste due variabili puntano allo stesso oggetto String. Console.WriteLine(object.ReferenceEquals(s1, s2)); // => True Questa tecnica di ottimizzazione ha senso solo se si sta lavorando con stringhe lunghe che appaiono in più punti delle applicazioni. Un ulteriore momento idoneo per utilizzare questa tecnica è quando si hanno molte istanze di un componente lato server che contiene siffatte variabili stringa, ad esempio una stringa di connessione a database. Anche se queste stringhe non cambiano durante l’esecuzione del programma, di solito vengono lette da un file, e perciò, il compilatore non può ottimizzare automaticamente la loro allocazione in memoria. Utilizzando il metodo Intern, si può aiutare la propria applicazione a produrre un “footprint” di memoria più piccolo. Si può anche utilizzare il metodo shared IsInterned per controllare se una stringa è nel pool interno (nel qual caso viene restituita la stringa stessa) o meno (nel qual caso il metodo restituisce null): // Continua l’esempio precedente… if ( string.IsInterned(s1) != null ) { // Questo blocco viene eseguito poiché s1 è nel pool interno. } 17 18 Programmare Microsoft Visual C# 2005 Ecco un ulteriore semplice suggerimento sulle prestazioni: cercate di raccogliere più operatori di concatenazione nella stessa istruzione invece di disseminarli su righe separate. Il compilatore C# può ottimizzare operazioni multiple di concatenazione solo se si trovano nello stesso statement. Il tipo CultureInfo La classe System.Globalization.CultureInfo definisce un oggetto che si può ispezionare per determinare alcune proprietà principali di qualsiasi lingua installata e che si può utilizzare come argomento in molti metodi del tipo String e di altri tipi. La classe espone la proprietà static CurrentCulture, che restituisce l’oggetto CultureInfo della lingua corrente: // Ottiene informazioni sulla nazionalità corrente. CultureInfo ci = CultureInfo.CurrentCulture; // Assumendo che il linguaggio corrente sia Italian, otteniamo: Console.WriteLine(ci.Name); // => it Console.WriteLine(ci.EnglishName); // => Italian Console.WriteLine(ci.NativeName); // => italiano Console.WriteLine(ci.LCID); // => 16 Console.WriteLine(ci.TwoLetterISOLanguageName); // => it Console.WriteLine(ci.ThreeLetterISOLanguageName); // => ita Console.WriteLine(ci.ThreeLetterWindowsLanguageName); // => ITA Si possono ottenere ulteriori informazioni sulla nazionalità attraverso l’oggetto TextInfo, esposto dall’omonima proprietà: TextInfo ti = ci.TextInfo; Console.WriteLine(ti.ANSICodePage); // => 1252 Console.WriteLine(ti.EBCDICCodePage); // => 20280 Console.WriteLine(ti.OEMCodePage); // => 850 Console.WriteLine(ti.ListSeparator); // => ; L’oggetto CultureInfo espone due proprietà, NumberFormat e DateTimeFormat, che restituiscono informazioni su come vengono formattati numeri e date in base a una determinata nazionalità. Ad esempio, si consideri questo codice: // Come si dice “Sunday” in Germania? // Prima crea un oggetto CultureInfo per German/Germany. // (Si noti che si deve passare una stringa nel formato “locale-COUNTRY” se // un determinato linguaggio è parlato in più paesi.) CultureInfo ciDe = new CultureInfo(“de-DE”); // Poi si ottiene il corrispondente oggetto DateTimeFormatInfo. DateTimeFormatInfo dtfi = ciDe.DateTimeFormat; // Ecco la risposta. Console.WriteLine(dtfi.GetDayName(DayOfWeek.Sunday)); // => Sonntag Le stringhe “locale-COUNTRY” si trovano in molti punti del .NET Framework. Il metodo static GetCultures restituisce un array di tutte le culture installate, pertanto si possono ispezionare tutte le lingue che il proprio sistema operativo supporta: Capitolo 1 Tipi fondamentali .NET // Ottiene informazioni su tutte le culture installate. CultureInfo[] ciArr = CultureInfo.GetCultures(CultureTypes.AllCultures); // Stampa l’abbreviazione e il nome inglese di ciascuna cultura. foreach ( CultureInfo c in ciArr ) { Console.WriteLine(“{0} ({1})”, c.Name, c.EnglishName); } Il metodo static GetCultureInfo, nuovo della versione 2.0, permette di recuperare una versione read-only in cache di un oggetto CultureInfo. Se si utilizza ripetutamente questo metodo per richiedere la stessa cultura, viene restituito lo stesso oggetto CultureInfo presente in cache, risparmiando così il tempo di istanziamento: CultureInfo ci1 = CultureInfo.GetCultureInfo(“it-IT”); CultureInfo ci2 = CultureInfo.GetCultureInfo(“it-IT”); // Dimostra che la seconda invocazione ha restituito un oggetto in cache. Console.WriteLine(object.ReferenceEquals(ci1,ci2)); // => True L’oggetto ausiliario TextInfo permette di convertire una stringa in maiuscolo, minuscolo o in grafia titolo (ossia, “Queste Sono Quattro Parole”) per una determinata lingua: // Crea un oggetto CultureInfo per il francese canadese. (Se possibile utilizza un oggetto in cache.) CultureInfo ciFr = CultureInfo.GetCultureInfo(“fr-CA”); // Converte una stringa in stile titolo utilizzando le regole del francese canadese. s = ciFr.TextInfo.ToTitleCase(s); Gran parte dei metodi stringa il cui risultato dipende dalla nazionalità accetta un oggetto CultureInfo come argomento, cioè Compare, StartsWith, EndsWith, ToLower e ToUpper. (Questa caratteristica è nuova di .NET 2.0 per gli ultimi quattro metodi). Vediamo come si può passare questo oggetto al metodo String.Compare in modo da poter confrontare le stringhe in base alle regole di confronto definite da una determinata lingua. Una versione in overload del metodo Compare accetta quattro argomenti: le due stringhe da confrontare, un valore Boolean che indica se il confronto è caseinsensitive, e un oggetto CultureInfo che specifica il linguaggio da utilizzare: string s1 = “cioè”; string s2 = “CIOÈ”; // Si può creare un oggetto CultureInfo al volo. if ( string.Compare(s1, s2, true, new CultureInfo(“it”)) == 0 ) { Console.WriteLine(“s1 = s2”); } Vi è anche una versione in overload che confronta due sottostringhe: if (string.Compare(s1, 1, s2, 1, 4, true, new CultureInfo(“it”)) == 1) { Console.WriteLine(“s1’s first four chars are greater than s2’s”); } 19 20 Programmare Microsoft Visual C# 2005 Se non si passa un oggetto CultureInfo al metodo Compare, il confronto viene eseguito utilizzando la nazionalità associata al thread corrente. Si può modificare questo valore di nazionalità assegnando un oggetto CultureInfo alla proprietà CurrentCulture del thread corrente, come segue: // Utilizza la cultura Italian per tutte le operazioni stringa e i confronti. Thread.CurrentThread.CurrentCulture = new CultureInfo(“it-IT”); Si possono anche confrontare valori in base a una cultura invariante, in modo che l’ordine in cui vengono valutati i risultati è lo stesso indipendentemente dalla nazionalità del thread corrente. In questo caso si può passare il valore di ritorno della proprietà static CultureInfo.InvariantCulture: if ( string.Compare(s1, s2, true, CultureInfo.InvariantCulture) == 0 ) { … } Il .NET Framework 2.0 offre il nuovo tipo enumerativo StringComparison che permette di eseguire confronti e test di uguaglianza utilizzando la cultura corrente, la cultura invariante e i valori numerici dei singoli caratteri, sia in modo case-sensitive sia case-insensitive. Per ulteriori dettagli e esempi, si legga il precedente paragrafo “Confronto e ricerca di stringhe”, in questo capitolo. La classe Encoding Tutte le stringhe .NET memorizzano i propri caratteri in formato Unicode, pertanto talvolta si potrebbe doverle convertire da e verso altri formati: ad esempio, ASCII o le varianti UCS Transformation Format 7 (UTF-7) o UTF-8 del formato Unicode. Si può fare ciò con la classe Encoding del namespace System.Text. La prima cosa da fare quando si converte una stringa .NET Unicode da o verso un ulteriore formato è creare l’opportuno oggetto di codifica. La classe Encoding espone opportunamente i più comuni oggetti di codifica attraverso le seguenti proprietà statiche: ASCII, Unicode (con ordinamento dei byte little-endian), BigEndianUnicode, UTF7, UTF8, UTF32 e Default (la code page ANSI corrente di sistema). Ecco un esempio di come si può convertire una stringa Unicode in una sequenza di byte che rappresentano la stessa stringa in formato ASCII: string text = “A Unicode string with accented vowels: àèéìòù”; Encoding uni = Encoding.Unicode; byte[] uniBytes = uni.GetBytes(text); Encoding ascii = Encoding.ASCII; byte[] asciiBytes = Encoding.Convert(uni, ascii, uniBytes); // Converte i byte ASCII nuovamente in una stringa. string asciiText = new string(ascii.GetChars(asciiBytes)); Console.WriteLine(asciiText); // => A Unicode string with accented vowels: ?????? Capitolo 1 Tipi fondamentali .NET Si possono anche creare altri oggetti Encoding con il metodo static GetEncoding, che accetta o un numero di code page o il nome della code page e che genera una NotSupportedException se la code page non è supportata: // Ottiene l’oggetto encoding per la code page 1252. Encoding enc = Encoding.GetEncoding(1252); Il metodo GetEncodings (nuovo del .NET Framework 2.0) restituisce un array di oggetti EncodingInfo, che forniscono informazioni su tutti gli oggetti Encoding e le code page installate sul computer: foreach ( EncodingInfo ei in Encoding.GetEncodings() ) { Console.WriteLine(“Name={0}, DisplayName={1}, CodePage={2}”, ei.Name, ei.DisplayName, ei.CodePage); } Il metodo GetChars si aspetta che l’array di byte che gli viene passato contenga un numero intero di caratteri. (Ad esempio, deve terminare con il secondo byte di un carattere a due byte). Questo vincolo può essere un problema quando si legge l’array di byte da un file o da un altro tipo di stream, e si sta lavorando con un formato di stringa che permette uno, due o tre byte per carattere. In questi casi, si deve utilizzare un oggetto Decoder, che ricorda lo stato tra invocazioni consecutive. Per ulteriori informazioni, si legga la documentazione MSDN. Formattazione di valori numerici Il metodo statico Format della classe String permette di formattare una stringa e di inserire in essa uno o più valori numerici o di data, nel modo simile al metodo Console.Write. La stringa da formattare può contenere dei placeholder di argomenti, nel formato {N} dove N è un indice che parte da 0: // Stampa il valore di una variabile stringa. string xyz = “foobar”; string msg = msg = string.Format(“The value of {0} variable is {1}.”, “XYZ”, xyz); // => Il valore della variabile XYZ è foobar. Se l’argomento è numerico, si può aggiungere un carattere due punti dopo l’argomento indice e poi un carattere che indica che tipo di formattazione si sta richiedendo. I caratteri disponibili sono G (General), N (Number), C (Currency), D (Decimal), E (Scientific), F (Fixed-point), P (Percent), R (Round-trip) e X (Hexadecimal): // Formatta un valore Currency in base alla nazionalità corrente. msg = string.Format(“Total is {0:C}, balance is {1:C}”, 123.45, -67); // => Total is $123.45, balance is ($67.00) Il formato numero utilizza le virgole (o per essere più precisi, il separatore delle migliaia definito dalla nazionalità corrente), per raggruppare le cifre: msg = string.Format(“Total is {0:N}”, 123456.78); // => Total is 123,456.78 21 22 Programmare Microsoft Visual C# 2005 Si può accodare un intero dopo il carattere N per arrotondare o estendere il numero di cifre dopo il punto decimale: msg = string.Format(“Total is {0:N4}”, 123456.785555); // => Total is 123,456.7856 Il formato decimale funziona solo con valori interi e genera una FormatException se si passa un argomento non integer; si può specificare una lunghezza che, se più lunga del risultato, fa sì che vengano aggiunti uno o più zeri in testa: msg = string.Format(“Total is {0:D8}”, 123456); // => Total is 00123456 Il formato a virgola fissa è utile con i valori decimali, e si può indicare quante cifre decimali devono essere visualizzate (due se si omette la lunghezza): msg = string.Format(“Total is {0:F3}”, 123.45678); // => Total is 123.457 La notazione scientifica (o esponenziale) visualizza i numeri nel formato n.mmmE+eeee, e si può controllare quante cifre decimali vengono utilizzate nella parte mantissa: msg = string.Format(“Total is {0:E}”, 123456.789); // => Total is 1.234568E+005 msg = string.Format(“Total is {0:E3}”, 123456.789); // => Total is 1.235E+005 Il formato generale viene convertito nel formato a virgola fissa o esponenziale, in base a quale formato produce il risultato più compatto: msg = string.Format(“Total is {0:G}”, 123456); // => Total is 123456 msg = string.Format(“Total is {0:G4}”, 123456); // => Total is 1.235E+05 Il formato percento converte un numero in una percentuale con due cifre decimali per default, utilizzando il formato specificato dalla cultura corrente: msg = string.Format(“Percentage is {0:P}”, 0.123); // => Total is 12.30 % Il formato round-trip converte un numero in una stringa che contiene tutte le cifre significative in modo che la stringa possa essere in seguito riconvertita in un numero senza alcuna perdita di precisione: // Il numero di cifre che si passa dopo il carattere “R” viene ignorato. msg = string.Format(“Value of PI is {0:R}”, Math.PI); // => Value of PI is 3.1415926535897931 Infine, il formato esadecimale converte i numeri in stringhe esadecimali. Se si specifica una lunghezza, nel numero, se necessario, vengono inseriti degli zeri in testa: msg = string.Format(“Total is {0:X8}”, 65535); // => Total is 0000FFFF Si possono costruire stringhe di formato custom utilizzando alcuni caratteri speciali, il cui significato è riassunto nella Tabella 1-1. Ecco alcuni esempi: msg = msg = // Un msg = string.Format(“Total is {0:##,###.00}”, 1234.567); // => Total is 1,234.57 string.Format(“Percentage is {0:##.000%}”, .3456); // => Percentage is 34.560% esempio di prescalatura string.Format(“Length in {0:###,.00 }”, 12344); // => Total is 12.34 Capitolo 1 Tipi fondamentali .NET // Due esempi di formato esponenziale msg = string.Format(“Total is {0:#.#####E+00}”, 1234567); // => Total is 1.23457E+06 msg = string.Format(“Total is {0:#.#####E0}”, 1234567); // => Total is 1.23457E6 // Due esempi con sezioni separate msg = string.Format(“Total is {0:##;<##>}”, -123); // => Total is <123> msg = string.Format(“Total is {0:#;(#);zero}”, 1234567); // => Total is 1234567 In alcuni casi si possono utilizzare due o tre sezioni per evitare la logica if o switch. Ad esempio, si può sostituire il codice seguente: if (n1 > n2) { msg = “n1 is greater than n2”; } else if (n1 < n2) { msg = “n1 is less than n2”; } else { msg = “n1 is equal to n2”; } con il seguente codice, più conciso ma alquanto più criptico: msg = string.Format(“n1 is {0:greater than;less than;equal to} n2”, n1 – n2); Una caratteristica poco nota del metodo String.Format, ma anche di tutti i metodi che lo usano internamente, come il metodo Console.Write, permette di specificare la larghezza di un campo e decidere di allineare il valore a destra o a sinistra: // Crea una tavola di numeri, dei loro quadrati e delle radici quadrate. // Stampa l’intestazione della tavola. Console.WriteLine(“{0,-5} | {1,7} | {2,10:N2}”, “N”, “N^2”, “Sqrt(N)”); for ( int n = 1; n <= 100; n++ ) { // N è allineato a sinistra in un campo ampio 5 caratteri, // N^2 è allineato a destra in un campo ampio 7 caratteri, e Sqrt(N) viene visualizzato con // 2 cifre decimali ed è allineato a destra in un campo ampio 10 caratteri. Console.WriteLine(“{0,-5} | {1,7} | {2,10:N2}”, n, Math.Pow(n, 2), Math.Sqrt(n)); } La larghezza del campo si specifica dopo il simbolo virgola; si utilizza una larghezza positiva per valori allineati a destra e una larghezza negativa per valori allineati a sinistra. Se si vuole fornire un formato predefinito, si utilizza un simbolo due punti come separatore dopo il valore della larghezza. Come si vede nel precedente esempio, le larghezze del campo sono supportate anche per valori numerici, stringa e Date. Infine, si possono inserire parentesi graffe letterali raddoppiandole nella stringa di formato: Console.WriteLine(“ {{{0}}}”, 123); // => {123} 23 24 Programmare Microsoft Visual C# 2005 Tabella 1-1 Caratteri speciali di formattazione nelle stringhe di formattazione custom Formato Descrizione # Segnaposto per una cifra o uno spazio. 0 Segnaposto per una cifra o uno zero. . Separatore decimale. , Separatore delle migliaia; se utilizzato subito prima del separatore decimale, funziona come prescalatura. (Per ciascuna virgola in questa posizione, il valore viene diviso per 1000 prima della formattazione). % Visualizza il numero come valore percentuale. E+000 Visualizza il numero in formato esponenziale, ossia, con una E seguita dal segno di esponente, e poi un numero di cifre dell’esponente pari al numero di zeri dopo il segno più. E-000 Analogo al precedente simbolo esponente, ma il segno dell’esponente viene visualizzato solo se negativo. ; Separatore di sezione. La stringa del formato può contenere uno, due o tre sezioni. In caso di due sezioni, la prima si applica ai valori positivi e allo zero, e la secondo si applica ai valori negativi. In caso di tre sezioni, vengono utilizzate, rispettivamente, per valori positivi, negativi e zero. \char Carattere di escape, per inserire caratteri che altrimenti verrebbero interpretati come caratteri speciali (ad esempio, \; per inserire un punto e virgola e \\ per inserire un backslash). ‘...’ “...” Un gruppo di caratteri letterali. Si può aggiungere una sequenza di caratteri letterali racchiudendoli tra apici singoli o doppi. altro Ogni altro carattere è interpretato alla lettera e viene inserito nella stringa risultato così com’è. Formattazione di valori Date Il metodo String.Format supporta anche valori data e ora sia in formato standard sia custom. La Tabella 1-2 riassume i formati standard data e ora e consente di individuare rapidamente il formato che si sta cercando. DateTime aDate = new DateTime(2005, 5, 17, 15, 54, 0); string msg = string.Format(“Event Date/Time is {0:f}”, aDate); // => Event Date Time is Tuesday, May 17, 2005 3:54 PM Se non si trova un formato standard data/ora adatto ai propri scopi, si può creare un formato custom utilizzando i caratteri speciali elencati nella Tabella 1-3: msg = string.Format(“Current year is {0:yyyy}”, DateTime.Now); // => Current year is 2005 I caratteri di formattazione / e : sono particolarmente elusivi poiché vengono sostituiti dal separatore data/ora di default definito per la nazionalità corrente. In alcuni casi, in modo particolare quando si formattano date per un comando SQL SELECT o INSERT, si vuol essere certi che un determinato separatore sia utilizzato in tutte le occasioni. Capitolo 1 Tipi fondamentali .NET In questo caso, si deve utilizzare il carattere di escape backslash per forzare uno specifico separatore: // Formatta una data nel formato mm/dd/yyyy, indipendentemente dalla nazionalità corrente. msg = string.Format(@”{0:MM\/dd\/yyyy}”, aDate); // => 05/17/2005 Tabella 1-2 Formati standard per valori Date e Time1 Formato Descrizione Pattern Esempio d Data breve MM/dd/yyyy 1/6/2005 D Data lunga dddd, MMMM dd, yyyy Thursday, January 06, 2005 f Data e ora completa (data lunga e ora breve) dddd, MMMM dd, yyyy HH: mm Thursday, January 06, 2005 3:54 PM F Data e ora completa (data dddd, MMMM dd, yyyy HH: lunga e ora lunga) mm:ss Thursday, January 06, 2005 3:54:20 PM g Generale (data breve e ora breve) MM/dd/yyyy HH:mm 1/6/2005 3:54 PM G Generale (data breve e ora lunga) MM/dd/yyyy HH:mm:ss 1/6/2005 3:54:20 PM M,m Mese e Giorno MMMM dd January 06 Y,y Anno e Mese MMMM, yyyy January, 2005 t Ora breve HH:mm 3:54 PM T Ora lunga HH:mm:ss 3:54:20 PM s Data e ora ordinabile (conforme all’ISO 8601) utilizzando la cultura corrente yyyy-MM-dd HH:mm:ss 2005-01-06T15:54:20 u Data e ora universale (conforme all’ISO 8601), non influenzato dalla cultura corrente yyyy-MM-dd HH:mm:ss 2005-01-06 20:54:20Z U Data e ora universale ordinabile dddd, MMMM dd, yyyy HH: mm:ss Thursday, January 06, 2005 5:54:20 PM R,r RFC1123 ddd, dd MMM yyyy HH’: ’mm’:’ss ‘GMT’ Thu, 06 Jan 2005 15:54:20 GMT O,o RoundtripKind (utile per ripristinare tutte le proprietà durante il parsing) yyyy-MM-dd HH:mm: ss.fffffffK 2005-01-06T15:54: 20.0000000-08:00 1 Si noti che i formati U, u, R e r utilizzano il formato Universal (Greenwich) Time, indipendentemente dal fuso orario locale, pertanto i valori di esempio per questi formati sono 5 ore avanti dei valori di esempio degli altri formati (che assumono il fuso orario USA della costa atlantica). La colonna Pattern specifica la corrispondente stringa di formato custom costituita dai caratteri elencati nella Tabella 1-3. 25 26 Programmare Microsoft Visual C# 2005 Tabella 1-3 Sequenze di caratteri che possono essere utilizzati nei formati custom Date e Time Formato Descrizione d Giorno del mese (una o due cifre come richiesto) dd Giorno del mese (sempre due cifre, con uno zero in testa se richiesto) ddd Giorno della settimana (abbreviazione di tre caratteri) dddd Giorno della settimana (nome completo) M Numero del mese (uno o due cifre come richiesto) MM Numero del mese (sempre due cifre, con uno zero in testa se richiesto) MMM Nome del mese (abbreviazione di tre caratteri) MMMM Nome del mese (nome completo) y Anno (ultima cifra o ultime due, nessuno zero in testa) yy Anno (ultime due cifre) yyyy Anno (quattro cifre) H Ora in formato 24 ore (uno o due cifre come richiesto) HH Ora nel formato 24 ore (sempre due cifre, con uno zero in testa se richiesto) h Ora nel formato 12 ore (uno o due cifre come richiesto) hh Ora nel formato 12 ore m Minuti (uno o due cifre come richiesto) mm Minuti (sempre due cifre, con uno zero in testa se richiesto) s Secondi (uno o due cifre come richiesto) ss Secondi t Il primo carattere dell’indicatore AM/PM f tt Frazioni di secondo, rappresentate con una cifra. (ff significa frazioni di secondo in due cifre, fff in tre cifre e via dicendo fino a 7 fs in una riga.) Frazioni di secondo, rappresentate con una cifra opzionale. Simile a f, eccetto che può essere utilizzato con DateTime.ParseExact senza generare una eccezione se vi sono meno cifre del previsto. (Nuovo di .NET 2.0.) L’indicatore AM/PM z Offset del fuso orario, solo ora (uno o due cifre come richiesto) zz Offset del fuso orario, solo ora (sempre due cifre, con uno zero in testa se richiesto) zzz / Offset del fuso orario, ora e minuto (valori ora e minuto sempre di due cifre, con uno zero in testa se richiesto) Il carattere “Z” se la proprietà Kind del valore DateTime è Utc; l’offset del fuso orario (ad es. “-8:00”) se la proprietà Kind è Local; un carattere vuoto se la proprietà Kind è Unspecified. (Nuovo di .NET 2.0.) Separatore di default della data : Separatore di default dell’ora \char Carattere di escape, per includere caratteri letterali che altrimenti verrebbero considerato caratteri speciali Comprende un formato data/ora predefinito nella stringa risultato. F K %format Capitolo 1 Tipi fondamentali .NET Tabella 1-3 Sequenze di caratteri che possono essere utilizzati nei formati custom Date e Time Formato ‘…’ “…” altro Descrizione Un gruppo di caratteri letterali. Si può aggiungere una sequenza di caratteri letterali racchiudendoli tra apici singoli o doppi. Qualsiasi altro carattere viene interpretato alla lettera e inserito nella stringa risultato così com’è. Il tipo Char La classe Char rappresenta un singolo carattere. Non c’è molto da dire su questo tipo di dato, se non che espone diversi utili metodi statici che permettono di testare se un carattere soddisfa un determinato criterio. Questi metodi sono in overload e accettano un Char, o un valore String più un indice della stringa. Ad esempio, si controlla se un carattere è una cifra come segue: // Controlla un singolo valore Char. bool ok = char.IsDigit(‘1’); // => True // Controlla l’N-mo carattere di una stringa. ok = char.IsDigit(“A123”, 0); // => False Ecco l’elenco dei metodi statici più utili che testano singoli caratteri: IsControl, IsDigit, IsLetter, IsLetterOrDigit, IsLower, IsNumber, IsPunctuation, IsSeparator, IsSymbol, IsUpper e IsWhiteSpace. Si può convertire un carattere in maiuscolo e minuscolo con i metodi statici ToUpper e ToLower. Per default questi metodi operano in base alla nazionalità del thread corrente, ma si può passare loro un oggetto CultureInfo opzionale, o si possono utilizzare le versioni invarianti rispetto alla cultura ToUpperInvariant e ToLowerInvariant: char newChar = char.ToUpper(‘a’); // => A newChar = char.ToLower(‘H’, new CultureInfo(“it-IT”)); // => h char loChar = char.ToLowerInvariant(‘G’); // => g Si può convertire una stringa in un Char per mezzo dell’operatore CChar o del metodo Char.Parse. O si può utilizzare il nuovo metodo statico TryParse (aggiunto nel .NET Framework 2.0) per controllare se la conversione è possibile ed eseguirla in una operazione: if ( char.TryParse(“a”, out newChar) ) { // newChar contiene il carattere ‘a’. } Il tipo StringBuilder Come è noto, un oggetto String è immutabile, e il suo valore non cambia mai dopo che la stringa è stata creata. Ciò significa che in qualsiasi momento si applichi un metodo che ne modifica il valore, si sta effettivamente creando un nuovo oggetto di tipo String. Ad esempio, il seguente statement: S = S.Insert(3, “1234”); 27 28 Programmare Microsoft Visual C# 2005 non modifica la stringa originale in memoria. Invece, il metodo Insert crea un nuovo oggetto String, che viene quindi assegnato alla variabile stringa S. L’oggetto stringa originale in memoria viene infine recuperato durante la successiva garbage collection a meno che un’ulteriore variabile punti ad esso, come accade spesso con stringhe del pool interno. La superiorità dello schema di allocazione della memoria di .NET assicura che questo meccanismo introduca un overhead relativamente basso; ciò nondimeno, troppe operazioni di allocazione e rilascio possono degradare le prestazioni della propria applicazione. L’oggetto System.Text.StringBuilder offre una soluzione a questo problema. Si può considerare un oggetto StringBuilder come un buffer che può contenere una stringa che ha la capacità di crescere da zero caratteri alla capacità corrente del buffer. Finché non si supera questa capacità, la stringa viene assemblata nel buffer e non viene né allocata né rilasciata alcuna memoria. Se la stringa diventa più lunga della capacità corrente, l’oggetto StringBuilder crea in modo trasparente un buffer più grande. Il buffer di default contiene inizialmente 16 caratteri, ma si può modificare questa dimensione assegnando una capacità differente nel costruttore di StringBuilder o assegnando un valore alla proprietà Capacity: // Crea un oggetto StringBuilder con capacità iniziale di 1,000 caratteri. StringBuilder sb = new StringBuilder(1000); Si può processare la stringa contenuta nell’oggetto StringBuilder con diversi metodi, gran parte dei quali è omonimo e funziona in modo analogo ai metodi esposti dalla classe String: ad esempio, i metodi Insert, Remove e Replace. Il modo più comune per costruire una stringa all’interno di un oggetto StringBuilder è per mezzo del relativo metodo Append, che accetta un argomento di qualsiasi tipo e lo accoda alla stringa interna corrente: // Crea un elenco delimitato da virgola dei primi 100 interi. for ( int n = 1; n <= 100; n++ ) { // Si noti che due metodi Append sono più veloci di un unico Append, // il cui argomento è la concatenazione di N e “,”. sb.Append(n); sb.Append(“,”); } // Inserisce una stringa all’inizio del buffer. sb.Insert(0, “List of numbers: “); Console.WriteLine(sb); // => List of numbers: 1,2,3,4,5,6, La proprietà Length restituisce la lunghezza corrente della stringa interna: // Continua l’esempio precedente… Console.WriteLine(“Length is {0}.”, sb.Length); // => Length is 309. Esiste anche un metodo AppendFormat, che permette di specificare una stringa di formato, in modo analogo al metodo String.Format, e un metodo AppendLine (nuovo di .NET 2.0) che accoda una stringa e il terminatore di default della riga: for ( int n = 1; n <= 100; n++ ) { sb.AppendLine(n.ToString()); } Capitolo 1 Tipi fondamentali .NET La seguente procedura confronta quanto rapidamente le classi String e StringBuilder eseguono un gran numero di concatenazioni di stringhe: string s = “”; const int TIMES = 10000; Stopwatch sw = new Stopwatch(); sw.Start(); for ( int i = 1; i <= TIMES; i++ ) { s += i.ToString() + “,”; } sw.Stop(); Console.WriteLine(“Regular string: {0} milliseconds”, sw.ElapsedMilliseconds); sw = new Stopwatch(); sw.Start(); StringBuilder sb = new StringBuilder(TIMES * 4); for ( int i = 1; i <= TIMES; i++ ) { // Si noti come si possono fondere due metodi Append. sb.Append(i).Append(“,”); } sw.Stop(); Console.WriteLine(“StringBuilder: {0} milliseconds.”, sw.ElapsedMilliseconds); Il risultato di questo benchmark può essere realmente sorprendente poiché mostra che l’oggetto StringBuilder può essere più di 100 volte più veloce dell’ordinaria classe String. Il rapporto effettivo dipende da quante iterazioni si eseguono e da quanto sono lunghe le stringhe coinvolte. Ad esempio, se TIMES è impostata a 20.000, sul mio computer la stringa standard richiede 5 secondi per terminare il ciclo, mentre il tipo StringBuilder richiede solo 8 millisecondi! Il tipo SecureString Il modo in cui le stringhe .NET vengono implementate ha alcune serie implicazioni inerenti alla sicurezza. Infatti, se in una stringa si memorizzano informazioni confidenziali, ad esempio una password o un numero di carta di credito, un ulteriore processo che può leggere nello spazio degli indirizzi della propria applicazione può anche leggere i relativi dati. Benché ottenere l’accesso allo spazio degli indirizzi di un processo non è cosa banale, si consideri che alcune porzioni del proprio spazio degli indirizzi vengono spesso salvate nel file di swap del sistema operativo, dove leggerle è molto più facile. Il fatto che le stringhe siano immutabili significa che non si può azzerare realmente una stringa dopo averla utilizzata. Ancor peggio, poiché una stringa è soggetta alla garbage collection, potrebbero esserci diverse copie di essa in memoria, il che a sua volta accresce la probabilità che una di esse finisca nel file di swap. Una applicazione .NET Framework 1.1 che vuole assicurare il più alto grado di confidenzialità dovrebbe tenersi alla larga dalle stringhe standard e utilizzare qualche altra tecnica, ad esempio un Char o un array di Byte crittografato che viene decodificato solo un istante prima di utilizzare la stringa e viene azzerato subito dopo. 29 30 Programmare Microsoft Visual C# 2005 La versione 2.0 del .NET Framework rende questo processo più facile con l’introduzione nel namespace System.Security del tipo SecureString. Sostanzialmente, un’istanza di SecureString è un array di caratteri che viene crittografato attraverso le Data Protection API (DPAPI). A differenza di una stringa standard, e analogamente al tipo StringBuilder, il tipo SecureString non è immutabile: lo si costruisce un carattere per volta per mezzo del metodo AppendChar, simile a ciò che si fa con il tipo StringBuilder, e si possono anche inserire, rimuovere e modificare singoli caratteri per mezzo dei metodi InsertAt, SetAt e RemoveAt. Facoltativamente, si può rendere la stringa immutabile invocando il metodo MakeReadOnly. Infine, per ridurre il numero di copie che vagano in memoria, le istanze di SecureString vengono fissate (pinned), il che significa che non possono essere spostate dal garbage collector. Il tipo SecureString è così sicuro che non espone né un metodo per inizializzarlo con una stringa né un metodo che ne restituisce il contenuto come testo in chiaro: il primo compito richiede una serie di invocazioni ad AppendChar, il secondo può essere eseguito con l’aiuto del tipo Marshal, come spiegherò presto. Un oggetto SecureString non è serializzabile e perciò non si può neanche salvarlo su file per un recupero in seguito. E naturalmente non si può inizializzarlo a partire da una stringa cablata nel proprio codice, poiché ciò andrebbe contro lo scopo previsto di questo tipo. Quali sono allora le opzioni per inizializzare correttamente un oggetto SecureString? Una prima opzione è memorizzare la password in chiaro in un file protetto da una Access Control List (ACL) e leggerla un carattere per volta. Questa opzione, tuttavia, non è a prova di bomba, e in alcuni casi non può comunque essere applicata, poiché i dati confidenziali vengono immessi dall’utente a runtime. Un’ulteriore opzione, in effetti la sola opzione che si può adottare quando l’utente immette i dati confidenziali a runtime, è far sì che l’utente inserisca il testo un carattere per volta, e crittografarli al volo. Il seguente frammento di codice mostra come si può simulare un controllo TextBox protetto da password in una applicazione Windows Forms che non memorizza mai il proprio contenuto in una stringa ordinaria: public SecureString password = new SecureString(); private void txtPassword_KeyPress(object sender, KeyPressEventArgs e) { if ( (int) e.KeyChar == 8) { // Backspace: rimuove il carattere dalla stringa sicura. if (txtPassword.SelectionStart > 0) { password.RemoveAt(txtPassword.SelectionStart - 1); } } else if ( (int)e.KeyChar >= 32) { // Elimina la selezione corrente. if (txtPassword.SelectionLength > 0) Capitolo 1 Tipi fondamentali .NET { for ( int i = txtPassword.SelectionStart + txtPassword.SelectionLength - 1; i >= txtPassword.SelectionStart; i--) { password.RemoveAt(i); } } // Carattere ordinario: lo inserisce nella stringa sicura. if ( txtPassword.SelectionStart == txtPassword.TextLength ) { password.AppendChar(e.KeyChar); } else { password.InsertAt(txtPassword.SelectionStart, e.KeyChar); } // Visualizza (e memorizza) un asterisco nel controllo textbox. e.KeyChar = ‘*’; } } Come ho già detto prima, l’oggetto SecureString non espone alcun metodo che restituisce il contenuto come testo in chiaro. Invece, si devono utilizzare due metodi del tipo Marshal, del namespace System.Runtime.InteropServices: // Converte la password in una BSTR unmanaged. IntPtr ptr = Marshal.SecureStringToBSTR(password); // Per scopi dimostrativi, converte la BSTR in una stringa ordinaria a la usa. string pw = Marshal.PtrToStringBSTR(ptr); … // Azzera la BSTR unmanaged utilizzata per la password. Marshal.ZeroFreeBSTR(ptr); Naturalmente, il codice precedente non è realmente sicuro, poiché ad un certo punto si è assegnata la password a una stringa ordinaria. In alcuni casi, ciò è inevitabile, ma almeno questo approccio assicura che la stringa col testo in chiaro esista per una quantità di tempo più breve. Un approccio alternativo migliore è far sì che la stringa Basic unmanaged (BSTR) sia processata da un blocco di codice unmanaged. I vantaggi di questa tecnica si vedono realmente quando si utilizza un membro che accetta un’istanza di SecureString, ad esempio la proprietà Password del tipo ProcessStartInfo: // Esegue Notepad con un account utente differente. ProcessStartInfo psi = new ProcessStartInfo(“notepad.exe”); psi.UseShellExecute = false; psi.UserName = “Francesco”; psi.Password = password; Process.Start(psi); Tipi numerici Come è noto, i tipi short, int e long, altro non sono che classi .NET Int16, Int32 e Int64. Riconoscendone la natura di classe e utilizzandone metodi e proprietà, si possono sfruttare meglio questi tipi. Le informazioni riportate nei paragrafi successivi si applicano a tutte le classi numeriche del .NET Framework, 31 32 Programmare Microsoft Visual C# 2005 come Boolean, Byte, SByte, Int16, Int32, Int64, UInt16, UInt32, UInt64, Single, Double e Decimal. Proprietà e metodi Tutti i tipi numerici, e anche tutte le classi .NET, espongono il metodo ToString, che ne converte il valore numerico in stringa. Questo metodo è particolarmente utile quando si accoda il valore numerico a un’ulteriore stringa: double myValue = 123.45; string res = “The final value is “ + myValue.ToString(); Il metodo ToString è “consapevole” della cultura e per default utilizza la cultura associata al thread corrente. Ad esempio, utilizza una virgola come separatore decimale se la cultura del thread corrente è Italian o German. I tipi numerici definiscono degli overload del metodo ToString per accettare una stringa di formato o un oggetto formatter custom. (Per ulteriori dettagli, si consulti il precedente paragrafo “Formattazione di valori numerici” di questo capitolo). // Converte un intero in esadecimale. res = 1234.ToString(“X”); // => 4D2 // Visualizza PI con 6 cifre (in tutto). double d = Math.PI; res = d.ToString(“G6”); // => 3.14159 Si può utilizzare il metodo CompareTo per confrontare un numero con un altro valore numerico dello stesso tipo. Questo metodo restituisce 1, 0 o -1, in base a se l’istanza corrente è maggiore, uguale o minore del valore passato come argomento: float sngValue = (float) 1.23; // Confronta la variabile float sngValue con 1. int res = sngValue.CompareTo(1); if ( res > 0 ) { Console.WriteLine(“sngValue is > 1”); } else if ( res < 0 ) { Console.WriteLine(“sngValue is < 1”); } else { Console.WriteLine(“sngValue is = 1”); } L’argomento deve essere dello stesso tipo del valore di cui si sta applicando il metodo CompareTo, pertanto si deve convertirlo se necessario. Tutte le classi numeriche espongono i campi statici MinValue e MaxValue, che restituiscono il valore più piccolo e più grande che si può esprimere con il corrispondente tipo: // Visualizza il valore massimo memorizzabile in una variabile Double. Console.WriteLine(Double.MaxValue); // => 1.79769313486232E+308 Capitolo 1 Tipi fondamentali .NET Le classi numeriche che supportano valori floating-point, cioè le classi Single e Double, espongono alcune ulteriori proprietà shared a sola lettura. La proprietà Epsilon restituisce il più piccolo numero positivo (non nullo) che può essere memorizzato in una variabile di quel tipo: Console.WriteLine(Single.Epsilon); // => 1.401298E-45 Console.WriteLine(Double.Epsilon); // => 4.94065645841247E-324 I campi NegativeInfinity e PositiveInfinity restituiscono una costante che rappresenta un valore infinito, mentre il campo NaN restituisce una costante che rappresenta il valore Not-a-Number (NaN è il valore che si ottiene, ad esempio, quando si valuta la radice quadrata di un numero negativo). In alcuni casi, si possono utilizzare valori infiniti nelle espressioni: // Un numero diviso infinito è 0. Console.WriteLine(1 / Double.PositiveInfinity); // => 0 Le classi Single e Double espongono anche metodi statici che permettono di testare se contengono valori speciali, come IsInfinity, IsNegativeInfinity, IsPositiveInfinity e IsNaN. Formattazione di numeri Tutte le classi numeriche supportano una forma in overload del metodo ToString che permette di applicare una stringa di formato: int intValue = 12345; string res = intValue.ToString(“##,##0.00”); // => 12,345.00 Il metodo utilizza la nazionalità corrente per interpretare la stringa di formattazione. Ad esempio, nel codice precedente utilizza la virgola come separatore delle migliaia e il punto come separatore decimale se in esecuzione su un sistema americano, ma inverte i due separatori su un sistema italiano. Si può anche passare un oggetto CultureInfo per formattare un numero per una determinata cultura: CultureInfo ci = new CultureInfo(“it-IT”); res = intValue.ToString(“##,##0.00”, ci); // => 12.345,00 L’istruzione precedente funziona poiché ToString accetta un oggetto IFormatProvider per formattare il valore corrente, e l’oggetto CultureInfo espone questa interfaccia. In questo paragrafo, mostrerò come si può sfruttare un ulteriore oggetto .NET che implementa questa interfaccia, l’oggetto NumberFormatInfo. La classe NumberFormatInfo espone molte proprietà che determinano come viene formattato un valore numerico, come NumberDecimalSeparator (il carattere separatore decimale), NumberGroupSeparator (il carattere separatore delle migliaia), NumberDecimalDigits (numero di cifre decimali), CurrencySymbol (il carattere utilizzato per la valuta), e molti altri. Il modo più semplice per creare un oggetto NumberFormatInfo valido è per mezzo del metodo statico CurrentInfo della classe NumberFormatInfo; il valore restituito è un oggetto NumberFormatInfo readonly basato sulla nazionalità corrente: NumberFormatInfo nfi = NumberFormatInfo.CurrentInfo; 33 34 Programmare Microsoft Visual C# 2005 (Si può anche utilizzare la proprietà InvariantInfo, che restituisce un oggetto NumberFormatInfo read-only di default che è indipendente dalla nazionalità). Il problema con il codice precedente è che l’oggetto NumberFormatInfo restituito è read-only, pertanto non si può modificare nessuna delle sue proprietà. Questo oggetto è perciò virtualmente inutile poiché il metodo ToString utilizza comunque in modo implicito la nazionalità corrente quando formatta un valore. La soluzione è creare un clone dell’oggetto NumberFormatInfo di default e poi modificarne le proprietà, come nel seguente frammento: // Formatta un numero con le opzioni di formattazione della nazionalità corrente, // ma utilizza una virgola // per il separatore decimale e uno spazio per il separatore delle migliaia. // (È necessario un cast poiché il metodo Clone restituisce un Object.) NumberFormatInfo nfi = (NumberFormatInfo) NumberFormatInfo.CurrentInfo.Clone(); // L’oggetto nfi è assegnabile, pertanto si possono modificarne le proprietà. nfi.NumberDecimalSeparator = “,”; nfi.NumberGroupSeparator = “ “; // Ora si può formattare un valore con l’oggetto custom NumberFormatInfo. float sngValue = 12345.5F; Console.WriteLine(sngValue.ToString(“##,##0.00”, nfi)); // => 12 345,50 Per l’elenco completo delle proprietà e dei metodi NumberFormatInfo, si veda la documentazione MSDN. Parsing di stringhe in numeri Tutti i tipi numerici supportano il metodo shared Parse, che analizza la stringa passata come argomento e restituisce il corrispondente valore numerico. La forma più semplice del metodo Parse accetta un argomento stringa: // La riga successiva assegna 1234 alla variabile. short shoValue = short.Parse(“1234”); Una forma in overload del metodo Parse accetta un valore enumerato NumberStyle come secondo argomento. NumberStyle è un valore codificato a bit che specifica quali parti del numero sono ammesse nella stringa da analizzare. I valori NumberStyle validi sono AllowLeadingWhite (1), AllowTrailingWhite (2), AllowLeadingSign (4), AllowTrailingSign (8), AllowParentheses (16), AllowDecimalPoint (32), AllowThousand (64), AllowExponent (128), AllowCurrencySymbol (256) e AllowHexSpecifier (512). Si può specificare quali parti delle stringhe sono valide utilizzando su questi valori l’operatore Or a bit, o si può utilizzare qualche valore composto predefinito, come Any (511, permette tutto), Integer (7, permette il segno in coda e spazi in testa e in coda), Number (111, come Integer ma permette il separatore delle migliaia e il punto decimale), Float (167, come Integer ma permette il separatore decimale e l’esponente) e Currency (383, permette tutto eccetto l’esponente). L’esempio seguente estrae un Double da una stringa e riconosce gli spazi e tutti i formati supportati: double dblValue = double.Parse(“ 1,234.56E6 “, NumberStyles.Any); // A dblValue viene assegnato il valore 1234560000 Capitolo 1 Tipi fondamentali .NET Si può essere più specifici su cosa è valido e cosa non lo è: NumberStyles style = NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign; // Funziona e assegna -123.45 a sngValue. float sngValue = float.Parse(“-123.45”, style); // Genera una FormatException a causa del separatore delle migliaia. sngValue = float.Parse(“12,345.67”, style); Una terza forma in overload del metodo Parse accetta qualsiasi oggetto IFormatProvider, pertanto si può passargli un oggetto CultureInfo: // Esegue il parsing di una stringa in base alle regole dell’italiano. sngValue = float.Parse(“12.345,67”, new CultureInfo(“it-IT”)); Tutti i tipi numerici del .NET Framework 2.0 espongono un nuovo metodo denominato TryParse, che permette di evitare le onerose eccezioni se una stringa non contiene un numero in un formato valido. (Questo metodo è disponibile in .NET 1.1 solo per il tipo Double). Il metodo TryParse accetta una variabile byreference come secondo argomento e restituisce true se l’operazione di parsing ha avuto esito positivo: int intValue = 0; if ( int.TryParse(“12345”, out intValue) ) { // intValue contiene il risultato dell’operazione di parsing. } else { // La stringa non contiene un valore integer in un formato valido. } Un secondo overload del metodo TryParse accetta un valore enumerato NumberStyles e un oggetto IFormatProvider come secondo e terzo argomento: NumberStyles style = NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign | NumberStyles.AllowThousands; float aValue = 0; if ( float.TryParse(“-12345.67”, style, new CultureInfo(“it-IT”), out aValue) ) { // aValue contiene un numero valido } Il tipo Convert La classe System.Convert espone diversi metodi shared che aiutano nella conversione da e verso i molti tipi di dati disponibili in .NET. Nella forma più semplice, questi metodi possono convertire qualsiasi tipo base in un ulteriore tipo: // Converte la stringa “123.45” in un Double. double dblValue = Convert.ToDouble(“123.45”); La classe Convert espone molti metodi ToXxxx, uno per ciascun tipo base: ToBoolean, ToByte, ToChar, ToDateTime, ToDecimal, ToDouble, ToInt16, ToInt32, ToInt64, ToSByte, ToSingle, ToString, ToUInt16, ToUInt32 e ToUInt64: 35 36 Programmare Microsoft Visual C# 2005 // Converte un valore Double in un intero. int intValue = Convert.ToInt32(dblValue); I metodi ToXxxx che restituiscono un tipo integer, cioè ToByte, ToSByte, ToInt16, ToInt32, ToInt64, ToUInt16, ToUInt32 e ToUInt64, espongono un overload che accetta una stringa e una base, e converte una stringa che contiene un numero in questa base. La base può essere solo 2, 8, 10 o 16: // Converte da una stringa contenente una int result = Convert.ToInt32(“11011”, 2); // Converte da un numero ottale. result = Convert.ToInt32(“777”, 8); // => // Converte da un numero esadecimale. result = Convert.ToInt32(“AC”, 16); // => rappresentazione binaria di un numero. // => 27 511 172 Si può eseguire la conversione nella direzione opposta, ossia da un intero alla rappresentazione in stringa di un numero in una base differente, per mezzo degli overload del metodo ToString: // Determina la rappresentazione binaria di un numero. string text = Convert.ToString(27, 2); // => 11011 // Determina la rappresentazione esadecimale di un numero. (Nota: il risultato è in minuscolo.) text = Convert.ToString(172, 16); // => ac La classe Convert espone due metodi che rendono agevoli le conversioni da e verso stringhe codificate in Base64. (È questo il formato utilizzato per gli allegati e-mail MIME). Il metodo ToBase64String accetta un array di byte e lo codifica come stringa in Base64. Il metodo FromBase64String effettua la conversione nella direzione opposta: // Un array di 16 byte (due sequenze identiche di 8 byte) byte[] b1 = new byte[]{12, 45, 213, 88, 11, 220, 34, 0, 12, 45, 213, 88, 11, 220, 34, 0}; // Lo converte in una stringa Base64. string s64 = Convert.ToBase64String(b1); Console.WriteLine(s64); // Lo riconverte in un array di byte, e lo visualizza. byte[] b2 = Convert.FromBase64String(s64); foreach ( byte b in b2 ) { Console.Write(“{0} “, b); } Una nuova opzione in .NET 2.0 permette di inserire automaticamente un separatore di riga ogni 76 caratteri del valore restituito da un metodo ToBase64String: s64 = Convert.ToBase64String(b1, Base64FormattingOptions.InsertLineBreaks); Inoltre, Convert espone i metodi ToBase64CharArray e FromBase64CharArray, che converte un array di Byte da e verso un array di Char invece che in String. Infine, la classe espone anche un generico metodo ChangeType che può convertire (o almeno tentare di convertire) un valore in un altro tipo. Per creare l’oggetto System.Type da passare come secondo argomento al metodo si deve utilizzare l’operatore typeof: Capitolo 1 Tipi fondamentali .NET // Converte un valore in Double. Console.WriteLine(Convert.ChangeType(value, typeof(double))); Generatori di numeri aleatori Il tipo System.Random permette di generare una serie di valori random. Si può impostare il seme della generazione dei numeri aleatori nel metodo costruttore di questa classe: // L’argomento deve essere un intero a 32 bit. Random rand = new Random(12345); Quando si passa un determinato valore-seme, si ottiene sempre la stessa sequenza aleatoria. Per ottenere sequenze differenti ogni volta che si esegue l’applicazione, si può far sì che il seme dipenda dall’ora corrente: // Queste conversioni sono necessarie poiché la proprietà Ticks // restituisce un valore a 64 bit che deve essere troncato in un intero a 32 bit. int seed = (int) (DateTime.Now.Ticks & int.MaxValue); rand = new Random(seed); Dopo che si ha un oggetto Random inizializzato, si possono estrarre valori aleatori interi positivi a 32 bit ogni volta che si interroga il metodo Next dell’oggetto: for ( int i = 1; i <= 10; i++ ) { Console.WriteLine(rand.Next()); } Si può anche passare uno o due argomenti per mantenere il valore di ritorno nell’intervallo desiderato: // Ottiene un valore nell’intervallo da 0 a 1,000. int intValue = rand.Next(1000); // Ottiene un valore nell’intervallo da 100 a 1,000. intValue = rand.Next(100, 1000); Il metodo NextDouble restituisce un numero random floating-point compreso tra 0 e 1: double dblValue = rand.NextDouble(); Infine, si può popolare un array di Byte con valori random con il metodo NextBytes: // Ottiene un array di 100 valori byte random. byte[] buffer = new byte[100]; rand.NextBytes(buffer); Nota Benché il tipo Random sia idoneo nella maggior parte di tipi di applicazioni, ad esempio, quando si sviluppano giochi di carte, i valori che genera sono facilmente riproducibili e non sono sufficientemente aleatori per essere utilizzati nella crittografia. Per un più robusto generatore di valori random, si dovrebbe utilizzare la classe RNGCryptoServiceProvider, del namespace System.Security.Cryptography. 37 38 Programmare Microsoft Visual C# 2005 Il tipo DateTime System.DateTime è la principale classe .NET per lavorare con valori data e ora. Non solo offre un posto per memorizzare i valori data, ma espone anche molti metodi utili per lavorare con valori data e ora. Si può inizializzare un valore DateTime in diversi modi: // Crea un valore Date fornendo anno, mese e giorno. DateTime dt = new DateTime(2005, 1, 6); // January 6, 2005 // Fornisce anche ore, minuti e secondi. dt = new DateTime(2005, 1, 6, 18, 30, 20); // January 6, 2005 6:30:20 PM // Aggiunge un valore in millisecondi (mezzo secondo in questo esempio). dt = new DateTime(2005, 1, 6, 18, 30, 20, 500); // Crea un valore tempo dai tick (10 milioni di tick = 1 secondo). long ticks = 20000000; // 2 secondi // Viene considerato il tempo trascorso dal giorno 1 Gen dell’anno 1. dt = new DateTime(ticks); // 1/1/0001 12:00:02 AM Si possono utilizzare le proprietà static Now e Today: // dt // dt La proprietà Now restituisce data e ora di sistema. = DateTime.Now; // Ad esempio, October 17, 2005 3:54:20 PM La proprietà Today restituisce solo la data di sistema only. = DateTime.Today; // Ad esempio, October 17, 2005 12:00:00 AM La proprietà shared UtcNow restituisce l’ora corrente espressa in coordinate UTC (Universal Time Coordinates) e permette di confrontare i valori temporali prodotti in differenti fusi orari; questa proprietà ignora l’impostazione Daylight Saving Time (l’ora legale) se già attiva per il fuso orario corrente: dt = DateTime.UtcNow; Dopo aver ottenuto un valore Date inizializzato, si possono recuperare singole porzioni utilizzando una delle relative proprietà a sola lettura, cioè Date (la parte della data), TimeOfDay (la parte dell’ora), Year (anno), Month (mese), Day (giorno), DayOfYear (giorno dell’anno), DayOfWeek (giorno della settimana), Hour (ora), Minute (minuti), Second (secondi), Millisecond (millisecondi) e Ticks (tick): // Oggi è il primo giorno del mese corrente? if ( DateTime.Today.Day == 1 ) { Console.WriteLine(“First day of month”); } // Quanti giorni sono passati dal 1 Gennaio? Console.WriteLine(DateTime.Today.DayOfYear); // Ottiene l’ora corrente — si noti che sono compresi i tick. Console.WriteLine(DateTime.Now.TimeOfDay); // => 10:39:28.3063680 La proprietà TimeOfDay è peculiare poiché restituisce un oggetto TimeSpan, che rappresenta una differenza tra date. Capitolo 1 Tipi fondamentali .NET Benché questa classe sia distinta dalla classe DateTime, ne condivide molte proprietà e metodi e opera quasi sempre in combinazione a valori DateTime, come si vedrà a breve. Una nota per il programmatore curioso: un valore DateTime viene memorizzato come numero di tick (1 tick = 100 nanosecondi) trascorsi dal 1 Gennaio, 0001; questo formato di memorizzazione può funzionare per qualsiasi data tra il 1/1/0001 e il 12/ 12/9999. In .NET 2.0 questo valore tick occupa 62 bit, e i rimanenti due bit vengono utilizzati per preservare l’informazione se al valore date/time è applicato il Daylight Saving Time (l’ora legale) e se il date/time è relativo al fuso orario corrente (il default) o è in Universal Time (UTC). Somma e sottrazione di date La classe DateTime espone diversi metodi di istanza che permettono di sommare e sottrarre un numero di anni, mesi, giorni, ore, minuti o secondi da e a un valore DateTime. I nomi di questi metodi non lasciano dubbio sulla loro funzionalità: AddYears, AddMonths, AddDays, AddHours, AddMinutes, AddSeconds, AddMilliseconds, AddTicks. Si può aggiungere un valore intero quando si utilizza AddYears e AddMonths e un valore decimale in tutti gli altri casi. In tutti i casi, si può passare un argomento negativo per sottrarre piuttosto che per aggiungere un valore: // dt // dt // dt La data di domani = DateTime.Today.AddDays(1); La data di ieri = DateTime.Today.AddDays(-1); Che ora sarà tra 2 ore e 30 minuti? = DateTime.Now.AddHours(2.5); // Un modo CPU-intensivo per sospendere per 5 secondi. DateTime endTime = DateTime.Now.AddSeconds(5); do {} while ( DateTime.Now < endTime ); Il metodo Add accetta un oggetto TimeSpan come argomento. Prima di poterlo utilizzare, si deve saper creare un oggetto TimeSpan, scegliendo uno dei metodi costruttori in overload: // Un valore a 64 bit viene interpretato come valore Ticks. TimeSpan ts = new TimeSpan(13500000); // 1.35 secondi // Tre valori interi vengono interpretati come ore, minuti, secondi. ts = new TimeSpan(0, 32, 20); // 32 minuti, 20 secondi // Quattro valori interi vengono interpretati come giorni, ore, minuti, secondi. ts = new TimeSpan(1, 12, 0, 0); // 1 giorno e mezzo // (Si noti che gli argomenti non vengono verificati per errori out-of-range; perciò, // l’istruzione successiva produce lo stesso risultato di quella precedente.) ts = new TimeSpan(0, 36, 0, 0); // 1 giorno e mezzo // Un quinto argomento viene interpretato come millisecondi. ts = new TimeSpan(0, 0, 1, 30, 500); // 90 secondi e mezzo Ora si è pronti per aggiungere una data arbitraria o un intervallo di tempo a un valore DateTime: // Che ore saranno tra 2 giorni, 10 ore e 30 minuti? dt = DateTime.Now.Add(new TimeSpan(2, 10, 30, 0)); 39 40 Programmare Microsoft Visual C# 2005 La classe DateTime espone anche un metodo di istanza Subtract che opera in modo simile: // Che ore erano 1 giorno, 12 ore e 20 minuti fa? dt = DateTime.Now.Subtract(new TimeSpan(1, 12, 20, 0)); Il metodo Subtract ha un overload che accetta un ulteriore oggetto DateTime come argomento, nel qual caso restituisce l’oggetto TimeSpan che rappresenta la differenza tra le due date: // Quanti giorni, ore, minuti e secondi sono trascorsi // dall’inizio del terzo millennio? DateTime startDate = new DateTime(2001, 1, 1); TimeSpan diff = DateTime.Now.Subtract(startDate); Ottenuto un oggetto TimeSpan, si possono estrarre le informazioni sepolte in esso utilizzando una delle molte proprietà, i cui nomi sono autoesplicativi: Days, Hours, Minutes, Seconds, Milliseconds, Ticks, TotalDays, TotalHours, TotalMinutes, TotalSeconds e TotalMilliseconds. Anche la classe TimeSpan espone metodi come Add, Subtract, Negate e CompareTo. Il metodo CompareTo permette di determinare se un valore DateTime è maggiore o minore di un altro valore DateTime: // La data di oggi è successiva al 30 Ottobre 2005? int res = DateTime.Today.CompareTo(new DateTime(2005, 10, 30)); if ( res > 0 ) { // Successiva al 30 Ott 2005 } else if ( res < 0 ) { // Precedente al 30 Ott 2005 } else { // Oggi è il 30 Ott 2005. } Per default, i valori DateTime sono relativi al fuso orario corrente e non vanno mai confrontati valori provenienti da fusi orari differenti, a meno che non siano in formato UTC (si veda il successivo paragrafo “Lavorare con i fusi orari”, di questo capitolo). Inoltre, quando si valuta la differenza tra due date nello stesso fuso orario, si può ottenere un risultato errato se si è verificata una transizione da o verso l’ora legale tra le due date. Motivo in più per utilizzare le date in formato UTC. Il metodo IsDaylightSavingTime (nuovo di .NET 2.0) permette di rilevare se l’ora legale è attiva per il fuso orario corrente: if ( DateTime.Now.IsDaylightSavingTime() ) { Console.Write(“Daylight Saving Time is active”); } Capitolo 1 Tipi fondamentali .NET Infine, la classe DateTime espone due metodi statici che possono essere pratici in molte applicazioni: // Testa un anno bisestile. Console.WriteLine(DateTime.IsLeapYear(2000)); // => true // Recupera il numero dei giorni di un determinato mese. Console.WriteLine(DateTime.DaysInMonth(2000, 2)); // => 29 Nonostante l’abbondanza di metodi data e ora, il tipo DateTime non offre un modo semplice per calcolare il numero intero di anni o di mesi trascorsi tra due date. Ad esempio, non si può calcolare l’età di una persona utilizzando questo statement: int age = DateTime.Now.Year - aPerson.BirthDate.Year; poiché il risultato sarebbe di una unità maggiore del valore corretto se la persona non ha ancora compiuto gli anni nell’anno corrente. Ho preparato due routine riusabili che forniscono la funzionalità mancante: // Restituisce il numero intero di anni tra due date. public static int YearDiff(DateTime startDate, DateTime endDate) { int result = endDate.Year - startDate.Year; if ( endDate.Month < startDate.Month || (endDate.Month == startDate.Month && endDate.Day < startDate.Day)) { result--; } return result; } // Restituisce il numero intero di mesi tra due date. public static int MonthDiff(DateTime startDate, DateTime endDate) { int result = endDate.Year * 12 + endDate.Month – (startDate.Year * 12 + startDate.Month); if ( endDate.Month == startDate.Month && endDate.Day < startDate.Day ) { result--; } return result; } Formattazione di date Il tipo DateTime ridefinisce il metodo ToString per accettare un formato standard di data tra quelli specificati nella Tabella 1-2, o un formato utente creato assemblando i caratteri elencati nella Tabella 1-3: // Il 6 Gennaio 2005, 6:30:20.500 PMU.S. Eastern Time. DateTime dt = new DateTime(2005, 1, 6, 18, 30, 20, 500); // Visualizza una data utilizzando il formato standard LongDatePattern. string dateText = dt.ToString(“D”); // => Thursday, January 06, 2005 // Visualizza una data utilizzando un formato custom. dateText = dt.ToString(“d-MMM-yyyy”); // => 6-Jan-2005 41 42 Programmare Microsoft Visual C# 2005 Si può formattare un valore DateTime in altri modi utilizzando alcuni metodi peculiari che solo questo tipo espone: Console.WriteLine(dt.ToShortDateString()); // => Console.WriteLine(dt.ToLongDateString()); // => Console.WriteLine(dt.ToShortTimeString()); // => Console.WriteLine(dt.ToLongTimeString()); // => Console.WriteLine(dt.ToFileTime()); // => Console.WriteLine(dt.ToOADate()); // => // I prossimi due risultati variano in base al fuso orario Console.WriteLine(dt.ToUniversalTime()); // => Console.WriteLine(dt.ToLocalTime()); // => 1/6/2005 Thursday, January 06, 2005 6:30 PM 6:30:20 PM 127495062205000000 38358.7710706019 in cui si è. 1/7/2005 12:30:20 PM 1/6/2005 12:30:20 PM Alcuni di questi formati possono richiedere un’ulteriore spiegazione: • Il metodo ToFileTime restituisce un valore senza segno di 8 byte che rappresenta la data e l’ora del numero di intervalli di 100 nanosecondi trascorsi dal 1/1/1601 12:00 AM. Il tipo DateTime supporta anche il metodo ToFileTimeUtc, che ignora il fuso orario locale. • Il metodo ToOADate converte un valore compatibile con l’OLE Automation. (È un valore Double simile ai valori data utilizzati in Microsoft Visual Basic 6.) • Il metodo ToUniversalTime considera il valore DateTime come ora locale e lo converte in Universal Time Coordinates (UTC). • Il metodo ToLocalTime considera il valore DateTime come valore UTC e lo converte in ora locale. La classe DateTime espone due metodi shared, FromOADate e FromFileTime, per scandire un valore di data OLE Automation o una data formattata come FileTime. Parsing di date L’operazione complementare alla formattazione della data è il parsing. La classe DateTime fornisce un metodo static Parse per eseguire un parsing di ogni grado di complessità: DateTime dt = DateTime.Parse(“2005/1/6 12:30:20”); La flessibilità di questo metodo diviene evidente quando gli si passa un oggetto IFormatProvider come secondo argomento: ad esempio, un oggetto CultureInfo o un oggetto DateTimeFormatInfo. L’oggetto DateTimeFormatInfo è concettualmente simile all’oggetto NumberFormatInfo descritto prima (si veda il paragrafo “Formattazione di numeri”), eccetto che contiene informazioni sui separatori e sui formati ammessi nei valori data a ora: Capitolo 1 Tipi fondamentali .NET // Ottiene una copia assegnabile dell’oggetto DateTimeFormatInfo della nazionalità corrente. DateTimeFormatInfo dtfi = (DateTimeFormatInfo) DateTimeFormatInfo.CurrentInfo.Clone(); // Modifica i separatori di data e ora. dtfi.DateSeparator = “-”; dtfi.TimeSeparator = “.”; // Ora siamo pronti a scandire una data formattata in modo non standard. dt = DateTime.Parse(“2005-1-6 12.30.20”, dtfi); Molti sviluppatori non americani apprezzeranno la capacità di eseguire il parsing di date in formati diversi da mese/giorno/anno. In questo caso, bisogna assegnare un pattern correttamente formattato alle proprietà ShortDatePattern, LongDatePattern, ShortTimePattern, LongTimePattern o FullDateTimePattern dell’oggetto DateTimeFormatInfo prima di effettuare il parsing: // Si prepara a scandire date (dd/mm/yy), in formato breve o lungo. dtfi.ShortDatePattern = “d/M/yyyy”; dtfi.LongDatePattern = “dddd, dd MMMM, yyyy”; // Entrambi questi statement assegnano la data 6 Gennaio 2005 dt = DateTime.Parse(“6-1-2005 12.30.44”, dtfi); dt = DateTime.Parse(“Thursday, 6 January, 2005”, dtfi); Si può utilizzare l’oggetto DateTimeFormatInfo per recuperare nomi standard o abbreviati dei giorni della settimana e dei mesi, in base alla nazionalità corrente o a qualsiasi nazionalità: // Visualizza i nomi abbreviati dei mesi. foreach ( string s in DateTimeFormatInfo.CurrentInfo.AbbreviatedMonthNames ) { Console.WriteLine(s); } Aspetto ancor più interessante, è che si possono impostare nomi dei giorni della settimana e dei mesi con stringhe arbitrarie se si ha un oggetto DateTimeFormatInfo assegnabile, e quindi si può utilizzare l’oggetto per eseguire il parsing di una data scritta in qualsiasi linguaggio, compresi quelli inventati. (Sì, compreso il Klingon!) Un modo ulteriore per eseguire il parsing delle stringhe in formati diversi da mese/ giorno/anno è utilizzare il metodo shared ParseExact. In questo caso, si passa la stringa di formato come secondo argomento, e si può passare null come terzo argomento se non è necessario un oggetto DateTimeFormatInfo per qualificare ulteriormente la stringa da sottoporre al parsing: // Questo statement assegna la data 6 Gennaio 2005 dt = DateTime.ParseExact(“6-1-2005”, “d-M-yyyy”, null); Il secondo argomento può essere uno dei formati DateTime supportati elencati nella Tabella 1-2. Nel .NET Framework 2.0, è stato aggiunto il nuovo formato “F” per supportare il metodo ParseExact quando vi è un numero variabile di cifre decimali. 43 44 Programmare Microsoft Visual C# 2005 Entrambi i metodi Parse e ParseExact generano un’eccezione se la stringa di input non è conforme al formato previsto. Come è noto, le eccezioni possono aggiungere un bel po’ di overhead alle proprie applicazioni e bisogna cercare di evitarle se possibile. La versione 2.0 del .NET Framework estende la classe DateTime con i metodi TryParse e TryParseExact, che restituiscono true se il parsing ha esito positivo e memorizza il risultato del parsing in una variabile DateTime passata come secondo argomento: DateTime aDate; if ( DateTime.TryParse(“January 6, 2005”, out aDate) ) { // aDate contiene la data scandita. } Un ulteriore overload del metodo TryParse accetta un oggetto IFormatProvider (ad esempio, un’istanza CultureInfo) e un valore DateTimeStyles codificato a bit; il secondo argomento permette di specificare se sono accettati spazi in testa o in coda e se viene assunta l’ora locale o universale (questa seconda caratteristica è nuova di .NET 2.0): CultureInfo ci = new CultureInfo(“en-US”); if ( DateTime.TryParse(“ 6/1/2005 14:26 “, ci, DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal, out aDate) ) { // aDate contiene la data scandita. } Se si specifica il valore enumerato DateTimesStyles.AssumeUniversal, l’ora viene assunta essere nel formato universal time (UTC) e viene automaticamente convertita nel fuso orario locale. Per default, i valori data sono assunti come relativi al fuso orario corrente. Lavorare con i fusi orari I valori DateTime nella versione 1.1 del .NET Framework pativano di una seria limitazione: era sempre previsto che memorizzassero un’ora locale, piuttosto che un’ora normalizzata in UTC. Questa assunzione causa alcuni problemi di difficile soluzione, il più serio dei quali è un problema che si manifesta quando un valore data viene serializzato in un fuso orario e deserializzato in un fuso differente, utilizzando l’oggetto SoapFormatter o l’oggetto XmlSerializer. Questi due oggetti, infatti, memorizzano le informazioni sul fuso orario assieme al valore effettivo della data: quando l’oggetto viene deserializzato in un fuso orario differente, la parte oraria della data viene automaticamente adattata per riflettere la nuova posizione geografica. Il più delle volte, questo comportamento è corretto, ma a volte provoca un malfunzionamento dell’applicazione. Supponiamo che una persona sia nata in Italia il 1 Gennaio 1970 alle 2 AM; se questo valore di data viene serializzato in XML e inviato a un computer a New York (ad esempio, per mezzo di un Web service o salvando le informazioni in un file che viene poi trasferito via FTP o HTTP) sembrerebbe che la persona sia nata il 31 Dicembre 1969 alle 8 PM. Come si vede, il problema con le date in .NET 1.1 ha origine dal fatto di non poter specificare se un valore memorizzato in Capitolo 1 Tipi fondamentali .NET una variabile DateTime deve essere considerato relativo al fuso orario corrente o è un valore UTC assoluto. Questo problema è stato risolto abbastanza efficacemente in .NET 2.0 aggiungendo una nuova proprietà Kind al tipo DateTime. Questa proprietà è un valore enumerato DateTimeKind che può essere Local, Utc o Unspecified. Per compatibilità all’indietro con le applicazioni .NET 1.1, per default un valore DateTime ha una proprietà Kind impostata a DateTimeKind.Local, a meno che non si specifichi un valore differente nel costruttore: // 14 Febbraio 2005 alle 12:00 AM, valore UTC DateTime aDate = new DateTime(2005, 2, 14, 12, 0, 0, DateTimeKind.Utc); // Testa la proprietà Kind. Console.WriteLine(aDate.Kind.ToString()); // Utc La proprietà Kind è read-only, ma si può utilizzare il metodo static SpecifyKind per creare un differente valore DateTime se si vuole passare da ora locale a UTC o viceversa: // Il prossimo statement modifica la proprietà Kind (ma non modifica il valore date/time!). DateTime newDate = DateTime.SpecifyKind(aDate, DateTimeKind.Utc); Una nota importante: la proprietà Kind è presa in considerazione solo quando di serializza e si deserializza un valore data, e viene ignorato quando si effettuano i confronti. In .NET 1.1, i valori DateTime vengono serializzati come numeri a 64 bit per mezzo della proprietà Ticks. In .NET 2.0, tuttavia, quando si salva un valore DateTime in un file o in un campo di database si deve salvare anche la nuova proprietà Kind, altrimenti il meccanismo di deserializzazione patirebbe degli stessi problemi che si osservano in .NET 1.1. Il modo più semplice per farlo è per mezzo del nuovo metodo di istanza ToBinary (che converte l’oggetto DateTime in un valore a 64 bit) e il nuovo metodo statico FromBinary (che converte un valore a 64 bit in un valore DateTime): // Converte in un valore Int64. long lngValue = aDate.ToBinary(); … // Riconverte da un Int64 a un valore DateTime. newDate = DateTime.FromBinary(lngValue); Si può anche serializzare un valore DateTime come testo. In questo caso si deve utilizzare il metodo ToString con il formato “o” (nuovo del .NET Framework 2.0). Questo formato serializza tutte le informazioni inerenti a una data, compresa la proprietà Kind e il fuso orario (se la data non è in formato UTC), e si può rileggerlo per mezzo di un metodo ParseExact se si specifica il nuovo valore enumerato DateTimeStyles.RoundtripKind: // Serializza una data in formato UTC. string text = aDate.ToString(“o”, CultureInfo.InvariantCulture); // Lo deserializza in un nuovo valore DateTime. newDate = DateTime.ParseExact(text, “o”, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); 45 46 Programmare Microsoft Visual C# 2005 Il tipo TimeZone Il .NET Framework supporta le informazioni sul fuso orario attraverso l’oggetto System.TimeZone, che si può utilizzare per recuperare le informazioni sul fuso orario impostate nelle impostazioni “Data e Ora” di Windows: // Ottiene l’oggetto TimeZone del fuso orario corrente. TimeZone tz = TimeZone.CurrentTimeZone; // Visualizza il nome del fuso orario, senza e con l’ora legale. // (Risultati ottenuti eseguendo il codice in Italia.) Console.WriteLine(tz.StandardName); // => W. Europe Standard Time Console.WriteLine(tz.DaylightName); // => W. Europe Daylight Time Il blocco più interessante di informazioni è l’offset dallo Universal Time (UTC), che si recupera per mezzo del metodo GetUTCOffset. A questo metodo si deve passare un argomento data poiché l’offset dipende da se l’ora legale è in vigore. Il valore restituito è in tick: // Visualizza l’offset orario del fuso W. Europe a Marzo 2005, // con ora legale non attiva. Console.WriteLine(tz.GetUtcOffset(new DateTime(2005, 3, 1))); // => 01:00:00 // Visualizza l’offset orario del fuso W. Europe in Luglio, // con l’ora legale attiva. Console.WriteLine(tz.GetUtcOffset(new DateTime(2005, 7, 1))); // => 02:00:00 Il metodo IsDaylightSavingTime restituisce true se l’ora legale è in vigore: // Ora legale non in vigore a Marzo Console.WriteLine(tz.IsDaylightSavingTime(new DateTime(2005, 3, 1))); // => False Infine, si può determinare quando l’ora legale inizia e termina in determinato anno recuperando un array di oggetti DaylightTime con il metodo GetDaylightChanges dell’oggetto TimeZone: // Recupera l’oggetto DaylightTime per l’anno 2005. DaylightTime dlc = tz.GetDaylightChanges(2005); // Si noti che si potrebbero ottenre date iniziali e finali differenti se si // esegue questo codice in un paese diverso dagli USA. Console.WriteLine(“Starts at {0}, Ends at {1}, Delta is {2} minutes”, dlc.Start, dlc.End, dlc.Delta.TotalMinutes); // => Starts at 3/27/2005 2:00:00 A.M., ends at 10/30/2005 3:00:00 A.M. // Delta is 60 minutes. Il tipo Guid Il tipo System.Guid espone diversi metodi statici e di istanza che possono essere d’aiuto quando si lavora con i GUID, ossia quei numeri a 128 bit che servono per identificare univocamente gli elementi e che sono onnipresenti nella programmazione Windows. Il metodo static NewGuid è utile per la generazione di un nuovo identificatore univoco: // Crea un nuovo GUID. Guid guid1 = Guid.NewGuid(); Capitolo 1 Tipi fondamentali .NET // Per definizione, qui otterrete certamente un output diverso. Console.WriteLine(guid1.ToString()); // => 3f5f1d42-2d92-474d-a2a4-1e707c7e2a37 Se si ha già un GUID, ad esempio un GUID letto da un campo di database, si può inizializzare una variabile Guid passando la rappresentazione GUID come stringa o come array di byte al costruttore del tipo: // Inizializza da una stringa. Guid guid2 = new Guid(“45FA3B49-3D66-AB33-BB21-1E3B447A6621”); Esistono solo due ulteriori cose che si possono fare con un oggetto Guid: si può convertirlo in un array di Byte con il metodo ToByteArray e si può confrontare l’uguaglianza di due valori Guid utilizzando il metodo Equals (derivato da System.Object): // Converte in un array di byte. byte[] bytes = guid1.ToByteArray(); foreach (byte b in bytes) { Console.Write(“{0} “, b); // => 239 1 161 57 143 200 172 70 185 64 222 29 59 15 190 205 } // Confronta due GUID. if ( !guid1.Equals(guid2) ) { Console.WriteLine(“GUIDs are different.”); } Valori Enum Qualsiasi Enum che si definisce nella propria applicazione deriva da System.Enum, che a sua volta eredita da System.ValueType. In definitiva, perciò, gli Enum definiti dall’utente sono tipi value, ma sono speciali poiché non si possono definire ulteriori proprietà, metodi o eventi. Tutti i metodi che espongono vengono ereditati da System.Enum. (Si noti che in C# è illegale derivare esplicitamente una classe da System.Enum). Tutti gli esempi in questo paragrafo si riferiscono al seguente blocco Enum: // Questo Enum definisce il tipo di dato accettato per un valore inserito dall’utente. public enum DataEntry { IntegerNumber, FloatingNumber, CharString, DateTime, } Per default, al primo tipo enumerato viene assegnato il valore 0. Si può modificare questo valore iniziale se si vuole, ma si è incoraggiati a non farlo. Infatti, conviene che 0 sia un valore valido per i blocchi Enum che si definiscono; altrimenti, una variabile Enum non inizializzata conterrebbe un valore non valido. 47 48 Programmare Microsoft Visual C# 2005 La documentazione .NET definisce alcuni principi per i valori Enum: • Utilizzare nomi senza il suffisso Enum; utilizzare nomi al singolare per tipi Enum ordinari e al plurale per tipi Enum codificati a bit. • Utilizzare la grafia Pascal per il nome dell’Enum e dei suoi membri. (Fanno eccezione le costanti dell’API di Windows, che di solito sono in maiuscolo). • Utilizzare interi a 32 bit a meno che non sia necessario un intervallo più esteso, il che accade normalmente solo se si ha un Enum codificato a bit con più di 32 valori possibili. • Non utilizzare gli Enums per gli insiemi aperti, ossia insiemi che si potrebbe dover espandere in futuro (ad esempio, le versioni del sistema operativo). Visualizzazione e parsing di valori Enum La classe Enum ridefinisce il metodo ToString per restituire il valore in un formato stringa leggibile. Questo metodo è utile quando si vuole esporre una stringa (non localizzata) all’utente finale: DataEntry de = DataEntry.DateTime; // Visualizza il valore numerico. Console.WriteLine(Convert.ToDecimal(de)); // => 3 // Visualizza il valore simbolico. Console.WriteLine(de.ToString()); // => DateTime O si può utilizzare la possibilità di passare un carattere di formato a una versione in overload del metodo ToString. I soli caratteri di formato supportati sono G,g (generale), X,x (esadecimale), F,f (a virgola fissa) e D,d (decimale): // I formati General e fixed visualizzano il nome dell’Enum. Console.WriteLine(de.ToString(“F”)); // => DateTime // Il formato Decimal visualizza il valore dell’Enum. Console.WriteLine(de.ToString(“D”)); // => 3 // Il formato esadecimale visualizza otto cifre hex. Console.WriteLine(de.ToString(“X”)); // => 00000003 L’opposto di ToString è il metodo static Parse, che accetta una stringa e la converte nel corrispondente valore enumerato. Essendo ereditato dalla classe generica Enum, il metodo Parse restituisce un oggetto generico, pertanto si deve utilizzare una conversione esplicita per assegnare l’oggetto a una specifica variabile enumerativa: de = (DataEntry) Enum.Parse(typeof(DataEntry), “CharString”); Il metodo Parse genera una ArgumentException se il nome non corrisponde a un valore enumerato definito. I nomi vengono confrontati in modo case-sensitive, ma si può passare un argomento opzionale True se non si vuole tener conto della grafia della stringa: // *** Questo statement genera un’eccezione. Console.WriteLine(Enum.Parse(de.GetType(), “charstring”)); // Questo funziona perché viene utilizzato il confronto case-insensitive. Console.WriteLine(Enum.Parse(de.GetType(), “charstring”, true)); Capitolo 1 Tipi fondamentali .NET Altri metodi Enum Il metodo statico GetUnderlyingType restituisce il tipo base di una classe enumerata: Console.WriteLine([Enum].GetUnderlyingType(de.GetType)) ‘ => System.Int32 Il metodo IsDefined permette di controllare se un valore numerico è accettabile come valore enumerato di una determinata classe: if ( Enum.IsDefined(typeof(DataEntry), 3) ) { // 3 è un valore valido per la classe DataEntry. de = (DataEntry) 3; } Il metodo IsDefined è utile poiché l’operatore di casting non controlla se il valore da convertire è nell’intervallo valido del tipo enumerato di destinazione. In altri termini, il seguente statement non genera alcuna eccezione: // Questo codice produce un risultato non valido, ma non genera un’eccezione. de = (DataEntry) 123; Un modo ulteriore per controllare se un valore numerico è accettabile per un oggetto Enum è il metodo GetName, che restituisce il nome del valore enumerato o restituisce null se il valore è non valido: if ( Enum.GetName(typeof(DataEntry), 3) != null ) { de = (DataEntry) 3; } Si possono elencare rapidamente tutti i valori di un tipo enumerato con i metodi GetNames e GetValues. Il primo restituisce un’array di String che contiene i singoli nomi (ordinati per valore corrispondente); il secondo restituisce un array di oggetti che contiene i valori numerici: // Elenca tutti i valori in DataEntry. string[] names = Enum.GetNames(typeof(DataEntry)); Array values = Enum.GetValues(typeof(DataEntry)); for ( int i = 0; i <= names.Length - 1; i++ ) { Console.WriteLine(“{0} = {1}”, names[i], (int) values.GetValue(i)); } Ecco l’output del precedente frammento di codice: IntegerNumber = 0 FloatingNumber = 1 CharString = 2 DateTime = 3 49 50 Programmare Microsoft Visual C# 2005 Valori codificati a bit Il .NET Framework supporta uno speciale attributo Flags che si può utilizzare per specificare che un oggetto Enum rappresenta un valore codificato a bit. Ad esempio, creiamo una nuova classe denominata ValidDataEntry, che permette allo sviluppatore di specificare due o più tipi di dati validi per i valori immessi dall’utente finale: [Flags] public enum ValidDataEntry { None = 0, // Definire sempre un valore Enum pari a 0. IntegerNumber = 1, FloatingNumber = 2, CharString = 4, DateTime = 8 } La classe FlagAttribute non espone alcuna proprietà, e i relativi costruttori non accettano alcun argomento: la presenza di questo attributo è sufficiente per etichettare questo tipo Enum come codificato a bit. I tipi enumerati codificati a bit si comportano esattamente come ordinari valori Enum eccetto che il relativo metodo ToString riconosce l’attributo Flags. Se un tipo enumerato è composto da due o più valori flag, questo metodo restituisce l’elenco di tutti i valori corrispondenti, separati da virgole: ValidDataEntry vde = ValidDataEntry.IntegerNumber | ValidDataEntry.DateTime; Console.WriteLine(vde.ToString()); // => IntegerNumber, DateTime Se nessun bit è impostato, il metodo ToString restituisce il nome del valore enumerato corrispondente al valore zero: ValidDataEntry vde2 = 0; Console.WriteLine(vde2.ToString()); // => None Se il valore non corrisponde a una combinazione valida di bit, il metodo Format restituisce il numero immutato: vde = (ValidDataEntry) 123; Console.WriteLine(vde.ToString()); // => 123 Anche il metodo Parse viene influenzato dall’attributo Flags: vde = (ValidDataEntry) Enum.Parse(vde.GetType(), “IntegerNumber, FloatingNumber”); Console.WriteLine(Convert.ToInt32(vde)); // => 3