Ordinamento di matrici in Visual Studio 2005/2008
Utilizzando i VSTO (Visual Studio Tools per Office) è senz’altro importante sfruttare le notevoli potenzialità di Visual Basic 2008 o, almeno, 2005. In particolare quelle relative al trattamento di matrici su RAM. La cosa può essere utile persino in Excel, per non parlare di Word, nell’ipotesi di sfruttare matrici di dati interni, ad esempio relative a statistiche, dati più o meno standard, senza ricorrere, specie nel caso di Excel, a tabelle inserite nel documento o modello.
Questo articolo di tipo didattico si occupa di un aspetto particolare, l’ordinamento (Sort in inglese), parlando di diversi punti non sempre ben documentati né chiari nemmeno a chi si occupa di Visual Basic in generale.
Il metodo Sort applicabile senza problemi a ogni tipo di elenco monodimensionale, tramite la classe Array è particolarmente utile e d’uso immediato. Ma come effettuarlo quando si hanno matrici di due o più dimensioni? La cosa è fattibile - e i manuali specifici lo spiegano abbastanza bene – anche se concettualmente delicata e complicata. In ogni caso, il metodo Sort a prima vista non sembra si applichi direttamente a matrici pluridimensionali. Esaminiamo gradualmente cosa si può fare, partendo da un caso semplice per giungere a due delle più articolate soluzioni.
Ordinamento di due vettori correlati
Per chi ha fretta e magari è un principiante ecco una ricetta di facile consumo e comprensione, in forma di applicazione console, che serve a ordinare una coppia di vettori correlati, con il primo che fa da chiave, ergo funge da base per il sort, che ovviamente deve riguardare anche il secondo vettore.
Module Module1
Function OrdinaDueVettori(ByVal Vett1 As Array, ByVal Vett2 As Array)
' Ordinamento in base a Vett1 anche del secondo Vett2
Dim Vett1Old = Vett1.Clone
Dim Vett2Old = Vett2.Clone
Array.Sort(Vett1) ' Ordinamento del vettore chiave
Dim k = 0
For i = 0 To UBound(Vett1) ' oppure: To V1.Length - 1
k = Array.IndexOf(Vett1Old, Vett1(i))
Vett2(i) = Vett2Old(k)
Next
' Restituisci entrambi i vettori, ordinati
Return Vett1
Return Vett2
End Function
Sub Main()
Dim V1() = {3, 5, 4, 6, 2}
Dim V2() = {"Mele", "Pere", "Susine", "Pesche", "Fichi"}
OrdinaDueVettori(V1, V2)
Dim ind = 0
For ind = 0 To UBound(V1) ' oppure: To V1.Length - 1
Console.WriteLine("Codice: {0} Frutto: {1}", V1(ind).ToString, V2(ind))
Next
Console.ReadLine() Next
. . . . ECCETERA . . .
End Sub
End Module
L’estensione al caso Excel o Word + VSTO è abbastanza immediata: basta sostituire a Main un opportuno evento come tipicamente il Click su un qualche pulsante, ovvero in parole povere un qualche Button1_Click con quel che segue.
Commenti stringati. Va premesso che un’alternativa cui di sicuro avranno pensato in molti è l’impiego di un classico algoritmo specifico, a partire dal pur banale bubble sort, nel qual caso il procedimento può applicarsi a un numero qualsiasi di dimensioni. Ad esempio lo spostamento di “bolle” nel bubble sort procederebbe, in parallelo, su tutte le altre dimensioni oltre che sulla chiave. Esercizio lasciato a chi legge queste noterelle (e comun que richiamato in fondo a questo post).
Ma qui l’obiettivo didattico è, di proposito, l’impiego del metodo Sort offerto gratis da Visual Studio 2005 e 2008. Il nostro procedimento di cui è facile rendersi conto si basa su due punti:
- La copia dei valori dei due vettori originari, rispettivamente in Vett1Old e Vett2Old;
- La trascrizione, dopo il sort di Vett1, dei valori di Vett2Old in Vett2, tenendo conto della posizione (indice) originaria ricavata dalla posizione (IndexOf) di ciascun nuovo valore Vett1(i) nel vecchio Vett2Old.
Più difficile da dire che da capire, no? Di un qualche interesse – magari anche per chi si trova nel mezzo del cammin di sua vita di principiante – l’utilizzo del metodo Clone che crea una copia di valori del duo Vett1 e Vett2. Esso, in primo luogo, sintetizza un loop del genere seguente (idem con patate per Vett2):
For i = 0 To UBound(Vett1)
Vett1Old(i) = Vett1(i)
Next
Ma Clone serve anche per un motivo più sottile (e da non dimenticare). Se infatti, per distrazione, si ricorresse a istruzioni Vett1Old = Vett1 e Vett2Old = Vett2 il nostro bell’algoritmo fallirebbe! Questo perché si otterrebbero copie per riferimento, per cui entrambi Vett1Old e Vett1 verrebbero ordinati, ergo il vecchio legame tra i due vettori originari andrebbe di fatto perduto.
Nota Questa prima ricetta, in realtà ha un’alternativa che sarà più chiara Al termine dell’articolo. Suspense...
Una possibile disillusione
Si sta sempre parlando a principianti del mondo .NET, che magari provengono dal mondo Excel le cui tabelle, strutturate o semplici, si possono ordinare sulla base di uno o più campi (ovvero colonne di quel certo intervallo).
La prima tentazione nasce da una tabellina 2D come la seguente:
Dim Tab(,) = {{3, 5, 4, 6, 2}, {"Mele", "Pere", "Susine", "Pesche", "Fichi"}}
For Each t In Tab
MsgBox(t.ToString)
Next
(notare il ciclo For Each singolo, che a partire da VS 2005, se non vado errato e come non molti sanno, riuassume e sintetizza due loop annidati classici spazzolando per righe e per colonne, in quest’ordine.
Come anticipato, l’applicazione del metodo Sort a una siffatta Tab, viene subito delusa da uno sdegnoso rigetto del compilatore, in quanto Sort non sembrerebbe che preveda un argomento per distinguere la chiave di ordinamento fra i due (o più) campi della matrice, la qual cosa è invece possibile in ambiente Excel, come ben sa chi proviene dal mondo Office ed è, probabilmente, più portato di altri a sperare che l’ordinamento avvengo con altrettanta semplicità anche altrove. Peccato che simili cose non sono, perlomeno direttamente, possibili nel più nobile mondo .NET.
Nota Ma attenzione! Non si faccia confusione tra tabelle insorporate sul foglio e matrici VB.
Bubble sort rivisitato. Per la gioia dei più... piccini e, magari, per risvegliare la memoria di distratti e immemori, riporto infine una possibile routine del genere, applicata a una generica matrice pluridimensionale Matr.
Dim swScambio = False
Do
swScambio = False
For i = 0 To UBound(Matr) - 1
If Matr(i, 0) > Matr(i + 1, 0) Then
For j = 0 To UBound(Matr, 2)
Temp = Matr(i, j)
Matr(i, j) = Matr(i + 1, j)
Matr(i + 1, j) = Temp
Next
swScambio = True
End If
Next igli oggettiLoop Until Not swScambio
Ordinamento di matrici, in due modalità
Personalmente sarei portato ad auspicare che in un qualche rilascio futuro venga supportato uno specifico oggetto che chiamerei “Table”, formalmente e sostanzialmente analogo a quello di Excel.
Nota Ricordo che anche in Word è presente un oggetto del genere, concettualmente analogo anche se strutturato in modo un po’ diverso...
Tale oggetto “Table” sarebbe l’equivalente naturale, in memoria, dei ben noti, omonimi parenti dei database relazionali che risiedono su disco.
Ma lasciando perdere sogni e aspirazioni, che lasciano il tempo che trovano, e rimanendo ancorati al tema sort, i procedimenti che qui suggerisco sono due, il secondo, basato sul nuovo linguaggio LINQ ergo valido soltanto col Framework 3.5, vale a dire con Visual Studio 2008 (e l’imminente 2010, sicuramente).
Il primo, almeno a quanto mi consta, è noto a pochi. Infatti tra i vari manuali di cui dispongo solo la Bibbia di Francesco Balena “Programmare Microsoft Visual Basic .NET”- ed. Mondadori. Eccone subito un esempio, in veste di Console application (ma anche qui – come nei successivi esempi – dovrebbe essere facile l’estensione a un evento di Excel/Word scatenato da qualche controllo, secondo quanto già detto sopra):
Module Module1
Structure Persona ' Oppure Class
Public Cognome As String
Public Nome As String
Public DataNascita As Date
Sub New(ByVal cognome As String, ByVal nome As String, ByVal datanascita As Date)
Me.Cognome = cognome
Me.Nome = nome
Me.DataNascita = datanascita
End Sub
End Structure ' Oppure End Class
Sub Main
' Crea una matrice di prova
Dim Persone() As Persona = _
{New Persona("Rossi", "Paolo", #4/2/1980#), _
New Persona("Bianchi", "Luisa", #7/12/1985#), _
New Persona("Verdi", "Emilio", #8/5/1975#), _
New Persona("Landi", "Remo", #5/10/1981#), _
New Persona("Renzi", "Paola", #4/2/1980#)}
' Crea un vettore correlato contenente le date di nascita
Dim DateNascita(UBound(Persone)) As Date
Dim j As Integer = 0
For j = 0 To UBound(Persone)
DateNascita(j) = Persone(j).DataNascita
Next
' Ordina Persone su data di nascita, usando DateNascit come chiavi
Array.Sort(DateNascita, Persone)
' Mostra il risultato
Dim P As Persona = Nothing
For Each P In Persone
Console.WriteLine(P.Cognome & " " & P.Nome & " " & P.DataNascita)
Next
Console.ReadLine()
End Sub
End Module
I commenti inseriti dovrebbero rendere eloquente il procedimento. Due sono le cose che si apprendono:
- Una matrice che, in buona sostanza, corrisponde a una tabella viene definita partendo da una struttura che ne definisce i campi e su tale base è possibile creare una matrice nel modo indicato nello snippet;
· Il sospirato metodo Sort si applica a una siffatta matrice (evviva!) ma per farlo – ecco il segreto ignoto ai più – bisogna creare un vettore parallelo contenente gli elementi del campo che vogliamo sia la chiave di ordinamento.
Nota Di passaggio ricordo che per ottenere l’ordinamento in senso discendente si deve ricorrere al metodo Reverse.
Si noti poi che anche il vettore parallelo viene ordinato, per cui non è indispensabile caricarlo di nuovo le volte successive alla prima:
' Amplia di un’unità la matrice Persone
Dim MaxInd As Integer = Ubound(Persone) + 1
Redim Preserve Persone(MaxInd)
' Amplia pure il vettore correlato (non occorre ricaricarlo)
Redim Preserve DateNascita(MaxInd)
' Aggiungi una Persona
Persone(MaxInd) = New Persona("Brambilla", "Mauro", #5/7/1982#)
' Ordina di nuovo la matrice estesa
Array.Sort(DateNascita, Persone)
Intrichi a parte, si può dire tutto risolto per il sospirato oggetto “Tabella” ? Non del tutto. Infatti stiamo parlando solo dell’ordinamento. Infatti vanno gestite a parte altre funzionalità, in primo luogo la selezione, filtro in gergo database, di elementi dotati di determinate caratteristiche.
A questi più ampi requisiti risponde la seconda soluzione, che si affida al nuovo linguaggio LINQ (ripeto e insisto: supportato solo a partire di VS 2008), Questo di regola si occupa di database relazionali, essendo derivato dal noto linguaggio di query SQL, ma fornisce anche “motori” (“provider”, in gergo) per altre fonti di dati, come gli archivi XML. Nell’edizione LINQ To Objects si rivolge appunto ai più svariati oggetti in memoria e promette (e mantiene) fra l’altro articolate opzioni di ordinamento, valide in particolare per liste di classi. Vediamo subito una tipica ricetta, che si rifà in parte all’esempietto di apertura:
Module Module1
Class Ordine ' Oppure Structure
Public COD As Integer
Public Frutto As String
Public Giacenza As Integer
End Class 'Oppure Structure
Sub Main()
Dim ElencoOrdini As New List(Of Ordine)
Dim Codici() = {3, 5, 4, 6, 2}
Dim Frutti() = {"Mele", "Pere", "Susine", "Pesche", "Fichi"}
Dim Giacenze() = {250, 150, 100, 75, 90}
Dim i = 0
For i = 0 To UBound(Codici)
ElencoOrdini.Add(New Ordine With _
{.COD = Codici(i), _
.Frutto = Frutti(i), _
.Giacenza = Giacenze(i)})
Next
' Mostra situazione prima del Sort
For Each Ord In ElencoOrdini
Console.WriteLine(Ord.COD.ToString & " - " _
& Ord.Frutto & " - " & Ord.Giacenza.ToString)
Next
Console.WriteLine()
' Esegui il Sort in linguaggio LINQ
Dim Ordini = From Ord In ElencoOrdini _
Order By Ord.COD _
Select Ord
' Mostra situazione dopo il Sort
Console.WriteLine("Ordini ordinati per CODice")
For Each Ord In Ordini
Console.WriteLine(Ord.COD.ToString & " - " _
& Ord.Frutto & " - " & Ord.Giacenza.ToString)
Next
Console.ReadLine()
End Sub
End Module
Anche in questo caso si parte con la definizione di una struttura o classe ad hoc e, rispetto al caso precedente ora si ricorre al List(Of <T>) per definire e successivamente popolare un elenco di oggetti Ordine. Si noti la nuova particolare sintassi relativa all’aggiunta di elementi alla lista, con il punto che segue With: New Ordine With {.COD =... .Frutto =... }. Come dovrebbe essere limpido a tutti, stavolta si è fatto ricorso a tre vettori di costanti Codici, Frutti e Giacenze, per amore di variante.
Il clou del programmino è dato dalle istruzioni specifiche in LINQ, che do per auto esplicative. Aggiungo piuttosto che, stavolta sono offerte ulteriori possibilità, in particolare con le clausole Where (per filtrare secondo vari criteri) e Join (unione di tabelle correlate). La materia esula dagli scopi di questo post, comunque terminiamolo con un esempio di filtro, che palesemente oltre al riordino restituisce tutti e soli gli elemento il cui codice è maggiore di 3:
Dim Ordini = From Ord In ElencoOrdini _
Where Ord.COD > 3
Order By Ord.COD _
Select Ord
Due ultimissime annotazioni. La prima è l’intercambiabilità fra struttura e classe espressa nei commenti di entrambi gli esempi. La seconda tratta la Classe definendone le proprietà semplicemente come variabili Public. La cosa apparirà brutale a chi ha il vezzo (o malvezzo?) di definire sempre e in modo completo le Property, mediante Get e Set anche quando non servono.
Nota L’edizione imminente 2010 di Visual Studio ora permette di definire le Property anche senza Get e Set (come già accade in C#). A mio avviso è un mero orpello sintattico, che mira solo a scoraggiare il malvezzo di cui sopra... Beninteso la distinzione fra struttura e classe è importante, almeno nei casi meno semplici rispetto a quelli qui trattati.
La seconda osservazione è relativa a una differenza sostanziale fra i due procedimenti, che potrebbe essere sfuggita ai più distratti. Nel primo l’ordinamento modifica la matrice originaria, mentre LINQ crea una nuova versione dell’elenco. La cosa è facilmente verificabile e si spiega altrettanto bene, per il fatto che LINQ può anche filtrare, con Where.
Riflettete, gente.
Chiarimento finale sul metodo Array a due argomenti
Riflettendo ho poi compreso il preciso significato del metodo Sort dell’oggetto System.Array. La sintassi è la seguente, col secondo argomento opzionale:
Array.Sort(VettChiavi, Matrice)
Ove, si badi bene!, l’ordinamento, per così dire in prima battuta, si applica alla matrice monodimensionale VettChiavi mentre viene ordinata, in parallelo, anche la Matrice che – essa soltanto – può avere più dimensioni o campi che dir si voglia. La cosa è stata ben chiarita nella penultima parte dell’articolo. Ma allora perché ritornarci?
Il motivo è una sottigliezza che sfugge facilmente: VettChiavi NON è obbligatoriamente una replica del campo di Matrice scelto per il sort e può anche essere un vettore esterno, ovviamente di pari potenza, anche diverso dai campi della Matrice.
L’esempio seguente estensibile al caso in cui il secondo argomento è una matrice a più dimensioni chiarisce la faccenda:
Dim Codici()= {3, 5, 4, 6, 2}
Dim Frutti()= {"Mele", "Pere", "Susine", "Pesche", "Fichi"}
Array.Sort(Codici, Frutti)
Dim k = 0
For k = 0 To UBound(Codici)
Console.WriteLine(Codici(k).ToString & " - " & Frutti(k))
Next
Non sfuggirà che si tratta del problemino esposto in apertura, che diventa perciò obsoleto (anche se didatticamente resta valido, spero).
?>