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