Sei sulla pagina 1di 50

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 lapertura dei file, il parsing XML e laggiornamento 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 allinizio 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.

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 loggetto corrente ha lo stesso valore
delloggetto 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 delloggetto.
Questo metodo viene utilizzato quando loggetto una chiave di collection e
tavole hash. In teoria, il codice hash dovrebbe essere univoco per una determinata
istanza doggetto 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 delloggetto. 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
delloggetto. Ad esempio, i tipi di base come int, double e string ridefiniscono questo
metodo per restituire il valore numerico, o stringa, delloggetto. 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,
allomonimo 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 alloperatore == 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) delloggetto corrente.

Programmare Microsoft Visual C# 2005

Finalize
Un metodo overridable che il .NET Framework invoca quando
loggetto sottoposto a garbage collection. (Per ulteriori informazioni su questo
metodo, si veda il paragrafo Metodi Finalize e Dispose nel Capitolo 2, Durata
delloggetto).
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 luguaglianza 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 delloggetto.
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 loggetto Square.
double Side;
Position Position;
// Il puntatore alla stringa Name viene allocato nello slot delloggetto 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 unaltra 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 lassegnazione di un array a una variabile
Array copia solo il riferimento alloggetto, non il contenuto dellarray. 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).

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,
unassegnazione tra tipi value richiede la copia di ogni campo delloggetto, mentre
lassegnazione di un valore reference a una variabile richiede solo la copia dellindirizzo
delloggetto (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 lindirizzo 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 laltro.
Se questo valore boxed viene assegnato in seguito a una variabile del tipo originale
(ossia di tipo value), loggetto 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, limplementazione 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 lobject 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, largomento 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 unoperazione 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 uninterfaccia 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 lindexer del tipo String e restituisce il carattere
presente nella posizione determinata dallindice specificato (che parte da zero):
string s = ABCDEFGHIJ;
Console.WriteLine(s.Length); // => 10
// Index parte da zero.
Console.WriteLine(s[3]); // => D

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 loperazione 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 dallinizio di una stringa.
Con C# basta caricare i caratteri da eliminare in un array di Chars e passare larray 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 allinterno 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;
lutilizzo 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 unaltra 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 lindice 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:

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 (anchesso 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);

Loggetto StringComparer ha lulteriore 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 largomento 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 listanza non
null prima di invocare il metodo:
// Questo statement genera uneccezione 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 luguaglianza,
poich ha senso raramente decidere se una stringa maggiore o minore di unaltra
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 allinterno di unaltra


per mezzo del metodo Contains, ancheesso 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 allinterno di una stringa


per mezzo dei metodi IndexOf e LastIndexOf, che restituiscono, rispettivamente,
lindice della prima e dellultima 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


lultima occorrenza di un carattere tra quelli dellarray 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 l11 carattere.
string result = s.Substring(10); // => ABCDEF
// Estrae 4 caratteri dopo l11 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 dallindice, mentre il metodo Remove rimuove un determinato
numero di caratteri, partendo dallindice 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 lindice 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 allinizio, 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

Loperazione 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
dallordine (in altri termini, se una stringa lanagramma dellaltra).
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
nellarray 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 dallarray 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 loperazione opposta, cio concatenare


tutti gli elementi di un array di stringhe. Questo metodo accetta opzionalmente lindice
iniziale e il numero massimo di elementi da considerare.
// (Continua lesempio precedente)
// Riassembla la stringa aggiungendo coppie CR-LF, ma salta il primo elemento dellarray.
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 lordine 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 nellarray 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 dellapplicazione (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 loperatore 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 lindirizzo
del valore stringa nel pool.
Come si visto prima, il compilatore in grado di utilizzare il pool interno per
ottimizzare linizializzazione 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 allapplicazione 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
allelemento 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 lesecuzione 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 lesempio 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 loggetto
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 loggetto


TextInfo, esposto dallomonima propriet:
TextInfo ti = ci.TextInfo;
Console.WriteLine(ti.ANSICodePage); // => 1252
Console.WriteLine(ti.EBCDICCodePage); // => 20280
Console.WriteLine(ti.OEMCodePage); // => 850
Console.WriteLine(ti.ListSeparator); // => ;

Loggetto 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 labbreviazione 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

Loggetto 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(s1s first four chars are greater than s2s);
}

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 lordine 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 lopportuno 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 loggetto 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 larray 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 larray
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 largomento numerico, si pu aggiungere un carattere due punti dopo largomento


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 lintestazione 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.

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 dellesponente pari al numero di zeri dopo il segno pi.

E-000

Analogo al precedente simbolo esponente, ma il segno dellesponente 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

Data breve

MM/dd/yyyy

1/6/2005

Data lunga

dddd, MMMM dd, yyyy

Thursday, January 06, 2005

Data e ora completa


(data lunga e ora breve)

dddd, MMMM dd, yyyy HH:


mm

Thursday, January 06, 2005


3:54 PM

Data e ora completa (data dddd, MMMM dd, yyyy HH:


lunga e ora lunga)
mm:ss

Thursday, January 06, 2005


3:54:20 PM

Generale (data breve e


ora breve)

MM/dd/yyyy HH:mm

1/6/2005 3:54 PM

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

Ora breve

HH:mm

3:54 PM

Ora lunga

HH:mm:ss

3:54:20 PM

Data e ora ordinabile (conforme allISO 8601) utilizzando la cultura corrente

yyyy-MM-dd HH:mm:ss

2005-01-06T15:54:20

Data e ora universale


(conforme allISO 8601),
non influenzato dalla
cultura corrente

yyyy-MM-dd HH:mm:ss

2005-01-06 20:54:20Z

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

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)

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)

Anno (ultima cifra o ultime due, nessuno zero in testa)

yy

Anno (ultime due cifre)

yyyy

Anno (quattro cifre)

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)

Ora nel formato 12 ore (uno o due cifre come richiesto)

hh

Ora nel formato 12 ore

Minuti (uno o due cifre come richiesto)

mm

Minuti (sempre due cifre, con uno zero in testa se richiesto)

Secondi (uno o due cifre come richiesto)

ss

Secondi

Il primo carattere dellindicatore AM/PM

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.)
Lindicatore AM/PM

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; loffset 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 dellora

\char

Carattere di escape, per includere caratteri letterali che altrimenti verrebbero considerato
caratteri speciali
Comprende un formato data/ora predefinito nella stringa risultato.

%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 lN-mo carattere di una stringa.
ok = char.IsDigit(A123, 0); // => False

Ecco lelenco 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 delloperatore 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. Loggetto stringa
originale in memoria viene infine recuperato durante la successiva garbage collection
a meno che unulteriore 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. Loggetto 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, loggetto 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 nelloggetto 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 allinterno 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 allinizio 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 lesempio 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 loggetto StringBuilder pu essere pi di 100 volte pi veloce dellordinaria 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 laccesso 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
lintroduzione nel namespace System.Security del tipo SecureString. Sostanzialmente,
unistanza 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 laiuto 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 dallutente a runtime.
Unulteriore opzione, in effetti la sola opzione che si pu adottare quando
lutente immette i dati confidenziali a runtime, far s che lutente 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, loggetto 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 unistanza
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 unulteriore 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
listanza 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);
}

Largomento 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

Listruzione precedente funziona poich ToString accetta un oggetto IFormatProvider


per formattare il valore corrente, e loggetto CultureInfo espone questa interfaccia.
In questo paragrafo, mostrer come si pu sfruttare un ulteriore oggetto .NET che
implementa questa interfaccia, loggetto 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 loggetto 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 delloggetto 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();
// Loggetto nfi assegnabile, pertanto si possono modificarne le propriet.
nfi.NumberDecimalSeparator = ,;
nfi.NumberGroupSeparator = ;
// Ora si pu formattare un valore con loggetto custom NumberFormatInfo.
float sngValue = 12345.5F;
Console.WriteLine(sngValue.ToString(##,##0.00, nfi)); // => 12 345,50

Per lelenco 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
loperatore 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 lesponente)
e Currency (383, permette tutto eccetto lesponente). Lesempio 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 dellitaliano.
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 loperazione di parsing ha
avuto esito positivo:
int intValue = 0;
if ( int.TryParse(12345, out intValue) )
{
// intValue contiene il risultato delloperazione 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 loggetto System.Type da passare come secondo argomento al metodo si
deve utilizzare loperatore 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:
// Largomento 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 lapplicazione, si
pu far s che il seme dipenda dallora 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 delloggetto:
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


nellintervallo desiderato:
// Ottiene un valore nellintervallo da 0 a 1,000.
int intValue = rand.Next(1000);
// Ottiene un valore nellintervallo 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 dellanno 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 lora corrente espressa in coordinate UTC


(Universal Time Coordinates) e permette di confrontare i valori temporali prodotti in
differenti fusi orari; questa propriet ignora limpostazione Daylight Saving Time (lora
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 dellora), Year (anno), Month (mese), Day (giorno),
DayOfYear (giorno dellanno), 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 lora 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 linformazione se al valore date/time applicato il Daylight
Saving Time (lora 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,
// listruzione 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 loggetto TimeSpan che rappresenta la differenza
tra le due date:
// Quanti giorni, ore, minuti e secondi sono trascorsi
// dallinizio 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 lora 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 lora
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 labbondanza 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 let 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 nellanno 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 unulteriore spiegazione:


Il metodo ToFileTime restituisce un valore senza segno di 8 byte che rappresenta
la data e lora 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 lOLE 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
Loperazione 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.
Loggetto
DateTimeFormatInfo

concettualmente
simile
alloggetto
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 delloggetto 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 delloggetto 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 loggetto 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 loggetto 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 uneccezione 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, unistanza 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 lora 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, lora 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 unora locale, piuttosto che
unora 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
loggetto SoapFormatter o loggetto XmlSerializer. Questi due oggetti, infatti,
memorizzano le informazioni sul fuso orario assieme al valore effettivo della data:
quando loggetto 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 dellapplicazione. 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 allindietro
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 loggetto 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 loggetto
System.TimeZone, che si pu utilizzare per recuperare le informazioni sul fuso orario
impostate nelle impostazioni Data e Ora di Windows:
// Ottiene loggetto TimeZone del fuso orario corrente.
TimeZone tz = TimeZone.CurrentTimeZone;
// Visualizza il nome del fuso orario, senza e con lora 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 loffset dallo Universal Time (UTC), che si


recupera per mezzo del metodo GetUTCOffset. A questo metodo si deve passare un argomento
data poich loffset dipende da se lora legale in vigore. Il valore restituito in tick:
// Visualizza loffset 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 loffset orario del fuso W. Europe in Luglio,
// con lora legale attiva.
Console.WriteLine(tz.GetUtcOffset(new DateTime(2005, 7, 1))); // => 02:00:00

Il metodo IsDaylightSavingTime restituisce true se lora 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 lora legale inizia e termina in determinato anno


recuperando un array di oggetti DaylightTime con il metodo GetDaylightChanges delloggetto TimeZone:
// Recupera loggetto DaylightTime per lanno 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 daiuto
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 luguaglianza
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
dallutente 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 dallutente.
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 dellEnum e dei suoi membri. (Fanno
eccezione le costanti dellAPI 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) allutente 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 dellEnum.
Console.WriteLine(de.ToString(F)); // => DateTime
// Il formato Decimal visualizza il valore dellEnum.
Console.WriteLine(de.ToString(D)); // => 3
// Il formato esadecimale visualizza otto cifre hex.
Console.WriteLine(de.ToString(X)); // => 00000003

Lopposto 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 loggetto 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 uneccezione.
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 loperatore di casting non controlla se il valore da


convertire nellintervallo 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 uneccezione.
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 unarray 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 loutput 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 dallutente 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 lattributo Flags. Se un tipo
enumerato composto da due o pi valori flag, questo metodo restituisce lelenco 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 dallattributo Flags:


vde = (ValidDataEntry) Enum.Parse(vde.GetType(), IntegerNumber, FloatingNumber);
Console.WriteLine(Convert.ToInt32(vde));
// => 3