C#

MODIFICATORI e OPERATORI

Eccoci ad un argomento un po' tedioso forse ma assolutamente fondamentale. I modificatori e gli operatori sono ovviamente ovunque in C# e ne costituiscono un componente fondamentale.
I modificatori riguardano la modalità di accesso ai membri ai quali sono applicati, siano essi proprietà, metodi o altro ancora. Ne mostro ora l'elenco, tenete presente che il loro uso sarà molto più chiaro col tempo, man mano che sentirete la necessità di usarli per i vostri programmi.

Modificatore  Significato / uso
private E' il modificatore più restrittivo che consente l'accesso a un membro o a un tipo soltanto alla classe o alla struct al quale esso appartiene.
public E' invece il più permissivo, consente accessibilità completa, senza restrizioni
protected consente l'accesso nell'ambito della stessa classe o struttura o da quelle da esse derivate (parlando di classi, per quest'ultimo dettaglio)
internal l'accesso è in questo caso permesso all'interno dell'assembly al quale il membro o il tipo appartengono
protected internal permette l'accesso nell'ambito dell'assembly o anche a un altro purchè ad accedere siano classi derivate da quello a cui il membro appartiene.
static permette l'accesso a classi o membri senza necessità di creare istanze.
readonly definisce un campo sul quale saranno possibili solo operazioni di lettura. Tale campo andrà inizializzato in fase di prima dichiarazione pertanto.
volatile un dato marcato volatile può essere modificato da più threads che lavorano contemporaneamente, ciascuno dei quali pertanto non blocca l'accesso agli altri
new anche new può essere visto come un modificatore (oltre per creare nuovi oggetti ecc...) e ha lo scopo di negare l'ereditarietà, di un metodo, per esempio, rispetto alla classe base. Viene utilizzato anche come vincolo, come vedremo

Risulta più chiaro ora, ad esempio, come mai il metodo main sia dichiarato static, in questo modo può essere usato come entry point senza richiamare istanze della classe che lo contiene.
Ricordatevi il significato di questi modificatori, li incontrerete ovunque nei programmi scritti in C#

OPERATORI

Se per i modificatori, tutto sommato non c'è molto da dire, ben diverso è il discorso per tutti gli operatori che troviamo in questo linguaggio. Come vedremo c'è tanto da imparare.
Gli operatori si possono suddividere in tanti modi. Questo dipende anche dalla polivalenza di molti di essi. Io ho scelto una certa logica, su MSDN e in giro per il Web ne troverete altre, l'importante è che comprendiate bene il loro uso che comunque, in questo paragrafo, cercherò di illustrare con molti esempi.

Categoria Operatori
Aritmetici + - * / %
Logici & | ^ ~ && || !
Concatenazione +
Incremento e decremento ++  --
Shift <<  >>
Comparazione < > <= >= == !=
Accesso ai membri .
Indicizzazione []
Cast ()
Condizionale (ternario) ?:
Delegati (concatenazione e rimozione) +  -
Creazione di oggetti new
Informazione sui tipi sizeof typeof is as
Check sugli overflow = += -= *= /= %= &= |= ^= <<= >>=
Indirezione e indice []
Coalescenza ??

Gli operatori aritmetici non dovrebbero presentare alcuna particolarità. E' da notare la polivalenza del
+
che non solo, banalmente, somma i numeri ma, come noto, concatena le stringhe ed agisce anche in altri ambiti. A questo proposito possiamo sottolineare la doppia veste di operatore unario, in quanto indica la positività di un numero, e binario, in quanto funge da tramite tra due entità.
Tra gli operatori logici iniziamo da
&
che può essere utilizzato come operatore unario in ambito unsafe per restituire l'indirizzo in memoria del suo operando. Dato appunto il contesto unsafe per ora non ne parliamo. In ambito safe esso è applicabile agli interi ed ai booleani. Nel primo caso esegue un confronto bit a bit (bitwise) nel secondo un confronto di tipo logico che risulta vero solo se le due parti sono entrambe vere. Il primo caso è particolarmente interessante. Ad esempio

Console.WriteLine(89 & 44);

dà come output il numero 8. Facile capire perchè:

89 in bit 1 0 1 1 0 0 1
and              
44 in bit 0 1 0 1 1 0 0
risultato 0 0 0 1 0 0 0

In pratica sia 1 solo in presenza di 1 & 1 Dal momento che il numero 1000 binario è 8 decimale, ecco spiegato il risultato.
Se applichiamo & ai booleani si applica una ovvia tabella che restituisce true solo se da entrambe le parti abbiamo true, cioè:

true & true   -> true
true & false  -> false
false & true  -> false
false & false -> false

Un dicorso del tutto analogo si applica al secondo operatore, | che invece performa l'or logico su valori booleani e bit a bit sugli interi.

Andiamo a vedere l'esempio precedente:

Console.WriteLine(89 | 44);

il risultato è 125. Ecco perchè:

89 in bit 1 0 1 1 0 0 1
or              
44 in bit 0 1 0 1 1 0 0
risultato 1 1 1 1 1 0 1

dove 1111101 è 125 binario
Applicando l'operatore ai booleani vale la seguente tabella:

true | true -> true
true | false -> true
false | true -> true
false | false -> false

Altro operatore interessante è ^ che viene usato per lo XOR o OR esclusivo. In caso di booleani si ha true se e solo se è true uno dei due operandi. Anche qui, sugli interi, si lavora bit a bit.

Console.WriteLine(89 ^ 44);

ci restituisce 117

89 in bit 1 0 1 1 0 0 1
or              
44 in bit 0 1 0 1 1 0 0
risultato 1 1 1 0 1 0 1

Come avrete già capito 1110101 è 117 decimale. La tabella valevole per i booleani, anche qui intuibile è:

true | true -> false
true | false -> true
false | true -> true
false | false -> false

L'operatore ~ invece effettua il complemento bit a bit di fatto rovesciando in un certo senso il suo operando. Può essere applicato su int, unit, long, ulong

Console.WriteLine(~5);

ci dà come risultato -6 e questo è subito spiegabile:

5 =  0000 0000 0000 0000 0000 0000 0000 0101
-6 = 1111 1111 1111 1111 1111 1111 1111 1010


Molto facile e comune è l'operatore && che esegue un AND logico su due valori booleani. La tabella è la stessa che abbiamo visto per & al quale si può spesso sovrapporre come utilizzo. La principale differenza, oltre al fatto che && non lavora sugli interi, che questo operatore valuta la parte destra solo se necessario. Cioè un'epressione tipo:

false && true

valuta solo il primo operando e acquisisce il valore false, inutile guardare cosa c'è a destra.

Console.WriteLine(true && true);
Console.WriteLine(false && true);
Console.WriteLine(1 && 1);

L'output di queste tre istruzioni (se il programma potesse girare) è:

True
False

e....
 
error CS0019: Non è possibile applicare l'operatore '&&' a operandi di tipo 'int' e 'int'

come detto && non funziona sugli interi..
Invece è possibile scrivere

Console.WriteLine(false & true);

che dà come risultato false, ovviamente. Rispetto all'uso di && però viene valutata anche la parte destra e questa è un'inutile inefficienza.

Il discorso è del tutto analogo parlando di || grazie al quale abbiamo un OR logico. Anche in questo caso lavoriamo sui booleani e la tabella dei valori è quella vista per | in precedenza.

Console.WriteLine(true || true);
Console.WriteLine(false || true);


restituisce

True
True


lo stesso output si ha con | che però valuta sempre destra e sinistra il che è penalizzante, almeno nel caso espresso nella prima riga (nella seconda bisogna comunque procedere alla valutazione del ramo di destra)..

L'ultimo simbolo tra gli operatori logici è il ! che corrisponde al NOT, in pratica la negazione dell'operando.

Console.WriteLine(!true);
Console.WriteLine(!false);


questo frammento di codice restituisce ovviamente:

False
True


L'operando deve essere di tipo booleano, sia esso una variabile, un valore o una espressione, in pratica non potete scrive cose del tipo:

!2

mentre si può:

!(x == 0)

ad esempio, come anche nei casi precedenti, evidentemente, in quanto viene coinvolta una espressione booleana.

Gli operatori di incremento ++ e --, sono tra i più semplici e non sembrerebbero contenere particolarità. In realtà le cose possono non essere così banali. Vediamo un esempio:
 
  Esempio 13.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;

class Test
{
  public static void Main()
  {
    int x = 5;
    int y = 5;
    Console.WriteLine(x++);
    Console.WriteLine(x);
    Console.WriteLine(++y);
    Console.WriteLine(y);
  }
}

L'output è il seguente:

5
6
6
6

Vedete? L'output alla riga 9 è diverso rispetto a uello della sua omologa riga 11. Esiste una differenza tra il preporre l'operatore ++ e metterlo invece dopo la variabile. Si parla di preincremento e di postincremento. In pratica alla riga 9 viene prima letto di valore di x, che viene stampato e poi è eseguita l'operazione di incremento che ci viene rivelata dall'outpur proposto alla riga 10. Mentre alla riga 11 viene eseguito dapprima l'incremento poi l'operazione di scrittura. Potete provare anche con -- (predecremento e postdecremento)e otterrete un output simile. Questo è un fatto su cui porre attenzione per evitare magari di ottenere risultati inattesi.

Gli operatori di shift invece non sono banali. Partiamo con un esempio:

  Esempio 13.2
1
2
3
4
5
6
7
8
9
10
11
12
using System;

class Test
{
  public static void Main()
  {
    int x = 5;
    int y = 5;
    Console.WriteLine(x << 3);
    Console.WriteLine(y >> 2);
  }
}

L'output è:

40
1

Gli operatori << e >> comportano uno shift in termini di bit che è definito tramite il numero che li segue. Il numero 5 in ormato binario è:
0101
effettuando uno shift di 3 posizioni verso sinistra  diventa
0101000
ovvero 40.
Invece se effettuiamo uno shift verso destra di 2 posizioni si ha:
1
che è appunto il risultato proposto alla riga 10.
Si possono fare tutti gli esempio che volete, ma bisogna porre un po' di attenzione:

int x = 1:
x << 31 // vale -2147483648

questo perchè siamo passati da
0000 0000 0000 0000 0000 0000 0000 0001
a
1000 0000 0000 0000 0000 0000 0000 0000

Ancora più interessante è notare questo:

int x = 1;
Console.WriteLine(x << 34);

L'output è il numero 4. Perchè mai? Perchè int è definito, come sappiamo, su 32 bit e quindi operare uno spostamento di 34 bit equivale, detto in soldoni, ad effettuare uno spostamento di 34 % 32 posizioni  quindi 1 diventa 100 ovvero 4 (ricordiamoci che siamo nel mondo "binario"). Se avessimo definito x come long invece, dal momento che ci muoviamo su 64 bit invece di 32, il risultato sarebbe stato.
17179869184 che in binario è proprio 1 seguito da 34 zeri. Vi lascio il piacere di sperimentare, personalmente trovo la cosa un po' tediosa :-).
Tuttavia occorre fare attenzione a cose di questo tipo

byte x = 4;
byte y = x << 2;

Il compilatore si lamenta:

error CS0266: Non è possibile convertire in modo implicito il tipo 'int' in 'byte'. È presente una conversione esplicita. Probabilmente manca un cast.

Questo perchè internamente anche l'operatore >>, così come <<, converte a int, per cui è necessario ricorrere ad un cast per far funzionare le cose. Una soluzione, pertanto, è la seguente:

byte y = (byte)(x << 2); 

Comunque i byte sono argomento accettabile per gli operatori di shift, con le dovute precauzioni, così come lo sono ovviamwente gli int, i long e anche i char.  Cosa succede, appunto, applicandolo a questi ultimi? Molto semplice, ad esempio:

Console.WriteLine('a' << 2);

dà come risultato il numero 388 che è 97 << 2. Basta ricordare che 'a' codificato ASCII è proprio il numero 97.

Più semplici sono gli operatori di confronto o comparazione. La tabella che segue è banale:

Simbolo Significato


> Maggiore di
> Minore dì
>= Maggiore o uguale di
<= Minore o uguale di
== Uguale a
!= Diverso da

Un esempio dovrebbe essere più che sufficiente:

  Esempio 13.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;

class Test
{
  public static void Main()
  {
    Console.Write("Inserisci un numero: ");
    int x1 = Int32.Parse(Console.ReadLine());
    Console.Write("Inserisci un altro numero: ");
    int x2 = Int32.Parse(Console.ReadLine());
    if (x1 > x2) Console.WriteLine("primo maggiore del secondo");
    if (x1 == x2) Console.WriteLine("Uguali");
    if (x1 < x2) Console.WriteLine("Secondo maggiore del primo");
  }
}

L'operatore di accesso ai membri, il punto, lo abbiamo già incontrato praticamente ovunque. In pratica esso ci permette di "entrare" da una unità più grossa, ad una più piccola, finmo ad arrivare ai membri di classi o strutture.
Il classico operatore [ ] è anch'esso un nostro gradito ospite che ben conosciamo. In realtà esso non è utilizzato solo per le matrici e indicizzatori ma anche per attributi e puntatori, Questi ultimi due aspetti, meno consueti, li vedremo altrove. Per il momento non ci sono particolarità ulteriori.

Dell'uso delle parentesi tonde() per il cast ne parleremo altrove.

L'operatore ternario o condizionale  è invece un caso un po' particolare, non fosse che per il fatto che è costituito da una coppia , ovvero : ? che vengono usati separatamente. Di esso si può anche fare a meno ma è abbastanza espressivo ed è presente in formato del tutto simile anche in altri linguaggi. In pratica, se avete una parte di codice come la seguente:

if (x == 0) y = 4
else y = 3


può essere riscritto come:

y = (x == 0) ? 4 : 3;

Esempio:

  Esempio 13.4
1
2
3
4
5
6
7
8
9
10
11
12
using System;

class Test
{
  public static void Main()
  {
    Console.Write("Inserisci un numero: ");
    int x1 = Int32.Parse(Console.ReadLine());
    string s1 = (x1 % 2 == 0) ? "numero pari" : "numero dispari";
    Console.WriteLine("Hai inserito un " + s1);
  }
}

in stile funzionale questo operatore può essere usato come valore di ritorno di un metodo.

return x == 9 ? 3 : 8;

Proseguendo nell'analisi degli operatori presenti nella tabella introduttiva, per ora tralasciamo quanto riguarda i delegati e anche l'uso di new in sede di creazione degli oggetti, entrambi gli argomenti sono trattati in altre sezioni. Occupiamoci invece degli operatori che forniscono informazioni sui tipi. Il primo è
sizeof che fornisce la dimensioe in byte dei tipi che costituiscono i suoi operandi ed è utile in particolare per analizzare quei tipi, come gli enumerativi o le struct definite dall'utente, di cui non conoscono a priori le dimensioni. Questo però ha senso solo in ambito unsafe e per ora non ce ne occupiamo (è già complesso di suo l'ambito normale). Potete comunque provare per avere la certezza del peso dei soliti tipi:

Console.WriteLine(sizeof(Int32));
Console.WriteLine(sizeof(Char));


typeof ha un uso più complicato e lo vedremo nella sezione degli esempi. In particolare la sua utilità è legata ai concetti di Reflection e metadata, chiaramente al di là dei nostri attuali scopi e delle nostre conoscenze nello studio del linguaggio. Se si vogliono informazioni sul tipo di un certo elemento per ora faremo uso del già noto GetType()

int x = 0;
Console.WriteLine(x.GetType());


Di uso molto più semplice e immediato è is. Questo operatore permette di determinare se un oggetto è compatibile con un certo tipo. La sua sintassi è molto semplice:

elemento is tipo

Un esempio banale, tanto banale che già in fase di compilazione vi verrà suggerito il risultato, è il seguente:

  Esempio 13.5
1
2
3
4
5
6
7
8
9
10
11
using System;

class Test
{
  public static void Main()
  {
    int x = 0;
    if (x is Int32) Console.WriteLine("x e' un intero");
    if (!(x is Char)) Console.WriteLine("x non e' un carattere");
  }
}

Ovviamente un test del genere ha senso quando vi siano complesse gerarchie di classi e si debba testare se un certo elemento appartiene ad una data classe di vostro interesse, ma l'uso è fondamentalmente questo, e non dovreste riscontrare difficoltà di sorta.

Interessante è anche as che compie una conversione tra tipi reference compatibili o tipi nullable. Il suo funzionamento è molto simile ad un cast ma con un fondamentale differenza: mentre se quest'ultimo non è realizzabile abbiamo un errore, con as la mancata assegnazione provoca semplicemente che il ricevente abbia valore null. Questo non è proprio bello, a mio avviso, ma ce lo dobbiamo tenere com'è. Di seguito un semplice esempio. Parleremo ancora di is e as in molti altri punti del nostro percorso:

  Esempio 13.6
1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
class Program
{
  public static void Main()
  {
    object obj01 = 5;
    object obj02 = "pippo";
    string s1 = obj01 as string; //null!
    string s2 = obj02 as string;
    Console.WriteLine(s1);
    Console.WriteLine(s2);
  }
}

L'ultimo operatore di cui parliamo in questo paragrafo è ?? ovvero l'operatore di "coalescenza". Esso restituisce l'operando di sinistra se non è null, altrimenti restituisce il destro. L'operatore di coalescenza può essere applicato solo su tipi Nullable.

  Esempio 13.7
1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
class Program
{
  public static void Main()
  {
    int? x = null;
    int y = x ?? 8;
    Console.WriteLine(y);
    int? z = 6;
    int t = z ?? 9;
    Console.WriteLine(t);
  }
}

L'output è:

8
6

Come è naturale. Nel primo caso infatti x è null quindi, come da definizione, viene restituito il valore di destra, siamo alla riga 7 esposta alla 8. Nel secondo caso z vale 6, ovvero non è null, quindi è quel valore, che si trova a sinistra dell'operatore di coalescenza, è quello che viene preso in considerazione.
E' interessante notare che ?? è l'unico operatore che permette di assegnare un tipo nullable ad uno non nullable, altre strade porterebbero ad un errore in compilazione. Attenzione anche ai cast.