C#

ARRAY E COLLEZIONI

Eccoci ad un altro argomento cardine per il nostro linguaggio e anche per altri: gli array. La potenza di questa struttura dati, la sua duttilità, i tanti modi per manipolarla la rendono praticamente indispensabile da un punto di vista pratico. A ciò si aggiunge una notevole semplicità concettuale. Nonostante questo non è il caso di prendere sottogamba il discorso che nasconde peculiarità decisamente interessanti ma anche in qualche modo insidiose.
Da un punto di vista concettuale si tratta di una collezione di elementi dello stesso tipo ciascuno dei quali è individuato tramite un indice. Tale indice è costituito da numeri interi progressivi. Sottolineo il fatto che gli oggetti devono essere dello stesso tipo, diversamente da quanto avviene ad esempio nei linguaggi dinamici anche se esiste un escamotage per simulare un comportamento più malleabile (cosa che peraltro non consiglio). Tuttavia essi possono essere di qualsiasi tipo (anche classi o delegati, per dire). Da un punto di vista tecnico gli array sono organizzati in una zona contihua di memoria, il che garantisce una buona efficienza in fase di accesso ai singoli elementi che è il vero punto di fornza degli array. Di questa zona viene fornito l'indirizzo (offset) e la dimensione.
Gerarchicamente parlando tutti gli array discendono dalla classe System.Array a sua volta derivante dall'immancabile System.Object. Avremo quindi a disposizione proprietà e metodi in abbondanza. In questo senso gli array sono reference types e verso ciascun array verrà pertanto fornito, come accennato, un riferimento.

La dichiarazione di un array può essere statica o dinamica; nel primo caso assegneremo una dimensione all'array in fase di codifica, nel secondo lo faremo a runtime. Quello che è sicuro è che una volta attribuita un dimensione questa è immutabile e non potrà essere modificata nell'intero corso del programma. Ci sono altre stutture, come vedremo, che permettono di effettuare cambiamenti alle proprie dimensioni. Ogni elemento di un array è, come detto, accessibile tramite indice. Questo è posto all'interno di una coppia di parentesi quadrate []. In C# gli array sono 0-based, ovvero gli indici, interi col segno, iniziano da 0; questa è una specifica del CLS ed è quindi caratteristica comune a tutti i linguaggi .Net compliant. Tra l'altro gli array con indice iniziale 0 sono stati oggetto di ottimizzazioni prestazionali da parte di Microsoft.  Il CLR comunque supporta, se proprio li volete, anche array non 0 based, come vedremo in un esempio più avanti. Detto questo, da qualunque valore partiate gli indici  procedono sequenzialmente senza salti.

Vediamo ora come dichiarare un array formalmente. La forma più semplice è:

tipo[] nomearray;

ovvero, ad esempio:

(1)  int[] arr01;

che crea semplicemente un riferimento ad un array che sarà composto da elementi di tipo intero. Il suo nome è arr01 e, con questo tipo di dichiarazione, non è utilizzabile direttamente. A questo scopo si deve aggiungere una seconda riga:

(2) arr01 = new int[100];

e allora abbiamo creato un array di interi capace di ospitare 100 elementi. Ovviamente ci sono scritture più compatte:

int arr01 = new int[100];

è la sintesi della (1) e (2) precedenti. Ma che cosa contiene l'array che abbiamo appena creato? Come vedremo è possibile fornire una inizializzazione diretta per ogni singolo elemento. In assenza di essa ogni elemento contiente il valore di default per il tipo. Quindi avremo ad esempio

0      per gli interi
0.0   
per i numeri i virgola
blank 
per i caratteri
""    
la stringa vuota per le stringhe
null  
per le classi
ecc...

In questo caso quindi avremo un array che contiene 100 elementi tutti uguali a 0.
Vediamo ora come dichiarare gli array nelle varie modalità che il linguaggio ci mette a disposizione:

Dichiarazione Spiegazione


int[] ar01 = {1,2,3}; Viene dichiarato un nuovo array di interi contenente i 3 elementi con i valori indicati all'interno delle parentesi graffe. Le dimensione dell'array sono implicitamente espresse dal numero degli elementi


int[] ar02 = new int[3]; Definizione vista in precedenza. ar02 è un array composto da 3 elementi i quali, in assenza di dichiarazione esplicita degli stessi saranno 0 0 0 , valore di deafult per il tipo intero


int[] ar03 = new int[3] {1,2,3}; Equivale a quella specificata nel primo caso, solo in forma più verbosa. Si può definire una sintesi delle prime due definizioni.


int[] ar04 = new int[] {1,2,3}; L'array risultante è uguale ad ar01 ed ar03 dei casi precedenti. Anche qui, abbiamo una indicazione implicita delle dimensioni dell'array.
   

Per array di piccole dimensioni la 1 e la 2 sono le forme più usate per quelli grandi dipende dal metodo di inizializzazione.
In queste definizioni incontriamo un nuovo impotantissimo operatore, ovvero new. Esso ha lo scopo di caricare un nuovo elemento nello heap. E' implicitamente evocato anche nella definizione di ar01. Parleremo ancora di new, che ritroveremo in numerosissime alte occasioni, nel paragrafo destinato ad illustrare gli operatori in C#.
Ma come è fatto un array? Da un punto di vista morfologico, da usare più che altro come schema mentale per meglio comprenderli, possiamo immaginarli così:

int[] arr01 = {1,2,3,4,5};

ecco qua:

arr01-dati 1 2 3 4 5
arr01-indici 0 1 2 3 4

A proposito degli indici va ricordato che C# non permette, a differenza di alcuni altri linguaggi, l'uso di indici negativi; se provate ad usarli otterrete un warning in compilazione ed un errore a runtime. L'accesso ai singoli elementi, come già detto, avviene attraverso l'operatore []. Esso ci permette insomma di estrarre i valori ed assegnarli, purchè al suo interno vi sia indicato un intero coerente, col range di valori ammessi come indici, quindi al'interno del massimo e del minimo consentiti. Ad esempio nel caso di arr01 appena visto l'intervallo di valori va da 0 a 4. Vediamo l'assegnazione e l'estrazione del valore nel seguente esempio completo:

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

class Test
{
  public static void Main()
  {
    int[] arr01 = new int[5];
    arr01[2] = 7;
    Console.WriteLine(arr01[1]);
    Console.WriteLine(arr01[2]);
  }
}

che restituisce il seguente output:

0
7

come è ovvio; il primo valore è quello alla seconda cella dell'array e deve essere 0. Il valore 7 è quello da noi assegnato con l'istruzione alla riga 8. Se avessimo usato per l'assegnazione un indice non valido avremmo avuto un errore, un'eccezione, a runtime molto chiara nel suo contenuto

System.IndexOutOfRangeException:

"indice fuori range", ci suggerisce. Meglio questo di un silente comportamento anomalo del programma con una serie di risultati misteriosi e arcani, fonte di bugs di difficile rilevazione, a volte. Il check dei range normalmente non influisce in modo significativo sulle prestazioni e comunque, se proprio siete preoccupati di quyesto, è possibile evitare questo controllo.
Un altro punto importante per l'operazione di l'assegnazione è quello relativo alla corretta gestione dei tipi. E' ovviamente necessario conservare la compatibilità tra il valore che si vuole assegnare e quello base dell'array. Ad esempio:

int[] arr01 = new int[] {1,2,3}

Se a questo punto scrivessimo qualcosa come:

arr01[0] = "ciao";

vi arriverebbe un messaggio dal compilatore:

error CS0029: Impossibile convertire implicitamente il tipo 'string' in 'int'.

quindi, come è intuitivo, una stringa non la potete infilare al posto di un intero. Invece è legale scrivere:

arr01[0] = 'a';

in quanto esiste la conversione implicita da carattere ad intero. In questo caso il valore inserito sarebbe 97, ovvero il codice Ascii di 'a'. Bisogna comunque fare attenzione, il compilatore ci aiuta fin che può ma miracoli non ne fa neanche lui. Il paragrafo relativo alle conversioni di tipo sarà molto interessante anche per queste finalità.

Ora che abbiamo più chiaroi concetti di base andiamo a vedere un po' di "internals". Quando scriviamo qualcosa come:

int[] arr01 = {1,2,3};

abbiamo creato un reference type, è noto. Di fatto vengono interessati sia lo stack (molto marginalmente) che lo heap. Il primo conterrà una variabile di nome arr01 che a sua volta conterrà il valore di memoria iniziale della zona dello heap che contiene gli elementi. Visivamente avremmo:



Questo ha un'altra interessante conseguenza, sia pure del tutto trasparente in molti casi, cioè che elementi di tipo valore, come gli interi del nostro esempio, possono andare a vivere all'interno dello heap. Solo le variabili locali di tipo valore sono allocate nello stack. Dall'immagine qualcuno potrebbe arguire che in realtà anche lo stack è interessato alla creazione di un array. Come detto esso contiene solo ed esclusivamente un indizzo di memoria che si trova all'interno dello heap. Tutti i dati reali dell'array, ivi compresa la sua lunghezza stanno nello heap. Il primo dato presente infatti nell'immagine precedente è la lunghezza dell'array.

Torniamo ora al discorso del tipo base dell'array. Come abbiamo detto nell'introduzione, altri linguaggi, ad esempio molti di quelli dinamici come Ruby e Python, permettono una notevole libertà di tipizzazione per gli elementi di un array. E' possibile qualche cosa del genere in C#? Si, con un piccolo trucco. Vediamo  il seguente esempio:

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

class Test
{
  public static void Main()
  {
    object[] arr01 = new object[4];
    arr01[0] = 'a';
    arr01[1] = 1;
    arr01[2] = "ciao";
    arr01[3] = 2.1;
    Console.WriteLine(arr01[0]);
    Console.WriteLine(arr01[2]);
  }
}

Il trucco è molto semplice: è sufficiente definire un array il cui tipo base è la radice del type system, ovvero object. In questo modo possiamo comprendere nello stesso array, come nell'esempio, interi, stringhe, caratteri e float e altro ancora, volendo. Ritengo questa soluzione utile in qualche raro caso ma ritengo anche che abbia un certo peso prestazionalmente parlando, non ho fatto test specifici in merito ma sono presenti operazioni di boxing che non possono non influire in maniera sensibile e a fronte di array di grossa dimensione il carico dovrebbe essere significativo. Vi lascio il piacere di fare dei test, il solito Ildasm è molto chiaro in proposito, ad esempio i seguenti frammenti di codice danno origine ad un IL abbastanza chiaro:


int[] arr01 = new int[2];
arr01[0] = 1;
arr01[1] = 2;
object[] arr01 = new object[2];
arr01[0] = 1;
arr01[1] = 2;


.method public hidebysig static void Main() cil managed
{
.entrypoint
// Code size 17 (0x11)
.maxstack 3
.locals init (int32[] V_0)
IL_0000: nop
IL_0001: ldc.i4.2
IL_0002: newarr [mscorlib]System.Int32
IL_0007: stloc.0
IL_0008: ldloc.0
IL_0009: ldc.i4.0
IL_000a: ldc.i4.1
IL_000b: stelem.i4
IL_000c: ldloc.0
IL_000d: ldc.i4.1
IL_000e: ldc.i4.2
IL_000f: stelem.i4
IL_0010: ret
} // end of method Test::Main
.method public hidebysig static void Main() cil managed
{
.entrypoint
// Code size 27 (0x1b)
.maxstack 3
.locals init (object[] V_0)
IL_0000: nop
IL_0001: ldc.i4.2
IL_0002: newarr [mscorlib]System.Object
IL_0007: stloc.0
IL_0008: ldloc.0
IL_0009: ldc.i4.0
IL_000a: ldc.i4.1
IL_000b: box [mscorlib]System.Int32
IL_0010: stelem.ref
IL_0011: ldloc.0
IL_0012: ldc.i4.1
IL_0013: ldc.i4.2
IL_0014: box [mscorlib]System.Int32
IL_0019: stelem.ref
IL_001a: ret
} // end of method Test::Main

Fate le vostre considerazioni, vi ho evidenziato in rosso le righe a mio avviso più critiche.

Altro aspetto interessante è quello relativo alla tipizzazione implicita che si riagancia in qualche modo a quanto appena visto:

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

class Test
{
  public static void Main()
  {
    var arr01 = new [] {1, 1.1, 90};
    // var arr02 = new [] {1, "a", 0.7};
  }
}

Le righe 7 e 8 introducono ciascuna un array (tramite una dichiarazione un po' diversa dal solito) per il quale, come effetto della keyword var che introduce l'inferenza di tipo, è il compilatore che deve dedurre il tipo migliore per tutti gli elementi. Compito portato a termine con successo per arr01. La riga 8 è invece commentata in quanto il compilatore emitterebbe un errore se gliela dessimo in pasto:

error CS0826: Impossibile trovare il tipo migliore per la matrice tipizzata in modo implicito

In pratica ci dice che non riesce a trovare un tipo che vada bene per tutti gli elementi. D'accordo, ci sarebbe object ma in questo caso il team di sviluppo del compilatore ha posto dei paletti, non si può fare per evitare di introdurre pesanti overhead in caso di grossi array. Insomma va bene la libertà ma non l'anarchia totale (altro indizio che ci fa pensare che usare array di oggetti, visti in precedenza, non sia proprio una gran soluzione). Gli array implicitamente tipizzati vanno bene in alcuni ambiti, li ritroveremo quando parleremo di LINQ, ma non sono da usare in maniera smodata

ARRAY MULTIDIMENSIONALI

Gli array che abbiamo visto finora sono monodimensionali ma C# supporta benissimo anche quelli a più dimensioni in due forme: array rettangolari e jagged array. Quando parliamo di "più dimensioni" intendiamo array n-dimensionali con n grande a piacere. Iniziamo quindi a parlare degli array rettangolari, illustrando subito la modalità standard di dichiarazione:

int[ , ] rectarr01= new int[3,3]; // definisce un array rettangolare 3 x 3
int[ , , ] rectarr02 = new int[3,3,3]; // definisce un aray rettangolare 3 x 3 x 3
int[ , ] rectarr03 = new int[] {[1,2], [3,4]}; // definisce un array 2 x 2 inizializzato con valori diversi dal default

L'accesso al singolo elemento avviene specificando tutte le sue coordinate, quindi serviranno 2 indici per un array bidimensionale, 3 per per un tridimensionale e così via. Vediamo ora un esempio esplicativo:

  Esempio 11.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;

class Test
{
  public static void Main()
  {
    int[,] marr01 = new int[3, 3];
    for(int i = 0; i <= 2; i++)
    {
      for(int j = 0; j <= 2; j++)
      {
        marr01[i,j] = ((i + 1) * 10) + j + 1;
      }
    }
    for(int i = 0; i <= 2; i++)
    {
      for(int j = 0; j <= 2; j++)
      {
        Console.Write(marr01[i,j] + " ");
      }
    Console.WriteLine();
    }
  }
}

che presenta il seguente output:

11 12 13
21 22 23
31 32 33

La riga 7 definisce un array 3 x 3 che viene riempito di valori dalla riga 8 alla 14.
Come è facile vedere il codice si complica abbastanza e quei cicli for innestati non sono il massimo per la manutenibilità. Figuriamoci quando si aumenta il numero di dimensioni. Per ora teniamoceli così e proseguiamo invece con un'altra osservazione che mi pare abbastanza interessante. Definiamo infatti due array come segue:

public static void Main()
{
int[] ar1 = new int[1];
int[,] ar2 = new int[1, 1];
}

ovvero creiamo un array monodimensionale di un solo elemento e uno bidimensionale 1x1 e vediamo cosa ci suggerisce il fido ildasm:

.entrypoint
// Code size 17 (0x11)
.maxstack 2
.locals init (int32[] V_0,
int32[0...,0...] V_1)
IL_0000: nop
IL_0001: ldc.i4.1
IL_0002: newarr [mscorlib]System.Int32
IL_0007: stloc.0
IL_0008: ldc.i4.1
IL_0009: ldc.i4.1
IL_000a: newobj instance void int32[0...,0...]::.ctor(int32,int32)
IL_000f: stloc.1
IL_0010: ret


Vi ho evidenziato le due righe che si riferiscono alla creazione degli array. Nel primo caso viene generato un array tramite la, ovvia, chiamata newarr seguita dal tipo. Nel secondo la costruzione è più complessa e si avvale della istanziazione di una oggetto seguito da una chiamata (.ctor) che effettua l'inizializzazione. Come si può capire il primo sistema è molto più efficiente. E c'è dell'altro. Proviamo ad effettuare un'assegnazione di valore aggiungendo queste istruzioni:

ar1[0] = 5;
ar2[0,0] = 5;


che daranno origine rispettivamente a:

IL_0012: ldc.i4.5
IL_0013: stelem.i4


e

IL_0017: ldc.i4.5
IL_0018: call instance void int32[0...,0...]::Set(int32,int32,int32)

La prima sequenza usa un comando di storing (stelem) nativo del CLR (altre istruzioni dedicate agli array sono ldelem, ldelema, ldlen), la seconda è chiaramente più complessa. .Net è ottimizzato per l'uso di array monodimensionali e quindi tutto questo vuol dire semplicemente che gli array multidimensionali sono strumenti potenti ma non esattamente i più efficienti del linguaggio. Usateli solo quando necessari, tra l'altro essi, per tutta una serie di motivi, sono trattati dal CLR come se fossero non 0-based. Insomma introducono un overhead non indifferente.

Gli array rettangolari possono essere visti come casi particolari degli array jagged. A loro volta questi ultimi sono degli array di array quindi possono avere una forma rettangolare o decisamente irregolare. Anche la definizione è un po' particolare:

int[][] jagged01 = new int[n][];

Questa scrittura ci indica la creazione di un array composto di altri n array al momento non noti ma che andranno esplicitati prima dell'uso di jagged01. Ci vuole un esempio, lo so:

  Esempio 11.5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;

class Test
{
  public static void Main()
  {
    int[][] jagged = new int[3][];
    jagged[0] = new int[3] { 1, 5, 7 };
    jagged[1] = new int[7];
    jagged[2] = new int[2] { 7, 8 };
    for(int i = 0; i < 3; i++)
    {
      for(int j = 0; j < jagged[i].Length; j++)
      Console.Write(jagged[i][j] + " ");
      Console.WriteLine();
    }
  }
}

Alla riga 7 viene definito un array jagged composto da altri 3 array. L'output è il seguente e ci mostra come è fatto l'array nel suo complesso:

1 5 7
0 0 0 0 0 0 0
7 8

Per la definizione alla riga 7 ildasm ci mostra il codice MSIL generato:

 maxstack 1
.locals init (int32[][] V_0)
IL_0000: nop
IL_0001: ldc.i4.3
IL_0002: newarr int32[]
IL_0007: stloc.0
IL_0008: ret


Volendo complicare un po' le cose è possibile mischiare array jagged e multidimensionali. Ad esempio:

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

class Test
{
  public static void Main()
  {
    int[][,] mixjag = new int[3][,];
    mixjag[0] = new int[,] { { 1, 2 }, { 3, 4 } };
    mixjag[1] = new int[,] { { 10, 20 }, { 30, 40 } };
    mixjag[2] = new int[,] { { 100, 200 }, { 300, 400 } };
    Console.WriteLine("mixjag[1][1,0]: " + mixjag[1][1, 0]);
  }
}

L'inizalizzazione è svolta dalle righe 8 alla 10 e non è particolarmente agevole (quindi figuratevi se le cose si complicassero ulteriormente ad esempio aumentando il numero delle dimensioni) mentre alla riga 11 accediamo ad un singolo elemento. Francamente non mi è mai capitato di vedere qualcuno che si è torturato con un codice simile....

Fino ad ora abbiamo usato dei tipi base predefiniti ma possiamo creare array anche con elementi appartenenti a tipologie create dall'utente.
Vediamo scosa si può fare:

  Esempio 11.7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;

class Test
{
  struct foo
  {
    public int x;
    public string s;
  }

  public static void Main()
  {
    foo[] arr01 = new foo[5];
    Console.WriteLine(arr01[0].x);
    arr01[1].x = 5;
    arr01[1].s = "ciao";
    Console.WriteLine(arr01[1].s);
  }
}

Il codice nell'esempio 11.7 presenta dalla riga 5 alla 9 la creazione di una struct, per la quale vi rimando all'apposito paragrafo. La riga 13 crea un nuovo array ogni elemento del quale è appunto una struct con i suoi bravi campi, un intero ed una stringa correttamente inizializzati di default (evidenziato alla riga 14) e con valori da noi assegnati alle righe 15 e 16. Una struct è un tipo valore e quindi non ci sono problemi particolari. Proviamo a vedere cosa succere con una clase (anche in questo caso potrete tornare sull'argomento dopo aver studiato le classi)

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

class Test
{
  class foo
  {
    public int x;
    public string s;
  }

  public static void Main()
  {
    foo[] arr01 = new foo[5];
    Console.WriteLine(arr01[1].x);
  }
}

Questo programma compila, con un paio di warning trascurabili ma genera ina eccezione a runtime. Questo perchè una classe non istanziata è null di default e quindi abbiamo un puntamento ad elemento null, che non è permesso dal linguaggio (per fortuna, direi...). Per rimediare una prima soluzione è quella che propone di istanziazione istanziare ogni elemento:

foo[] arr01 = new foo[5];
for (int i = 0; i < arr01.Length; i++)
arr01[i] = new foo();

Così funziona. E' la soluzione più semplice ma vedremo studiando le classi che si può lavorare in altro modo.

FOREACH

Comoda istruzione, caratteristica delle collezioni in C#. Tipicamente si applica a tutti quegli elementi che implementano le interfacce
System.Collections.IEnumerable o
System.Collections.Generic.IEnumerable<T>

Anche di queste riparleremo, ad ogni modo gli array sono tra questi soggetti validi. Lo scopo di foreach è quello di scorrere l'elemento sul quale è applicato raccogliendo le informazioni che vogliamo in  modalità readonly. Questo significa che è un'istruzione sicura in quanto tramite essa non è possibile modificare i dati che vengono ispezionati. Di contro non può essere usata per inizializzare un array, evidentemente. Una sua particolarità è di poter nativamente gestire eventuali array non 0-based. La sintassi è molto semplice e coinvolge anche la parolina chiave in:

foreach(tipo variabile in array)

Vediamo un esempio:

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

class Test
{
  public static void Main()
  {
    int x = 4;
    int[] arr01 = new int[] {1,2,3,4,5};
    foreach(int i in arr01)
    Console.WriteLine(x * i);
  }
}

Se le istruzioni che si accompagnano a foreach sono più di una è, ovviamente, necessario usare le parentesi graffe che delimitano il blocco di codice.
Anche foreach può essere interrotta da break, return, throw o goto oppure può saltare un ciclo grazie a continue, come è facile verificare, vi do giusto un suggerimento:

foreach(int i in arr01)
{
  Console.WriteLine(i);
  if (i == 3) break;
}

Ci sono alcuni dibattiti relativi all'efficienza di foreach rispetto al classico ciclo for. In generale la differenza non è enorme anche se, in base ad alcuni test, sembrerebbe che for sia più efficiente laddove basta una singola selezione su ogni elemento dell'array mentre se l'elmento selezionato deve essere riusato allora foreach potrebbe fare qualcosina meglio. In generale mi piace la leggibilità e la sicurezza di foreach e non ho mai avuto problemi di performance col suo uso, così come con l'uso di for. Tutto sommato non conviene farsi troppi problemi.

DEEP COPY E SHALLOW COPY

L'argomento è interessante anche in senso generale e ci può far capire alcuni dei meccanismi relativi ai tipi in questo linguaggio (in .Net in generale).
Partiamo da un esempio che poi commenteremo:

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

class Test
{
  public static void Main()
  {
    int[] arr01 = new int[] { 1, 2, 3, 4, 5 };
    int[] arr02 = arr01;
    arr01[2] = 9;
    foreach (int i in arr01) Console.Write(i + " ");
    Console.WriteLine();
    foreach (int i in arr02) Console.Write(i + " ");
  }
}

che produce il seguente output:

1 2 9 4 5
1 2 9 4 5

I due array, arr01 e arr02, sono uguali, come testimonia l'output prodotto. Però, alla riga 9, noi abbiamo modificato il valore solo ad arr01. Perchè questo cambiamento si è riflesso anche in arr02? Si tratta del problema delle shallow copy. Visivamente si può spiegare così:

int[] arr01 = new int[] { 1, 2, 3, 4, 5 };

produce in memoria, come ormai noto, una situazione del genere:


ma più interessante è notare che

int[] arr02 = arr01;

produce questa situazione:



ovvero, non viene creato un nuovo array riversandovi in esso i valori di quello originale ma viene soltanto copiato un riferimento. Questo è importante perchè si capisce che cambiando i valori in una zona di memoria che viene puntata da due (o più, non c'è limite) entità allora vengono cambiati i valori relativi a tutte queste. E' un tipico esempio di shallow copy, molto efficiente come è facile intuire, dal momento che viene copiato solo un riferimento ma anche un po' pericolosa ad ignorarne i contenuti più intimi. L'istruzione alla riga 8 non fa altro che cambiare un valore in una zona di memoria puntata da due array ed entrambi, anche quello apparentemente non coinvolto, saranno in realtà modificati. E' facile verificare che lo stesso identico risultato si ottiene se alla riga 8 la sostituzione la effettuiamo su di un elemento di arr02. Il framework ci offre altre vie per effettuare una copia di array ma nessuna di queste soddisfa la realizzazione di una deep copy. Però la questione non è così semplice e presta il fianco a qualche complicazione. Teniamo come base l'esempio 11.10. Le istruzioni ci cui ci occuperemo in relazione al loro comportamento sono Clone e CopyTo. Non sono esclusive degli array e le troveremo altrove.
Vediamo in azione la prima, sostituendo la riga 8 con la seguente:

int[] arr02 = (int[])arr01.Clone();

e l'output che è il seguente:

1 2 9 4 5
1 2 3 4 5

Ma allora... ce l'abbiamo fatta... abbiamo una deep copy... si e no. Se Usassimo CopyTo avremmo lo stesso risultato, useremo, invece della riga 8, le 2 seguenti istruzioni:

int[] arr02 = new int[5];
arr01.CopyTo(arr02, 0);


come detto, ci sarà lo stesso output.
Insomma, abbiamo una deep copy? MSDN, se avete voglia di consultarlo in merito, vi darà una delusione e vi dirà di no. Finchè si tratta di array di tipo valore la cosa in realtà funziona. In questo caso infatti abbiamo una copia bit x bit ed in effetti nella pratica viene creata un'area di memoria in cui riversare il risultato di tale copia. Clone, come CopyTo copia i valori ma quando ha a che fare con reference type copia solo i riferimenti e non gli oggetti ai quali questi puntano. La cosa vale in maniera più generalizzata con i tipi nativi come anche le strnghe. Se però definiamo un tipo custom come una classe allora le cose vanno diversamente. Lo vediamo ancora meglio con un esempio, nel quale vi sono certamente alcune cose che non vi saranno molto chiare se non avete già un certo skill su questo tipo di programmazione ma in questa sede valgono i concetti, sul codice potrete sempre tornare successivamente:

  Esempio 11.11
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System;

public class Stringa
{
  public string s;
}

class Test
{
  public static void Main()
  {
    Stringa[] x1 = new Stringa[1];
    x1[0] = new Stringa(); // necessaria istanziazione di classe
    x1[0].s = "ciao";
    Stringa[] x2 = new Stringa[1];
    x1.CopyTo(x2, 0);
    Stringa[] x3 = (Stringa[])x1.Clone();
    Console.WriteLine(x1[0].s);
    Console.WriteLine(x2[0].s);
    Console.WriteLine(x3[0].s);
    x2[0].s = "mamma";
    Console.WriteLine(x1[0].s);
    Console.WriteLine(x2[0].s);
    Console.WriteLine(x3[0].s);
  }
}

che ci restituisce:

ciao
ciao
ciao
mamma
mamma
mamma

Tutto regolare? Insomma.... è abbastanza facile dedurre che le modifiche effettuate su un singolo elemento (alla riga 14 e alla riga 21 avvengono le modifiche in parola) hanno effetto su tutti gli array presenti nel programma. La causa è il fatto che, anche in questo caso, vengono copiati dei riferimenti e, come abbiamo visto, questo comporta la proliferazione delle variazioni che introduciamo. Non c'è quindi una copia completamente indipendente dei singoli elementi che pertanto non vivono di vita propria. Visivamente abbiamo:



Con 3 classi all'interno ci ciascun array (x1, x2, x3) al cui interno abbiamo dei riferimenti alla stringa.
Avrete comunque notato che i metodi come Copy e CopyTo si comportano megli di = che si limita a buttare lì un puntatore e non fa altro.
Vogliamo a questo punto fare una deep copy? Il sistema è un po' macchinoso ma funziona:

  Esempio 11.12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;

public class Stringa
{
  public string s;
}

class Test
{
  public static void Main()
  {
    Stringa[] x1 = new Stringa[1];
    Stringa[] x2 = new Stringa[1];
    x1[0] = new Stringa();
    x2[0] = new Stringa();
    x1[0].s = "ciao";
    x2[0].s = x1[0].s;
    Console.WriteLine(x1[0].s);
    Console.WriteLine(x2[0].s);
    x1[0].s = "mamma";
    Console.WriteLine(x1[0].s);
    Console.WriteLine(x2[0].s);
  }
}

che ci dà:

ciao
ciao
mamma
ciao

da qui si vede che la modifica introdotta alla riga 20 ha effetto solo su x1 e non su x2 che quindi è del tutto indipendente.
Il problema della shallow copy non riguarda i value types. Cosa succede però quando i due tipi, per valore e per riferimento, si mischiano? Vediamo l'esempio seguente:

  Esempio 11.13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;

public struct P
{
  public int x;
  public int y;
  public int z;
  public int[] arr;
}

class Test
{
  public static void Main()
  {
    P p1 = new P();
    p1.x = 0;
    p1.y = 1;
    p1.z = 2;
    p1.arr = new int[] { 1, 2, 3 };

    P p2 = new P();
    p2 = p1;
    p2.x = 10;
    p2.arr[1] = 20;
    Console.WriteLine(p1.x);
    Console.WriteLine(p1.arr[1]);
  }
}

Qui dalla riga 3 alla 9 abbiamo definito una struct, ovvero un value type. Al suo interno, riga 8 definiamo un array, un reference. Come si può notare dall'output

0
20

la variazione introdotta alla riga 23 non ha effetti sul valore di x memorizzato in p1. Al contrario la variazione sul valore modificato nell'array di p2 alla riga 24 ha effetto anche sull'array di p1. In pratica il legame introdotto alla riga 22 ha creato la solita shallow copy relativamente al tipo reference, cioè l'array. Pertanto, anche se l'array è contenuto in un tipo valore, la struct, i  problemi restano i soliti.

ARRAY COME PARAMETRI

C'è un altro uso degli array che è bene conoscere, ed è quello come parametri o come argomenti di ritorno di funzioni. L'utilizzo lo illustrerò con un paio di esempi se avete chiaro il discorso sui metodi, dovrebbe essere tutto chiaro.

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

class Test
{
  static void StampaArray(int[] arr1)
  {
    for (int i = 0; i < arr1.Length; i++)
    Console.WriteLine(arr1[i]);
  }

  public static void Main()
  {
    int[] arr0 = new int[] { 1, 2, 3, 4 };
    StampaArray(arr0);
  }
}

Nell'esempio 11.14 la riga 5 dichuiara un metodo che ha come parametro in ingresso un array.
Detto metodo è richiamato alla 14, come evidenziato, inviando come parametro un array.

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

class Test
{
  static int[] PassaArray()
  {
    int[] arr1 = new int[] { 1, 2, 3, 4 };
    return (arr1);
  }

  public static void Main()
  {
    int[] arr0 = PassaArray();
    foreach(int i in arr0)
    Console.Write(i);
  }
}

In questo caso invece alla riga 5 definiamo un metodo che restituisce un array di interi. Tale metodo viene richiamato alla riga 13 allo scopo di passare l'array arr1 come riferimento per l'array arr0.

LAVORARE CON GLI ARRAY

Uno dei vantaggi del lavorare con gli array è la possibilità di utilizzare tanti comodi strumenti precotti che vi consentono di effettuare parecchie operazioni sugli array stessi senza reinventarsi ogni votla l'acqua calda. Sul sito MSDN troverete tutti i dettagli qui di seguito, in alcuni casi con esempi, illustro quelli più utili. Alcune cose le abbiamo già viste ma forse vale la pena di ripeterle, nel caso in cui questo elenco vi servisse come un piccolo reference.

Metodo

Spiegazione

Array.AsReadOnly

Rimappa l'array in una collection read-only.

BinarySearch

In una array monodimensionale ordinato cerca un elemento usando un algoritmo di ricerca binaria. Restituisce l'indice dell'elemento trovato

Clear

Pone a zero, o false, o null, o blank a seconda del caso un gruppo di elementi di un array definito da un indice iniziale e da un numero indicante quanti elementi sono oggetto della modifica

using System;
class program
{
  public static void Main()
  {
    int[] x = {1,2,3,4};
    Array.Clear(x,0,2);
    foreach(int i in x)  Console.Write(i + " ");
  }
}

Questo programma ha come output 0 0 3 4

Clone

Crea una shallow copy di un array. Può essere necessaria una operazione di cast

using System;
class program
{
  public static void Main()
  {
    int[] x = {1,2,3,4};
    int[] y = (int[])x.Clone();
  }
}

ConstrainedCopy

Effettua una copia di un array in un altro partendo da un dato indice nell'array sorgente e da un altro dato indice nell'array destinazione garantendo che nel caso in cui la copia non andasse a buon fine nessuna modifica verrà memorizzata

ConvertAll

Converte un array di un tipo in un array di un altro tipo.

Copy

Copia un range di elementi da un array ad un altro, eseguendo le conversioni ed i cast richiesti. E' una istruzione che può risultare utile e ha due formati:

Array.Copy(sorgente, destinazione, numero di elementi) oppure

Array.Copy(sorgente, indice di partenza, destinazione, indice di partenza, numero di elementi).

Un esempio che illustra il primo caso:

using System;
class program
{
  public static void Main()
  {
    int[] x = {1,2,3,4};
    int[] y = new int[3];
    Array.Copy(x, y, 3);
    foreach(int i in y)   Console.Write(i + "-");
  }
}

CopyTo

Effettua una copia di di tutti gli elementi di un array monodimensionale in un altro. Contrariamente a quanto detto su molti forum e siti questa non è una deep copy. Una deep copy, come abbiamo già detto,  può soltanto essere implementata manualmente in maniera esplicita.

using System;
class program
{
  public static void Main()
  {
    int[] x = {1,2,3,4};
    int[] y = new int[4];
    x.CopyTo(y, 0); // 0 è l'indice di partenza
    foreach(int i in y)   Console.Write(i + "-");
  }
}
 

CreateInstance

Crea una nuova istanza della classe Array. Ne parliamo di seguito a questa tabella

Equals

Determina l'uguaglianza di due array, funzionalità ereditata da object.

Exists

Specifica se nell'array ci sono elementi che specificano un certo predicato. Di seguito un esempio banale:

using System;
class program
{
  public static void Main()
  {
    int[] x = {1,2,3,4};
    Console.WriteLine(Array.Exists(x, major));
  }
  private static bool major (int i)
  {
    if (i > 2) return true;
    else return false;
  }
}

 

Find

Cerca il primo elemento che risponde ad una certa regola e ne restituisce l'elemento stesso.Rivedendo l'esempio precedente:

using System;
class program
{
  public static void Main()
  {
    int[] x = {1,2,8,4};
    Console.WriteLine(Array.Find(x, major));
  }
  private static bool major (int i)
  {
    if (i > 2) return true;
    else return false;
  }
}

FindAll

Trova tutte le occorrenze che soddisfano una certa condizione

using System;
class program
{
  public static void Main()
  {
    int[] x = {1,2,8,4};
    int[] s = (Array.FindAll(x, major));
    foreach (int y in s)   Console.Write(y + " ");
  }
  private static bool major (int i)
  {
    if (i > 2) return true;
    else return false;
  }
}
 

FindIndex

Restituisce l'indice del primo elemento che soddisfa la condizione. Come esempio basta modificare quello relativo a Find mettendo la funzione FindAll.

FindLast

Restituisce l'ultimo elemento che soddisfa la condizione. Anche in questo caso basta modificare l'esempio precedente.

FindLastIndex

Restituisce l'indice dell'ultimo elemento che soddisfa il predicato.

using System;
class program
{
  public static void Main()
  {
    int[] x = {1,2,8,1.6,0};
    int s = (Array.FindLastIndex(x, major));
   Console.Write(s);
  }
  private static bool major (int i)
  {
    if (i > 2) return true;
    else return false;
  }
}

restituisce 4 ovvero l'indice di 6 che è l'ultimo elemento maggiore di 2 come imposto dalla regola.
 

foreach

Ne abbiamo parlato sopra

GetEnumerator

Restituisce IEnumerator per l'array. Foreach ne fa le veci in quasi tutti i casi

GetHashCode

Derivato da Object restituisce un codice hash per l'array

GetLength

Restituisce il numero degli elementi di un array in una data dimensione che deve essere compresa tra 0 e rank (vedi più avanti) 

GetLongLength

Come GetLength ma il numero di elementi è espresso tramite un intero 64 bit anzichè 32.

GetLowerBound

GetUpperBound

Individuano il minore e maggior indice in una data dimensione di un array. L’uso è Array.GetLowerBound(n-1)  dove n è il numero di dimensioni dell’array. Lo stesso va per GetUpperBound.

using System;
class program
{
  public static void Main()
  {
    int[] arr = new int[] {1,2,3,4,5};
    Console.WriteLine(arr.GetLowerBound(0));
    Console.WriteLine(arr.GetUpperBound(0));
 }
}

 Il risultato è 0 4. 

Ovviamente il limite minore è molto spesso 0 ma se prendono in esame array creati con createInstance o con altri linguaggi potrebbe non essere vero.

GetValue

restituisce il valore all'indice indicato da GetValue

using System;
class program
{
  public static void Main()
  {
    int[] arr = new int[] {1,2,3,4,5};
   Console.WriteLine(arr.GetValue(0));
  }
}

 per array a due dimensioni si usa GetValue(x, y) per array a 3 dimensioni GetValue(x, y, z) ecc...

Initialize

Inizializza gli elementi di un array per tramite di un costruttore, per i tipi che lo ammettono.

IndexOf

Restituisce la prima occorrenza di un dato elemento in un array o -1 se questo non esiste. Utile quindi per sapere dell'esistenza o meno di un elemento in un array

using System;
class test
{
  public static void Main()
  {
    int[] x = new int[5]{1,2,3,4,5};
    Console.WriteLine(Array.LastIndexOf(x,3));
    Console.WriteLine(Array.LastIndexOf(x,9));
  }

 questo programma ha come output 2 e -1 ovvero l'indice del numero 3 e nell'array e l'indice di "non esistenza" di 9.
 

LastIndexOf

Recupera e restituisce l'ultimo indice di un elemento cercato.

using System;
class program
{
  public static void Main()
  {
    int[] arr = new int[] {1,1,2,2,2,2}; 
    Console.WriteLine(Array.LastIndexOf(arr, 2));
  }
}

 il risultato è 5, ovvero l'indice più alto al quale troviamo un 2.

MemberWiseClone

Ereditata da Object crea una shallow copy

Resize

Dal momento che non è possibile cambiare le dimensioni di un array dinamicamente una strada percorribile, se non si vuole usare un'altra struttura dati più flessibile, è usare Resize che rialloca gli elementi di un array in una sua copia di diverse dimensioni.

using System;
class program
{
  public static void Main()
  {
    int[] arr = new int[] {1,2,3,4,5,6,7,8};
    foreach (int i in arr)     Console.Write(i + " ");
    Console.WriteLine();
    Array.Resize(ref arr, 10);
    foreach (int i in arr)     Console.Write(i + " ");
    Console.WriteLine();     Array.Resize(ref arr, 5);
    foreach (int i in arr)     Console.Write(i + " ");
  }
}

 L'output è il seguente:

 

 

Il primo rimanggiamento dell'array introduce due nuovi elemento che vengono inseriti ed inizializzati a 0. Il secondo Resize ne riduce le dimensioni ai soli primi 5 elementi. Questo è un modo efficace per ridimensionare gli array nel caso monodomensionale. A testimonianza del fatto che viene effettivamente creato un nuovo array si può far notare come eventuali altri riferimenti all’array originale restino invariati. Esempio:

using System;
class program
{
    public static void Main()
    {
        int[] ar1 = {1,2,3,4};
        int[] ar2; 
        ar2 = ar1;
        Array.Resize(ref ar1, 10);
        foreach (int x in ar1) Console.Write(x + " ");
       Console.WriteLine();
        foreach (int x in ar2) Console.Write(x + " ");
     }
}
 

Reverse

Inverte gli elementi di un array quindi il primo diventa l'ultimo e viceversa; è possibile ordinare solo range specifici.

Array.Reverse(arr) inverte un array in modo completo

Array.Reverse(arr, 1,4) inverte solo 4 elementi a partire da quello avente indice 1 compreso.

SetValue

Assegna un valore nella locazione di un dato indice. mioArray.SetValue(elemento, indice). E possibile specificare più indici per array a più dimensione (fino a 3) mioArray.SetValue(elemento, indice1, indice2, indice3)

Sort

Ordina gli elementi di un array. Questa istruzione ha diverse varianti. Vediamo un sempio di ordinamento completo e limitato di un array. Non si possono sortare array multidimensionali (ne risulta una rank exception)

using System;
class Program
{
  public static void Main()
  {
    int[] arr1 = new int[] {1,18, 7, 33, 18, 17, 44, 0};
    int[] arr2 = new int[] {1,18, 7, 33, 18, 17, 44, 0};
    Array.Sort(arr1);
    foreach (int x in arr1) Console.Write(x + " ");
    Console.WriteLine();
    Array.Sort(arr2, 2, 5); 
    // ordina 5 elementi a partire da quello all’indice 2
    foreach (int x in arr2) Console.Write(x + " ");
  }
}

ed ecco l'output:

0 1 7 17 18 18 33 44

1 18 7 17 18 33 44 0

ToString

Ereditato da Object, non serve per stampare un array, ma solo il tipo degli elementi dello stesso, se applicato all’array “intero”.

int[] x1 = new int[5];
Console.WriteLine(x1.ToString());

Ci restituisce

System.Int32[]

TrueForAll

Specifica se gli elementi di un array soddisfano un certo predicato



Ritengo molto utili alcune istruzioni per compiere operazioni di routine. Ad esempio IndexOf, come specificato utile per trovare l'esistenza o meno di un certo elemento in un array. O clear. Comunque queste sono mie preferenze personali, vi invito davvero a dare uno sguardo all'elenco qui sopra, troverete molti spunti, magari dopo un paio di letture.

Concludo questo lungo ma, ripeto, utilissomo paragrafo on qualcosa un po' fuori dagli schemi che magari non userete mai.

Di particolare interesse, a mio avviso, il metodo CreateInstance in pratica un sistema alternativo per istanziare un array direttamente a livello di classe (System.Array è una classe astratta e non permette costruttori) anche senza conoscere il tipo degli elementi, che può essere passato per via parametrica. Per esempio è possibile scrivere codice siffatto:

 Type arrayType = typeof(int);
Array arr1 = Array.CreateInstance(arrayType , 100);
arrayType = typeof(char);
Array arr2 = Array.CreateInstance(arrayType , 10);

Come si vede arrayType cambia ed è passato per parametro. 

Nell’esempio che segue noi useremo typeof legato agli interi, in questo esempio l’uso è solo ed esclusivamente didattico. Una volta creata l’istanza possiamo e dobbiamo usare i metodi SetValue e GetValue per mettere e ricavare dei valori. Non è infatti possibile operare in modo chiamiamolo classico su di un array creato in questo modo usando l’operatore *+ proprio per la natura in qualche modo indefinita dell’array.

  Esempio 11.16
1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
 
class program
{
  public static void Main()
  {
    Array ar = Array.CreateInstance(typeof(int), 3);
    for (int x = 0; x < 3; x++)
    ar.SetValue(x, x);
    for (int x = 0; x < 3; x++)
    Console.WriteLine(ar.GetValue(x));
  }
}

CreateInstance permette ovviamente di creare  array aventi più dimensioni ma anche, qui sta la novità, non zero based, ovvero aventi il primo indice diverso da zero. E’ l’unico modo (almeno che io sappia)per avere questo tipo di array. L’esempio seguente, sfruttando uno dei numerosi overload di CreateInstance,  consiglio di andarli a sbirciare sul solito sito MSDN, crea un array a due dimensioni di due elementi ciascuna, definite alla riga 6, il primo indice della prima dimensione è 3 mentre la seconda dimensione parte dall’indice 50. Di primo acchito non è così semplice raccapezzarsi, soprattutto con la gestione degli indici ma dopo qualche prova le cose diventano più facili. Il problema maggiore sta nell’evitare di andare al di là dei valori consentiti per gli indici consentiti il che solleverebbe una eccezione di tipo OutOfRange. Consideriamo comunque che array non 0-based non sono CLS compliant; in aggiunta si ha un certo handicap anche da un punto di vista prestazionale, visto che il discorso relativo agli array è ottimizzato a livello di CLR per quelli “classici”. Detto questo si tratta di una possibilità che forse avrete raramente bisogno di usare ma che può comunque essere utile conoscere:

  Esempio 11.17
1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
class program
{
  public static void Main()
  {
    int[] d = {2, 2};
    int[] i = {3, 50};
    Array ar = Array.CreateInstance(typeof(int), d, i);
    ar.SetValue(3, 3, 50);
    Console.WriteLine(ar.GetValue(3,50));
    Console.WriteLine(ar.GetValue(4,50));
  }
}

Ad essere sincero non ricordo di avere mai utilizzato CreateInstance se non a scopo esemplificativo per cui ritengo che anche a voi capiterà raramente. Probabilmente viene utilizzato internamente al framework, dovrei verificare.

Esistono poi una serie di utili proprietà, alcune già viste:

Proprietà

Spiegazione

IsFixedSize

True se l'array ha una lunghezza fissa

IsReadOnly

True se l'array è read-only

IsSynchronized

Interessante proprietà ci dice se l'accesso all'array è sincronizzato. Utile in funzione della gestione dei thread

Length

Restituisce la lunghezza dell'array

using System;
class Test
{
    public static void Main()
    {
       int[] arr1 = new int[5];
       int[,] arr2 = new int[4,3];
        Console.WriteLine(arr1.Length.ToString());
       Console.WriteLine(arr2.Length.ToString());
    }
}
 

LongLength

Restituisce la lunghezza dell'array usando un long anzichè un Int32, ovviamente si deve usare per array particolarmente lunghi. Potete adattare senza problemi l’esempio precedente.

Rank

Restituisce il numero di dimensioni dell'array

using System;
class Test
{
    public static void Main()
    {
        int[] arr = new int[5];
        int[,] arr2 = new int[4,3];
       Console.WriteLine(arr.Rank.ToString());
       Console.WriteLine(arr2.Rank.ToString());
    }
}

SyncRoot

Restituisce un oggetto che può essere usato per sincronizzare l'accesso all'array. Anche questo verrà utile quando parleremo dei thread.

Length e Rank sono sicuramente i più utilizzati, in particolare il primo che fornisce un utilissimo parametro, il numero di elementi dell'array. Lo userete molto spesso e lo abiamo già visto in alcuni esempi..

CONVERSIONI

E’ possibile effettuare conversioni tra due array esattamente come per le variabili. Perchè ciò sia possibile, dati due array, chiamiamoli ar1 e ar2 devono 

•       avere le stesse dimensioni, 

•       essere basati su tipi compatibili, esplicitamente o implicitamente

      essere basati su reference type.

Ecco un esempio:

  Esempio 11.18
1
2
3
4
5
6
7
8
9
10
11
using System;
 
class program
{
  public static void Main()
  {
    string[] ar1 = new string[] {"aa", "bb"};
    object[] ar2 = new object[2];
    ar2 = ar1;
  }
}

Oppure, con un cast

  Esempio 11.19
1
2
3
4
5
6
7
8
9
10
11
using System;
 
class program
{
  public static void Main()
  {
    object[] ar1 = new object[] {"aa", "bb"};
    string[] ar2 = new string[2];
    ar2 = (string[])ar1;
  }
}

Inoltre è possibile inserire il concetto di covarianza per gli array, un po’ una formalizzazione di quanto visto, che, detto semplicemente, rimappa su di essi i rapporti di conversione implita ed esplicita dei reference types (il concetto non è applicabile ai value types). In base ad esso è possibile ad esempio che, dato un array di tipo A, si possano in essi inserire elementi di tipo B derivato da A. La regola  espressa in modo “ufficiale” recita:

se fra due tipi A e B esiste una conversione esplicita o una conversione implicita allora la stessa esiste tra due array di tipi A[R] e B[R] dove R indica il rank degli array. 

Un esempio che dà l’idea del concetto, in forma molto semplice, è il seguente che darò senza commenti in quanto si consiglia vivamente di rivederlo dopo la lettura del capitolo relativo alle classi in quanto è indispensabile una conoscenza almeno basilare di esse per una completa comprensione. 

  Esempio 11.20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;

class A
{
  public int x = 0;
}
 
class B : A
{
  public int y = 0;
}
 
class program
{
  public static void Main()
  {
    A[] ar1 = new A[5];
    B b = new B();
    B[] ar2 = new B[5];
    ar2 = (B[])ar1; // si noti il cast
    ar1[3] = b;
  }
}
 

CONCLUSIONI.

Non sono certo io a poter evidenziare l’importanza degli array. Basta osservare qualsiasi altro linguaggio di programmazione e troverete strutture del tutto analoghe; questo accade in quanto gli array sono alla base di tanti importanti algoritmi, sono utili, espressivi e molto potenti. Li userete moltissimo, per cui è bene prendere confidenza col loro utilizzo, iniziando dalle operazioni più comuni, la copia, la selezione e la sostituzione di elementi e così via. Si tratta di un compito relativamente semplice visti gli strumenti a disposizione e la già citata semplicità concettuale degli array. Detto questo direi che possiamo calare il sipario sugli array e buttarci sulla loro naturale evoluzione: le collezioni.