Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
88 04 54197 0 - Capitolo - 01 PDF
88 04 54197 0 - Capitolo - 01 PDF
Per evitare righe troppo lunghe, gli esempi di codice in questo capitolo assumono che le
seguenti istruzioni using siano utilizzate all’inizio di ciascun file sorgente:
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using System.Threading;
using System.Windows.Forms;
Il tipo System.Object
Tutte le classi derivano, direttamente o indirettamente, da System.Object, il che
significa che si può sempre assegnare un oggetto a una variabile System.Object senza
mai ottenere, facendolo, un errore di compilazione o a runtime:
// Il tipo object in C# è sinonimo di System.Object.
object o = new AnyOtherType();
Per inciso, si noti che le interfacce sono le uniche entità nel .NET Framework che
non derivano da System.Object.
1
2 Programmare Microsoft Visual C# 2005
• Equals Un membro statico che accetta due argomenti oggetto e restituisce true
se possono essere considerati uguali. È simile, e spesso è utilizzato al suo posto,
all’omonimo metodo di istanza, che fallirebbe se fosse invocato su una variabile di
tipo reference che è null.
• ReferenceEquals Un metodo static che accetta due argomenti oggetto e restituisce
true se referenziano la stessa istanza; pertanto, corrisponde all’operatore == di
Capitolo 1 Tipi fondamentali .NET 3
Object
Array
Attribute
Console
Delegate MulticastDelegate
AccessException
Environment
ArgumentException
GC ApplicationException NullReferenceException
....
MarshalByRefObject
Math
OperatingSystem
Random
String Boolean
Byte
TimeZone Char
Currency
ValueType DateTime
Decimal
Version Double
Enum
WeakReference Int16
Int32
Int64
IntPtr
ParamArray
Single
TimeSpan
Guid
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:
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.
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ù precisamen-
te, 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 struttu-
re). Tuttavia, questa descrizione non è strettamente accurata se la struttura comprende un
membro di un tipo reference.
Capitolo 1 Tipi fondamentali .NET 5
Molti articoli tecnici spiegano che i tipi value vengono allocati sullo stack e che non
occupano memoria nello heap managed, ma questa descrizione non è corretta al cento
percento: se un tipo value viene utilizzato nella definizione di un tipo reference, occupa
spazio nello heap, come in questo caso:
public class Square
{
// Questi due membri vanno nello heap, nello slot allocato per l’oggetto Square.
double Side;
Position Position;
// Il puntatore alla stringa Name viene allocato nello slot dell’oggetto Square,
// mentre i relativi caratteri vanno in un *altro* slot dello heap.
string Name;
}
Altri fattori possono influenzare la scelta tra un tipo value e un tipo reference. I tipi
value sono implicitamente sealed; pertanto si deve utilizzare una struttura se i propri
oggetti si comportano come tipo primitivo e non devono ereditare comportamenti
speciali da altri tipi, e altri tipi non devono derivare da esso. Inoltre, le Structure non
possono essere astratte e non possono contenere metodi virtuali, diversi da quelli
ereditati da System.Object.
Anche gli array .NET sono tipi reference, e l’assegnazione di un array a una variabile
Array copia solo il riferimento all’oggetto, non il contenuto dell’array. La classe Array
espone il metodo Clone per permettere di creare una copia shallow degli elementi.
(Una copia shallow è una operazione di copia che crea una copia di un oggetto ma non
degli oggetti figli).
6 Programmare Microsoft Visual C# 2005
Boxing e unboxing
Anche se si è principalmente interessati alle prestazioni, non sempre si deve
optare per i tipi value, poiché talvolta i tipi reference sono più veloci. Ad esempio,
un’assegnazione tra tipi value richiede la copia di ogni campo dell’oggetto, mentre
l’assegnazione di un valore reference a una variabile richiede solo la copia dell’indirizzo
dell’oggetto (4 byte nelle versioni Windows a 32 bit).
Quando si passa un tipo value a un metodo che si aspetta un argomento object si
verifica un tipo differente di degradazione delle prestazioni, poiché in questo caso il
valore deve essere sottoposto a boxing. Il boxing di un valore significa che il compilatore
crea un copia del valore nello heap managed e assegna l’indirizzo di questa copia a una
variabile object o a un argomento object in modo che il tipo possa poi essere utilizzato
come tipo reference. Un valore sottoposto al boxing (il cosiddetto valore boxed) non
mantiene un link al valore originale, il che significa che si può modificare uno dei due
senza influenzare l’altro.
Se questo valore boxed viene assegnato in seguito a una variabile del tipo originale
(ossia di tipo value), l’oggetto è detto unboxed e i dati vengono copiati dallo heap
managed alla memoria allocata per la variabile (ad esempio, sullo stack se è una variabile
locale). Non sorprendentemente, le operazioni di boxing e unboxing consumano tempo
di CPU e infine richiedono che sia recuperata della memoria durante una garbage
collection. In sintesi: se si eseguono molte assegnazioni o si eseguono frequentemente
operazioni che producono una sequenza di boxing e unboxing, l’implementazione di
un tipo reference potrebbe essere una scelta più saggia.
Il più delle volte, il boxing si verifica in modo trasparente, dove è richiesto un esplicito
cast o come operatore per riconvertire da object a un tipo value. Si può determinare
se una invocazione causa una operazione di boxing osservando la dichiarazione del
metodo con l’object browser o nella documentazione della classe. Se il metodo accetta
un argomento del tipo che si sta passando, non si verifica alcun boxing; se accetta un
generico argomento object, l’argomento verrà sottoposto al boxing. Quando si creano
i propri metodi, si può considerare di comprendere delle variazioni in overload che
accettano argomenti di tipi differenti ma anche una procedura generica che accetta un
argomento object.
In genere, non ha senso confrontare le prestazioni di un metodo che utilizza il boxing
con un metodo simile che non utilizza il boxing. Un benchmark informale mostra che
un ciclo serrato che invoca una funzione che richiede un’operazione di boxing può
essere fino a 30 volte più lento di un ciclo che non utilizza il boxing. Tuttavia, si deve
ripetere il ciclo 10 milioni di volte per osservare una differenza significativa in termini
assoluti, pertanto in pratica bisogna preoccuparsi del boxing solo nelle sezioni di
codice critiche in termini di prestazioni.
Talvolta si può utilizzare il boxing senza saperlo. In primo luogo, si esegue
implicitamente il boxing di una Structure se si invoca uno dei metodi virtuali che la
Structure eredita da System.Object: ad esempio, ToString. In secondo luogo, si esegue
implicitamente il boxing di una Structure se si invoca un metodo di un’interfaccia che
la struttura espone.
Capitolo 1 Tipi fondamentali .NET 7
Essere consapevoli del boxing permette di ottimizzare il proprio codice in modi non
alla portata del compilatore C#. Ad esempio, si consideri il seguente codice:
object res;
for (i = 1; i <= 1000; i++)
{
for (j = 1; j <= 100000; j++)
{
// GetObject accetta due argomenti Object, e perciò causa
// il boxing sia di i sia di j.
res = GetObject2(i, j);
}
}
Il valore della variabile i non cambia nel ciclo interno, tuttavia il codice che il
compilatore C# produce esegue il boxing di questo valore ogni volta che viene
invocato il metodo GetObject2. Eseguendo esplicitamente il boxing del valore, si può
raddoppiare la velocità di questo codice:
for (i = 1; i <= 1000; i++)
{
// Conserva il tipo value in una variabile oggetto.
object o = i;
for (j = 1; j <= 100000; j++)
{
res = GetObject2(o, j); // Solo j è boxed.
}
}
Tipi stringa
C# supporta il tipo di dato string, che è mappato alla classe System.String. Siccome
System.String è un tipo potente, si possono manipolare le stringhe per mezzo dei molti
metodi che il tipo espone.
Innanzitutto, la classe String espone molti metodi costruttori in overload, pertanto
si può creare una stringa in diversi modi: ad esempio, come sequenza di N caratteri
identici.
// Una sequenza di <N> caratteri
string s = new string(‘A’, 10); // => AAAAAAAAAA
Proprietà e metodi
Le sole proprietà della classe String sono Length e Chars. La prima restituisce il numero
di caratteri nella stringa; la seconda è l’indexer del tipo String e restituisce il carattere
presente nella posizione determinata dall’indice specificato (che parte da zero):
string s = “ABCDEFGHIJ”;
Console.WriteLine(s.Length); // => 10
// Index parte da zero.
Console.WriteLine(s[3]); // => D
8 Programmare Microsoft Visual C# 2005
Si può sfruttare la potenza della classe String invocandone uno dei molti metodi.
Ad esempio, si osservi quanto è semplice e leggibile l’operazione di inserimento di
una sottostringa:
s = s.Insert(3, “1234”); // => ABC1234DEFGHIJ
(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.
}
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 9
(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.
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
Il tipo StringComparer (anch’esso nuovo del .NET Framework 2.0) offre una tecnica
alternativa per effettuare i confronti fra stringhe. Le proprietà statiche di questo tipo
restituiscono un oggetto IComparer in grado di eseguire un tipo specifico di confronto
su stringhe. Ad esempio, ecco come si può eseguire un confronto case-insensitive in
base alla cultura corrente:
// Confronta due stringhe utilizzando la cultura locale in modalità case-insensitive.
res = StringComparer.CurrentCultureIgnoreCase.Compare(s1, s2);
// Confronta due stringhe utilizzando la cultura invariante in modalità case-sensitive.
res = StringComparer.InvariantCulture.Compare(s1, s2);
Una variabile stringa .NET viene considerata null se è nulla, e vuota se punta
a una stringa di zero caratteri. Nelle versioni precedenti di C# si dovevano testare
queste due condizioni separatamente, ma .NET 2.0 introduce il pratico metodo static
IsNullOrEmpty:
// Questi due statement sono equivalenti, ma solo il secondo funziona in .NET 2.0.
string s = “ABCDE”;
if ( s == null || s.Length == 0 )
{
Console.WriteLine(“Empty string”);
}
if ( string.IsNullOrEmpty(s) )
{
Console.WriteLine(“Empty string”);
}
Il modo più semplice per controllare se una stringa appare all’interno di un’altra è
per mezzo del metodo Contains, anche’esso nuovo di .NET 2.0:
// Il metodo Contains funziona solo in modalità case-sensitive.
s = “ABCDEFGHI ABCDEF”;
bool found = s.Contains(“BCD”); // => True
found = s.Contains(“bcd”); // => False
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);
12 Programmare Microsoft Visual C# 2005
Il metodo Insert restituisce la nuova stringa creata inserendo una sottostringa nella
posizione specificata dall’indice, mentre il metodo Remove rimuove un determinato
numero di caratteri, partendo dall’indice specificato:
result = s.Insert(4, “-123-”); // => ABCD-123-EFGHI ABCDEF
result = s.Remove(4, 3); // => ABCDHI ABCDEF
In .NET 2.0 è stata definita una versione in overload del metodo Remove che accetta
solo l’indice di partenza; questa nuova versione si può utilizzare come surrogato della
“classica” funzione Left:
// Estrae i primi 4 caratteri.
result = s.Remove(4); // => ABCD
Con il metodo static Join si può eseguire l’operazione opposta, cioè concatenare
tutti gli elementi di un array di stringhe. Questo metodo accetta opzionalmente l’indice
iniziale e il numero massimo di elementi da considerare.
// (Continua l’esempio precedente…)
// Riassembla la stringa aggiungendo coppie CR-LF, ma salta il primo elemento dell’array.
string newText = string.Join(“\r\n”, lines, 1, lines.Length - 1);
I metodi assenti
Nonostante il gran numero di metodi esposti dal tipo String, a volte si deve
scrivere una propria funzione per eseguire dei compiti ricorrenti. Ad esempio, non
vi è alcun metodo predefinito simile a PadLeft o PadRight ma in grado di centrare
una stringa in un campo di una determinata dimensione. Ecco il codice che esegue
questo compito:
public static string PadCenter(string s, int width, char padChar, bool truncate)
{
int diff = width - s.Length;
if ( diff == 0 || (diff < 0 && !(truncate)) )
{
// Restituisce la stringa così com’è.
return s;
}
Capitolo 1 Tipi fondamentali .NET 15
Per curiosità, ecco un metodo che inverte l’ordine dei caratteri in una stringa.
Ecco come rimediare con un metodo custom che permette anche di invertire solo una
parte della stringa:
public static string StringReverse(string s, int startIndex, int count)
{
char[] chars = s.ToCharArray();
if (count < 0)
{
count = s.Length - startIndex;
}
Array.Reverse(chars, startIndex, count);
return new string(chars);
}
{
count += 1;
index = source.IndexOf(search, index + 1);
}
while ( index >= 0 );
return count;
}
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 che lo statement successivo modifichi la stringa…
s.ToUpper();
// …ma così non è poiché il risultato non è stato riassegnato a s.
Console.WriteLine(s); // => abcde
// Ecco il modo corretto per invocare i metodi stringa.
s = s.ToUpper();
Il tipo CultureInfo
La classe System.Globalization.CultureInfo definisce un oggetto che si può
ispezionare per determinare alcune proprietà principali di qualsiasi lingua installata e
che si può utilizzare come argomento in molti metodi del tipo String e di altri tipi.
La classe espone la proprietà static CurrentCulture, che restituisce l’oggetto
CultureInfo della lingua corrente:
// Ottiene informazioni sulla nazionalità corrente.
CultureInfo ci = CultureInfo.CurrentCulture;
// Assumendo che il linguaggio corrente sia Italian, otteniamo:
Console.WriteLine(ci.Name); // => it
Console.WriteLine(ci.EnglishName); // => Italian
Console.WriteLine(ci.NativeName); // => italiano
Console.WriteLine(ci.LCID); // => 16
Console.WriteLine(ci.TwoLetterISOLanguageName); // => it
Console.WriteLine(ci.ThreeLetterISOLanguageName); // => ita
Console.WriteLine(ci.ThreeLetterWindowsLanguageName); // => ITA
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 è case-
insensitive, 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”);
}
La classe Encoding
Tutte le stringhe .NET memorizzano i propri caratteri in formato Unicode, pertanto
talvolta si potrebbe doverle convertire da e verso altri formati: ad esempio, ASCII o le
varianti UCS Transformation Format 7 (UTF-7) o UTF-8 del formato Unicode. Si può
fare ciò con la classe Encoding del namespace System.Text.
La prima cosa da fare quando si converte una stringa .NET Unicode da o verso un
ulteriore formato è creare l’opportuno oggetto di codifica. La classe Encoding espone
opportunamente i più comuni oggetti di codifica attraverso le seguenti proprietà
statiche: ASCII, Unicode (con ordinamento dei byte little-endian), BigEndianUnicode,
UTF7, UTF8, UTF32 e Default (la code page ANSI corrente di sistema).
Ecco un esempio di come si può convertire una stringa Unicode in una sequenza
di byte che rappresentano la stessa stringa in formato ASCII:
string text = “A Unicode string with accented vowels: àèéìòù”;
Encoding uni = Encoding.Unicode;
byte[] uniBytes = uni.GetBytes(text);
Encoding ascii = Encoding.ASCII;
byte[] asciiBytes = Encoding.Convert(uni, ascii, uniBytes);
// Converte i byte ASCII nuovamente in una stringa.
string asciiText = new string(ascii.GetChars(asciiBytes));
Console.WriteLine(asciiText); // => A Unicode string with accented vowels: ??????
Capitolo 1 Tipi fondamentali .NET 21
Si possono anche creare altri oggetti Encoding con il metodo static GetEncoding,
che accetta o un numero di code page o il nome della code page e che genera una
NotSupportedException se la code page non è supportata:
// Ottiene l’oggetto encoding per la code page 1252.
Encoding enc = Encoding.GetEncoding(1252);
Il metodo GetChars si aspetta che l’array di byte che gli viene passato contenga un
numero intero di caratteri. (Ad esempio, deve terminare con il secondo byte di un
carattere a due byte). Questo vincolo può essere un problema quando si legge l’array
di byte da un file o da un altro tipo di stream, e si sta lavorando con un formato di
stringa che permette uno, due o tre byte per carattere. In questi casi, si deve utilizzare
un oggetto Decoder, che ricorda lo stato tra invocazioni consecutive. Per ulteriori
informazioni, si legga la documentazione MSDN.
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
22 Programmare Microsoft Visual C# 2005
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
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
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”;
}
Una caratteristica poco nota del metodo String.Format, ma anche di tutti i metodi
che lo usano internamente, come il metodo Console.Write, permette di specificare la
larghezza di un campo e decidere di allineare il valore a destra o a sinistra:
// Crea una tavola di numeri, dei loro quadrati e delle radici quadrate.
// Stampa l’intestazione della tavola.
Console.WriteLine(“{0,-5} | {1,7} | {2,10:N2}”, “N”, “N^2”, “Sqrt(N)”);
for ( int n = 1; n <= 100; n++ )
{
// N è allineato a sinistra in un campo ampio 5 caratteri,
// N^2 è allineato a destra in un campo ampio 7 caratteri, e Sqrt(N) viene visualizzato con
// 2 cifre decimali ed è allineato a destra in un campo ampio 10 caratteri.
Console.WriteLine(“{0,-5} | {1,7} | {2,10:N2}”, n, Math.Pow(n, 2), Math.Sqrt(n));
}
La larghezza del campo si specifica dopo il simbolo virgola; si utilizza una larghezza
positiva per valori allineati a destra e una larghezza negativa per valori allineati a sinistra. Se
si vuole fornire un formato predefinito, si utilizza un simbolo due punti come separatore
dopo il valore della larghezza.
Come si vede nel precedente esempio, le larghezze del campo sono supportate anche
per valori numerici, stringa e Date. Infine, si possono inserire parentesi graffe letterali
raddoppiandole nella stringa di formato:
Console.WriteLine(“ {{{0}}}”, 123); // => {123}
24 Programmare Microsoft Visual C# 2005
Formato Descrizione
# Segnaposto per una cifra o uno spazio.
0 Segnaposto per una cifra o uno zero.
. Separatore decimale.
, Separatore delle migliaia; se utilizzato subito prima del separatore decimale, funziona
come prescalatura. (Per ciascuna virgola in questa posizione, il valore viene diviso per
1000 prima della formattazione).
% Visualizza il numero come valore percentuale.
E+000 Visualizza il numero in formato esponenziale, ossia, con una E seguita dal segno di
esponente, e poi un numero di cifre dell’esponente pari al numero di zeri dopo il segno più.
E-000 Analogo al precedente simbolo esponente, ma il segno dell’esponente viene visualizzato
solo se negativo.
; Separatore di sezione. La stringa del formato può contenere uno, due o tre sezioni. In caso di due
sezioni, la prima si applica ai valori positivi e allo zero, e la secondo si applica ai valori negativi. In
caso di tre sezioni, vengono utilizzate, rispettivamente, per valori positivi, negativi e zero.
\char Carattere di escape, per inserire caratteri che altrimenti verrebbero interpretati come caratteri
speciali (ad esempio, \; per inserire un punto e virgola e \\ per inserire un backslash).
‘...’ Un gruppo di caratteri letterali. Si può aggiungere una sequenza di caratteri letterali
“...” racchiudendoli tra apici singoli o doppi.
altro Ogni altro carattere è interpretato alla lettera e viene inserito nella stringa risultato così com’è.
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
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-3 Sequenze di caratteri che possono essere utilizzati nei formati custom Date e Time
Formato Descrizione
d Giorno del mese (una o due cifre come richiesto)
dd Giorno del mese (sempre due cifre, con uno zero in testa se richiesto)
ddd Giorno della settimana (abbreviazione di tre caratteri)
dddd Giorno della settimana (nome completo)
M Numero del mese (uno o due cifre come richiesto)
MM Numero del mese (sempre due cifre, con uno zero in testa se richiesto)
MMM Nome del mese (abbreviazione di tre caratteri)
MMMM Nome del mese (nome completo)
y Anno (ultima cifra o ultime due, nessuno zero in testa)
yy Anno (ultime due cifre)
yyyy Anno (quattro cifre)
H Ora in formato 24 ore (uno o due cifre come richiesto)
HH Ora nel formato 24 ore (sempre due cifre, con uno zero in testa se richiesto)
h Ora nel formato 12 ore (uno o due cifre come richiesto)
hh Ora nel formato 12 ore
m Minuti (uno o due cifre come richiesto)
mm Minuti (sempre due cifre, con uno zero in testa se richiesto)
s Secondi (uno o due cifre come richiesto)
ss Secondi
t Il primo carattere dell’indicatore AM/PM
f 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.)
F 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.)
tt L’indicatore AM/PM
z Offset del fuso orario, solo ora (uno o due cifre come richiesto)
zz Offset del fuso orario, solo ora (sempre due cifre, con uno zero in testa se richiesto)
zzz Offset del fuso orario, ora e minuto (valori ora e minuto sempre di due cifre, con uno zero
in testa se richiesto)
K Il carattere “Z” se la proprietà Kind del valore DateTime è Utc; l’offset del fuso orario
(ad es. “-8:00”) se la proprietà Kind è Local; un carattere vuoto se la proprietà Kind è
Unspecified. (Nuovo di .NET 2.0.)
/ Separatore di default della data
: Separatore di default dell’ora
\char Carattere di escape, per includere caratteri letterali che altrimenti verrebbero considerato
caratteri speciali
%format Comprende un formato data/ora predefinito nella stringa risultato.
Capitolo 1 Tipi fondamentali .NET 27
Tabella 1-3 Sequenze di caratteri che possono essere utilizzati nei formati custom Date e Time
Formato Descrizione
‘…’ Un gruppo di caratteri letterali. Si può aggiungere una sequenza di caratteri letterali
“…” racchiudendoli tra apici singoli o doppi.
altro Qualsiasi altro carattere viene interpretato alla lettera e inserito nella stringa risultato così com’è.
Il tipo Char
La classe Char rappresenta un singolo carattere. Non c’è molto da dire su questo tipo
di dato, se non che espone diversi utili metodi statici che permettono di testare se un
carattere soddisfa un determinato criterio. Questi metodi sono in overload e accettano
un Char, o un valore String più un indice della stringa. Ad esempio, si controlla se un
carattere è una cifra come segue:
// Controlla un singolo valore Char.
bool ok = char.IsDigit(‘1’); // => True
// Controlla l’N-mo carattere di una stringa.
ok = char.IsDigit(“A123”, 0); // => False
Ecco l’elenco dei metodi statici più utili che testano singoli caratteri: IsControl, IsDigit,
IsLetter, IsLetterOrDigit, IsLower, IsNumber, IsPunctuation, IsSeparator, IsSymbol,
IsUpper e IsWhiteSpace.
Si può convertire un carattere in maiuscolo e minuscolo con i metodi statici ToUpper
e ToLower. Per default questi metodi operano in base alla nazionalità del thread corrente,
ma si può passare loro un oggetto CultureInfo opzionale, o si possono utilizzare le
versioni invarianti rispetto alla cultura ToUpperInvariant e ToLowerInvariant:
char newChar = char.ToUpper(‘a’); // => A
newChar = char.ToLower(‘H’, new CultureInfo(“it-IT”)); // => h
char loChar = char.ToLowerInvariant(‘G’); // => g
Si può convertire una stringa in un Char per mezzo dell’operatore CChar o del metodo
Char.Parse. O si può utilizzare il nuovo metodo statico TryParse (aggiunto nel .NET Framework
2.0) per controllare se la conversione è possibile ed eseguirla in una operazione:
if ( char.TryParse(“a”, out newChar) )
{
// newChar contiene il carattere ‘a’.
}
Il tipo StringBuilder
Come è noto, un oggetto String è immutabile, e il suo valore non cambia mai dopo
che la stringa è stata creata. Ciò significa che in qualsiasi momento si applichi un
metodo che ne modifica il valore, si sta effettivamente creando un nuovo oggetto di
tipo String. Ad esempio, il seguente statement:
S = S.Insert(3, “1234”);
28 Programmare Microsoft Visual C# 2005
non modifica la stringa originale in memoria. Invece, il metodo Insert crea un nuovo
oggetto String, che viene quindi assegnato alla variabile stringa S. L’oggetto stringa
originale in memoria viene infine recuperato durante la successiva garbage collection
a meno che un’ulteriore variabile punti ad esso, come accade spesso con stringhe del
pool interno. La superiorità dello schema di allocazione della memoria di .NET assicura
che questo meccanismo introduca un overhead relativamente basso; ciò nondimeno,
troppe operazioni di allocazione e rilascio possono degradare le prestazioni della propria
applicazione. L’oggetto System.Text.StringBuilder offre una soluzione a questo problema.
Si può considerare un oggetto StringBuilder come un buffer che può contenere
una stringa che ha la capacità di crescere da zero caratteri alla capacità corrente del
buffer. Finché non si supera questa capacità, la stringa viene assemblata nel buffer e
non viene né allocata né rilasciata alcuna memoria. Se la stringa diventa più lunga
della capacità corrente, l’oggetto StringBuilder crea in modo trasparente un buffer più
grande. Il buffer di default contiene inizialmente 16 caratteri, ma si può modificare
questa dimensione assegnando una capacità differente nel costruttore di StringBuilder
o assegnando un valore alla proprietà Capacity:
// Crea un oggetto StringBuilder con capacità iniziale di 1,000 caratteri.
StringBuilder sb = new StringBuilder(1000);
Il tipo SecureString
Il modo in cui le stringhe .NET vengono implementate ha alcune serie implicazioni
inerenti alla sicurezza. Infatti, se in una stringa si memorizzano informazioni
confidenziali, ad esempio una password o un numero di carta di credito, un ulteriore
processo che può leggere nello spazio degli indirizzi della propria applicazione può
anche leggere i relativi dati. Benché ottenere l’accesso allo spazio degli indirizzi di un
processo non è cosa banale, si consideri che alcune porzioni del proprio spazio degli
indirizzi vengono spesso salvate nel file di swap del sistema operativo, dove leggerle è
molto più facile.
Il fatto che le stringhe siano immutabili significa che non si può azzerare realmente
una stringa dopo averla utilizzata. Ancor peggio, poiché una stringa è soggetta alla
garbage collection, potrebbero esserci diverse copie di essa in memoria, il che a sua
volta accresce la probabilità che una di esse finisca nel file di swap. Una applicazione
.NET Framework 1.1 che vuole assicurare il più alto grado di confidenzialità dovrebbe
tenersi alla larga dalle stringhe standard e utilizzare qualche altra tecnica, ad esempio
un Char o un array di Byte crittografato che viene decodificato solo un istante prima di
utilizzare la stringa e viene azzerato subito dopo.
30 Programmare Microsoft Visual C# 2005
La versione 2.0 del .NET Framework rende questo processo più facile con
l’introduzione nel namespace System.Security del tipo SecureString. Sostanzialmente,
un’istanza di SecureString è un array di caratteri che viene crittografato attraverso le
Data Protection API (DPAPI). A differenza di una stringa standard, e analogamente
al tipo StringBuilder, il tipo SecureString non è immutabile: lo si costruisce un
carattere per volta per mezzo del metodo AppendChar, simile a ciò che si fa con
il tipo StringBuilder, e si possono anche inserire, rimuovere e modificare singoli
caratteri per mezzo dei metodi InsertAt, SetAt e RemoveAt. Facoltativamente, si
può rendere la stringa immutabile invocando il metodo MakeReadOnly. Infine,
per ridurre il numero di copie che vagano in memoria, le istanze di SecureString
vengono fissate (pinned), il che significa che non possono essere spostate dal
garbage collector.
Il tipo SecureString è così sicuro che non espone né un metodo per inizializzarlo
con una stringa né un metodo che ne restituisce il contenuto come testo in chiaro:
il primo compito richiede una serie di invocazioni ad AppendChar, il secondo può
essere eseguito con l’aiuto del tipo Marshal, come spiegherò presto. Un oggetto
SecureString non è serializzabile e perciò non si può neanche salvarlo su file per
un recupero in seguito. E naturalmente non si può inizializzarlo a partire da una
stringa cablata nel proprio codice, poiché ciò andrebbe contro lo scopo previsto di
questo tipo. Quali sono allora le opzioni per inizializzare correttamente un oggetto
SecureString?
Una prima opzione è memorizzare la password in chiaro in un file protetto da una
Access Control List (ACL) e leggerla un carattere per volta. Questa opzione, tuttavia,
non è a prova di bomba, e in alcuni casi non può comunque essere applicata, poiché i
dati confidenziali vengono immessi dall’utente a runtime.
{
for ( int i = txtPassword.SelectionStart + txtPassword.SelectionLength - 1;
i >= txtPassword.SelectionStart; i--)
{
password.RemoveAt(i);
}
}
// Carattere ordinario: lo inserisce nella stringa sicura.
if ( txtPassword.SelectionStart == txtPassword.TextLength )
{
password.AppendChar(e.KeyChar);
}
else
{
password.InsertAt(txtPassword.SelectionStart, e.KeyChar);
}
// Visualizza (e memorizza) un asterisco nel controllo textbox.
e.KeyChar = ‘*’;
}
}
Come ho già detto prima, l’oggetto SecureString non espone alcun metodo che
restituisce il contenuto come testo in chiaro. Invece, si devono utilizzare due metodi
del tipo Marshal, del namespace System.Runtime.InteropServices:
// Converte la password in una BSTR unmanaged.
IntPtr ptr = Marshal.SecureStringToBSTR(password);
// Per scopi dimostrativi, converte la BSTR in una stringa ordinaria a la usa.
string pw = Marshal.PtrToStringBSTR(ptr);
…
// Azzera la BSTR unmanaged utilizzata per la password.
Marshal.ZeroFreeBSTR(ptr);
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,
32 Programmare Microsoft Visual C# 2005
come Boolean, Byte, SByte, Int16, Int32, Int64, UInt16, UInt32, UInt64, Single,
Double e Decimal.
Proprietà e metodi
Tutti i tipi numerici, e anche tutte le classi .NET, espongono il metodo ToString,
che ne converte il valore numerico in stringa. Questo metodo è particolarmente utile
quando si accoda il valore numerico a un’ulteriore stringa:
double myValue = 123.45;
string res = “The final value is “ + myValue.ToString();
L’argomento deve essere dello stesso tipo del valore di cui si sta applicando il
metodo CompareTo, pertanto si deve convertirlo se necessario.
Tutte le classi numeriche espongono i campi statici MinValue e MaxValue, che restituiscono
il valore più piccolo e più grande che si può esprimere con il corrispondente tipo:
// Visualizza il valore massimo memorizzabile in una variabile Double.
Console.WriteLine(Double.MaxValue); // => 1.79769313486232E+308
Capitolo 1 Tipi fondamentali .NET 33
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
Una forma in overload del metodo Parse accetta un valore enumerato NumberStyle
come secondo argomento. NumberStyle è un valore codificato a bit che specifica quali
parti del numero sono ammesse nella stringa da analizzare. I valori NumberStyle
validi sono AllowLeadingWhite (1), AllowTrailingWhite (2), AllowLeadingSign (4),
AllowTrailingSign (8), AllowParentheses (16), AllowDecimalPoint (32), AllowThousand
(64), AllowExponent (128), AllowCurrencySymbol (256) e AllowHexSpecifier (512).
Si può specificare quali parti delle stringhe sono valide utilizzando su questi valori
l’operatore Or a bit, o si può utilizzare qualche valore composto predefinito, come
Any (511, permette tutto), Integer (7, permette il segno in coda e spazi in testa e in
coda), Number (111, come Integer ma permette il separatore delle migliaia e il punto
decimale), Float (167, come Integer ma permette il separatore decimale e l’esponente)
e Currency (383, permette tutto eccetto l’esponente). L’esempio seguente estrae un
Double da una stringa e riconosce gli spazi e tutti i formati supportati:
double dblValue = double.Parse(“ 1,234.56E6 “, NumberStyles.Any);
// A dblValue viene assegnato il valore 1234560000
Capitolo 1 Tipi fondamentali .NET 35
Una terza forma in overload del metodo Parse accetta qualsiasi oggetto
IFormatProvider, pertanto si può passargli un oggetto CultureInfo:
// Esegue il parsing di una stringa in base alle regole dell’italiano.
sngValue = float.Parse(“12.345,67”, new CultureInfo(“it-IT”));
Tutti i tipi numerici del .NET Framework 2.0 espongono un nuovo metodo
denominato TryParse, che permette di evitare le onerose eccezioni se una stringa
non contiene un numero in un formato valido. (Questo metodo è disponibile in
.NET 1.1 solo per il tipo Double). Il metodo TryParse accetta una variabile by-
reference come secondo argomento e restituisce true se l’operazione di parsing ha
avuto esito positivo:
int intValue = 0;
if ( int.TryParse(“12345”, out intValue) )
{
// intValue contiene il risultato dell’operazione di parsing.
}
else
{
// La stringa non contiene un valore integer in un formato valido.
}
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:
36 Programmare Microsoft Visual C# 2005
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 rappresentazione binaria di un numero.
int result = Convert.ToInt32(“11011”, 2); // => 27
// Converte da un numero ottale.
result = Convert.ToInt32(“777”, 8); // => 511
// Converte da un numero esadecimale.
result = Convert.ToInt32(“AC”, 16); // => 172
Si può anche passare uno o due argomenti per mantenere il valore di ritorno
nell’intervallo desiderato:
// Ottiene un valore nell’intervallo da 0 a 1,000.
int intValue = rand.Next(1000);
// Ottiene un valore nell’intervallo da 100 a 1,000.
intValue = rand.Next(100, 1000);
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 RNG-
CryptoServiceProvider, del namespace System.Security.Cryptography.
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
Benché questa classe sia distinta dalla classe DateTime, ne condivide molte proprietà e
metodi e opera quasi sempre in combinazione a valori DateTime, come si vedrà a breve.
Una nota per il programmatore curioso: un valore DateTime viene memorizzato
come numero di tick (1 tick = 100 nanosecondi) trascorsi dal 1 Gennaio, 0001; questo
formato di memorizzazione può funzionare per qualsiasi data tra il 1/1/0001 e il 12/
12/9999. In .NET 2.0 questo valore tick occupa 62 bit, e i rimanenti due bit vengono
utilizzati per preservare l’informazione se al valore date/time è applicato il Daylight
Saving Time (l’ora legale) e se il date/time è relativo al fuso orario corrente (il default)
o è in Universal Time (UTC).
Il metodo Add accetta un oggetto TimeSpan come argomento. Prima di poterlo utilizzare, si
deve saper creare un oggetto TimeSpan, scegliendo uno dei metodi costruttori in overload:
// Un valore a 64 bit viene interpretato come valore Ticks.
TimeSpan ts = new TimeSpan(13500000); // 1.35 secondi
// Tre valori interi vengono interpretati come ore, minuti, secondi.
ts = new TimeSpan(0, 32, 20); // 32 minuti, 20 secondi
// Quattro valori interi vengono interpretati come giorni, ore, minuti, secondi.
ts = new TimeSpan(1, 12, 0, 0); // 1 giorno e mezzo
// (Si noti che gli argomenti non vengono verificati per errori out-of-range; perciò,
// l’istruzione successiva produce lo stesso risultato di quella precedente.)
ts = new TimeSpan(0, 36, 0, 0); // 1 giorno e mezzo
// Un quinto argomento viene interpretato come millisecondi.
ts = new TimeSpan(0, 0, 1, 30, 500); // 90 secondi e mezzo
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));
Per default, i valori DateTime sono relativi al fuso orario corrente e non vanno mai
confrontati valori provenienti da fusi orari differenti, a meno che non siano in formato
UTC (si veda il successivo paragrafo “Lavorare con i fusi orari”, di questo capitolo).
Inoltre, quando si valuta la differenza tra due date nello stesso fuso orario, si può
ottenere un risultato errato se si è verificata una transizione da o verso l’ora legale tra le
due date. Motivo in più per utilizzare le date in formato UTC.
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
poiché il risultato sarebbe di una unità maggiore del valore corretto se la persona
non ha ancora compiuto gli anni nell’anno corrente. Ho preparato due routine riusabili
che forniscono la funzionalità mancante:
// Restituisce il numero intero di anni tra due date.
public static int YearDiff(DateTime startDate, DateTime endDate)
{
int result = endDate.Year - startDate.Year;
if ( endDate.Month < startDate.Month ||
(endDate.Month == startDate.Month && endDate.Day < startDate.Day))
{
result--;
}
return result;
}
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
42 Programmare Microsoft Visual C# 2005
Parsing di date
L’operazione complementare alla formattazione della data è il parsing. La classe
DateTime fornisce un metodo static Parse per eseguire un parsing di ogni grado di
complessità:
DateTime dt = DateTime.Parse(“2005/1/6 12:30:20”);
Aspetto ancor più interessante, è che si possono impostare nomi dei giorni della
settimana e dei mesi con stringhe arbitrarie se si ha un oggetto DateTimeFormatInfo
assegnabile, e quindi si può utilizzare l’oggetto per eseguire il parsing di una
data scritta in qualsiasi linguaggio, compresi quelli inventati. (Sì, compreso il
Klingon!)
Un modo ulteriore per eseguire il parsing delle stringhe in formati diversi da mese/
giorno/anno è utilizzare il metodo shared ParseExact. In questo caso, si passa la stringa
di formato come secondo argomento, e si può passare null come terzo argomento
se non è necessario un oggetto DateTimeFormatInfo per qualificare ulteriormente la
stringa da sottoporre al parsing:
// Questo statement assegna la data 6 Gennaio 2005
dt = DateTime.ParseExact(“6-1-2005”, “d-M-yyyy”, null);
Il secondo argomento può essere uno dei formati DateTime supportati elencati
nella Tabella 1-2. Nel .NET Framework 2.0, è stato aggiunto il nuovo formato “F” per
supportare il metodo ParseExact quando vi è un numero variabile di cifre decimali.
44 Programmare Microsoft Visual C# 2005
una variabile DateTime deve essere considerato relativo al fuso orario corrente o è un
valore UTC assoluto.
Questo problema è stato risolto abbastanza efficacemente in .NET 2.0 aggiungendo
una nuova proprietà Kind al tipo DateTime. Questa proprietà è un valore enumerato
DateTimeKind che può essere Local, Utc o Unspecified. Per compatibilità all’indietro
con le applicazioni .NET 1.1, per default un valore DateTime ha una proprietà Kind
impostata a DateTimeKind.Local, a meno che non si specifichi un valore differente nel
costruttore:
// 14 Febbraio 2005 alle 12:00 AM, valore UTC
DateTime aDate = new DateTime(2005, 2, 14, 12, 0, 0, DateTimeKind.Utc);
// Testa la proprietà Kind.
Console.WriteLine(aDate.Kind.ToString()); // Utc
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 DateTime-
Styles.RoundtripKind:
// Serializza una data in formato UTC.
string text = aDate.ToString(“o”, CultureInfo.InvariantCulture);
Il tipo TimeZone
Il .NET Framework supporta le informazioni sul fuso orario attraverso l’oggetto
System.TimeZone, che si può utilizzare per recuperare le informazioni sul fuso orario
impostate nelle impostazioni “Data e Ora” di Windows:
// Ottiene l’oggetto TimeZone del fuso orario corrente.
TimeZone tz = TimeZone.CurrentTimeZone;
// Visualizza il nome del fuso orario, senza e con l’ora legale.
// (Risultati ottenuti eseguendo il codice in Italia.)
Console.WriteLine(tz.StandardName); // => W. Europe Standard Time
Console.WriteLine(tz.DaylightName); // => W. Europe Daylight Time
Il blocco più interessante di informazioni è l’offset dallo Universal Time (UTC), che si
recupera per mezzo del metodo GetUTCOffset. A questo metodo si deve passare un argomento
data poiché l’offset dipende da se l’ora legale è in vigore. Il valore restituito è in tick:
// Visualizza l’offset orario del fuso W. Europe a Marzo 2005,
// con ora legale non attiva.
Console.WriteLine(tz.GetUtcOffset(new DateTime(2005, 3, 1))); // => 01:00:00
// Visualizza l’offset orario del fuso W. Europe in Luglio,
// con l’ora legale attiva.
Console.WriteLine(tz.GetUtcOffset(new DateTime(2005, 7, 1))); // => 02:00:00
Infine, si può determinare quando l’ora legale inizia e termina in determinato anno
recuperando un array di oggetti DaylightTime con il metodo GetDaylightChanges dell’og-
getto TimeZone:
// Recupera l’oggetto DaylightTime per l’anno 2005.
DaylightTime dlc = tz.GetDaylightChanges(2005);
// Si noti che si potrebbero ottenre date iniziali e finali differenti se si
// esegue questo codice in un paese diverso dagli USA.
Console.WriteLine(“Starts at {0}, Ends at {1}, Delta is {2} minutes”,
dlc.Start, dlc.End, dlc.Delta.TotalMinutes);
// => Starts at 3/27/2005 2:00:00 A.M., ends at 10/30/2005 3:00:00 A.M.
// Delta is 60 minutes.
Il tipo Guid
Il tipo System.Guid espone diversi metodi statici e di istanza che possono essere d’aiuto
quando si lavora con i GUID, ossia quei numeri a 128 bit che servono per identificare
univocamente gli elementi e che sono onnipresenti nella programmazione Windows. Il
metodo static NewGuid è utile per la generazione di un nuovo identificatore univoco:
// Crea un nuovo GUID.
Guid guid1 = Guid.NewGuid();
Capitolo 1 Tipi fondamentali .NET 47
Esistono solo due ulteriori cose che si possono fare con un oggetto Guid: si può
convertirlo in un array di Byte con il metodo ToByteArray e si può confrontare l’uguaglianza
di due valori Guid utilizzando il metodo Equals (derivato da System.Object):
// Converte in un array di byte.
byte[] bytes = guid1.ToByteArray();
foreach (byte b in bytes)
{
Console.Write(“{0} “, b);
// => 239 1 161 57 143 200 172 70 185 64 222 29 59 15 190 205
}
Valori Enum
Qualsiasi Enum che si definisce nella propria applicazione deriva da System.Enum,
che a sua volta eredita da System.ValueType. In definitiva, perciò, gli Enum definiti
dall’utente sono tipi value, ma sono speciali poiché non si possono definire ulteriori
proprietà, metodi o eventi. Tutti i metodi che espongono vengono ereditati da
System.Enum. (Si noti che in C# è illegale derivare esplicitamente una classe da
System.Enum).
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.
48 Programmare Microsoft Visual C# 2005
• Utilizzare nomi senza il suffisso Enum; utilizzare nomi al singolare per tipi Enum
ordinari e al plurale per tipi Enum codificati a bit.
• Utilizzare la grafia Pascal per il nome dell’Enum e dei suoi membri. (Fanno
eccezione le costanti dell’API di Windows, che di solito sono in maiuscolo).
• Utilizzare interi a 32 bit a meno che non sia necessario un intervallo più esteso,
il che accade normalmente solo se si ha un Enum codificato a bit con più di 32
valori possibili.
• Non utilizzare gli Enums per gli insiemi aperti, ossia insiemi che si potrebbe
dover espandere in futuro (ad esempio, le versioni del sistema operativo).
L’opposto di ToString è il metodo static Parse, che accetta una stringa e la converte
nel corrispondente valore enumerato. Essendo ereditato dalla classe generica Enum,
il metodo Parse restituisce un oggetto generico, pertanto si deve utilizzare una
conversione esplicita per assegnare l’oggetto a una specifica variabile enumerativa:
de = (DataEntry) Enum.Parse(typeof(DataEntry), “CharString”);