Home

 
 
 
 
 



 
 
 
 

 
 
 

 
 
 
 
 









Blog2theMax
Il blog del team di Code Architects

In questo periodo sto facendo molta consulenza per aziende italiane alle prese con la migrazione da VB6, usando ovviamente la versione preliminare di VB Migration Partner. Oramai la beta è stabile e si comporta davvero molto bene. In un solo giorno, ad esempio, abbiamo migrato una applicazione di circa 20mila righe che lavora in modo intensivo con ADO in circa mezza giornata, e tutto funziona a meraviglia!

Anche se la migrazione sta andando bene, a volte i programmi migrati sono più lenti del codice VB6 originario. Per esperienza le cause sono principalmente due: (1) l'uso di COM Interop per accedere agli oggetti ADO, (2) un uso massiccio di concatenazione di stringhe. Per il primo problema non c'è molto da fare, purtroppo: occorre migrare il codice di accesso al database da ADO a ADO.NET, un compito più semplice a dirsi che a farsi. Per il secondo problema ci sarebbe invece una soluzione semplice: basterebbe sostituire la variabile stringa con una variabile StringBuilder, e le concatenazioni sarebbero automaticamente più veloci. Peccato che questa sostituzione comporti una revisione completa del codice, perchè richiede di trasformare tutti gli operatori & (concatenazione) con il metodo Append, per non parlare dei casi in cui la variabile stringa viene usata come argomento a funzioni come Trim o Left. Ad esempio, il seguente codice richiede ben 17 secondi sul mio sistema a 3GHz:

Dim s As String = ""
Dim sw As Stopwatch = Stopwatch.StartNew()
For i As Integer = 1 To 100000
    s = s + "*"
Next
MsgBox(sw.Elapsed.ToString)

Per fortuna la soluzione è davvero semplice: basta creare una classe che utilizzi internamente un oggetto StringBuilder, che ridefinisca gli operatori & e +, e che supporti la conversione implicita da-a stringa. Il codice di questa classe StringBuilder6 si scrive davvero in pochi minuti:

Imports System.Text

' a wrapper for the StringBuilder object, with support for + and & operators

Public Class StringBuilder6

    Private buffer As New StringBuilder

    ' return the inner string

    Public Overrides Function ToString() As String
       
Return buffer.ToString()
    End Function

    Public Shared Operator +(ByVal op1 As StringBuilder6, ByVal op2 As String) As StringBuilder6
        op1.buffer.Append(op2)
        Return op1
    End Operator

    Public Shared Operator &(ByVal op1 As StringBuilder6, ByVal op2 As String) As StringBuilder6
        op1.buffer.Append(op2)
        Return op1
    End Operator

    ' convert to string

    Public Shared Widening Operator CType(ByVal op As StringBuilder6) As String
       
Return op.ToString()
    End Operator

    ' convert from string

    Public Shared Widening Operator CType(ByVal str As String) As StringBuilder6
        Dim op As New StringBuilder6()
        op.buffer.Append(str)
        Return op
    End Operator

End Class

A questo punto per velocizzare il codice visto prima è sufficiente modificare UNA SOLA ISTRUZIONE, ovvero la dichiarazione della variabile stringa:

Dim s As StringBuilder6 = ""

Dopo questa sostituzione, il ciclo visto prima viene eseguito in 8 millesimi di secondo, ovvero circa 2000 volte più velocemente!!! Non male, per una fix così semplice :-)

Indipendentemente dal fatto che state migrando codice da VB6 o se avete scritto codice VB.NET (o C#) da zero: se trovate dei punti in cui fate uso massiccio di concatenazione di stringhe e che pensate possano rallentare l'esecuzione, provate a sostituire la variabile stringa con un oggetto StringBuilder6: in alcuni casi il programma girerà molto ma molto più velocemente.

 

12/16/2007 11:44:27 AM (GMT Standard Time, UTC+00:00) #  | Comments [2] | 

Premessa. Non so se questa piccola scoperta che ho fatto di recente sia già documentata da qualche parte su Internet. Non ne ho mai sentito parlare, ma mi sembra strano che nessun’altro ci abbia pensato prima. Comunque, ci sono arrivato da solo e questo è sufficiente per scriverne sul blog.


Mi sembra inutile rimarcare il fatto che uno dei problemi che assilla chi scrive software per professione è la protezione dalle copie. In più, se programmate con .NET avete anche il problema di proteggere le applicazioni dalla decompilazione. Ovviamente i due problemi sono collegati, perchè se volete proteggere una applicazione con qualche meccanismo software (ad es. la lettura delle caratteristiche della macchina su cui il cliente installa la sua copia) occorre evitare che una semplice decompilazione + ricompilazione possa permettere ad un hacker neanche troppo esperto di bypassare la vostra protezione.

Non si tratta certo di problemi nati con .NET. Una quindicina di anni fa campavo sviluppando tool per programmatori con la mia azienda personale – la SoftWhale, dove “Whale” = “Balena” per chi non lo sapesse – e uno dei miei prodotti più gettonati era NOWAY, un programma che crittografava gli eseguibili MS-DOS proteggendoli quindi dalla decompilazione (un problema molto sentito da chi lavorava in Clipper) e permetteva di lanciarli solo sui computer per i quali la software house forniva al cliente dei codici di sblocco. NOWAY era scritto in puro Assembly 8086 e non è sopravvissuto al passaggio da MS-DOS a Windows.

In commercio esistono molti tool per .NET che servono a risolvere questi problemi, incluso obfuscator, compilatori “nativi” e chiavi hardware. Se volete proteggere la vostra IP (Intellectual Property) dovreste darci una occhiata approfondita.Per nostra fortuna, in Code Architects scriviamo soprattutto applicazioni Enterprise che non richiedono questo tipo di protezione, quindi non ho mai approfondito la questione. Immagino che questi tool facciano bene il loro lavoro, e li testerò se e quando avremo la necessità di farlo.

Se però (a) scrivete perlopiù applicazioni Windows Forms, (b) non vendete software pacchettizzato ed eseguite direttamente voi le installazioni presso il cliente, e (c) fate anche assistenza post-vendita, allora è possibile ottenere la protezione dalla decompilazione e dalle copie illegali con un meccanismo davvero molto semplice.

Cominciamo scrivendo una semplice applicazione Windows Forms. In questo esempio uso VB2005 ma ovviamente la tecnica si applica a qualsiasi linguaggio .NET.

Imports System.Reflection

Friend Module Module1

 

   <STAThread()> _

   Public Sub Main()

      ' in this demo we call the secret routine both directly and via reflection

      ' just to prove that either method works

       MySecretCode("Direct call")

       GetType(Module1).InvokeMember("MySecretCode", BindingFlags.InvokeMethod, _

          Nothing, Nothing, New Object() {"Reflection call"})

   End Sub

 

   Public Sub MySecretCode(ByVal title As String)

#If Not COPYPROTECT Then

      ' here goes all the code that you want to protect from decompilation

      MessageBox.Show("Running secret code!", title)

      ' ...

#End If

   End Sub

End Module

Poichè la variabile di compilazione COPYPROTECT non è definita, il codice nel metodo MySecretCode è incluso nell’eseguibile come se il blocco #IF non esistesse. In questo esempio proteggo un solo metodo, ma in una applicazione reale dovreste ripetere il procedimento con tutti i metodi che contengono del codice critico, magari dove implementate gli algoritmi più interessanti oppure dove controllate che l’utente abbia la licenza di eseguire il programma sul computer in questione. È importante che tutti i blocchi #IF... #ENDIF siano completamente contenuti in singoli metodi, ovvero che non capiti mai che uno di questi blocchi contenga le keyword Sub o Function (o le corrispettive End Sub o End Function). Per lo stesso motivo, potete utilizzare il meccanismo anche con le Property ma solo se utilizzate i blocchi #IF all’interno del blocco Get o Set. I blocchi #IF non devono assolutamente includere field, definizioni di eventi, o altro. 

Per completare la preparazione è opportuno definire una nuova configurazione per la soluzione. Dal menù Build selezionate il comando Configuration Manager, dalla combobox in alto a sinistra selezionate <New> e create una configurazione chiamata CopyProtected, che copia le impostazioni iniziali dalla configurazione Release.

Dopo esservi accertati che la nuova configurazione è quella attiva, create una costante condizionale di compilazione chiamata COPYPROTECT e impostata al valore True. In C# questa costante si crea dalla pagina Build delle proprietà di progetto, mentre in VB2005 dovete cliccare sul pulsante Advanced Compile Options che trovate nella pagina Compile:

Grazie a questa nuova configurazione, potete passare facilmente dalla versione normale (ossia Release) a quella copy-protected. Compilando l’applicazione in entrambe le configurazioni, avrete due eseguibili con lo stesso nome (ad es. Project1.exe) e contenenti gli stessi metadati (perchè le #IF sono sempre all’interno dei metodi e non includono campi, proprietà, o altro). L’esempio che segue presume che abbiate compilato una applicazione che si chiama Project1, che NON è firmata con uno strong name.

Preparate il setup, se necessario, utilizzando la versione Release, ma ricordatevi anche di memorizzare separatamente (ad es. su una chiavetta USB) l’eseguibile ottenuto in configurazione copy-protected e l’utility NGEN fornita con .NET Framework. Infine, recatevi presso il vostro cliente e seguite accuratamente la semplice procedura:

1) Lanciate il setup della applicazione, o più banalmente utilizzate XCOPY per copiare tutti i file sul disco rigido del cliente. In questo esempio supporremo che l’installazione sia avvenuta nella directory c:\myapp e che quindi il file eseguibile sia c:\myapp\Project1.exe.

2) Aprite una finestra di comandi, navigate nella directory che contiene l’esegubile e lanciate NGEN per creare una immagine in codice nativo della applicazione. Il comportamento di default di NGEN è OK, quindi non preoccupatevi troppo delle varie opzioni. (Lasciate aperta questa finestra, perchè vi servirà presto.)

3) Aprite una seconda finestra di comandi, navigate nella directory c:\windows\assembly\NativeImages_v2.0.50727_32\nomeassembly, dove nomeassembly è il nome dell’EXE ma senza l’estensione (Project1 in questo esempio). Noterete che in questa directory contiene una sottodirectory il cui nome è una stringa di caratteri esadecimali (nel mio esempio ho ottenuto d37032afe4f6f44588d52ca99d7bb1e5). Questa directory contiene un eseguibile dal nome nomeassembly.ni.exe (Project1.ni.exe nel nostro esempio), che contiene la versione compilata (senza IL e quindi non decompilabile) dell’eseguibile completo.

4) Dal prompt della seconda finestra dei comandi, con un comando COPY o MOVE spostate il file <nomeassembly>.ni.exe su qualche altra directory del disco rigido, ad esempio:
     MOVE d37032afe4f6f44588d52ca99d7bb1e5\Project1.ni.exe c:\
Questa operazione serve ad evitare che questo eseguibile sia cancellato dalla successiva invocazione di NGEN. Lasciate aperta anche questa seconda finestra dei comandi.

5) Sovrascrivete il file c:\myapp\Project1.exe con la versione copy-protected che avete sulla chiavetta USB.

6) Dalla prima finestra di comandi lanciate nuovamente NGEN, questa volta sulla versione copy-protected. Se questo programma venisse eseguito ora probabilmente andrebbe in errore o comunque non funzionerebbe bene, perchè alcune parti del codice non sono presenti, quindi NON lo mandate in esecuzione.

7) Dalla seconda finestra dei comandi, eseguite un comando DIR e noterete che la directory creata precedentemente (d37032afe4f6f44588d52ca99d7bb1e5, in questo esempio) è stata sostituita oppure è stata affiancata da un’altra directory dal nome random. (Nell’esempio che sto eseguendo mentre scrivo queste note è stata creata una directory di nome 9990426218ec334e9d3d62f41cb9a255.) Questa nuova directory contiene una nuova versione di Project1.ni.exe, che corrisponde alla versione in codice nativo dell'’eseguibile incompleto.

8) Sempre dalla seconda finestra dei comandi, copiate la copia precedente di Project1.ni.exe (salvata al punto #4) sulla nuova copia, ad esempio con questo comando:
     MOVE c:\project1.ni.exe 9990426218ec334e9d3d62f41cb9a255
rispondendo Yes alla domanda se intendiamo sovrascrivere il file Project1.ni.exe.

Fatto! Ora potete staccare la chiavetta USB e lasciare sul disco rigido del vostro cliente solo l'assembly "copy-protected" (in versione IL) e l'assembly completo (solo in versione compilata in modo nativo e quindi non decompilabile). Per quanto può sembrare incredibile, l'applicazione funziona correttamente e mostra le due message box :-) Ma se provate a usare ILDASM o Reflector per sbirciare dentro il metodo MySecretCode (e qualsiasi altro metodo il cui interno è racchiuso con un #IF Not COPYPROTECT) troverete che il metodo è vuoto!

Ovviamente neanche Reflector riesce a decompilare alcunchè, visto che proprio manca il codice IL. Nè aiuta usare ILDASM sull’eseguibile Project1.ni.exe, perchè l’unica cosa che si ottiene è visualizzare un manifest senza codice IL:

Come può funzionare questa piccola magia? La spiegazione è semplice: quando lanciate un qualsiasi eseguibile .NET, per prima cosa il CLR va a controllare se vi è una “native image” che corrisponde esattamente a quella applicazione, e la corrispondenza viene cercata confrontando il nome della applicazione e la sua firma (la stringona esadecimale). Se esiste una immagine nativa, quella viene lanciata al posto dell’eseguibile. La condizione però perchè tutto il codice funzioni correttamente è che i metadati coincidano anch’essi, altrimenti troppe funzioni del CLR non funzionerebbero a dovere (il garbage collector, tanto per citarne il più importante). Nel nostro caso noi abbiamo creato due eseguibili con gli stessi metadati (ecco perchè è importante che i blocchi #IF non devono eliminare alcun metodo, campo, o proprietà), e siamo riusciti a mantenere sul disco rigido la versione IL “incompleta” mentre mandiamo in esecuzione la versione nativa “completa”.

Oltre a proteggere dalla decompilazione, molto simpaticamente questo meccanismo protegge anche dalle copie illegali. Infatti, se provate a spostare tutti i file (compreso l’eseguibile Project1.ni.exe) su un’altro computer, è praticamente certo che l'applicazione NON funzionerà correttamente. Da quel che ho potuto capire, questo accade perchè il nome della directory creata da NGEN – ovvero la “firma” di un particolare eseguibile – dipende anche da qualche informazione di sistema e quindi varia da macchina a macchina.

Per quanto interessante, questa tecnica ha alcune limitazioni abbastanza serie:

A) Se si intende proteggere una DLL peer Windows Forms l'assembly non deve essere segnato con uno strong name. Il motivo è che nel manifest degli assembly che referenziano una DLL con strong-name viene memorizzato anche l'hash dell'assembly in questione, e questo è un valore che cambia quando si compila la versione copy-protected. Quindi, visto che la DLL non può avere uno strong-name, neanche gli EXE che usano la DLL possono averlo, il che rappresenta una limitazione notevole. Questo limite non esiste per le applicazioni ASP.NET.

B) Se avviene un errore in una applicazione ASP.NET protetta oppure si modifica il Web config in qualsiasi modo, l'applicazione smette di funzionare. Il problema si risolve con un IISRESET.

C) Se avviene qualche modifica sostanziale all’hardware o al software di sistema – ad es. la sostituzione della CPU, l’aggiunta di memoria, l'upgrade del sistema operativo, ecc. – il .NET Framework non può più usare l’immagine nativa e lancerà l’eseguibile normale, che ovviamente non funzionerà bene perchè alcune porzioni di codice mancano. In quel caso occorre ripetere l’installazione e tutto il procedimento descritto sopra. È importante quindi mettere in guardia il vostro cliente dai rischi che corre facendo questi upgrade durante la notte, il fine settimana, o mentre siete in vacanza alle Bahamas.

Per mitigare il problema potete almeno fare in modo che l’applicazione mostri un chiaro messaggio di errore quando l’immagine nativa non è aggiornata e il CLR torna ad usare l’eseguibile “incompleto”. Per fare questo è sufficiente avere una istruzione MessageBox.Show in un blocco #IF COPYPROTECT (senza il Not), ad es:

Friend Module Module1
    Public Sub Main()     

#If COPYPROTECT Then

        MessageBox.Show("This app requires reinstallation. Please contact tech support.")

        Exit Sub

#End If

        ' Here the real application begins

        ' ...

    End Sub

End Module

Per quanto ne so, Microsoft non ha mai documentato quali modifiche al sistema disabilitano l’immagine nativa creata con NGEN, quindi non posso essere più preciso su questo punto. Se qualcuno conosce qualche articolo che spiega questi casi, lo menzioni nei commenti.

Per il resto, ho fatto un po’ di prove, sembra che tutto quanto fili liscio e che la tecnica non abbia altre controindicazioni. Se vi imbattete in qualche problema, fatemelo sapere.

6/4/2007 9:06:09 PM (GMT Daylight Time, UTC+01:00) #  | Comments [4] | 

Finalmente ho trovato il tempo per controllare una a una tutte le segnalazioni dei lettori su errori tipografici riscontrati nel mio ultimo libro su Visual Basic. Il risultato è un nuovo documento di errata corrige, che potete scaricare dalla home page del libro. I numeri di pagina si riferiscono alla versione inglese del libro, ma i numeri di pagina nella versione italiana sono spesso identici o comunque molto vicini.

Per la comodità di chi ha già downloadato questo documento, ho marcato con "New" tutte le aggiunte recenti. Da notare che quasi tutte le correzioni ai sorgenti non impattano sul funzionamento del codice, ad eccezione di un fix che serve a correggere un errore nella routine Evaluate del capitolo sulle regular expression.

7/1/2006 8:29:59 AM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Purtroppo non riesco a stare dietro ai vari commenti a questo blog, e ogni tanto me ne perdo qualcuno davvero interessante. In particolare, non avevo letto subito quello di Pasquale Esposito a un mio post di fine Aprile. In quel post parlavo di una mia DLL per VB6 e chiudevo chiedendo ai lettori VB6 quando si sarebbero decisi a passare a .NET. Pasquale ha risposto con un commento lungo e esauriente, di cui riporto qui i pezzi salienti:


Ci chiedi quand'è che ci decidiamo a migrare a VB.NET. Vedi, io sono rimasto profondamente deluso dal passaggio al byte code operato dalla Microsoft. Da circa dieci anni produco software shareware che distribuisco su Internet o nei CD allegati a riviste di informatica (ultima pubblicazione: Soluzione Bilancio 1.0 recensito a pag. 97 di PC Magazine di maggio 2006). Ho provato a tastare il terreno distribuendo qualche piccolo applicativo in .NET e mi sono subito reso conto che i tempi non sono affatto maturi per passare a questa tecnologia, almeno per chi utilizza la metodologia shareware come canale di distribuzione. Lo affermo per almeno due motivi:

1) Non è per niente facile chiedere ad un utente che non ha il Framework installato sulla propria macchina di scaricarlo da Internet solo per far funzionare la propria applicazione: per chi non ha una connessione ADSL sognificherebbe effettuare un download di qualche ora. Inoltre, anche chi ha installato il SP2 di WinXP, non è detto che abbia scelto di includere il Framework. Conclusione: molto probabilmente, l'utente in questione sceglierà di rinunciare all'applicativo in .NET e cercherà qualcos'altro di meno problematico. In poche parole, il "DLL Hell" di VB6 ha lasciato il posto al ".NET Framework Hell" di VB.NET.

2) Programmare in byte code significa produrre software estremamente vulnerabile, non solo dal punto di vista del cracking ma perfino da quello del reverse-engineering. Basta pochissima esperienza per entrare in possesso del codice sorgente altrui ed è necessario ricorrere a buoni obfuscator di terze parti per proteggere le proprie applicazioni. Questi strumenti non soltanto sono alquanto costosi ma molto spesso fanno uso di codice nativo (anziché di byte code), rendendo la piattaforma .NET non più autosufficiente. Conclusione: non ritengo che programmare in byte code sia conveniente per chi produce shareware. Al contrario, un applicativo in VB6 può essere crackato soltanto da esperti e in nessun caso è possibile ottenere i sorgenti. Il byte code, quindi, non è adatto a fini commerciali. Molti programmatori VB6 non lo hanno mai preso in considerazione, altrimenti sarebbero passati a Java già molto tempo fa.

.....

L'unico motivo che mi potrebbe far pensare alla migrazione a VB.NET è il timore che, dopo Windows Vista, Microsoft possa abbandonare strategicamente la tecnologia COM a 32 bit. Spero davvero che ciò non accada perché ciò significherebbe costringere la gente a buttare via tutto il software attualmente in commercio. Insomma, VB6 è senz'altro lo strumento che utilizzerò nei prossimi 4-5 anni dato che i runtime di base saranno ancora presenti in Windows Vista (lo stesso Microsoft Anti-Spyware è stato progettato in VB6), dopodiché potrò considerare l'ipotesi di effettuare la migrazione.

.....

La richiesta che vorrei rivolgerti è questa: VB6 non è morto. E non è neanche obsoleto. Molti programmatori si g uadagnano da vivere con VB6 e vorrebbero che tu ti occupassi ancora di questo strumento. Come hai reso il tuo MsgHookX nuovamente disponibile online, così dovresti fare con tutta la tua produzione che riguarda VB6. Anzi, dovresti creare ancora per l'ambiente VB6. Infine, se puoi, riferisci alla Microsoft che esiste ancora un esercito di programmatori VB6 che sarebbe pronto ad acquistare una nuova versione unmanaged del suo strumento di sviluppo. Dal punto di vista commerciale, sarebbe senz'altro una mossa vincente!



Io trovo che l’opinione di Pasquale sia fondata, nel senso che se io facessi il suo lavoro (vendere shareware) potrei avere dei problemi a passare armi e bagagli a .NET. Pero’ ci sono alcune considerazioni da fare.

*) Le dimensioni del runtime: La stesso problema se lo ponevano i programmatori shareware 10 anni fa, quando confrontavano il runtime di VB6 con i piccoli eseguibili scritti in C. Ma allora c'è da chiedersi: perchè gli shareware-isti che lavorano in VB hanno avuto spesso più successo di quelli che lavorano in altri linguaggi come C ? La risposta, a mio avviso, è che scrivere un programma in VB richiedeva una frazione del tempo necessario a scriverlo in C quindi a parità di impegno è possibile creare programmi più potenti e ricchi di funzioni. (solo il Delphi può competere con VB quanto a produttività ) Forse non altrettanto veloci di quelli scritti in C, ma sufficientemente veloci per la maggior parte dei compiti.

Rapportiamo questa esperienza ad oggi: i 20M circa del framework sono circa 15 volte più grandi del runtime di VB6, pero' è anche vero che ADSL è almeno 15 volte più veloce del dialup, quindi la proporzione regge. Anzi, se i vostri clienti hanno la fibra ottica, neanche se ne accorgono. Insomma, chi tanti anni fa ha fatto la scelta di passare a VB infischiandosene delle dimensioni del runtime ha avuto ragione. Secondo me, lo stesso accade a chi oggi decide di passare a VB.NET

Certo, non tutti gli utenti hanno la ADSL, ma il ragionamento che farei io se fossi un autore shareware è: quanto mi interessano davvero questi utenti? i clienti migliori per il software e per i servizi sono le aziende e il power-user, e quelli l'ADSL ce l'hanno sempre. Se qualcuno non ha i 20€ al mese per pagare la connettività, difficilmente pagherebbe per i miei programmi. Gli unici che non rientrano in questo mio ragionamento sono le aziende e gli utenti che spenderebbero volentieri questi soldi, ma purtroppo non sono serviti da ADSL. Pero' oggettivamente si tratta di casi che diventeranno sempre più rari, e penso che in 2-3 anni saranno impossibili da trovare. (A parte il fatto che uno si puo' collegare a internet anche con una scheda UMTS...)

Allora, la domanda da porsi è: se sono un autore di software (shareware o non), vale la pena davvero continuare ad usare uno strumento che era eccezionale 10 anni fa ma adesso è decisamente superato? In questi anni io credo di avere dimostrato di saper fare davvero di tutto con il "vecchio Visual Basic", eppure oggi quando devo tornare a scrivere codice con VB6 mi sento un impedito. Non si tratta solo del linguaggio, ma anche dell'IDE e degli strumenti a corredo. Dopo pochi mesi con il VB.NET ero già molto più produttivo che in VB6 (che avevo usato per 10 anni). Oggi che conosco bene il .NET Framework credo di essere, mediamente, almeno tre volte più veloce. Ovvero scrivo un programma in un terzo del tempo che ci mettevo prima. Io vendo programmi principalmente ad aziende, ma anche se facessi shareware mi porre la stessa domanda: vale la pena rinunciare a questa enorme produttività per raggiungere qualche utente in più, che probabilmente non comprerebbe comunque il mio software?

*) Anche se il .NET Framework è ben pesante, non è strettamente necessario distribuirlo tutto con le proprie applicazioni. In teoria una applicazione WinForm ha bisogno di circa un terzo dell'intero framework. Anche se non mi sono mai interessato più di tanto alla questione, ho letto che ci sono dei programmi che sono in grado di comprire un eseguibile .NET e tutte e sole le librerie che utilizza , senza cioè richiedere una installazione completa del framework. Se le dimensioni del runtime fossero davvero un problema, proverei a fare qualche ricerca più approfondita su questi prodotti.

*) come lo stesso Pasquale fa notare, lo shareware si puo' diffondere anche con mezzi che non siano Internet, ad es. sui CD allegati alle riviste. Pero' è un dato di fatto che ci sono sempre meno riviste che allegano i CD, proprio perchè la maggior parte degli utenti ha una linea veloce e preferisce scaricare dalla Rete per essere sicuri di avere la versione più recente. Ad esempio, Microsoft Press, Mondadori e altri publisher importanti non allegano più i CD ai propri libri (a meno che il contenuto non superi i 30-40M) e loro prendono queste decisioni solo dopo analisi di mercato fatte per bene.

*) se fossi un programmatore VB6, a rendere ancora più semplice e più netta la mia decisione di passare a .NET c'è la considerazione che tutto quello che scrivo oggi con VB6 dovrà forzatamente essere buttato via tra qualche anno, vuoi perchè Microsoft non supporterà più VB, vuoi perchè i programmi e i controlli ActiveX potrebbero non funzionare bene con le prossime versioni di Windows. E' giusto fare pressione su Microsoft per evitare che cio' avvenga, ma le probabilità di fare rimangiare le loro decisioni sono prossime allo zero. Quindi è un piccolo "suicidio professionale" puntare tutto su questa speranza e nel frattempo fare finta che il mondo sia quello di dieci anni fa. Se consigliassi a qualcuno di continuare a scrivere codice VB6 sarei un vero incosciente.

*) Il discorso sul byte code: E' innegabile che un programma che possa essere decompilato facilmente pone dei seri problemi. Purtroppo .NET non offre una netta risposta a questo problema, ma solo mezze soluzioni (tipo installare sul server come servizio o sito asp.net). Quando Microsoft lanciò le prime beta di .NET cinque anni fa, questo aspetto mi sembrava davvero fondamentale, in grado addirittura di rallentare la diffusione del nuovo linguaggio.

Le cose sono andate diversamente, per fortuna. C'è da tenere presente che il problema del reverse-engineering è molto sentito da tutti i programmatori, non solo quelli che vendono shareware. Se uno mette sul mercato uno shareware e qualcuno ne fa il reverse engineering, è molto facile dimostrare che il nuovo programma è un clone. Basta decompilarli entrambi e mostrare a tutto il mondo il risultato, sottolineando che il proprio programma è stato messo sul mercato prima del clone. Certo non è efficace quanto una azione legale (che pure è possibile, ma costosa) ma nel mondo di Internet è sufficiente per perdere la reputazione. Certo, uno puo' fare il reverse engineering e poi modificare il codice, ma è facile mettere delle "trappole" in giro per il sorgente, ossia delle istruzioni che non fanno nulla e che chi ha clonato il programma non saprebbe giustificare. Per togliere queste trappole uno si deve studiare meglio tutto il sorgente, ma allora il lavoro del copiatore si complica.

A parte queste considerazioni, c'è da sottolineare il fatto che questo problema è sempre esistito nel mondo dello sviluppo software. Molti linguaggi che in passato hanno avuto successo erano dei byte-code decompilabili: ad esempio dBASE, il Clipper (spacciato come compilatore), Java, e lo stesso Visual Basic fino alla versione 4.0. E anche se il linguaggio era compilato, resta il fatto che i tracciati dei record e le tabelle dei database - ovvero un aspetto fondamentale delle applicazioni gestionali - sono sempre stati visibili e interpretabili. Se vuoi capire come funziona un gestionale, la prima cosa da fare è vedere come sono strutturate le sue tabelle del DB.

Insomma, non sto dicendo che il problema non esiste: sto dicendo che è meno serio di quello che molti programmatori credono.


Termino il post rispondendo direttamente a Pasquale: hai ragione a dire che VB6 non è morto. Ci sono moltissimi programmatori che ancora lo usano e il mio libro su VB6 continua a vendere abbastanza bene, addirittura meglio di alcuni libri su VB.NET (non dei miei libri su VB.NET, per fortuna! :-) )

Pero' hai (molto) torto a dire che NON è obsoleto. Non si tratta di "rinnegare" il passato e soprattutto non dobbiamo parlare di VB come di un cagnolino a cui ci siamo affezionati, che non si vuole rottamare per ragioni sentimentali (ho letto anche interventi di questo tenore sui vari forum....). Quando è nato era eccezionale e per anni io ho campato (e bene!) con questo prodotto, ma se lo confronti con VB.NET (e con l'IDE di Visual Studio 2005) ti accorgi che il Visual Basic 6 è davvero morto e sepolto.

Ecco perchè non ho firmato alcuna petizione pro-VB6 e perchè sto facendo di tutto per convincere gli sviluppatori VB6 a passare il prima possibile a .NET: quando cominci a lavorare con la programmazione ad oggetti "vera", con gli attributi e reflection, i controlli di Windows Forms, le applicazioni ASP.NET e tutto il resto, il Visual Basic 6 ti sembra tanto ma tanto lontano.

NOTA: anche se non sviluppo più sotto VB6, forse cercando sul mio disco rigido troverei qualche cosa interessante che non ho ancora pubblicato. In tal caso sarà un piacere metterla a disposizione di tutti su questo sito. L'ho fatto per anni e posso continuare a farlo anche ora...

6/12/2006 8:07:06 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

In Visual Basic 6 esiste una comodissima proprietà dell'oggetto App che permette di determinare se vi sono altre istanze in esecuzione della stessa applicazione. Manco a dirlo, questa proprietà non è mai stata migrata in VB.NET anche se, per fortuna, una applicazione scritta in VB 2005 può almeno usare l'evento StartupNextInstance:

' to display this code, open the Application page of the My Project designer and click the Application Events button
Namespace
My
  
Partial Friend Class MyApplication
      Private Sub MyApplication_StartupNextInstance(ByVal sender As Object, ByVal e As Microsoft.VisualBasic.ApplicationServices.StartupNextInstanceEventArgs) Handles Me.StartupNextInstance
         ' another instance of this application has been launched
      End Sub
  
End Class
End
Namespace

Il problema di questo approccio è che l'evento viene sollevato nella prima istanza della applicazione, non nella nuova istanza, quindi alla partenza una applicazione VB.NET non può sapere con certezza se è l'unica istanza o meno. In altre parole, può solo sapere se e quando un'altra applicazione parte, non se l'applicazione corrente è la prima e unica istanza in esecuzione.

In .NET 2.0 è stato introdotto il nuovo tipo System.Threading.Semaphore che permette di risolvere il problema in modo davvero elegante. Un semaforo è un oggetto che può essere incrementato e decrementato. La cosa interessante è che se il semaforo ha un nome, allora esso è condiviso da tutte le applicazioni .NET in esecuzione che richiedono un riferimento al semaforo con quel particolare nome. Basta quindi che l'applicazione crei alla partenza un semaforo con un nome univoco (ad es. un nome che include il percorso dell'eseguibile, che è quasi certamente univoco) e più applicazioni possono condividere il contatore interno al semaforo. L'unico problema che resta da risolvere è assicurarsi che il valore del semaforo sia correttamente ripristinato quanto l'applicazione termina, ma anche questo è facile da ottenere usando un metodo Finalize.

In aggiunta alla proprietà PrevInstance - che restituisce False se l'applicazione era l'unica istanza al momento della partenza - la seguente classe VB6App espone anche la proprietà InstanceCount, che restituisce il numero totale di istanze in esecuzione in quel momento, incluso quindi l'applicazione corrente. Ecco il codice della classe VB2005:

Class VB6App
   ' the default instance
  
Private Shared DefValue As New VB6App

   ' the system-wide semaphore
  
Private semaphore As System.Threading.Semaphore
   ' initial count for the semaphore (very high value)
  
Private Const MAXCOUNT As Integer = 10000

   Private Sub New()
      Dim ownership As Boolean = False
      ' create a unique name, but strip invalid characters
     
Dim name As String = "VB6App_" & System.Reflection.Assembly.GetExecutingAssembly().Location.Replace(":", "").Replace("\", "")
      semaphore = New System.Threading.Semaphore(MAXCOUNT, MAXCOUNT, name, ownership)
      ' decrement its value 
      semaphore.WaitOne()
      ' if we got ownership, this app has no previous instances
      m_PrevInstance = Not ownership
   End Sub

   ' the PrevInstance property returns True if there was a previous instance running 
  
' when the default instance was created
  
Private Shared m_PrevInstance As Boolean

   Public Shared ReadOnly Property PrevInstance() As Boolean
      Get
         Return m_PrevInstance 
      End Get
   End Property

   ' return the total number of instances of the same application that are currently running 
  
Public Shared ReadOnly Property InstanceCount() As Integer
      Get
         ' release the semaphore and grab the previous count 
        
Dim prevCount As Integer = DefValue.semaphore.Release()
         ' acquire the semaphore again
        
DefValue.semaphore.WaitOne()
         ' eval the number of other instances that are currently running 
        
Return MAXCOUNT - prevCount
      End Get
   End Property

   Protected Overrides Sub Finalize()
      ' increment the semaphore when the application terminates
     
semaphore.Release()
   End Sub

End Class

Notate che questa classe contiene un metodo Finalize senza implementare IDisposable. Si tratta di uno dei casi speciali in cui è giusto violare il pattern Dispose-Finalize.

Per comprende come funziona la classe, basta ricordare che il metodo Release dell'oggetto Semaphore incrementa il valore interno, mentre WaitOne lo decrementa. L'unica accortezza da usare con questo codice è di testare la proprietà VB6App.PrevInstance il prima possibile, ad esempio nel metodo Sub Main o nell'evento Load del form principale, per dare la possibilità alla classe di conservare il suo valore alla partenza del programma. Lo stesso form potrebbe poi testare il valore di InstanceCount in uscita, ad esempio se è necessario eseguire un codice di cleanup quando l'ultima istanza della applicazione termina:

Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
   If Not VB6App.PrevInstance Then 
      ' open the common log file
      ' ...
   End
If
End Sub

Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
   If VB6App.InstanceCount = 1 Then
     
' close the common log file.
      ' ...
  
End If
End Sub

Visto che l'utilità di questa classe, l'ho anche tradotta in C#:

public class VB6App
{
   // the default instance 
   private static VB6App DefValue = new VB6App(); 
   // the system-wide semaphore
   private System.Threading.Semaphore semaphore; 
   // initial count for the semaphore (very high value)
   private const int MAXCOUNT = 10000;

   private VB6App() 
   { 
      // create a named (system-wide semaphore)
      bool ownership = false
      // create the semaphore or get a reference to an existing semaphore

      string name = "VB6App_" + System.Reflection.Assembly.GetExecutingAssembly().Location.Replace(":", "").Replace("\", "");
      semaphore = new System.Threading.Semaphore( MAXCOUNT, MAXCOUNT, name, ref ownership); 
      // decrement its value
     
semaphore.WaitOne(); 
      // if we got ownership, this app has no previous instances
      m_PrevInstance = !ownership;

   }

   // the PrevInstance property returns True if there was a previous instance running
  
// when the default instance was created

   private static bool m_PrevInstance ;

   public static bool PrevInstance 
  
      get 
     
         return m_PrevInstance ; 
     
   }

   // return the total number of instances of the same application that are currently running

   public static int InstanceCount 
  
      get 
     
         // release the semaphore and grab the previous count 
        
int prevCount = DefValue.semaphore.Release(); 
         // acquire the semaphore again
        
DefValue.semaphore.WaitOne(); 
         // eval the number of other instances that are currently running 
        
return MAXCOUNT - prevCount; 
     
   }

   ~VB6App() 
  
      // increment the semaphore when the application terminates
     
semaphore.Release(); 
   }
}

 

5/27/2006 11:32:35 AM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

In pochi giorni mi sono arrivate due mail da altrettanti lettori, che chiedono come sia possibile - in una finestra Windows Forms che sfrutta l'evento Validating dei controlli per validare l'input - saltare la validazione se l'utente chiude il form. Se non si salta la validazione, infatti, l'effetto è che l'utente potrebbe non riuscire a chiudere il form. La risposta è molto semplice, ma evidentemente non è proprio ovvia, quindi la ripeto qui a beneficio di tutti.

Se l'utente chiude un form cliccando sul pulsante "X" o scegliendo "Close" dal menu di sistema, scatta prima l'evento Validating del controllo che ha il focus poi, anche se il primo evento ha decretato che la validazione è fallita, scatta l'evento FormClosing. La cosa interessante è che, se l'evento Validating ha impostato e.Cancel = True, nell'evento FormClosing è possibile resettare questo valore a False. In definitiva, quindi, per evitare che il codice nell'evento Validating impedisca all'utente di chiedere il form sono sufficienti queste istruzioni:

Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As FormClosingEventArgs) Handles Me.FormClosing
   If e.CloseReason = CloseReason.UserClosing OrElse e.CloseReason = CloseReason.WindowsShutDown Then
      e.Cancel = False
   End If
End Sub

Un modo più radicale per evitare il problema è di nascondere il pulsante "X" oppure il comando "Close" del menu di sistema della finestra, impostando la proprietà ControlBox a False. Oppure potete disattivarla (magari anche solo temporaneamente) con il seguente codice:

Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
   SetCloseCommandState(Me.Handle, False)
End Sub

Private Declare Function GetSystemMenu Lib "user32" (ByVal hWnd As IntPtr, ByVal bRevert As Integer) As IntPtr
Private Declare Function EnableMenuItem Lib "user32" (ByVal hMenu As IntPtr, _
   ByVal wIDEnableItem As Integer, ByVal wEnable As Integer) As Integer
Const MF_GRAYED As Integer = &H1&
Const MF_BYCOMMAND As Integer = &H0&
Const SC_CLOSE As Integer = &HF060&

Sub SetCloseCommandState(ByVal hWnd As IntPtr, ByVal newState As Boolean)
   Dim hMenu As IntPtr = GetSystemMenu(hWnd, 0)
   Dim wFlags As Integer = MF_BYCOMMAND
   If newState Then
     
wFlags = wFlags And Not MF_GRAYED
   Else
     
wFlags = wFlags Or MF_GRAYED
   End If
  
EnableMenuItem(hMenu, SC_CLOSE, wFlags)
End Sub

5/13/2006 12:42:44 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Ieri mi scrive un mio lettore, Claudio Fontana, con una richiesta apparentemente semplice: come fare per evitare sfarfallii strani quando si devono aggiornare la maggior parte dei controlli sul form? Il problema si fa sentire soprattutto quando occorre aggiungere migliaia di elementi a listbox e treeview.

In VB6 questo problema si risolveva molto semplicemente impostando la proprietà Visible (o Enabled) a False durante l'aggiornamento, e re-impostandola a True alla fine delle operazioni: la cosa interessante è che, a meno di non inserire una istruzione Refresh esplicita, il controllo non veniva effettivamente reso invisibile o "grayed", ma il suo nuovo contenuto appariva di botto, senza alcun effetto di flickering. Non solo: se il controllo veniva reso invisibile l'aggiornamento veniva anche eseguito molto più velocemente, tipicamente nella metà del tempo solitamente necessario. Sfortunatamente, in .NET il trucco di rendere i controlli temporaneamente invisibili non funziona più, nel senso che il refresh avviene effettivamente e quindi il controllo scompare effettivamente per tutta la durata dell'aggiornamento. Occorre pensare a qualcos'altro.

Alcuni controlli Windows Forms espongono i metodi BeginUpdate e EndUpdate, che di fatto ottengono non solo di "congelare" un controllo durante le operazioni di aggiornamento ma anche di sveltire notevolmente le operazioni stesse, che diventano anche 2.5 volte più veloci. Però solo quattro controlli espongono questa proprietà: ListBox, ComboBox, TreeView e ListView. Se il vostro form contiene molti controlli di altro tipo è necessario pensare a qualcos'altro, e questo è il problema che mi ha sottoposto Claudio, dopo aver googlato a destra e a manca sulla Rete alla ricerca di una soluzione, senza alcun esito.

Poichè il problema era intrigante, ho deciso di dedicarci qualche minuto, e sono arrivato ad una soluzione abbastanza interessante e (se Claudio ha googlato bene) anche inedita. L'idea è davvero molto semplice: (1) si "fotografa" l'aspetto corrente del form, copiandolo pixel per pixel in una bitmap, (2) si crea un controllo PictureBox grande quanto il form e si carica la bitmap nella PictureBox, (3) si aggiunge la PictureBox alla collection Controls e la si porta in primo piano, in modo da coprire tutti gli altri controlli, (4) mentre l'utente vede l'immagine congelata del form, si aggiornano tutti i controlli, usando i metodi BeginUpdate/EndUpdate se possibile per velocizzare le operazioni, (5) quando l'update è completato, si elimina la PictureBox in modo che l'utente torni a vedere il vero contenuto del form.

Tutto questo algoritmo è in realtà molto semplice, e si riduce a una decina di righe. Però ho pensato di creare una classe ad-hoc, in modo che il codice sia facilmente riutilizzabile, rilasci correttamente le risorse, e si protegga da utilizzi errati:

Public Class FormFreezer
  
Implements IDisposable

   ' The form being frozen
   Dim Form As Form
   ' the auxiliary PictureBox that will cover the form
   Dim PictureBox As PictureBox
   ' the number of times the Freeze method has been called
   Dim FreezeCount As Integer = 0

   ' create an instance associated with a given form
   ' and optionally freeze the form right away
   Public Sub New(ByVal form As Form, Optional ByVal freezeIt As Boolean = False)
      Me.Form = form
      If freezeIt Then Me.Freeze()
   End Sub

   ' freeze the form 
   Public Sub Freeze()
      ' Remember we have frozen the form once more
      FreezeCount += 1
      ' Do nothing if it was already frozen
      If FreezeCount > 1 Then Exit Sub

      ' Create a PictureBox that resizes with its contents
      PictureBox = New PictureBox()
      PictureBox.SizeMode = PictureBoxSizeMode.AutoSize
      ' create a bitmap as large as the form's client area and with same color depth
      Dim frmGraphics As Graphics = Form.CreateGraphics()
      Dim rect As Rectangle = Form.ClientRectangle
      PictureBox.Image = New Bitmap(rect.Width, rect.Height, frmGraphics)
      frmGraphics.Dispose()

      ' copy the screen contents, from the form's client area to the hidden bitmap
      Dim picGraphics As Graphics = Graphics.FromImage(PictureBox.Image)
      picGraphics.CopyFromScreen(Form.PointToScreen(New Point(rect.Left, rect.Top)), New Point(0, 0), New Size(rect.Width, rect.Height))
      picGraphics.Dispose()

      ' Display the bitmap in the picture box, and show the picture box in front of all other controls
      Form.Controls.Add(PictureBox)
      PictureBox.BringToFront()
   End Sub

   ' unfreeze the form
   ' Note: calls to Freeze and Unfreeze must be balanced, unless force=true 
   Public Sub Unfreeze(Optional ByVal force As Boolean = False)
      ' exit if nothing to unfreeze
      If FreezeCount = 0 Then Exit Sub
      ' remember we've unfrozen the form, but exit if it is still frozen
      FreezeCount -= 1
      ' force the unfreeze if so required
      If force Then FreezeCount = 0
      If FreezeCount > 0 Then Exit Sub

      ' remove the picture box control and clean up
      Form.Controls.Remove(PictureBox)
      PictureBox.Dispose()
      PictureBox = Nothing
   End Sub

   ' return true if the form is currently frozen
   Public ReadOnly Property IsFrozen() As Boolean
      Get
         Return FreezeCount > 0
      End Get
   End Property

   ' ensure that resources are cleaned up correctly
   Public Overridable Sub Dispose() Implements IDisposable.Dispose
      Me.Unfreeze(True)
   End Sub
End
Class

Questo è invece la versione C#, tradotta da Claudio:

public class FormFreezer: IDisposable
{

   // The form being frozen

   Form form;

   // the auxiliary PictureBox that will cover the form

   PictureBox pictureBox;

   // the number of times the Freeze method has been called

   int FreezeCount = 0;

 

   // create an instance associated with a given form

   // and freeze the form in base of flag freezeIt

   public FormFreezer(Form form, bool freezeIt)
   {

      this.form = form;

      if (freezeIt) this.Freeze();

   }

 

   // freeze the form 

   public void Freeze()
   {

      // Remember we have frozen the form once more

      // Do nothing if it was already frozen

      if (++FreezeCount > 1) 
         return;

      // Create a PictureBox that resizes with its contents

      pictureBox = new PictureBox();

      pictureBox.SizeMode = PictureBoxSizeMode.AutoSize;

      

      // create a bitmap as large as the form's client area and with same color depth

      Graphics frmGraphics = form.CreateGraphics();

      Rectangle rect = form.ClientRectangle;

      pictureBox.Image = new Bitmap(rect.Width, rect.Height, frmGraphics);

      frmGraphics.Dispose();

 

      // copy the screen contents, from the form's client area to the hidden bitmap

      Graphics picGraphics = Graphics.FromImage(pictureBox.Image);

      picGraphics.CopyFromScreen(form.PointToScreen(new Point(rect.Left, rect.Top)), new Point(0, 0), new Size(rect.Width, rect.Height));

      picGraphics.Dispose();

 

      // Display the bitmap in the picture box, and show the picture box in front of all other controls

      form.Controls.Add(pictureBox);

      pictureBox.BringToFront();

   }

 

   // unfreeze the form

   // Note: calls to Freeze and Unfreeze must be balanced, unless force=true

   public void Unfreeze(bool force)
   {

      // exit if nothing to unfreeze

      if ( FreezeCount == 0 ) 
         return ;

      // remember we've unfrozen the form, but exit if it is still frozen

      FreezeCount -= 1;

      // force the unfreeze if so required

      if (force) 
         FreezeCount = 0;

      if (FreezeCount > 0) 
         return;

      // remove the picture box control and clean up

      pictureBox.Controls.Remove(pictureBox);

      pictureBox.Dispose();

      pictureBox = null;

   }

 

   // return true if the form is currently frozen

   public bool IsFrozen
   {

      get { return (FreezeCount > 0); }

   }

 

   void IDisposable.Dispose()

   {

      this.Unfreeze(true);

   }

}

Usare la classe FormFreezer è semplicissimo. Ecco un esempio di codice (che deve trovarsi all'interno di una classe Form, in modo che Me punti al form corrente):

   Dim ff As New FormFreezer(Me, True)
   ' update controls here
  
' ...
  
ff.Unfreeze()

Poichè la classe implementa IDisposable, è possibile racchiudere il codice di aggiornamento in un blocco Using, sia in C# che in VB 2005, ed evitare la chiamata esplicita a Unfreeze:

   Using New FormFreezer(Me, True)
      ' Update controls here
     
' ...
  
End Using

Notate che le chiamate a Freeze e Unfreeze devono essere bilanciate, ad esempio se si eseguono due chiamate a Freeze occorreranno poi due chiamate a Unfreeze per "scongelare" effettivamente il form. Questo comportamento è simile a quello esposto dalle API di sistema che nascondono e mostrano il cursore del mouse, e funziona correttamente il form anche se il codice chiama Freeze e passa poi il controllo a un altro metodo che chiama anch'esso Freeze (a condizione pero' di usare la stessa istanza di FormFreezer).

5/3/2006 11:58:42 AM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

MsgHookX è una DLL ActiveX che permette ai programmatori VB6 di eseguire un subclassing intra-applicazione sicuro ed efficiente, ossia di intercettare qualsiasi messaggio che Windows invia a un controllo o finestra della applicazione corrente. Scrissi questa DLL diversi anni fa, ai tempi del VB5, e l'ho utilizzata in alcuni libri e articoli usciti nel frattempo. Questa DLL era disponibile sul vecchio sito VB2TheMax, ma non è sopravvissuta quando il sito è diventato .NET-2-The-Max e decidemmo di scartare tutti i contenuti relativi al vecchio VB6.

Beh, se devo giudicare dalle tante mail che continuo a ricevere dai lettori, si direbbe che gli sviluppatori VB6 continuano a voler subclassare i loro controlli e le loro finestre. Per questa ragione ho deciso di rendere MsgHookX nuovamente disponibile, sul sito americano a questo URL. Ecco un breve sommario di quello che la DLL è in grado di fare:

  • subclassing sicuro: può essere usata nella IDE e in break mode, senza rischiare alcun crash di sistema.
  • fornisce gli eventi BeforeMessage e AfterMessage, per una programmazione semplice di tipo event-driven
  • è anche in grado di notificare l'arrivo dei messaggi attraverso l'interfaccia secondaria IMsgHookEvents, per fornire prestazioni migliore e un debugging più facile (in alcuni casi gli eventi sono inibiti nella IDE di VB6)
  • alta flessibilità: potete decidere di chiamare la window procedure originale dall'evento/metodo BeforeMessage e opzionalmente di annullare l'elaborazione di default del messaggio. Potete anche visualizzare e modificare il valore che il messaggio sta per restituire al sistema operativo.
  • La type library della DLL include la definizione di oltre 300 costante simboliche, che definiscono i messaggi Windows più comuni, in modo da non dover ricorrere all'API Viewer per includere le definizioni nella proprie applicazioni.

Ora la mia domanda è: ma quando, quando, quando vi deciderete a passare a .NET ? :-)

4/29/2006 9:19:36 AM (GMT Daylight Time, UTC+01:00) #  | Comments [1] | 

Ieri ho dovuto risolvere un piccolo problema: durante la migrazione di una applicazione VB6 era necessario scartare un metodo, in modo cioè che fosse "invisibile" a VB.NET. Questo era necessario perchè il metodo in questione svolgeva le stesse funzioni di un metodo nativo di .NET. Ovviamente, è sempre possibile cancellare il metodo dopo la migrazione, ma se i metodi sono tanti oppure se occorre lanciare più volte il wizard sullo stesso programma VB6 dopo rifiniture successive, allora la cosa diventa scocciante.

Apparentemente, non si puo' dire al migration wizard di ignorare dei pezzi di codice, però la soluzione è comunque molto semplice. Basta racchiudere il pezzo di codice in questione (interi metodi o anche singole istruzioni) in un blocco #IF WIN32. La costante di compilazione WIN32, di cui probabilmente pochi programmatori VB6 si ricordano, fu introdotta ai tempi di Visual Basic 4, l'unica versione di questo linguaggio ad essere disponibile nella versione a 16 e 32 bit, e questa costante (insieme a WIN16) permetteva appunto di definire blocchi di codice che venivano compilati solo in una delle due versioni, dando allo stesso tempo la possibilità di manutenere un unico sorgente per entrambe. In definitiva quindi, il seguente pezzo di codice VB6:

#If WIN32 Then
   Private Sub Do Something(ByVal n As Integer)   
      ' ...
   End Sub
#End If

viene correttamente migrato dal wizard nel seguente codice VB.NET

#If WIN32 Then
   Private Sub DoSomething(ByVal n As Short)
      ' ...
   End Sub
#End If

ma poichè la costante WIN32 non è definita in VB.NET il pezzo di codice racchiuso tra #IF e #ENDIF sarà ignorato dal compilatore VB.NET.

Attenzione anche al fatto che se state migrando una applicazione VB6 che a sua volta è l'evoluzione di una applicazione scritta originariamente in VB4, è anche possibile che ci siano dei pezzi di codice che voi *volete* migrare in VB.NET, ma per quanto detto sopra queste porzioni non saranno poi eseguite in VB.NET. In tal caso dovrete cercare tutte le occorrenze di #IF WIN32 nel codice e cancellarle.

4/29/2006 8:03:57 AM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Molti, se non la maggior parte, degli esempi di controlli Windows Forms che si trovano in giro per la rete contengono chiamate a codice unmanaged nelle DLL di Windows, in particolare uno o più metodi SendMessage per ovviare alle (pochissime) mancanze controlli del .NET Framework. Il problema è che un controllo del genere crea dei problemi quando l'applicazione viene eseguita in modalità ClickOnce, poichè richiede di modificare la CAS associata alla particolare applicazione.

Anche se questo problema non ha soluzione in generale, quando si intende inviare un messaggio al controllo che si sta subclassando è possibile fare a meno di una chiamata esplicita a SendMessage e usare invece il metodo protetto DefWndProc che viene ereditato dalla classe Control. Supponiamo ad esempioi di voler scrivere una ComboBox "enhanced", che espone la proprietà TopIndex, che imposta o restituisce l'indice del primo elemento visibile nella porzione di lista del controllo. Queste due operazioni si possono implementare inviando rispettivamente i messaggi CB_SETTOPINDEX e CB_GETTOPINDEX al controllo. Ecco come fare, usando il metodo DefWndProc invece di SendMessage:

Public Class ComboBoxEx
  
Inherits System.Windows.Forms.ComboBox

   Public Property TopIndex() As Integer
      Get
         Const CB_GETTOPINDEX As Int32 = &H15B
         Dim m As New Message()
         m.HWnd = Me.Handle
         m.Msg = CB_GETTOPINDEX
         Me.DefWndProc(m)
         Return m.Result.ToInt32()
      End Get
      Set(ByVal value As Integer)
         Const CB_SETTOPINDEX As Int32 = &H15C
         Dim m As New Message()
         m.HWnd = Me.Handle
         m.Msg = CB_SETTOPINDEX
         m.WParam = New IntPtr(value)
         Me.DefWndProc(m)
      End Set
   End Property

End Class

Incidentalmente, questa ComboBox enhanced vi può venire utile se state migrando applicazioni VB6. Infatti, la ComboBox e la ListBox di VB6 supportano la proprietà TopIndex, mentre in .NET solo la ListBox espone questa proprietà. Se avete del codice VB6 che utilizza la proprietà TopIndex di una ComboBox, l'approccio più semplice è sostituire il controllo con una ComboBoxEx.

4/28/2006 12:35:06 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Nel mio post di ieri dicevo che probabilmente avrei partecipato alla conferenza del 23 marzo a Milano con Jay Roxe e Tyler Whitney, rispettivamente Product Manager di Visual Studio e Technical Lead di Visual Basic.

Mi devo correggere: sicuramente parteciperò alla conferenza in questione, visto che parlerò anche al keynote iniziale, in compagnia di Francesco Albano di Microsoft Italia. Ecco l'abstract della nostra sessione:

MIGRAZIONE DELLE APPLICAZIONI VB6 IN VB.NET
Anche se il numero delle software house che utilizzano Visual Studio cresce di giorno in giorno, il passaggio al mondo .NET è in qualche modo ritardato dalla presenza di numerose applicazioni “legacy” scritte in Visual Basic 6. In questa sessione si discuterà del processo di adozione di VB.NET e di come utilizzare le nuove feature di Visual Basic 2005 per facilitare la conversione di applicazioni VB6.

Tempo permettendo, durante la sessione farò anche vedere in anteprima uno strumento a cui sto lavorando da un mesetto circa, che non mancherà di interessare chi ha intenzione di migrare in maniera "indolore" le applicazioni VB6 esistenti e non vuole impazzire con il wizard incluso in Visual Studio. Maggiori informazioni a breve su questo blog....

3/9/2006 3:48:45 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Il 23 marzo a Milano c'è una conferenza da non perdere, non solo per l'argomento in sè - le novità di VB2005 e VSTO - quanto per la presenza del product manager di Visual Studio Jay Roxe e del Visual Basic Technical Lead Tyler Whitney. La conferenza è in inglese, ma Microsoft fornisce gentilmente cuffiette e traduzione simultanea. Maggiori informazioni su http://www.microsoft.com/italy/msdn/eventi/vbasic.mspx

Se ce la faccio, farò un salto pure io, quindi è probabile che ci vedremo là.  Per me che vivo a Bari, "un salto" significa svegliarmi alle 4 e prendere un paio di aerei....

3/8/2006 5:38:40 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Sto riorganizzando la mia collection di MP3 e mi sono trovato nella necessità di rinominare un numero davvero enorme di file. Ci sono molte utility sul mercato che permettono di farlo (e che sono anche in grado di usare i tag MP3 per rinominare i file), ma ho pensato che tanto valeva scriverne una io. Tanto, grazie alle regex il compito non sarebbe stato difficile. E infatti in qualche minuto ho tirato fuori il seguente codice. Come vedete, la maggior parte delle istruzioni servono a estrarre e validare gli argomenti dalla riga di comando:

Imports System.Text.RegularExpressions
Imports System.IO

Module Renx

  
Function Main(ByVal args() As String) As Integer
     
Console.WriteLine("RENX (C) Francesco Balena / Code Architects Srl")

      Dim recurse As Boolean = False
     
Dim renameMode As Boolean = False
     
Dim oldNamePattern As String = Nothing
     
Dim newNamePattern As String = Nothing

      ' analyze each argument
     
For Each arg As String In args
        
Select Case arg.ToLower()
           
Case "/s", "-s"
              
recurse = True
            
Case "/r", "-r"
              
renameMode = True
           
Case "/h", "-h"
              
Return ShowHelp(0)
           
Case Else
              
If oldNamePattern Is Nothing Then
                 
oldNamePattern = "^" & arg & "$"
              
ElseIf newNamePattern Is Nothing Then
                 
newNamePattern = arg
              
Else
                 
Return ShowHelp(1)
              
End If
        
End Select
     
Next

      ' check that we have both mandatory arguments
     
If oldNamePattern Is Nothing OrElse newNamePattern Is Nothing Then
        
Return ShowHelp(1)
     
End If
     
' create the regex and check that pattern syntax is ok
     
Dim reSearch As Regex
     
Try
        
reSearch = New Regex(oldNamePattern, RegexOptions.IgnoreCase)
        
' test the replace pattern as well
        
Dim tmp As String = reSearch.Replace("a dummy string", newNamePattern)
      
Catch ex As Exception
         Console.WriteLine(
"SYNTAX ERROR: {0}", ex.Message)
        
Return 3
     
End Try
     
Console.WriteLine()

      ' iterate over all files in current directory (and its subdirectories, if recurse mode)
     
Dim searchOpt As SearchOption = SearchOption.TopDirectoryOnly
     
If recurse Then searchOpt = SearchOption.AllDirectories

      Dim parsedFilesCount As Integer = 0
     
Dim renamedFilesCount As Integer = 0
     
Dim errorsCount As Integer = 0
     
For Each oldFile As String In Directory.GetFiles(Directory.GetCurrentDirectory(), "*.*", searchOpt)
         parsedFilesCount += 1
        
' the regex applies to name only
        
Dim oldName As String = Path.GetFileName(oldFile)
        
Dim ma As Match = reSearch.Match(oldName)
        
If ma.Success Then
           
' this is the new name
           
Dim newName As String = ma.Result(newNamePattern)
            Console.WriteLine(oldFile)
            Console.Write(
" => {0}", newName)
            renamedFilesCount += 1
           
' proceed with rename only if not in simulation mode
           
If renameMode Then
              
Try
                 
Dim dirName As String = Path.GetDirectoryName(oldFile)
                 
Dim newFile As String = Path.Combine(dirName, newName)
                  File.Move(oldFile, newFile)
              
Catch ex As Exception
                  Console.Write(
" -- ERROR: {0}", ex.Message)
                  errorsCount += 1
              
End Try
           
End If
           
Console.WriteLine()
        
End If
     
Next

      ' Display a report
     
If renameMode Then
        
Console.WriteLine("Summary: {0} parsed files, {1} renamed files, {2} errors", parsedFilesCount, renamedFilesCount, errorsCount)
     
Else
        
Console.WriteLine("Summary: {0} parsed files, {1} files affected", parsedFilesCount, renamedFilesCount)
         Console.WriteLine()
         Console.WriteLine(
"NOTE: Running in simulation mode. Specify the /R option to actually rename files.")
     
End If
     
' Return an error code
     
If errorsCount = 0 Then
        
Return 0
     
Else
        
Return 2
     
End If
  
End Function

   Function ShowHelp(ByVal exitCode As Integer) As Integer
     
Console.WriteLine()
      Console.WriteLine(
"Syntax: RENX <oldnamepattern> <newnamepattern> [/R] [/S] [/H]")
      Console.WriteLine(
" oldnamepattern : regex that selects the files to be renamed")
      Console.WriteLine(
" newnamepattern : regex that specifies how files must be renamed")
      Console.WriteLine(
" /R : rename files")
      Console.WriteLine(
" /S : iterate over subdirectories")
      Console.WriteLine(
" /H : display this help")
      Console.WriteLine(
"NOTE: By default the program runs in simulation mode, and just displays how files would be renamed.")
      Console.WriteLine(
" You must specify the /R option to actually rename the files.")
     
Return exitCode
  
End Function

End Module

Al minimo, la utility RENX richiede due argomenti: una regex che individua quali file nella directory corrente (e nelle sue subdirectory, se si specifica l'opzione /S) devono essere rinominati, e una regex che specifica come rinominare i file che soddisfano la prima regex. La potenza di RENX sta nel fatto che la regex che individua i file può (anzi, deve) specificare uno o più gruppi di caratteri, che vengono poi riutilizzati nella seconda regex. Supponiamo ad esempio che abbia una directory con i seguenti file

        01 Speak to Me.mp3
        02 On the Run.mp3
        03 Time.mp3
        04 The Great Gig in the Sky.vbr
        05 Money.mp3
        06 Us and Them.mp3
        07 Any Colour You Like.vbr
        08 Brain Damage.mp3
        09 Eclipse.vb3

e li voglia rinominare in questo modo:

        01 - Speak to Me - The Dark Side of the Moon.mp3
        02 - On the Run - The Dark Side of the Moon.mp3
        03 - Time - The Dark Side of the Moon.mp3
        04 - The Great Gig in the Sky - The Dark Side of the Moon.vbr
        05 - Money - The Dark Side of the Moon.mp3
        06 - Us and Them - The Dark Side of the Moon.mp3
        07 - Any Colour You Like - The Dark Side of the Moon.vbr
        08 - Brain Damage - The Dark Side of the Moon.mp3
        09 - Eclipse - The Dark Side of the Moon.vbr

Ecco allora come invocare RENX per effettuare la trasformazione:

        RENX "(\d\d) (.+?)(\..+)"    "${1} - ${2} - The Dark Side of the Moon${3}"

Notate che la prima regex crea tre gruppi racchiudendo dei caratteri tra parentesi: (\d\d) fa il match con due cifre, mentre (.+?) fa il match con il nome del file, e infine (\..+) fa il match con l'estensione del file, punto incluso. Il secondo argomento puo' riordinare a piacere queste tre quantità, usando la sintassi ${N}, dove N è il numero d'ordine del gruppo individuato dalla prima regex, ed è quindi in grado di inserire un trattino dopo il numero del brano e il nome dell'album dopo il titolo del brano.

Data la pericolosità del comando di rename, per default il comando RENX *non* effettua il rename e si limita a mostrare come i file sarebbero rinominati. Per effettuare effettivamente il rename occorre specificare l'opzione /R:

        RENX "(\d\d) (.+?)(\..+)"    "${1} - ${2} - The Dark Side of the Moon${3}" /R

Questo è tutto. Avete il sorgente e potete estendere l'utility come preferite (magari per trasformarla in un programma Windows Forms), e potete anche effettuare il download della versione in binario da questo link: Renx.zip (5.51 KB)

3/6/2006 5:15:52 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Qualche minuto fa ho caricato tutti gli esempi a corredo di Programming Microsoft Visual Basic 2005: The Language sulla home page del libro. E' uno zippone di 7.5M, che contiene migliaia di righe di codice riutilizzabile, accuratamente scritte, testate e ottimizzate a manina. Tra le tante cose che potete trovare:

  • una classe VB che lavora con le frazioni
  • un form base per semplificare il data entry
  • una classe per il caching dei file di testo
  • alcuni custom iterator per scrivere dei For Each migliori
  • dozzine di esempi con i generics
  • due valutatori di espressioni, il primo basato sui regex, il secondo implementato tramite compilazione on-the-fly
  • una utlity per mostrare le statistiche di un progetto VB (è una demo per le regex)
  • RegexTester, una utility stand-alone per aiutarvi a costruire, testare, e compilare le regular expression
  • un esempio di custom provider per My.Settings
  • un esempio di come intercettare qualsiasi evento da qualsiasi controllo su un form
  • benchmark basati su attributi custom
  • una completa infrastruttura per scrivere applicazioni Windows Forms estendibili via plug-in
  • attributi e DLL per scrivere applicazioni N-tier data-centriche
  • numerose macro per Visual Studio
  • visualizer per mostrare il contenuto di file, regex, e immagini durante il debug
  • .... e molto altro.

Sulla stessa pagina ho caricato un document di errata per gli errori trovati dopo che il libro è andato in stampa, e alcuni capitoli di esempio. Per accedere alla pagina occorre registrarsi.

Divertitevi con il codice ( ... e comprate il libro se lo trovate interessante! :-) )

2/28/2006 7:25:51 PM (GMT Standard Time, UTC+00:00) #  | Comments [2] | 

Più lavoro con i generics più mi piacciano e più modi scopro per semplificare il mio codice mediante il loro utilizzo. In particolare, mi piace la possibilità di scrivere codice type-safe, più conciso ed efficiente ma soprattutto più leggibile perchè non contiene dozzine di CType e DirectCast. Oggi ho raccolto in questo modulo le funzioni generiche che uso più spesso nei miei programmi. Sono molto brevi e semplici, ma mi hanno fatto risparmiare un bel po' di tempo e codice.

Module GenericFunctions

   ' Swap two variables
  
Public Sub Swap(Of T)(ByRef var1 As T, ByRef var2 As T)
     
Dim tmp As T = var1
      var1 = var2
      var2 = tmp
  
End Sub

   ' Type-safe version of the IIF function
  
' returns valueOnTrue if expression is True, else returns valueOnFalse
  
Public Function IIf(Of T)(ByVal expression As Boolean, ByVal valueOnTrue As T, ByVal valueOnFalse As T) As T
     
If expression Then
        
Return valueOnTrue
     
Else
        
Return valueOnFalse
     
End If
  
End Function

   ' Type-safe version of the Choose function
  
' returns the N-th element of a list of values, or the default value for T if index
  
' is less than 0 or higher than the number of values
  
Public Function Choose(Of T)(ByVal index As Integer, ByVal values() As T) As T
     
If index >= 0 AndAlso index < values.Length Then
        
Return values(index)
     
Else
        
Return Nothing
     
End If
  
End Function

   ' Return an array of the specified type
  
Public Function NewArray(Of T)(ByVal ParamArray values() As T) As T()
     
Return values
  
End Function

   ' Return the min value of a list
  
Public Function Min(Of T As IComparable)(ByVal firstValue As T, ByVal ParamArray values() As T) As T
     
Dim result As T = firstValue
     
For Each value As T In values
        
If result.CompareTo(value) > 0 Then result = value
     
Next
     
Return result
  
End Function

   ' Return the max value of a list
  
Public Function Max(Of T As IComparable)(ByVal firstValue As T, ByVal ParamArray values() As T) As T
     
Dim result As T = firstValue
     
For Each value As T In values
        
If result.CompareTo(value) < 0 Then result = value
     
Next
     
Return result
  
End Function

   ' Return True if a value is in specific range
  
Public Function InRange(Of T As IComparable)(ByVal testValue As T, ByVal minValue As T, ByVal maxValue As T) As Boolean
     
Return testValue.CompareTo(minValue) >= 0 AndAlso testValue.CompareTo(maxValue) <= 0
  
End Function

   ' Retrieve a dictionary element of a given type, or the provided default value if the element isn't found
  
' (two overloads)
  
Public Function GetDictionaryValue(Of TKey, TValue)(ByVal dict As Hashtable, ByVal key As TKey, ByVal defaultValue As TValue) As TValue
     
If dict.ContainsKey(key) Then
        
Return CType(dict(key), TValue)
     
Else
        
Return defaultValue
     
End If
  
End Function

   Public Function GetDictionaryValue(Of TKey, TValue)(ByVal dict As Dictionary(Of TKey, TValue), ByVal key As TKey, ByVal defaultValue As TValue) As TValue
     
' If the key is in the dictionary, the following statement stores the corresponding value
     
' in defaultValue, else it leave defaultValue unchanged
     
dict.TryGetValue(key, defaultValue)
     
Return defaultValue
  
End Function

End Module

L'uso della maggior parte dei metodi dovrebbe essere evidente. Una delle funzioni più utili è NewArray, in quanto permette di creare al volo un array da passare a un metodo. Supponiamo ad esempio che il metodo DoSomething richieda un vettore di interi. Ecco quali sono le opzioni a disposizione in VB2005:

    ' 1. Create the array first, than pass it
    Dim values() As Integer = {1, 2, 3, 4, 5}
    DoSomething(values)

    ' 2. Create the array on the fly using the nearly-undocumented syntax
    DoSomething(New Integer() {1, 2, 3, 4, 5})

Io di solito uso la seconda sintassi, ma ho notato che pochi programmatori VB la conoscono. Il codice diventa molto più pulito e leggibile con la funzione NewArray:

    ' 3. Use the NewArray generic function to create the array on the fly
    DoSomething(NewArray(1, 2, 3, 4, 5))

Il metodo NewArray si dimostra molto utile anche per creare dei cicli For il cui indice varia a piacere.

    ' Test whether "number" is a prime number in the range 1-1000
    For Each n As Integer In NewArray(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31)
       If (number Mod n) = 0 Then Console.Write("{0} is not prime", number): Exit For
    Next

2/23/2006 8:00:38 AM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

In VB6 - e più in generale, nel mondo COM - un oggetto si distrugge semplicemente impostandolo a Nothing (ovviamente, nell'ipotesi in cui non vi siano altre variabili che puntano a quella particolare istanza). Il runtime di VB invoca il metodo Class_Terminate, se esiste un handler per tale evento, poi dealloca tutte le risorse assegnate all'oggetto, inclusa la mamoria. Questo processo è ricorsivo, quindi se l'oggetto possiede altri oggetti COM (ad es. una connessione ADO), anche quegli oggetti sono distrutti.

Sappiamo bene, invece, che in .NET impostare una variabile oggetto a Nothing non fa scattare alcun evento. Se l'oggetto implementa il metodo Finalize, tale metodo sarà chiamato dal garbage collector di .NET solo qualche tempo dopo. L'intervallo di tempo tra l'impostazione a Nothing e l'esecuzione del codice di cleanup contentuo nel metodo Finalize dipende da tanti fattori, e potrebbe arrivare anche a minuti o ore, se il programma non alloca molti altri oggetti e quindi non stressa il garbage collector.

Questa differenza nel comportamento tra VB6 e VB.NET diventa spesso un problema davvero serio nella migrazione delle applicazioni. Se ad esempio il codice nell'evento Class_Terminate chiude una connessione al database, oppure un file o una porta seriale, oppure cancella delle informazioni confidenziali, o chiude un form, allora il ritardo tra la distruzione "logica" dell'oggetto e la sua distruzione "fisica" può compromettere il funzionamento della applicazione stesso, dopo la sua migrazione verso il mondo .NET.

Purtroppo non è possibile ottenere "in automatico" che VB.NET si comporti esattamente come VB6 in questo caso, pero' possiamo fare qualcosa che riduce il problema. Questo è possibile, ancora una volta, grazie ai generics. Basta inserire il metodo seguente in un modulo, in modo da renderlo visibile a tutta l'applicazione:

Public Sub SetNothing(Of T)(ByRef obj As T)
  
' Dispose of the object if possible
  
If obj IsNot Nothing AndAlso TypeOf obj Is IDisposable Then
     
DirectCast(obj, IDisposable).Dispose()
  
End If
  
' Decrease the reference counter, if it's a COM object
  
If Marshal.IsComObject(obj) Then
     
Marshal.ReleaseComObject(obj)
  
End If
  
obj = Nothing
End Sub

A questo punto, possiamo fare un "search and replace" in tutto il codice VB.NET prodotto dal wizard di migrazione, sostituendo le istruzioni var = Nothing con SetNothing.(var). Ovviamente, questo tecnica non risolve tutti i problemi menzionati in precedenza, perchè funziona solo con le variabili esplicitamente messe a Nothing via codice, e non quelle che sono azzerato implicitamente quando escono dallo scope corrente.

Poichè si tratta di una sostituzione con parti variabili, sembrerebbe che non sia possibile farlo in modo davvero automatico, e invece non è così, perchè possiamo usare le feature di ricerca e sostituzione di Visual Studio basate sulle regular expression. (Per maggiori info, vedere questo post.) Infatti, basta attivare la ricerca per regular expression, inserire la stringa <{:i} = Nothing come stringa da ricercare, e la stringa SetNothing(\1) come stringa di sostituzione. Et voilà :-)

NOTA per i refrattari alle regex: la prima stringa dice di ricercare una variabile (:i) che si trova all'inizio di parola (<); le graffe in {:i} eseguono il tagging del nome dell'identificatore, in modo da poter poi usare il nome della variabile nel testo di sostituzione (mediante il placeholder \1). Tutto qui, non era mica difficile.

 

2/14/2006 12:55:33 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Il tipo Date di VB6 e il tipo Date di VB.NET (o il tipo System.DateTime di .NET, il che è lo stesso) sono molto simili, eccetto che per un dettaglio importante: in VB6 le date sono conservate internamente con valori Double, la cui parte intera rappresenta il numero di giorni trascorsi dal 30-12-1899 (non chiedetemi perchè non dal 31 dicembre...), e la cui parte decimale rappresenta le ore, minuti, e secondi sotto forma di frazione di giorno.

Il wizard che migra le applicazioni VB6 in VB.NET non modifica in alcun modo le variabili di tipo Date però deve tenere conto del fatto che i programma VB6 eseguono spesso operazioni con le date sfruttando il formato di memorizzazione interno. Ad esempio, ecco come in VB6 si calcola la data corrispondente a sette giorni dalla data odierna:

    Dim da As Date
    da = Now + 7
    MsgBox da

Per migrare correttamente le operazioni aritmetiche sulle date, il wizard usa i metodi FromOADate e ToOADate della classe Date, in questo modo:

   Dim da As Date
   da = Now
   da = System.Date.FromOADate(da.ToOADate + 7)

Non è un bel codice a vedersi, ma funziona. Anche in questo caso, però, è possibile applicare qualche trucchetto per ridurre al minimo le differenze sintattiche tra il codice VB6 e VB.NET e supportare l'aritmetica tra le date anche in VB.NET senza inserire nuovi metodi. Il meccanismo si basa sulla definizione di un nuovo tipo VB6Date, che conserva internamente un valore Date, che supporta le conversioni implicite da e verso i tipi Date, String, e Double, e che supporta anche gli operatori + e - con interi e Double:

<DebuggerDisplay("{DateValue}")> _
Public Structure VB6Date

   ' the origin of all dates
  
Private Shared ReadOnly StartDate As Date = New Date(1899, 12, 30)

   ' this is where the actual date value is stored
  
Private DateValue As Date

   ' private constructor (Friend to be visible from the VB6DateFunctions module)
  
Friend Sub New(ByVal value As Date)
     
Me.DateValue = value
   End Sub

   ' override the ToString method, to support Console.Write, etc.
  
Public Overrides Function ToString() As String
     
Return DateValue.ToString()
  
End Function

   ' convert to/from Date values 
  
Public Shared Widening Operator CType(ByVal value As VB6Date) As Date
     
Return value.DateValue
  
End Operator

   Public Shared Widening Operator CType(ByVal value As Date) As VB6Date
     
Return New VB6Date(value)
  
End Operator

   ' convert to/from Integer values 
  
Public Shared Widening Operator CType(ByVal value As VB6Date) As Integer
     
Return value.DateValue.Subtract(StartDate).Days
  
End Operator

   Public Shared Widening Operator CType(ByVal value As Integer) As VB6Date
     
Return New VB6Date(StartDate.Add(New TimeSpan(value, 0, 0, 0)))
  
End Operator

   ' convert to/from Double values 
  
Public Shared Widening Operator CType(ByVal value As VB6Date) As Double
     
Return value.DateValue.Subtract(StartDate).Ticks / 864000000000
  
End Operator

   Public Shared Widening Operator CType(ByVal value As Double) As VB6Date
     
Return New VB6Date(StartDate.Add(New TimeSpan(CLng(value * 864000000000))))
  
End Operator

   ' convert to/from strings 
  
Public Shared Widening Operator CType(ByVal value As VB6Date) As String
     
Return value.ToString()
  
End Operator

   Public Shared Widening Operator CType(ByVal value As String) As VB6Date
     
Return New VB6Date(Date.Parse(value))
  
End Operator

   ' the + and - operators, with Integer and Double values
  
Public Shared Operator +(ByVal value As VB6Date, ByVal days As Integer) As VB6Date
     
Return New VB6Date(value.DateValue.Add(New TimeSpan(days, 0, 0, 0)))
  
End Operator

   Public Shared Operator -(ByVal value As VB6Date, ByVal days As Integer) As VB6Date
     
Return New VB6Date(value.DateValue.Subtract(New TimeSpan(days, 0, 0, 0)))
  
End Operator

   Public Shared Operator +(ByVal value As VB6Date, ByVal days As Double) As VB6Date
     
Return New VB6Date(value.DateValue.Add(New TimeSpan(CLng(days * 864000000000))))
  
End Operator

   Public Shared Operator -(ByVal value As VB6Date, ByVal days As Double) As VB6Date
     
Return New VB6Date(value.DateValue.Subtract(New TimeSpan(CLng(days * 864000000000))))
  
End Operator

End Structure

Notate l'attributo DebuggerDisplay, che permette di visualizzare la data contenuta in un oggetto VB6Date quando si sposta il cursore del mouse sul nome di una variabile (solo in Visual Studio 2005).

Per completare l'effetto, occorre scrivere anche dei rimpiazzi per le funzioni Now, Date (ovvero Today in VB.NET), e Time, in modo che anche queste funzioni restituiscano un oggetto di tipo VB6Date:

Public Module VB6DateFunctions
  
Public ReadOnly Property Now() As VB6Date
     
Get
        
Return New VB6Date(Microsoft.VisualBasic.DateAndTime.Now)
     
End Get
   
End Property

   Public ReadOnly Property [Today]() As VB6Date
     
Get
        
Return New VB6Date(Microsoft.VisualBasic.DateAndTime.Today)
     
End Get
  
End Property

   Public ReadOnly Property Time() As VB6Date
     
Get
        
Return New VB6Date(Now - Today)
     
End Get
  
End Property
End
Module

A questo punto è possibile migrare il codice VB6 contenente operazioni aritmetiche sulle date semplicemente sostituendo il tipo della variabile che ospita tali valori:

' VB.NET code
Dim da As VB6Date    ' <-- the only modified statement
da = Now + 7
MsgBox(da)

Con questo esempio non sto certo suggerendo di cambiare TUTTE le vostre variabili Date per usare il tipo custom VB6Date. Il mio obiettivo principale è soprattutto di mostrare come - con un po' di fantasia e soprattutto sfruttando le nuove feature di VB2005, in questo caso l'overloading degli operatori - sia spesso possibile ridurre la distanza tra due linguaggi simili ma subdolamente differenti come VB6 e VB.NET. Ovviamente il discorso potrebbe valere anche nella migrazione in .NET da altri linguaggi ancora (ad esempio Java).


In chiusura, mi piacerebbe fare un piccolo sondaggio:

1) quanti di voi sviluppano o anche solo manutengono applicazioni VB6 ?
2) se si, perchè l'applicazione non è stata ancora convertita in VB.NET ?
3) in generale, quali ritenete siano gli ostacoli principali alla conversione ?
4) e quali sono i principali pregi e difetti del wizard di migrazione fornito con Visual Studio ?

2/9/2006 12:51:24 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Ciao a tutti, 
   vi riporto di seguito il calendario ufficiale dei prossimi webcast architetturali tenuti da Maurizio, Pierre, me ed altri. Il primo appuntamento e' domani. Illustrero' le novita' della rinnovata versione dell'Enterprise Library di Patterns & Practices per poi proseguire fino a giugno con una nutrita serie di argomenti e approfondimenti su tecnologie reali.

Calendario dei prossimi Architect Webcast  
§Febbraio
7/2: Pattern architetturali per la realizzazione di applicazioni e servizi - Parte I
14/2: Pattern architetturali per la realizzazione di applicazioni e servizi - Parte II
21/2: Introduzione alla metodologia agile MSF 4.0 con Visual Studio 2005 Team System
28/2: Progettare il Web Testing nel mondo Enterprise con Visual Studio 2005 Team Test
§Marzo
07/3: BizTalk Server 2006: uno strumento per tutta l'azienda
14/3: BizTalk Server 2006: mille e uno usi di uno strumento versatile
21/3: BizTalk Server 2006 e lo sviluppo di applicazioni orientate ai servizi
28/3: WinFX: Windows Workflow Foundation - Parte I
§Aprile
04/4: WinFX: Windows Workflow Foundation - Parte II
11/4: Realizzare servizi distribuiti con Windows Communication Foundation - Parte I
19/4: Realizzare servizi distribuiti con Windows Communication Foundation - Parte II
27/4: Architettura SOA. Perché non se ne può fare a meno?
§Maggio
09/5: Le applicazioni client negli scenari d'integrazione - Parte I
16/5: Le applicazioni client negli scenari d'integrazione - Parte II
23/5: Interoperabilità e migrazione tra .NET e COM
§Giugno
06/6: Smart Client. Unire il meglio di idee e tecnologie diverse
13/6: Il dato al centro dell'informazione aziendale. Come gestirlo
20/6: Snellire i processi aziendali gestendo il flusso di informazioni con Office
27/6: Smart Document: la nuova faccia del documento

2/6/2006 5:54:25 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Quasi non ci speravo più, ma alla fine qualcuno è riuscito a risolvere il Quizzettone #9, ovvero come determinare una mano di poker in solo otto istruzioni. Anzi, il bravissimo Matteo Conta è riuscito a farlo in solo sette istruzioni. Graaaaande! Trovate la sua soluzione in coda ai vari commenti al post.

Come promesso, passo invece a illustrare la mia soluzione in otto istruzioni. La soluzione è in VB2005 (tratta dal nuovo libro Programming Microsoft Visual Basic 2005: The Language), ma potrebbe essere facilmente resa anche in C#:

Public Shared Function EvalPokerScore2(ByVal ParamArray cards() As String) As String
  
' Sort the array and create the sequence of values and of suits.
  
Array.Sort(cards)
  
Dim values As String = cards(0)(0) + cards(1)(0) + cards(2)(0) + cards(3)(0) + cards(4)(0)
  
Dim suits As String = cards(0)(1) + cards(1)(1) + cards(2)(1) + cards(3)(1) + cards(4)(1)

   Dim scores(,) As String = { _
      {
"12345|23456|34567|45678|56789|6789T|789JT|89JQT|9JKQT|1JKQT", "(.)\1\1\1\1", "StraightFlush"}, _
      {
"(.)\1\1\1", ".", "FourOfAKind"}, _
      {
"(.)\1\1(.)\2|(.)\3(.)\4\4", ".", "FullHouse"}, _
      {
".", "(.)\1\1\1\1", "Flush"}, _
      {
"12345|23456|34567|45678|56789|6789T|789JT|89JQT|9JKQT|1JKQT", ".", "Straight"}, _
      {
"(.)\1\1", ".", "ThreeOfAKind"}, _
      {
"(.)\1.?(.)\2", ".", "TwoPairs"}, _
      {
"(.)\1", ".", "OnePair"}}
  
For i As Integer = 0 To scores.GetUpperBound(0)
     
If Regex.IsMatch(values, scores(i, 0)) AndAlso Regex.IsMatch(suits, scores(i, 1)) Then
        
Return scores(i, 2)
     
End If
  
Next
  
Return "HighCard"
End Function

(Nel computo delle istruzioni, non si conta nè End If nè Next, secondo le regole enunciate nel quiz...)

Per prima cosa il programma crea due stringhe, values contentente la sequenza dei valori delle carte, suits contenente la sequenza dei semi, poi definisce il vettore bidimensionale scores con le informazioni per cercare i vari punteggi. Ogni riga del vettore scores contiene due regular expression - una che viene applicata alla stringa values, l'altra alla stringa suits. Se entrambe le regex sono soddisfatte, il programma restituisce il terzo elemento della riga, ovvero il nome della combinazione. Notate che la seconda regex (quella che testa i colori) viene usata solo in due casi, per StraightFlush (scala reale) e Flush (colore), quindi in tutti gli altri casi basta usare la regex "." (qualsiasi carattere).

La maggior parte delle regex che testano i valori usano le cosiddette "backreference", ad es. "(.)\1\1" significa "cerca un carattere qualsiasi (il punto) poi ricercalo altre due volte (\1\1): il risultato della ricerca è positivo se vi sono tre carte uguali una dopo l'altra. Notare che l'array cards è ordinato rispetto al valore delle carte, quindi abbiamo la sicurezza che in values le carte con lo stesso valore sono adiacenti.

Proprio perche la stringa values è ordinata per valori, le regex da usare per testare le scale (StraightFlush e Straight) contengono dei caratteri apparentemente fuori ordine. Ad esempio, la scala massima si testa con la regex "1JKQT" e non con sequenza più intuitiva (ma errata), "TJQK1".

Concludendo, anche se la soluzione di Matteo è a dir poco sbalorditiva e assolutamente geniale, direi che quella basata su regex è decisamente più leggibile e manutenibile.


NOTA: Questa soluzione è in 8 istruzioni, ma ho spiegato che esiste anche la possibilità di scriverla in sole sette istruzioni. Questo si ottiene evitando di dividere le carte in due stringhe distinte (values e suits), ed usando invece una sola stringa data dalla concatenazione delle coppie (valore,seme). Anche in questo caso è possibile scrivere delle regex che trovano correttamaente tutte le combinazioni, ma le regex stesse sono molto più complesse, quindi ho deciso che nel libro avrei pubblicato questa versione più lunga ma più leggibile.

In realtà è persino possibile modificare il precedente codice per ottenere una soluzione in sole sei istruzioni: infatti, invece di creare le stringhe values e suits fuori dal ciclo, potrei crearle al volo ad ogni iterazione del ciclo For..Next. Non solo, ma se aggiungo una ulteriore riga alla matrice scores, contenente i sequenti valori:

         {".", ".", "HighCard"}}

Allora posso evitare anche l'istruzione Return finale, portando il totale a sole cinque istruzioni, anche se in quest'ultimo caso devo tenermi il warning che VB emette perchè non tutti i possibili percorsi della funzione restituiscono un valore. Insomma, se davvero volete scrivere codice illeggibile, non c'è che l'imbarazzo della scelta :-)

Ultimissima nota: un pubblico ringraziamento e una pubblica lode anche a Paolo Possanzini, per essere stato il primo ad accanirsi e a risolvere, qualche mese fa mentre scrivevo il libro, questo quiz. Onore al merito!

2/3/2006 4:53:39 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Finalmente il mio ultimo librone è fisicamente disponibile su Amazon, e in un solo giorno è risalito di 8 mila posizioni in classifica: al momento in cui scrivo è alla posizione 1171, che è un ottimo piazzamento. Vi ricordo che sono disponibili altre informazioni sul libro, incluso un paio di capitoli di esempio in PDF, sulla home page che gestisco sul sito americano di dot-net-2-the-max.

Speriamo che anche l'analogo libro su C# e le classi base del Framework, di cui in questi giorni sto completando la revisione, abbia eguale fortuna...

Nel frattempo, il mio ultimo quizzettone è ancora insoluto, cosa insolita (ma non troppo, visto che questa volta era davvero tosto). Magari un aiutino ve lo posso dare: la mia soluzione in otto istruzioni si basa sulle regular expression e appare proprio nel capitolo sulle regex del libro in questione.

2/2/2006 8:06:50 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

In questo periodo mi sto interessando molto ai problemi di migrazione delle applicazioni VB6 verso VB.NET. Personalmente non ho realmente "migrato" nessuna delle mie applicazioni, perchè ho sempre preferito riscriverle da zero per sfruttare al massimo le feature di .NET e soprattutto perchè il wizard di conversione fa un lavoro oggettivamente penoso, producendo un codice brutto e non manutenibile. O meglio, il wizard fa un lavoro decente, ma il vero problema sono le incompatiblità tra VB6 e VB.NET.

Alcune di queste incompatibilità, tuttavia, possono essere superate con un po' di fantasia, soprattutto ora che VB2005 offre delle feature che prima non erano disponibili. Prendiamo ad esempio il problema "classico" degli array con indice diverso da zero, che hanno fatto dannare tutti gli sviluppatori VB6 alle prese con il porting verso VB.NET. Supponiamo di avere il seguente codice VB6

      Dim arr(1 to 10) as Integer
      Dim i As Integer, prod As Integer, v As Variant
      For i = LBound(arr) To UBound(arr)
         arr(i) = i
      Next
      For Each v in arr
         prod = prod * v
      Next

Il wizard di conversione sostituirà l'indice iniziale 1 con l'indice 0, quindi l'array avrà un elemento in più. E' evidente che alla fine dell'esecuzione il valore di prod sarà zero, mentre avrebbe dovuto pari al fattoriale di 10!. Si tratta di errori molto subdoli, che costringono in pratica a ristudiare attentamente tutto il codice e a testare da zero l'applicazione. L'approccio manuale migliore è sicuramente quello di modificare la dichiarazione per "scalare" l'array in modo che il suo primo elemento reale abbia indice zero, e poi modificare TUTTI i riferimenti agli elementi dell'array, per tenere conto dello shift:

      Dim arr(0 to 10-1) as Integer     
      Dim i As Integer, prod As Integer
      For i = LBound(arr) To UBound(arr)
         arr(i - 1)  = i
      Next

Ma anche questo approccio richiede molta attenzione e in certi casi non puo' essere usato, ad esempio quando l'array viene passato a una procedura che deve funzionare con array di qualunque tipo (e il cui codice non sa che deve scalare l'indice); in altri casi ancora, il programma potrebbe prendere delle decisioni in base al valore di LBound e UBound, che nella nuova versione è modificato.

La domanda che mi sono posto è: è possibile convertire quel codice in VB2005 senza doversi preoccupare di tutti questi problemi? La soluzione è stata relativamente semplice, grazie ai generics e a qualche trucchetto con l'ereditarietà.

' Base class

Public Class VBArrayBase
  
Protected Friend lowerIndex As Integer
  
Protected Friend upperIndex As Integer
End Class

' One dimensional array of type T

Public Class VBArray(Of T)
  
Inherits VBArrayBase
  
Implements IEnumerable

   Dim items() As T

   Sub New(ByVal lowerIndex As Integer, ByVal upperIndex As Integer)
     
Me.lowerIndex = lowerIndex
     
Me.upperIndex = upperIndex
     
ReDim items(upperIndex - lowerIndex)
  
End Sub

   Default Property Item(ByVal index As Integer) As T
     
Get
        
Return items(index - lowerIndex)
     
End Get
     
Set(ByVal value As T)
         items(index - lowerIndex) = value
     
End Set
  
End Property

   Public Function GetEnumerator() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator
     
Return items.GetEnumerator()
  
End Function

End Class

Notate con quanta semplicità è possibile supportare i cicli For Each: basta restituire l'oggetto IEnumerator dell'array interno. Occorre a questo punto estendere le istruzioni LBound e UBound alla nuova classe VBArray. Per fare ciò dovete creare un modulo pubblico a parte, con queste istruzioni:

Public Module ArrayFunctionsVB6
   Function LBound(ByVal arr As Array, Optional ByVal rank As Integer = 1) As Integer
     
Return Microsoft.VisualBasic.Information.LBound(arr, rank)
  
End Function

   Function UBound(ByVal arr As Array, Optional ByVal rank As Integer = 1) As Integer
     
Return Microsoft.VisualBasic.Information.LBound(arr, rank)
  
End Function

   Function LBound(ByVal arr As VBArrayBase, Optional ByVal rank As Integer = 1) As Integer
     
If rank = 1 Then
        
Return arr.lowerIndex
     
Else
        
Throw New IndexOutOfRangeException()
     
End If
  
End Function

   Function UBound(ByVal arr As VBArrayBase, Optional ByVal rank As Integer = 1) As Integer
     
If rank = 1 Then
        
Return arr.upperIndex
     
Else
        
Throw New IndexOutOfRangeException()
     
End If
  
End Function
End
Module

Notate che il modulo deve contenere due overload per LBound e UBound, uno per gli array standard e uno per i nostri nuovi array. Purtroppo non è possibile avere un progetto che referenzia due moduli differenti (uno nella libreria di compatibiltà di VB e uno in una nostra DLL) e prenda due overload dello stesso metodo da entrambi i moduli. Se cio' avviene solo uno dei due metodi è visto dal programma principale.

Altro dettaglio interessante: Il codice dei metodi LBound e UBound ha bisogno di accedere ai membri Friend della classe VBArray(Of T), ma non è possible usare il tipo VBArray(Of T) nella dichiarazione di questi metodi. Ecco perchè ho dovuto definire una classe base VBArrayBase e fare ereditare VBArray(Of T) da quella classe base.

Grazie alla classe VBArray(Of T) e al modulo ArrayFunctionsVB6, possiamo convertire il codice VB6 cambiando soltanto la dichiarazione del vettore:

     Dim arr As New VBArray(Of Short)(1, 10)      ' Short invece che Integer

Tutto il resto del programma funzionarà esattamente come in VB6, incluso il ciclo For Each e le chiamate a LBound/UBound. Provare per credere! :-)

1/29/2006 2:44:49 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Ci sono quizzettini relativamente semplici (come quelli che ho pubblicati negli ultimi mesi) e altre sfide logiche che richiedono un po' più di qualche secondo per essere risolti. Quello di oggi appartiene a questa seconda categoria, e merita quindi in pieno l'appellativo di QUIZZETTONE.

Il quesito è relativamente semplice: occorre scrivere un metodo EvalPokerScore in grado di determinare a quale punto del poker corrisponde una mano di cinque carte. Ciascuna carta è rappresentata da una stringa di due lettere, la prima delle quali rappresenta il valore della carta, a scelta tra le seguenti
       1, 2, 3, 4, 5, 6, 7, 8, 9, T (ten=dieci), J (jack), Q (queen), K (king)
la seconda lettera rappresenta ovviamente il seme, ed è una delle seguenti
       H (hearts=cuori), D (diamonds=quadri), S (spades=picche), C (clubs=fiori)

Il metodo EvalPokerScore deve restituire una stringa che indica il punto formato dalle cinque carte passate come argomento. Il risultato deve essere una delle stringhe seguenti
      "StraightFlush" (scala reale), "FourOfAKind" (poker), "FullHouse" (full), "Flush" (colore),
      "Straight" (scala), "ThreeOfAKind" (tris), "TwoPairs" (doppia coppia), "OnePair" (coppia), "HighCard" (nessun punto=carta più alta)

Notate che l'ordine dei punti è differente da quello che vi potreste aspettare: giocando al poker americano con tutte le 52 carte del mazzo, la probabilità e quindi il valore relativo di alcune combinazioni è differente da quello del poker come lo giochiamo dalle nostre parti. Ad esempio, il full batte sempre il colore. Ecco una lista di chiamate al metodo e il risultato atteso da ciascuna:

// These calls return "StraightFlush"
EvalPokerScore(
"1H", "2H", "3H", "4H", "5H");
EvalPokerScore(
"3H", "4H", "5H", "6H", "7H");
EvalPokerScore(
"6C", "7C", "8C", "9C", "TC");
EvalPokerScore(
"7S", "8S", "9S", "TS", "JS");
EvalPokerScore(
"8S", "9S", "TS", "JS", "QS");
EvalPokerScore(
"TD", "JD", "QD", "KD", "1D");
// These calls return "Flush"
EvalPokerScore(
"8S", "JS", "QS", "KS", "1S");
EvalPokerScore(
"7H", "8H", "9H", "TH", "KH");
EvalPokerScore(
"8C", "9C", "TC", "QC", "KC");
// These calls return "FourOfAKind"
EvalPokerScore(
"8C", "9D", "8D", "8H", "8S");
EvalPokerScore(
"TS", "TC", "QD", "TD", "TH");
EvalPokerScore(
"1C", "1S", "8S", "1H", "1D");
// These calls return "FullHouse"
EvalPokerScore(
"1C", "1D", "8C", "1D", "8S");
EvalPokerScore(
"9C", "9S", "8D", "8S", "9H");
EvalPokerScore(
"QC", "TD", "QD", "TS", "QS");
// These calls return "Straight"
EvalPokerScore(
"3S", "7S", "5S", "4S", "6D");
EvalPokerScore(
"6D", "7S", "8H", "9H", "TD");
EvalPokerScore(
"7S", "8D", "9D", "TH", "JC");
EvalPokerScore(
"TC", "JS", "QS", "KS", "1S");
// These calls return "ThreeOfAKind"
EvalPokerScore(
"9C", "9S", "8H", "TD", "9D");
EvalPokerScore(
"TC", "TS", "7S", "TH", "1D");
EvalPokerScore(
"TC", "TS", "8S", "TH", "KH");
// These calls return "TwoPairs"
EvalPokerScore(
"1C", "1F", "8F", "DP", "8Q");
EvalPokerScore(
"9C", "KF", "8F", "8P", "9Q");
EvalPokerScore(
"1C", "DF", "QF", "DP", "QQ");
// These calls return "OnePair"
EvalPokerScore(
"1C", "1H", "KH", "TS", "8S");
EvalPokerScore(
"9C", "KH", "QH", "8S", "9D");
EvalPokerScore(
"1S", "TS", "QC", "8C", "QC");
// These calls return "HighCard"
EvalPokerScore(
"1C", "QC", "KC", "TD", "8D");
EvalPokerScore(
"TC", "KC", "QD", "8D", "9H");
EvalPokerScore(
"1C", "TC", "KD", "8D", "QH");
EvalPokerScore(
"8D", "QD", "TD", "9D", "KH");

Scrivere il metodo EvalPokerScore non è proprio banale, ma neanche difficilissimo. Se siete in gamba potete risolverlo in una decina di minuti, se siete molto in gamba in cinque o meno. Ma adesso viene il bello: l'obiettivo del quizzettone non è solo di riuscire a risolvere il problema, ma di farlo con il minor numero di istruzioni possibile. Quindi

              Riuscite a scrivere il metodo EvalPokerScore con non più di otto istruzioni?

Nel conto delle istruzioni non è contaggiata la dichiarazione del metodo stesso. In altre parole:

// C#
public
string EvalPokerScore( params string[] cards )
{
   
// Non più di otto istruzioni qui
}

' VB
Public EvalPokerScore( ParamArray cards() As String ) As String
    
' Non più di otto istruzioni qui
End Function

Nel conteggio delle istruzioni, una istruzione corrisponde ad una assegnazione di variabile, chiamata a metodo e in generale a una sequenza di keyword che in C# sarebbe terminata con un punto-e-virgola e in VB con un newline oppure con il due-punti. Un blocco for, do, foreach, if corrisponde a una istruzione più quelle contenute nel blocco (questa precisazione è necessaria per non fare differenze tra C# e VB, poichè quest'ultimo richiede una keyword Next o EndIf per chiudere il blocco). Le graffe di apertura e chiusura blocchi in C# non sono conteggiate. Se si usa un anonymous delegates, occorre contare tutte le istruzioni nel metodo anonimo.

Se avete altre domande, lasciate pure un commento qui. Potete anche mandarmi le vostre soluzioni via email, se preferite, visto che potrebbe essere complicato formattarle a dovere (alcune istruzioni saranno necessariamente lunghe....)

Come ho anticipato, la soluzione non è proprio immediata nè semplice, però rappresenta quella che io chiamo "programmazione elegante". L'idea di questo quizzettone è tratta dal mio nuovo libro Programming Microsoft Visual Basic 2005: The Language, dove ovviamente mostro la soluzione e la spiego nei dettagli.

Il tema del quizzettone è un ottimo esempio del codice a corredo del libro e esemplifica il concetto alla base del libro stesso, ovvero che conoscere la sintassi del linguaggio e delle classi base di .NET Framework è una cosa, sapere applicare queste nozioni è una cosa completamente differente, ed è questo che distingue uno sviluppatore davvero in gamba dagli altri.

Come al solito, nessun premio in vile denaro al primo risolutore, ma solo imperitura fama sulle pagine di questo blog. :-)


NOTA: se otto istruzioni vi sembrano troppo poche, sappiate che è persino possibile farlo con sette, però a costo di complicare la struttura del codice. Meglio quindi una istruzione in più, ma mantenere l'eleganza del codice...

 

 

 

1/27/2006 9:01:12 AM (GMT Standard Time, UTC+00:00) #  | Comments [1] | 

Ho già parlato in questo blog di CodeWall.NET, un tool per la protezione degli eseguibili .NET 2.0 dalla decompilazione, a cui io e Vito Plantamura stiamo lavorando da un paio di mesi. Finalmente abbiamo una beta robusta e funzionante, e non vediamo l'ora di metterla online.

Date le finalità del prodotto, non è un mistero il fatto che abbiamo passato la maggior parte del tempo per implementare tutte le tecniche "anti-sprotezione" di nostra conoscenza, incluso alcune davvero esoteriche. E' un vero peccato non poterle descrivere nei dettagli, per motivi che dovrebbero essere evidenti. Comunque, qualche cosa la posso comunque raccontare.

Uno dei punti deboli di praticamente tutti i meccanismi di protezione di questo tipo è che, se un "cracker" trova il modo per sproteggere UN SOLO eseguibile protetto, egli è in grado di scrivere uno script o un tool in grado di sproteggere TUTTI gli eseguibili protetti, la qual cosa sarebbe ovviamente un vero disastro per noi e per i nostri clienti. Per ovviare a questo serissimo problema, ogni copia di CodeWall.NET è unica. Non solo utilizza una chiave differente, ma contiene anche un algoritmo di decifratura leggermente differente. Questo accorginmento rende l'attacco mediante script virtualmente impossibile.

Ecco un'altra tecnica che abbiamo adottato per rendere la protezione più robusta. Le applicazioni protette con CodeWall.NET possono contenere delle stringhe criptate, che saranno riportate in chiaro solo in fase di esecuzione, e solo se l'assembly "gira" sotto CodeWall.NET. Questo significa che se anche qualcuno riuscisse a estrarre un assembly dall'eseguibile protetto, dovrebbe capire come funziona il decrypt delle stringhe e poi decrittare le stringhe una a una. Attenzione, perchè non solo è possibile criptare le stringhe in questo modo: è anche possibile criptare nomi di classi e di metodi, quindi se queste stringhe non sono decrittate correttamente il programma non funzionerebbe affatto.

Ovviamente, il loader di CodeWall.NET è scritto in codice unmanaged, in modo da NON essere decompilabile con Reflector, Anakrino, o ILDASM. Un malintenzionato particolarmente motivato potrebbe usare un disassembler "classico", ma poi gli toccherebbe decifrare migliaia di opcode assembly "nativo", il che non è proprio alla portata di tutti. Purtroppo, pero', l'uso del codice unamanged limita l'utilizzo di CodeWall.NET ai soli eseguibili Win32 (Windows Forms e Console) che girano in modalità full-trust. Non possiamo quindi supportare appliczioni ASP.NET e neanche applicazioni ClickOnce, a meno che questi ultimi non girino appunto in full-trust mode.

Non abbiamo ancora stabilito il prezzo di listino di CodeWall.NET, ma prevediamo che si aggirerà sui 250 dollari (circa 200 euro) e quasi certamente ci sarà uno sconto per il periodo del lancio. A breve speriamo di mettere una versione "trial" disponibile sul sito. Nel frattempo, se avete domande lasciate pure un commento.

1/25/2006 7:12:03 PM (GMT Standard Time, UTC+00:00) #  | Comments [2] | 

Windows Communication Foundation (WCF) e Windows WorkflowFoundation possono essere utilizzati in un ambiente di produzione.

Ulteriori dettagli qui:

http://msdn.microsoft.com/winfx/getthebeta/golive/default.aspx

BIT e informazioni qui:

http://www.windowsworkflow.net

http://windowscommunication.net

 

 

1/18/2006 7:03:33 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Se potessi fare una survey istantanea, proverei a fare le seguenti due domande:

  1. Il programma che usate più frequentemente è Visual Studio?
  2. Avete mai usato le regular expression con il comando Find di Visual Studio?

Scommetterei che l'80% di voi risponderebbe SI alla prima domanda, ma che il 99% risponderebbe NO alla domanda successiva. Il che è alquanto bizzarro: stiamo parlando di una delle feature più potenzialmente utili dell'IDE eppure pochissimi la utilizzano o sanno addirittura che esiste.

Il vero problema è che la sintassi delle regular expression di Visual Studio è completamente differente da quella della classe Regex, quindi per usare questa feature occorrerebbe imparare un altro dialetto per le regular expression, e questo è troppo per la maggior parte degli sviluppatori. Microsoft dovrebbe usare le regex standard anche per questo comando: potrebbero farlo facilmente e in poco tempo, e senza creare problemi di compatibilità con i progetti esistenti.

In attesa che Microsoft si decida a fare questo piccola-grande innovazione, potete divertirvi con quello che avete a disposizione. Ecco alcuni esempi, tratti dal mio nuovo libro Programming Microsoft Visual Basic 2005: The Language:

:i = :z   Cerca nel codice le assegnazioni di un intero a una variabile. (:i sta per un qualsiasi identificativo, :z rappresenta una costante intera). In VB (ma non in C#) trova però anche dei false match, quando una espressione contiene un operatore di uguaglianza.

:i = :q   Cerca le assignazioni di una costante stringa (:q) a una variabile.

(Dim|Private|Public) :i As String   Cerca le dichiarazioni di variabili e field di tipo stringa (solo VB). E' facile adattarla ad altri tipi di dati.

Dim <(:Lu(:Ll)*)+> As   Cerca le dichiarazioni di variabili VB locali che usano un nome in PascalCase e che quindi violano le coding guideline di Microsoft (le variabli locali dovrebbero essere in camelCase)

^:b*'.+\n   Cerca le righe di commento in VB, ossia le righe che cominciano con apostrofo. (Non considera la keyword REM.) Sostituendo l'apostrofo con // si può usare questo pattern anche in C#

Dim {:i} As (.|\n)#<\1>    Evidenzia la porzione di codice tra la dichiarazione di una variabile locale e la prima occorrenza della variabile nel codice. Ripetendo questa ricerca in modo da matchare tutte le variabili locali in un metodo, si puo' controllare se vale la pena spostare la dichiarazione della variabile in modo da avvicinarla al suo primo utilizzo nel codice. (Vedi l'effetto in figura)

1/16/2006 4:56:25 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

In una settimana o due il mio nuovo libro Programming Microsoft Visual Basic 2005: The Language sarà disponibile su Amazon e altri negozi online.

Attualmente Mondadori è a buon punto nella traduzione in italiano dei file .doc, affidata anche questa (come i miei libri più recenti) a Natale Fino, che molti di voi conosceranno come uno dei soci di Infomedia. La versione in italiano dovrebbe essere disponibile entro febbraio.

Nel frattempo, potete avere un assaggio di quello che potrete trovare nel libro leggendo due capitoli (in inglese): il capitolo 11, "Generics", e il capitolo 18, "Reflection". Per leggere i due capitoli occorre essere utenti registrati della versione US del nostro sito dotnet2themax.

1/7/2006 4:58:18 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Incoraggiato dalle buone risposte al mio ultimo quizzettino sulla ottimizzazione, ne ho preparato un'altro sullo stesso tenore, anzi abbastanza simile, anche se credo sia abbastanza più semplice del precedente. Ma potro' dirlo solo dopo aver visto in quanto tempo lo risolverete (perchè oramai sono sicuro che lo risolverete, e presto!)

Supponiamo di avere un vettore bidimensionale di interi, e di voler calcolare il massimo della somma dei numeri in ciascuna colonna. Ecco il programma C# che risolve il problema

const int ROWS = 4000;
const int COLS = 4000;
int[,] arr = new int[ROWS, COLS];
// fill the array with random data (or read it from file, etc.)
Random rnd = new Random(1234);
for (int r = 0; r < ROWS; r++)
  
for (int c = 0; c < COLS; c++)
      arr[r, c] = rnd.Next(1, 10000);
// eval the max of the sum of all elements in each given column
Stopwatch sw = Stopwatch.StartNew();
int sumMax = int.MinValue ;
for (int c = 0; c < COLS; c++)
{
  
// eval the sum of elements in this column
  
int colsum = arr[0,c];
  
for (int r = 1; r < ROWS; r++)
      colsum += arr[r, c];
  
// update the summax value
  
if ( sumMax < colsum )
      sumMax = colsum;
}
Console.WriteLine(sw.ElapsedMilliseconds);
Console.WriteLine("Max is {0}", sumMax);       // Max is 20576003

Ed ecco la versione VB

Const ROWS As Integer = 4000
Const COLS As Integer = 4000
Dim arr(ROWS - 1, COLS - 1) As Integer
' fill the array with random data (or read it from file, etc.)
Dim rnd As New Random(1234)
For r As Integer = 0 To ROWS - 1
  
For c As Integer = 0 To COLS - 1
      arr(r, c) = rnd.Next(1, 10000)
  
Next
Next
' eval the max of the sum of all elements in each given column
Dim sw As Stopwatch = Stopwatch.StartNew()
Dim sumMax As Integer = Integer.MinValue
For c As Integer = 0 To COLS - 1
  
' eval the sum of elements in this column
  
Dim colsum As Integer = arr(0, c)
  
For r As Integer = 1 To ROWS - 1
      colsum += arr(r, c)
  
Next
  
' update the summax value
  
If sumMax < colsum Then sumMax = colsum
Next
Console.WriteLine(sw.ElapsedMilliseconds)
Console.WriteLine(
"Max is {0}", sumMax) ' Max is 20576003

La domanda è la solita: come possiamo ottimizzare questo codice in misura significativa?

Come ho detto, il quesito è abbastanza semplice e mi aspetto una soluzione in poche ore...

 

11/25/2005 1:17:43 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

In generale, penso che il namespace My di VB 2005 sia una bufala. Nei piani di Microsoft, questa nuova feature del linguaggio dovrebbe servire ad avvicinare i programmatori VB6 alla nuova versione, ma chiunque si è trovato a fronteggiare realmente la migrazione - del codice esistente o anche semplicemente della conoscenza - sa bene che non sono questi i motivi per cui il programmatori VB6 fanno ancora tanta resistenza a .NET. I veri problemi sono altri: il fatto che il wizard di traduzione non risolve tutti i problemi, il modello di ADO.NET così diverso da quello di ADO, e così via.

La mia impressione è che il namespace My non solo non servirà a conquistare nuovi convertiti alla religione .NET ma - cosa molto peggiore - servirà ad allontare qualche VB-ista dal "Verbo" del .NET Framework. Il namespace My non è che l'ennesimo strato che separa il linguaggio dal Framework vero e proprio. Avrebbero potuto farlo con una libreria di classi (e regalarla anche ai programmatori C#, poveretti!), invece hanno creato una infrastruttura che servirà a rendere il VB ancora una volta un linguaggio sui generis.

Quale sarà il risultato? secondo me, nel migliore dei casi saranno in pochi a usare questa feature, quindi alla fine non verrà estesa più di tanto. Nel caso peggiore, invece, molti programmatori VB, invece di studiare il .NET Framework e migliorare le loro conoscenze in materia, finiranno per usare questo strato che aggiunge poco o nulla e in alcuni casi addirittura offre meno flessibilità dell'accesso diretto alle classi del .NET Framework. Con l'aggravante che se vi fate viziare dal namespace My sarà più difficile passare a C# o a qualche altro linguaggio .NET.

In molti casi una chiamata a un metodo nel namespace My produce lo stesso effetto di una singola chiamata a un metodo del .NET Framework, quindi è lecito chiedersi perchè dovrei usare questa sovrastruttura se non porta alcun vantaggio, neanche a livello di maggiore concisione del codice? è una domanda ancora senza risposta. O meglio, il namespace My è una risposta a una domanda che in realtà nessuno ha mai fatto :-) ... D'altra parte, Microsoft non fa queste cose alla leggera e sono certo che sono stati fatti focus group e sondaggi per misurare in anticipo l'indice di gradimento di questa feature. Bah, vedremo....

Ad essere obiettivo, comunque, nel namespace My ci sono delle cosucce davvero interessanti che vorrei tanto che fossero parte del Framework. Non che siano impossibili da ottenere altrimenti (ovviamente, visto che è comunque codice managed) ma la facilità con cui si ottengono con il namespace My è degna di nota. Ecco qualche esempio:

L'evento StartupNextInstance di My.Application: permette di sapere quando un'altra istanza della applicazione è stata lanciata e di leggere l'argomento passato sulla linea di comando. In questo modo si implementano applicazioni single-instance senza giochi strani con Mutex o altre tecniche non proprio semplicissime.

La collection OpenForms dell'oggetto My.Application: è comodo poter ciclare su tutti i form aperti nella applicazione senza dover scrivere il codice che mantiene sempre aggiornato l'elenco.

I metodi CopyDirectory, CopyFile, DeleteDirectory, DeleteFile, MoveDirectory e MoveFile dell'oggetto My.Computer.FileSystem: permettono di copiare, cancellare e spostare file e directory mostrando la dialog animata standard di Windows, senza dover usare PInvoke per chiamare la API ShFileOperation.

Il metodo FindInFile dell'oggetto My.Computer.FileSystem: cerca una stringa (opzionalmente in modo case-insensitive) in tutti i file di testo in un folder o un albero di directory, e restituisce la collection dei file che contengono la stringa. Lavorando con le regex si possono fare ricerche molto più sofisticate, ma questo metodo è molto più semplice da usare.

L'oggetto My.Computer.Keyboard: espone proprietà a sola lettura che testano lo stato dei tasti di shift e di lock, risparmiando una chiamata alle API di Windows.

I metodi DownloadFile e UploadeFile dell'oggetto My.Computer.Network: eseguono il download o l'upload di un file all'URL specificato, mostrando anche una dialog con il progress dell'operazione e segnalando se l'utente ha cancellato il comando.

A parte queste poche eccezioni - e qualcun'altra che potrebbe essermi sfuggita - non mi pare ci sia un reale vantaggio ad usare i metodi e gli oggetti del namespace My rispetto alle chiamate standard al Framework.

P.S. Non includo in questo elenco gli oggetti My.Resources e My.Settings, perchè anche se fanno parte del namespace My sono comunque a disposizione anche degli sviluppatori C#, anche se con una sintassi differente.

11/23/2005 7:58:05 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Ecco un altro quizzettino concettualmente molto semplice, ma la cui soluzione potrebbe fornire qualche buono spunto per ottimizzare le vostre applicazioni. Supponiamo di avere un array di numeri floating-point e di voler dividere tutti i valori del vettore per il valore minimo nel vettore stesso. Ecco una possibile soluzione in VB e in C#:

' VB
' An array of 10 million elements
Dim arr(9999999) As Double
' Fill it with random values.
Dim rnd As New Random(1234)
For i As Integer = 0 To arr.Length - 1
   arr(i) = rnd.NextDouble() * 1000000 + 100
Next
' Start the benchmark
Dim sw As Stopwatch = Stopwatch.StartNew()
' Find the min value.
Dim min As Double = arr(0)
For i As Integer = 1 To arr.Length - 1
  
If min > arr(i) Then min = arr(i)
Next
' Divide all elements by the min value
For i As Integer = 0 To arr.Length - 1
   arr(i) /= min
Next
Console.WriteLine(sw.ElapsedMilliseconds)

// C#
// an array with 10 million elements
double[] arr = new double[10000000];
// fill it with random values.
Random rnd = new Random(1234);
for (int i = 0; i < arr.Length; i++)
   arr[i] = rnd.NextDouble() * 1000000 + 100;
// start the benchmark
Stopwatch sw = Stopwatch.StartNew();
// find the min value
double min = arr[0];
for (int i = 0; i < arr.Length; i++)
  
if (min > arr[i]) min = arr[i];
// divide all elements by the min value
for (int i = 0; i < arr.Length; i++)
   arr[i] /= min;
Console.WriteLine(sw.ElapsedMilliseconds);

Il codice è davvero semplice, eppure è possibile ottimizzarlo ulteriormente in modo significativo, anche del 100% (ossia due volte più veloce). Come?

11/22/2005 10:23:03 AM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Visual Studio 2005 arriva con dozzine di code snippet già pronte all'uso. A dire il vero, potremmo discutere per un po' sulla qualità e l'utilità di qualcuna di queste, però ce ne sono molte davvero ben fatte. Io ad esempio uso in continuazione prop per creare proprietà pubbliche C# che "wrappano" variabili private.

La dialog box Code Snippet Manager (menu Tools) permette di ispezionare i vari snippets uno alla volta, ma stranamente non permette di creare una lista di tutti gli snippets installati, per cui uno deve guardarseli uno a uno e annotare nome, significato, e shortcut di tastiera. Mentre preparavo il capitolo 4 di Programming Microsoft Visual Basic 2005 ho scritto questo piccolo programmino usa-e-getta che decodifica l'indice degli snippets e gli elenca a video. Essendo una applicazion console, basta redirezionare l'output su file per creare un documento da usare come riferimento.

Il programma accetta in input il percorso del file SnippetIndex.xml (VB) o SnippetsIndex.xml (C#) che contiene l'indice degli snippet. (E' curioso che questo file abbia un nome differente nei due linguaggi.) Pero' lanciandolo senza argomenti utilizza il percorso di default dell'indice degli snippet VB. Un commento nel listato spiega come usare invece l'indice di default per C#.

L'output è molto spartano - solo il nome dello snippet e la sua shortcut, suddiviso in categorie - ma potete modificare facilmente il sorgente per estrarre e mostrare altri attributi. Anzi, se fate delle modifiche, mandatemele e le posto sul sito o sul blog.

Imports System.IO
Imports System.Xml
Imports System.Text.RegularExpressions

Module Module1
  
Dim snippetsPath As String
  
Dim catNames As New Dictionary(Of String, String)

   Sub Main(ByVal args() As String)
     
' If no argument has been provided, use default path for snippets.
     
If args.Length = 0 Then
        
args = New String() {"C:\Program Files\Microsoft Visual Studio 8\Vb\Snippets\1033\SnippetIndex.xml"}
        
' Uncomment next line to list C# snippets
        
' args = New String() {"C:\Program Files\Microsoft Visual Studio 8\VC#\Snippets\1033\SnippetsIndex.xml"}
     
End If

      Dim snippetsFile As String = args(0)
      snippetsPath = Path.GetDirectoryName(snippetsFile)
     
' Load the snippet index file.
     
Dim xmlIndex As New XmlDocument()
      xmlIndex.Load(snippetsFile)
     
' We need two passes, because dirs and subdirs use a different XML element.
     
ParseSnippetIndex(xmlIndex, "//SnippetDir")
      ParseSnippetIndex(xmlIndex,
"//SnippetSubDir")
     
' Iterate over all the directories in the main snippet directory.
     
For Each dir As String In Directory.GetDirectories(snippetsPath)
         ParseSnippetFolder(dir,
"")
     
Next
   End Sub

   Sub ParseSnippetIndex(ByVal xmlIndex As XmlDocument, ByVal searchKey As String)
     
' Create the correspondence between relative paths and localized categories
     
For Each xmlEl As XmlElement In xmlIndex.SelectNodes(searchKey)
        
Dim elPath As XmlElement = DirectCast(xmlEl.SelectSingleNode("DirPath"), XmlElement)
        
Dim elName As XmlElement = DirectCast(xmlEl.SelectSingleNode("LocalizedName"), XmlElement)
         catNames.Add(elPath.InnerText, elName.InnerText)
     
Next
   End Sub

   Sub ParseSnippetFolder(ByVal dir As String, ByVal parentCategory As String)
     
' Retrieve the relative name of this subdirectory.
     
Dim relPath As String = dir.Substring(snippetsPath.Length)
     
' The default name for this category
     
Dim categoryName As String = parentCategory & Path.GetFileNameWithoutExtension(dir)
     
' Search this relative path in the snippet index.
     
Dim searchPath As String = "%InstallRoot%\Vb\Snippets\%LCID%" + relPath + "\"
     
If catNames.ContainsKey(searchPath) Then
        
' If found, use the localized category as appears in the index file
        
categoryName = parentCategory & catNames(searchPath)
     
End If
     
Console.WriteLine(categoryName.ToUpper())
     
' Parse individual snippets in this directory.
     
For Each file As String In Directory.GetFiles(dir, "*.snippet")
         ParseSnippetFile(file)
     
Next
     
' Parse all sub-categories
     
For Each subdir As String In Directory.GetDirectories(dir)
         ParseSnippetFolder(subdir, categoryName &
" / ")
     
Next
   End Sub

   Dim reTitle As New Regex("<Title>(.+?)</Title>")
   Dim reShortcut As New Regex("<Shortcut>(.+?)</Shortcut>")

   Sub ParseSnippetFile(ByVal snippetFile As String)
     
Dim text As String = File.ReadAllText(snippetFile)
     
' We use regexes to extract information for individual snippet files.
     
Dim maTitle As Match = reTitle.Match(text)
     
Dim maShortcut As Match = reShortcut.Match(text)
     
Dim title As String = maTitle.Groups(1).Value
     
Dim shortcut As String = maShortcut.Groups(1).Value
      Console.WriteLine(
" {0} [{1}]", title, shortcut)
  
End Sub
End
Module

11/13/2005 7:11:07 AM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Anche oggi un quizzettino davvero facile, anzi banale, tanto per scaldarci i neuroni in vista della WPC. Considerate questo codice VB2003 che converte gli elementi di un vettore di interi in un array di stringhe contentente la rappresentazione esadecimale di ciascun elemento

Dim intArray() As Integer = { 4, 6, 9, 10, 99, 233, 34, 88, 189}   ' ecc ecc.
Dim hexArray(intArray.Length - 1) As String
For i As Integer = 0 To intArray.Length - 1
   hexArray(i) = intArray(i).ToString("X")
Next

La domanda è: qual'è il numero minimo di istruzioni C# 2.0 o VB2005 che occorre scrivere per ottenere lo stesso risultato?

Nota per i C# 2.0: le istruzioni negli anonymous method vanno contate a parte. Quindi, una istruzione che usa un anonymous method che contiene uno statement conta come due istruzioni.

11/9/2005 12:57:35 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Oggi stavo rivedendo il capitolo sui tipi base di .NET, in particolare sul boxing e unboxing, e ho guardato con più attenzione il seguente codice:

' The version that does NOT cache the value type in a reference variable.
Dim start As Date = Now
For i As Integer = 1 To
1000
  
For j = 1 As Integer To
100000
      GetObject(i, j)
  
Next
Next
Console.WriteLine("Version 1: "
& Now.Subtract(start).ToString)
GC.Collect() : GC.WaitForPendingFinalizers()

' The version that caches the value type in a reference variable.
start = Now
For i = 1 As Integer To
1000
  
' Cache the value type in an Object variable.
  
Dim o As Object
= i
  
For j As Integer = 1  To
100000
      GetObject(o, j)
  
Next
Next
Console.WriteLine("Version 2: " & Now.Subtract(start).ToString)

GetObject è una routine molto semplice, che accetta due object e che quindi causa il boxing degli argomenti se sono dei value type:

Private Function GetObject(ByVal o As Object, ByVal o2 As Object) As Object
  
Return
o
End Function

Come indicano i commenti, la seconda parte del codice "ottimizza" l'esecuzione conservando in una variabile Object la versione boxed del contatore i, che non varia all'interno del ciclo piu' interno. C'è da aspettarsi che la seconda versione dei due cicli sia più veloce, anche se di poco, e infatti questo è quello che accade con Visual Basic .NET 2003. Per fortuna ho rifatto tutti i test con la nuova versione e mi sono accorto che in VB2005 le cose non stanno cosi': per quanto possa sembrare poco intuitivo, la versione che esegue il cache della variable boxed è il 30-40% più lenta dell'altra!

Per capire cosa accade ci vuole un po' di ILDASM, grazie al quale si vede che ogni volta che VB, prima di passare una variabile Object a un argomento Object, chiama il metodo GetObjectValue della classe RuntimeHelpers (namespace System.Runtime.CompilerServices). Questo spiega l'overhead osservato. La cosa strana è che questa chiamata è generata anche in VB2003, eppure non ribalta le conclusioni del benchmark. Poichè sto lavorando con la RTM ne deduco che l'overhead è reale (e non dovuto a pezzi di codice del CLR compilato in debug mode) quindi l'unica conclusione possibile, per il momento, è che la versione 2.0 del metodo GetObjectValue sia meno efficiente della versione 1.1.

Mi sono chiesto cosa faccia GetObjectValue esattamente, ma non mi sono spinto a usare Anakrino per disassemblare quella parte. Immagino che quella chiamata - che è generata dal compilatore VB ma non da C# - serva a compensare il fatto che in VB sono possibili chiamate late-bound e che quella variabile potrebbe anche conservare un pointer a un oggetto COM. Un giorno risolverò i miei dubbi, ma per adesso starò bene attento a eseguire sempre dei benchmark accurati prima di "ottimizzare" il mio codice in questo modo.

UPDATE: Come potete leggere nei commenti, mentre io mi chiedevo cosa facesse GetObjectValue, c'era Adrian Florea che già da tempo aveva scovato dei commenti nei sorgenti di Rotor che chiarivano il mistero. Ecco il suo post, che spiega tutto.

11/3/2005 5:06:52 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Sto revisionando il 3° capitolo sul controllo del flusso, e ho pensato di mettere in forma di quiz un piccolo tip che spiego nel libro. Considerate il seguente codice:

For i As Integer = 1 To 10
  
Dim exiting As Boolean = False
  
For j As Integer = 1 To 20
     
' If the Evaluate function returns zero you want to exit both loops
     
If Evaluate(i, j) = 0 Then
        
exiting = True
        
Exit For
     
End If
   Next
   If exiting Then Exit For
Next

Non è importante sapere cosa fa la funzione Evaluate, ma solo che se questa funzione restituisce zero allora dovete uscire immediatamente da entrambi i cicli. Il ciclo di cui sopra non è molto ottimizzato, perchè deve testare in continuazione la variabile exiting. Si potrebbe ottimizzare il codice usando una istruzione Goto che punta a una etichetta che si trova dopo il secondo Next, ma sappiamo bene che le Goto sono per gli smanettoni, non per i programmatori seri come noi. Allora, la domanda del quiz è semplice:

E' possibile ottimizzare questo codice eliminando la variabile "exiting" ed uscendo dal ciclo più interno senza usare una istruzione Goto?

La risposta è semplice, quindi mi aspetto una soluzione in tempi stretti...

11/1/2005 10:10:33 AM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Qualche settimana fa scrivevo un post a proposito della covarianza e controvarianza dei delegate in C# 2.0, stupendomi del fatto che in giro per la rete in tanti parlano di queste nuove feature ma senza spiegare a cosa possono servire realmente.

Poichè tutti gli eventi .NET sono gestiti mediante delegate, grazie alla controvarianza è possibile che un unico metodo possa gestire più eventi del tipo (sender, e). anche se la loro signature è leggermente differente. Supponiamo di avere questo metodo:

   Sub GestoreEvento(ByVal sender As Object, ByVal e As EventArgs
      ..   
   End
Sub

Un metodo di questo tipo è in grado di gestire tutti gli eventi del tipo (sender, e), anche se il secondo argomento non è un EventArgs ma un tipo che eredita da EventArgs. Quindi un metodo di questo tipo è in grado di gestire virtualmente tutti gli eventi definiti nel .NET Framework, con pochissime eccezioni. (L'evento AppDomain.AssemblyResolve è una di queste eccezioni, perchè non è un metodo Sub ma una Function che restituisce un oggetto Assembly.)

Anche se la controvarianza permette in teoria di far gestire tutti gli eventi di un oggetto da un unico metodo, nella pratica le cose sono un po' più complicate, perchè il metodo GestoreEvento ha modo di sapere quale oggetto sta scatenando l'evento (tramite l'argomento sender) ma non quale evento è stato scatenato. Per ottenere questa informazione occorre scrivere del codice un po' più complesso.

Insomma, alla fine ho scritto un componente EventInterceptor che potete sistemare sulla superficie di un Windows Forms e che vi permette di catturare tutti (o alcuni) eventi di un controllo sul form (o al limite di tutti i controlli sul form) per mezzo di un unico gestore. Trovate l'articolo e il codice sorgente a corredo a questo link.

10/31/2005 5:53:25 PM (GMT Standard Time, UTC+00:00) #  | Comments [0] | 

Come anticipavo in un precedente post, ecco il sommario definitivo del librone. Anzi del "libretto", visto che con solo 800 pagine è decisamente al di sotto dei miei standard :-)

Part I : The Basics
 1. Introducing the .NET Framework
(30 pag.): una veloce carrellata dei concetti più importanti della programmazione per .NET
 2. Basic Language Concepts (64 pag.): moduli, classi, variabili, array, operatori, ecc. con il minimo che serve sapere sulla ereditarietà e gli attributi per comprendere i capitoli che seguono prima del cap. 8 e 19.
 3. Control Flow and Error Handling (36 pag.): istruzioni base come If, Select, For, For Each, ecc. e gestione degli errori, con numerose tecniche poco note per migliorare l'efficienza del codice e il proprio stile di programmazione.
 4. Using Visual Studio 2005 (50 pag.): le tantissime novità dell'IDE e anche molte vecchie feature che non tutti conoscono; come scrivere un code snippet per VS2005, i template, le funzioni di refactoring, e soprattutto un breve ma "succoso" tutorial sulle macro.
 5. Debugging and Testing (46 pag.): breakpoint e tracepoint, data tip, scrivere un visualizer, le funzioni di trace e relativi trace listeners (incluso custom listeners), benchmark e profiling, unit testing e code coverage, ecc.

Part II : Object-Oriented Programming
 6.
Class Fundamentals (36 pag.): la "solita storia" su classi, metodi, proprietà, ecc. ma anche le nuove partial class e l'overloading degli operatori, il tutto condito da alcune tecniche di programmazione meno diffuse.
 7. Delegates and Events (24 pag.): un capitolo piccolo con numerosi dettagli su come sfruttare al meglio, nei programmi "reali", queste due feature di VB, incluso i nuovi eventi custom
 8. Inheritance (32 pag.): l'ereditarietà spiegata come meglio non potrei, inclusa l'ereditarietà visuale dei form, con numerosi esempi reali.
 9. Object Lifetime (26 pag.): tutto quello che potreste voler sapere su garbage collection, Dispose e Finalize, weak reference, generation, object resurrection, e altre tecniche in grado di moltiplicare le prestazioni delle applicazioni .NET che usano (male) la memoria.
10. Interfaces (26 pag.): costruire una interfaccia custom e soprattutto usare al meglio quelle che fornisce .NET, incluso IComparer e IEnumerable.
11. Generics (30 pag.): metà di quello che c'è da sapere su questa grande feature di .NET 2.0 (l'altra metà nel capitolo 13), incluso constraints e nullable types e soprattutto tanti esempi e tecniche di programmazione basate sui generics

Part III : Working with the .NET Framework
12. .
NET Basic Types (44 pag.): lavorare al meglio con stringhe, numeri e date, incluso alcune piccole/grandi novità introdotte con .NET 2.0
13. Arrays and Collections (48 pag.): array, jagged array, collection "tradizionali" e collection generics, con tanti piccoli tricks in grado di ridurre la quantità di codice a aumentare le prestazioni.
14. Regular Expressions (40 pag.): tutta la sintassi ma anche tantissimi esempi pratici, su come validare i dati, eseguire il parsing di file di testo e persino del codice. Se non conoscete le regex vi perdete innumerevoli possibilità di scrivere codice più compatto e veloce...
15. Files and Streams (38 pag.): una carrellata sui tipi in System.IO e le nuove (tante) feature in .NET 2.0, tra cui il supporto per ACL e la classe TextFieldParser.
16. The My Namespace (40 pag.): come usare questa feature VB-only per scrivere codice in modo più semplice, ma anche come estendere il namespace My a proprio piacimento
17. Assemblies and Resources (44 pag.): nonostante la loro importanza, le risorse (localizzabili o meno) sono usate poco e male dalla maggioranza dei programmatori; il capitolo include anche una completa descrizione delle nuove importantissime feature di NGEN

Parti IV : Advanced Topics
18. Reflection
(54 pag.): su reflection c'è tantissimo da dire, mi viene quasi voglia di scriverci un libro. Tra gli esempi ce n'è uno che genera codice "al volo", uno scheduler di azioni (con tanto di undo), e un comparer universale.
19. Custom Attributes (42 pag.): contiene alcuni esempi anche complessi di come un custom attribute può semplificare i propri programmi, ad es. per creare plug-in per Windows Forms e un framework per applicazioni n-tier.
20. Threads (48 pag.): l'oggetto Thread, i delegate asincroni, il thread pool, l'istruzione SyncLock, tutte le classi per la sincronizzazione, incluso i nuovi Semaphore, i problemi del multithreading nella applicazioni Windows Forms. Insomma, come scrivere applicazioni che usano thread multipli senza pentirsi di averci provato...
21. Object Serialization (30 pag.): serializzazione binaria, la serializazzione version-tolerant di .NET 2.0, i nuovi attributi per la serializzazione, serializzazione custom, i serialization surrogate, l'interfaccia IObjectReference, e altro ancora

Appendix
A: Migrating from Visual Basic 6 (24 pag.) a beneficio di chi arriva a VB2005 direttamente da VB6, un riassunto di tutte differenze tra i due linguaggi e dei controlli per la interfaccia utente (ovviamente solo WinForm, visto che in VB6 non esiste un analogo di asp.net).

I numeri delle pagine in ciascun capitolo sono ovviamente indicativi. Per queste stime ho costruito un foglio Excel che calcola il numero stimato di pagine partendo dalla dimensione dei file word, il numero di tabelle e figure, ecc.

La cifra caratteristica del libro è che, a dispetto del titolo, non è un semplice reference sul Visual Basic 2005. O meglio, oltre ad essere un completissimo reference, è un vero e proprio manuale di tecniche di programmazione basate sulle feature sia del linguaggio che del .NET Framework, tra cui i generics, multi-threading, reflection, custom attribute, serializzazione, delegate, regular expression, ed altro ancora. Il tutto con un occhio alla flessibilità e alla robustezza del codice, e ovviamente alla sua efficienza. Non credo di esagerare nel dire che non ho mai trovato qualcosa del genere in giro e che ho davvero scritto il libro che ho sempre desiderato scrivere, che rende finalmente giustizia a Visual Basic e alle feature di basso livello di .NET più in generale.

E' stato un bel tour de force, visto che ci lavoro da Maggio, estate compresa. Al momento sto completando gli ultimi capitoli, poi farò un'ultima revisione con la RTM per controllare che non ci siano sorprese, visto che tra Beta 2 e RC ce ne sono state un bel po'. (Un consiglio interessato: non fidatevi dei libri su .NET 2.0 che trovate in libreria a novembre, non possono essere aggiornati neanche alla RC, figuriamoci al prodotto finale...)

Poi finalmente si va in stampa! Se tutto va bene, dovrebbe essere nelle librerie americane a Dicembre o i primi di Gennaio. Ottimo regalo da chiedere a Babbo Natale, o al più tardi alla Befana :-)

10/25/2005 11:35:06 AM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Un quiz di Adrian Florea mi ha ricordato una tecnica che uso talvolta per evitare chiamate ricorsive a un metodo. La tecnica "classica" per evitare la ricorsione consiste nel definire un campo booleano a livello di classe e testarlo all'entrata del metodo. Questa tecnica è spesso usata negli eventi di tipo TextChanged che modificano a loro volta la proprietà Text di un controllo, e che quindi farebbero scattare una ricorsione infinita:

Dim insideTextChanged As Boolean

Private Sub TextBox1_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles TextBox1.TextChanged
  
' Exit if this is a recursive call.
  
If insideTextChanged Then Exit Sub
  
' Forbid recursive calls from now on.
   insideTextChanged = True
  
' ...
  
TextBox1.Text = TextBox1.Text & " "
  
' Permit recursive calls.
  
insideTextChanged = False
End Sub

Questo approccio funziona, ma è decisamente scomodo perchè richiede un bel po' di codice. Se poi esiste anche la minima possibilità che possa avvenire una eccezione, allora occorre che tutto il corpo del metodo sia avvolto in un blocco Try, che imposta insideTextChanged a False nella sezione Finally. Perchè non usare allora una funzione che permette di testare se siamo all'interno di una chiamata ricorsiva? Penso ad esempio a qualcosa di questo tipo:

Private Sub TextBox1_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles TextBox1.TextChanged
  
' Exit if this is a recursive call.
  
If IsRecursive() Then Exit Sub
   ' ...
  
TextBox1.Text = TextBox1.Text & " "
End Sub

Ecco come implementare la funzione IsRecursive:

<System.Runtime.CompilerServices.MethodImpl(Runtime.CompilerServices.MethodImplOptions.NoInlining)> _
Public Shared Function IsRecursive() As Boolean
  
Dim st As New StackTrace
  
' Check whether any method in the call stack is the same as the immediate caller.
  
For n As Integer = 2 To st.FrameCount - 1
     
If st.GetFrame(1).GetMethod() Is st.GetFrame(n).GetMethod() Then Return True
  
Next
  
Return False
End Function

Ecco la versione C#:

[System.Runtime.CompilerServices.MethodImpl(Runtime.CompilerServices.MethodImplOptions.NoInlining)]
public static bool IsRecursive() 
{

  
StackTrace st = new StackTrace();
   
// Check whether any method in the call stack is the same as the immediate caller.
  
for ( int n= 2; n < st.FrameCount; n++ )
   {

     
if ( st.GetFrame(1).GetMethod() == st.GetFrame(n).GetMethod()
        
return true;
   
}
  
return false;
}

Il metodo controlla il metodo del chiamante immediato - ossia st.GetFrame(1).GetMethod() - con il metodo di tutti gli altri chiamanti sullo stack, e restitusce True se trova un match. E' essenziale che il metodo IsRecursive sia marcato con l'attributo MethodImpl in modo da evitare che il JIT-compiler esegua l'inline del metodo e faccia saltare il meccanismo di controllo. Nella versione attuale del JIT compiler questo problema non dovrebbe mai accadere, perchè il JIT compiler non esegue l'inlining dei metodi che contengono un loop, ma per il futuro non si puo' mai dire, e questo attributo ci mette al riparo da sorprese. Per lo stesso motivo, non si dovrebbe chiamare IsRecursive da un metodo che potrebbe essere ottimizzato mediante inlining.

Per chi non lo sapesse, l'inlining è una tecnica di ottimizzazione grazie alla quale il JIT-compiler riesce a spostare il codice di un metodo direttamente nel codice del metodo chiamante, in modo da evitare una istruzione di chiamata. Solo metodi molto piccoli, 32 byte di IL o meno, che non hanno cicli o loop, e che non accettano dei value type come argomenti possono essere ottimizzati in questo modo, nella versione attuale del JIT compiler, quindi nella pratica l'inlining avviene in pochi casi, tipicamente per le proprietà che wrappano una variabile e per i metodi con al massimo 2-3 righe di codice.

10/18/2005 12:10:17 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

In un post del maggio scorso mi lamentavo del fatto che in Visual Studio 2005, scrivendo Implements IDisposable nella dichiarazione di una classe si otteneva il pattern Dispose-Finalize completo. Un sacco di codice, ma soprattutto un codice sbagliato, in quanto una classe disposable in genere NON deve implementare il metodo Finalize, in quanto il metodo Finalize serve solo se la classe crea e utilizza risorse unmanaged.

Le classi dotate di Finalize richiedono qualche ciclo di CPU in più durante l'istanziazione, ma questo non era il problema principale a mio avviso. Il fatto che Visual Studio creasse automaticamente codice errato mi dava un fastidio quasi fisico: chi avrà mai preso questo decisione a Redmond?

Beh, evidentemente non sono stato l'unico a lamentarsi, perchè ho appena scoperto - con molto piacere - che questa "feature" è stata rimossa nella CTP di Agosto (e immagino anche nella RC). Ora quando digitate Implements IDisposable, VS2005 risponde creando il codice seguente:

Class FooBar
  
Implements IDisposable

   Private disposedValue As Boolean = False ' To detect redundant calls

   ' IDisposable
  
Protected Overridable Sub Dispose(ByVal disposing As Boolean)
      If Not Me.disposedValue
Then
        
If disposing
Then
           
' TODO: free unmanaged resources when explicitly called
        
End
If
        
' TODO: free shared unmanaged resources
     
End
If
     
Me.disposedValue =
True
  
End Sub

#Region " IDisposable Support "

   ' This code added by Visual Basic to correctly implement the disposable pattern.
  
Public Sub Dispose() Implements IDisposable.Dispose
     
' Do not change this code. Put cleanup code in Dispose(ByVal disposing As Boolean) above.
     
Dispose(True)
      GC.SuppressFinalize(Me)
   End Sub

#End Region
End
Class

E' bello, una volta tanto, poter dire a posteriori "lo avevo detto io!" :-)

10/5/2005 7:35:35 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Per prima cosa, un po' di background su cosa è la covariance e la contravariance dei delegate in C# 2.0. Supponiamo di avere un delegate di questo tipo:

   delegate object GetControlData(TextBox ctrl);

Per covariance si intende il fatto che è possibile fare puntare il delegate a un metodo il cui valore di ritorno eredita dal tipo di valore di ritorno specificato dal delegate. Ad esempio, poichè il delegate GetControlData restituisce un object, la covarianza permette di fare puntare il delegate a qualsiasi metodo, indipendentemente dal tipo di oggetto restituito, purchè il metodo accetti un oggetto TextBox in input. L'unica condizione è che il metodo restituisca effettivamente qualcosa, quindi non posso fare puntare il delegate a un metodo void. Ad esempio, potrei creare un oggetto GetControlData che punta a questo metodo, poichè il tipo string deriva da object:

   string GetText(TextBox ctrl) 
   { return ctrl.Text; }

Per contravariance si intende il fatto che è possibile fare puntare il delegate a un metodo il cui argomento è una classe base dell'argomento specificato nella signature del delegate. Nel caso dell'esempio, la controvarianza permette di fare puntare un delegate di tipo GetControlData a un metodo che accetta un oggetto di tipo Control oppure Object, che sono entrambi classi base per il tipo TextBox definito nel delegate:

   object GetTag(Control ctrl) 
   { return ctrl.Tag; }

E' evidente che la covarianza e controvarianza non creano problemi di robustezza nel codice e permettono di utilizzare un delegate per chiamare un metodo senza che si possano verificare errori di type mismatch a runtime. Ovviamente, covarianza e controvarianza possono essere combinate, ad esempio per fare puntare un delegate GetControlData al seguente metodo:

   string GetText(Control ctrl) 
   { return ctrl.Text; }

E' importante notare che - anche se il metodo target accetta un Control generico - richiamando il metodo tramite il delegate occorrerà passare un argomento di tipo TextBox, perchè questo è il tipo definito nella signature del delegate.

Fine della divagazione su queste feature: si tratta di informazioni che trovate praticamente dappertutto sulla rete, con molti esempi. Insieme ai tanti esempi, però, troverete ben poche applicazioni realmente interessanti per queste feature, ma questo è un altro discorso e ci tornerò a breve in un altro post. Per il momento voglio concentrarmi su un aspetto che riguarda più da vicino chi programma in Visual Basic, ovvero che Visual Basic 2005 non supporta nè la covarianza nè la controvarianza.

Tutto vero? si e no. Perchè è vero che VB 2005 non supporta queste feature direttamente, ma ci si può arrivare lo stesso. Infatti, a molti è sfuggito il fatto che il supporto per covarianza e controvarianza è stato aggiunto anche a livello del .NET Framework, tant'è vero che in .NET 2.0 è possibile usare reflection per creare dei delegate che godono di entrambe queste proprietà. Ecco allora come fare in Visual Basic 2005. Supponiamo di avere il seguente delegate e la seguente funzione all'interno di un Windows form:

   Delegate Function GetControlData(ByVal ctrl As TextBox) As Object

   Function GetText(ByVal ctrl As Control) As String
      Return ctrl.Text
   End Function

Poichè VB2005 non supporta direttamente covarianza e controvarianza non è possibile creare direttamente una istanza del delegate GetControlData che punta al metodo GetText. Però possiamo arrivarci comunque via reflection, creando un MethodInfo che punta al metodo target e passando poi questo MethodInfo al metodo statico Delegate.CreateDelegate.

' the target method
Dim targetMethod As MethodInfo = Me.GetType().GetMethod("GetText")
' build the delegate through reflection
Dim deleg As GetControlData = DirectCast([Delegate].CreateDelegate( _
    
GetType(GetControlData), Me, targetMethod), GetControlData)
' show that the delegate works correctly
Console.WriteLine(deleg(Me.TextBox1))

Questo codice è solo leggermente più lento della creazione diretta del delegate, ma non è un problema serio perchè tipicamente un delegate si crea una sola volta per poi usarlo ripetutamente. Un altro problema minore è che il codice potrebbe fallire a runtime se il nome del metodo target è scritto male, ma tutto sommato si tratta di un bug che si scoprirebbe al primo test del codice.

Da notare che, almeno con la CTP di Agosto (non ho ancora provato con la RC), il supporto di .NET alla covarianza e controvarianza è imperfetto, in quanto non tutti gli overload di Delegate.CreateDelegate supportano questa feature. Ad esempio, esiste un overload che accetta direttamente il nome del metodo (anzichè il MethodInfo che punta al metodo target), ma questo overload va in errore (ArgumentException: Error binding to target method) se si tenta di creare un delegate la cui signature non corrisponde esattamente al metodo target.


Come ho già accennato prima, la covarianza e controvarianza hanno alcuni usi pratichi molto interessanti, ed è un peccato che non siano state valorizzate come meritano. In particolare, la controvarianza permette di implementare tecniche ganzissime sia con Windows Forms che con Web Form, tecniche che in .NET 1.x richiedevano dei tripli salti mortali. Per darvi una idea, grazie alla controvarianza sono riuscito a risolvere in cinque minuti un problema che in .NET 1.1 mi prese a suo tempo per almeno quattro ore!

C'è qualcuno che ha qualche idea in proposito, prima di leggere il mio articolo che apparirà tra qualche giorno su questo sito?

10/4/2005 11:09:15 AM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

I compilatori VB.NET e C# gestiscono le constanti stringa in un modo decisamente "smart", in quanto tutte le stringhe costanti con identico valore sono memorizzate in un'area di memoria comune denominata string intern pool. Ecco un pezzo di codice che mostra questa tecnica di ottimizzazione in azione:

' VB.NET
Dim s1 As String = "ABCDE"
Dim s2 As String = "ABC" & "DE"
' Prove that s1 and s2 point to the same element in the intern pool
Console.WriteLine(s1 Is s2) ' => True

// C#
string s1 = "ABCDE";
string s2 = "ABC" + "DE";
// Prove that s1 and s2 point to the same element in the intern pool
Console.WriteLine(String.ReferenceEquals(s1, s2)); // => True

Questa tecnica di ottimizzazione non influenza in modo sensibile la quantità di memoria usata dalla maggior parte delle applicazioni client, ma può fare la differenza se usata con oggetti istanziati migliaia di volte, come accade spesso in applicazioni server. Il problema è che questa tecnica funziona solo per le costanti stringa e non si applica a stringhe costruite a runtime:

 

' VB.NET ...continuing previous example...
Dim s3 As String = "ABC"
s3 &= "DE"
' s1 and s3 contain the same value but point to a different string
Console.WriteLine(s1 = s3) ' => True
Console.WriteLine(s1 Is s3) ' => False

// C# ... continuing previous example...
string s3 = "ABC";
s3 += "DE";
Console.WriteLine(s1 == s3) // => True
Console.WriteLine(String.ReferenceEquals(s1, s3) // => False

Supponete ad esempio di avere un componente nel data tier che contiene la stringa di connessione al database. Questa stringa di connessione è letta al momento di istanziare il componente - ad esempio da un file di configurazione XML - e quindi il compilatore non può memorizzarla nell'intern pool. Se questo componente è istanziato N volte vi saranno N copie della stringa di connessione in memoria, il chè costituisce uno spreco di memoria se la stringa è lunga e N è molto alto. Ci sono due modi per evitare questo spreco, a seconda di come la stringa di connessione può variare.

 

Se la stringa di connessione è esattamente la stessa per tutte le istanze allora la si può conservare in una variabile statica (Shared in VB), in modo che un'unica stringa è condivisa tra tutte le istanze del componente. Questo è il caso più semplice e non credo valga la pena approfondirlo ulteriormente.

 

Se invece la stringa di connessione può variare - per esempio, se usate un unico tipo le cui istanze possono collegarsi a due o più differenti database - non è possibile usare una variabile static, ma si può ricorrere ad una tecnica basata sul metodo String.Intern. Questo metodo riceve un argomento stringa e lo ricerca nell'intern pool: se la stringa è nel pool, il metodo restituisce un riferimento all'elemento esistente; se la ricerca fallisce, il metodo inserisce la stringa nel pool e restituisce un riferimento all'elemento appena aggiunto. Ecco come si potrebbe implementare la proprietà ConnectionString nell'ipotetico data object per sfruttare il pool di stringhe:

 

' VB.NET
Dim m_ConnectionString As String

Property ConnectionString() As String
   Get
      Return m_ConnectionString
   End Get
   Set(ByVal Value As String)
      m_ConnectionString = String.Intern(Value)
   End Set
End Property
 

// C#
private string m_ConnectionString;

public string ConnectionString
{
  get { return m_ConnectionString; }
  set { m_ConnectionString = String.Intern(value);}
}

La prima volt ache un certo valore è assegnato alla proprietà ConnectionString, la ricerca nel pool fallisce, il metodo String.Intern aggiunge la stringa al pool e restituisce un riferimento al nuovo elemento del pool. Se la stessa stringa di connessione è assegnata a una istanza differente del data object, il metodo String.Intern restituisce un riferimento all'elemento che è già nel pool e non è creato alcun duplicato. In questo modo la quantità di memoria complessiva usata dal componente è minore e si riduce anche il numero delle garbage collection necessarie. 

9/29/2005 11:17:25 AM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Sto aggiustando il capitolo sul controllo del flusso di esecuzione, dove parlo (tra le variecose) anche delle funzioni ricorsive. Nella maggior parte dei libri di programmazione che conosco (incluso molti "classici") la ricorsione è trattata in modo superficiale con il tipico esempio del fattoriale (che può essere risolto in modo più efficiente con un ciclo For) oppure per visitare strutture ad albero. Sembrerebbe che la ricorsione non serva in una applicazione gestionale "normale", il che ovviamente non è vero. Come tutte le tecniche di programmazione, basta essere attenti a sfruttare le occasioni.

Ecco ad esempio un metodo che trasforma un numero nel suo corrispondente alfanumerico (ad es. 1234 in "One Thousand Two Hundreds Thirty Four"). Il codice è preso dal libro per Microsoft Press e quindi il risultato è in inglese, ma è facile ottenere una versione che emette il risultato in italiano:

Function NumberToText(ByVal n As Integer) As String
   Select Case n
      Case Is < 0
         Return "Minus " & NumberToText(-n)
      Case 0
         Return ""
      Case 1 To 19
         Dim arr() As String = {"One", "Two", "Three", "Four", "Five", "Six", _
            "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen", _
            "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"}
         Return arr(n - 1) & " "
      Case 20 To 99
         Dim arr() As String = {"Twenty", "Thirty", "Forty", "Fifty", "Sixty", _
            "Seventy", "Eighty", "Ninety"}
         Return arr(n \ 10 - 2) & " " & NumberToText(n Mod 10)
      Case 100 To 199
         Return "One Hundred " & NumberToText(n Mod 100)
      Case 200 To 999
         Return NumberToText(n \ 100) & "Hundreds " & NumberToText(n Mod 100)
      Case 1000 To 1999
         Return "One Thousand " & NumberToText(n Mod 1000)
      Case 2000 To 999999
         Return NumberToText(n \ 1000) & "Thousands " & NumberToText(n Mod 1000)
      Case 1000000 To 1999999
         Return "One Million " & NumberToText(n Mod 1000000)
      Case 1000000 To 999999999
         Return NumberToText(n \ 1000000) & "Millions " & NumberToText(n Mod 1000000)
      Case 1000000000 To 1999999999
         Return "One Billion " & NumberToText(n Mod 1000000000)
      Case Else
         Return NumberToText(n \ 1000000000) & "Billions " _
            & NumberToText(n Mod 1000000000)
   End Select
End Function

Ecco la versione per gli amanti delle parentesi graffe. Poichè lo switch di C# non supporta i range, ho dovuto modificare il codice per usare una serie di istruzioni elseif. Un dettaglio interessante è come il codice costruisce un array di stringhe al volo per poi selezionare un elemento. E' possibile farlo anche in VB.NET, ma è una di quelle feature che piacciono soprattutto ai programmatori C#:

string NumberToText( int n)
{
   if ( n < 0 )
      return "Minus " + NumberToText(-n);
   else if ( n == 0 )
      return "";
   else if ( n <= 19 )
      return new string[] {"One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen"
         "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"}[n-1] + " ";
   else if ( n <= 99 )
      return new string[] {"Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"}[n / 10 - 2] + " " + NumberToText(n % 10);
   else if ( n <= 199 )
      return "One Hundred " + NumberToText(n % 100);
   else if ( n <= 999 )
      return NumberToText(n / 100) + "Hundreds " + NumberToText(n % 100);
   else if ( n <= 1999 )
      return "One Thousand " + NumberToText(n % 1000);
   else if ( n <= 999999 )
      return NumberToText(n / 1000) + "Thousands " + NumberToText(n % 1000);
   else if ( n <= 1999999 )
      return "One Million " + NumberToText(n % 1000000);
   else if ( n <= 999999999)
      return NumberToText(n / 1000000) + "Millions " + NumberToText(n % 1000000);
   else if ( n <= 1999999999 )
      return "One Billion " + NumberToText(n % 1000000000);
   else 
      return NumberToText(n / 1000000000) + "Billions " + NumberToText(n % 1000000000);
}

Non è codice complesso, ma non ho mai visto sui mille siti Internet una routine per la trasformazione di numeri in lettere altrettanto concisa ed efficace. Adoro la programmazione ad oggetti, i generics, gli attributi, e le regular expression, ma mi piace ricordare (e ricordarmi) che spesso si può scrivere ottimo codice, compatto e veloce, anche solo sfruttando feature che i principali linguaggi hanno da venti o trent'anni. :-)

9/27/2005 9:12:10 AM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Com'è noto, Visual Studio 2005 include un designer per creare automaticamente una classe My.Settings, che contiene le impostazioni sia a livello di applicazione che di utente. Anche se il designer è decisamente comodo, non è chiaro perchè usare un designer debba essere più semplice che scrivere una classe che esponga quelle impostazioni sotto forma di campi e proprietà, che le carichi automaticamente da disco e che, se modificate, le salvi automaticamente al momento di chiudere l'applicazione. Dal mio punto di vista, l'approccio scelto da Microsoft per VS2005 è la (solita?) esasperazione del concetto RAD, solo che in questo caso le operazioni necessarie per definire la classe My.Settings non fanno in realtà risparmiare tempo rispetto alla definizione via codice delle stesse entità.

Queste considerazioni mi sono venute spontanee non ragionando in astratto, ma mentre scrivevo le mie prime applicazioni.NET 2.0. Avevo necessità di un meccanismo per salvare tutte le impostazioni del programma in un file, un po' come quando in Visual Studio salviamo tutte le impostazioni di un progetto in un file .sln. A pensarci bene, il problema è simile (o meglio, identico) al problema di salvare le impostazioni relative all'utente e alla applicazione: si tratta di salvare e poi ricaricare un gruppo di variabili su disco, con la differenza che nel caso delle impostazioni di progetto la posizione del file non è fissa.

Insomma, alla fine ho scritto un meccanismo di persistenza per le variabili di progetto, basato su una classe base SettingsBase. Quando ho una serie di variabili globali in un progetto che devono essere salvate e ricaricate su richiesta, non devo fare altro che derivare una classe da SettingsBase, chiamandola ad esempio Globals:


Public Class Globals
  Inherits SettingsBase

  Public Documents() As String ' Un array di nomi di documenti
  Public WordWrap As Boolen ' True se occorre applicare il word wrapping

  ' ...

End Class

Tutte le variabili Public in questa classe possono essere salvate e ricaricate da file semplicemente usando i metodi Load e Save che la classe eredita da SettingsBase. Poichè un overload di questi metodi accetta uno Stream, è anche possibile salvare e ricaricare da altri medium, ad esempio un campo di database.

Usare questo approccio per implementare anche il salvataggio e il caricamento delle impostazioni di utente e di applicazioni è banale, e infatti ho anche definito delle classi UserSettingsBase e ApplicationSettingsBase. Ereditando da queste classi astratte è possibile creare insiemi di variabili distinte per ciascun utente oppure condivise da tutti gli utenti della applicazione. Come accade per la classe My.Settings, le variabili sono automaticamente caricate alla partenza della applicazione e salvate prima di uscire. A differenza di My.Settings, è possibile salvare le impostazioni su database centralizzati (cosa molto comoda per supportare applicazioni smart client con centinaia di utenti che possono fare il login da qualunque stazione della rete). Ma soprattutto, a differenza di My.Settings, queste classi sono utilizzabili anche in Visual Basic 2003 (e più in generale in .NET 1.1).

Il codice delle classi SettingsBase, UserSettingsBase e ApplicationSettingsBase non è proprio banale, quindi ho pensato di commentarlo a dovere e di scriverci un articolo di accompagnamento. Alla fine ne è venuto fuori un testo di oltre 30K, che non era quindi molto adatto a un post di blog, per cui ho deciso di pubblicarlo nella sezione articoli del sito, insieme al codice sorgente completo, al seguente link

L'articolo "Application e User Settings in Visual Basic 2003"
9/19/2005 7:12:40 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Come probabilmente già sapete, sono disponibili sul sito Microsoft le prime specifiche delle future versioni di Visual Basic e di Visual C#. La chiave di quasi tutte le nuove feature è il progetto Linq, che oramai è ampiamente dibattuto in articoli, blog, sessioni alla PDC e persino video, come se dovesse essere rilasciato la prossima settimana. Per dirla davvero in due righe, Linq permetterà di integrare meglio i linguaggi tradizionali con linguaggi db-oriented tipo SQL, e offrirà una sintassi standard per eseguire query su collection di oggetti e tabelle di database in modo molto simile.

Quasi tutte le nuove feature del linguaggio, ad esempio gli anonymous types e la dichiarazione implicita delle variabili (che è necessaria per supportare gli anonymous types), sono una conseguenza indiretta dell'adozione di Linq. Quando scrivo una istruzione Linq come Dim result = Select Name,Population From Country In Countries Where Country.Population < 1000000, il valore restituito in result è una collection di tipi anonimi che "derivano" (ma non nel senso dell'editarietà) dalla classe Country ma hanno solo i campi Name e Population. In quanto anonimi, questi tipi non hanno evidentemente un nome e quindi non li posso usare nella clausola As per dichiarare la variabile result. L'adozione di Linq ha portato anche alla necessità di estendere le classi "al volo" mediante extension methods e a offrire un supporto più esteso al late binding. Anche l'introduzione delle dynamic interfaces va nella stessa direzione.

Forse perchè quest'anno (come l'anno scorso, del resto) NON sono andato alla PDC, ma non mi sento contagiato da questa grande euforia. Per una serie di motivi che proverò a sintetizzare. Prima di continuare, però un piccolo ringraziamento a Dino per un suo post che mi ha dato la voglia di scrivere questa paginetta.

Prima di tutto, dopo anni che ci hanno spiegato (e in cui noi "esperti" abbiamo tentato di spiegarlo ai programmatori/lettori) che occorre scrivere codice strong-typed, improvvisamente c'è questa virata verso i linguaggi dinamici. Quando era il VB a incoraggiare il late binding erano tutti a dargli addosso, urlando che era una vergogna che un linguaggio moderno supportasse queste sconcezze. Oggi improvvisamente la parola late binding non è più un tabù, e anzi persino il C# ne sarà contagiato. Ovviamente, il late binding di VB9 e C#3 è ben più robusto quello che si aveva in VB6 (e si puo' tuttora avere in VB.NET) usando variabili Variant e Object, ma è comunque vero che le chiamate agli estension method e alle dynamic interfaces saranno molto meno performanti del solito. Senza contare il fatto che le dichiarazione implicite dei tipi renderà il codice meno leggibile di quello che è ora. Considerando che molti programmatori VB non si sono ancora abituati a Option Explicit, non lo considero certo un progresso.

La seconda considerazione, anche più importante, è che in realtà non mi pare che l'introduzione di Linq (e delle feature accessorie che porta in dote) serva a risolvere una esigenza realmente sentita tra gli sviluppatori. Da anni si scrivono applicazioni super-complesse usando i linguaggi tipo SQL per manipolare i dati e i linguaggi "tradizionali" come VB e C# per tutto il resto. Il problema non è nella sintassi con cui si accede ai dati, lo è nel fatto che è difficile mappare un paradigma "a oggetti" basato su relazioni uno-a-uno e uno-a-molti (le collection) in memoria con una memorizzazione in un db relazionale. Sarebbe bello avere un unico linguaggio per entrambi i compiti, ma almeno con i database relazionali forse questa fusione è semplicemente impossibile. Non mi pare che Linq risolva questi problemi.

La dimostrazione indiretta che questa convivenza è molto difficile è data dal fallimento del progetto ObjectSpaces, sulle cui versioni alfa si sono sprecati fiumi di inchiostro e ore e ore di sessioni alla conferenze varie. Ricordo le mie discussioni con amici e colleghi alla PDC 2003, ero quasi l'unico ad avere riserve mentali su quella meravigliosa tecnologia del futuro, proprio per i motivi sopra detti. Si è visto come è andata a finire.

Per fortuna, insieme alle feature appena descritte, il VB è stato espanso anche per colmare le (pochissime) differenze esistenti con il C# 2.0, ad esempio il supporto nativo per i nullable type e il rilassamento dei delegate. Non aggiungeranno gli iterators, almeno al momento, e anche il supporto per i metodi anonimi non è sicuro. D'altra parte i tipi nullable in .NET 2.0 sono praticamente inutili e gli iterator servono a un programmatore su mille. I metodo anonimi - o meglio, le lamba functions come dovrebbero chiamati - sono decisamente più interessanti, ma sono anche una pericolosi, soprattutto quando li si usa per registrare "al volo" dei gestori di eventi che non possono mai essere de-registrati. In mano a programmatori meno che accorti saranno soprattutto in grado di fare proliferare i bug.

Insomma, mi sembra che i linguaggi più diffusi nel mondo Windows stiano andando in una direzione molto accademica e molto poco pratica. Io non sono un esperto di linguaggi di programmazione, ma mi scontro quasi quotidianamente contro qualche limite del linguaggio VB o C# (intendo i limiti che non si possono superare semplicemente scrivendo una libreria esterna). Ad es. manca la possibilità di compilare al volo e aggiungere metodi e proprietà a una classe (e non solo nuove classi, come si può fare mediante Reflection.Emit). Se fosse possibile implementare una interfaccia in una classe già esistente (e già compilata in una DLL), potrei rendere serializable, remotizzabile o disposable una classe che non lo è. Le partial class di VS2005 non aiutano in questo caso e dagli esempi che ho visto neanche le dynamic interfaces di VB9 e C#3 saranno in grado di farlo.

Un'altra feature che sarebbe utile è l'aggiunta al volo di attributi a una classe o a un suo membro, il che permetterebbe ad esempio di variare il livello transazionale di un ServicedComponent o di decidere esattamente quali membri serializzare. Mi piacerebbe anche che il mio linguaggio includesse un valutatore di espressioni che sappia interpretare una stringa contenente operatori, chiamate a metodi e soprattutto variabili definite nel programma. (Quello è un tipo di late-binding che accetterei volentieri.) Oppure un meccanismo built-in per intercettare tutte le chiamate ai membri di una classe già compilata, una specie di oggetto proxy universale. Visto che sto sognando ad occhi aperti, gradirei tanto anche un meccanismo che permetta di influire sul processo di JIT-compilation e qualche hint per l'ottimizzazione del codice. Anche i generics dovrebbero essere resi più potenti di quel che sono oggi. (Ho scritto a proposito di questi limiti in questo post e in questo post). E perchè non la possibilità di scrivere codice IL inline?

Per il momento niente di tutto questo. Accontentiamoci di Linq e aspettiamo la prossima PDC :-)

9/18/2005 12:41:42 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Mentre sto rivedendo il capitolo sui generics, mi è tornato in mente un trucchetto semplice ma efficace per simulare i generics anche con .NET 1.x. Si tratta di sfruttare in modo poco ortodosso la capacità di definire degli alias per un tipo, mediante una direttiva Imports (VB) o using (C#). Ecco un esempio di quel che intendo, che implementa una collection di oggetti Widget (definiti da qualche parte nel namespace MyApplication):

' VB.NET
Imports ItemType = MyApplication.Widget

Public Class WidgetCollection
   Inherits CollectionBase

   Public Sub Add(ByVal item As ItemType)
      Me.List.Add(item)
   End Sub

   Public Sub Remove(ByVal item As ItemType)
      Me.List.Remove(item)
   End Sub

   Default Public Property Item(ByVal index As Integer) As ItemType
      Get
         Return CType(Me.List(index), ItemType)
      End Get
      Set(ByVal Value As ItemType)
         Me.List(index) = Value
      End Set
   End Property
End Class

// C#
using System;
using System.Collections;
using ItemType = MyApplication.Widget;

public class WidgetCollection: CollectionBase
{
   public void Add(ItemType item)
   {
      this.List.Add(item);
   }

   public void Remove(ItemType item)
   {
      this.List.Remove(item);
   }

   public ItemType this[int index]
   {
      get { return (ItemType) this.List[index]; }
      set { this.List[index] = value; }
   }
}

Quando avete bisogno di creare una collection di altro tipo, basta copiare il codice in un altro file, modificare il nome della classe e modificare l'istruzione Imports/using che definisce l'alias per ItemType. Ovviamente potete utilizzare questo meccanismo anche per simulare generics con due o più argomenti. Ad esempio, ecco una classe che implementa una hashtable contenente oggetti Widget e le cui chiavi sono stringhe:

' VB.NET
Imports ItemType = MyApplication.Widget
Imports KeyType = System.String

Public Class DictionaryTemplate
   Inherits DictionaryBase

   Public Sub Add(ByVal key As KeyType, ByVal item As ItemType)
      Me.Dictionary.Add(key, item)
   End Sub

   Public Sub Remove(ByVal key As KeyType)
      Me.Remove(key)
   End Sub

   Default Public Property Item(ByVal key As KeyType) As ItemType
      Get
         Return CType(Me.Dictionary(key), ItemType)
      End Get
      Set(ByVal Value As ItemType)
         Me.Dictionary(key) = Value
      End Set
   End Property
End Class

// C#
using System;
using System.Collections;
using ItemType = MyApplication.Widget;
using KeyType = System.String;

public class DictionaryTemplate : DictionaryBase
{
   public void Add(KeyType key, ItemType item)
   {
      this.Dictionary.Add(key, item);
   }

   public void Remove(KeyType key)
   {
      this.Dictionary.Remove(key);
   }

   public ItemType this[KeyType key]
   {
      get { return (ItemType) this.Dictionary[key]; }
      set { this.Dictionary[key] = value; }
   }
}

Questa tecnica aggiunge la possibilità di creare collection strong-typed in .NET 1.1 con la stessa facilità con cui si usano i generics in VS 2005, ma non implementa la seconda importante feature dei generics, ossia quella che permette di evitare il boxing dei tipi valore (come numeri e date). Per implementare anche questa feature occorre complicare notevolmente il codice: invece di ereditare semplicemente da CollectionBase occorre utilizzare un array tipizzato per contenere i valori ed implementare manualmente le interfacce ICollection, IList e IEnumerable. Ecco un esempio di collection di interi implementata in VB.NET .Il codice non è proprio banale, quindi lascio a voi la traduzione in C# (se proprio ci tenete):

Imports ItemType = System.Int32

Public Class ValueTypeCollectionTemplate
   Implements ICollection, IEnumerable, IList

   Dim items(15) As ItemType ' 16 items initially

   ' strong-typed members

   Public Sub Add(ByVal value As ItemType)
      If m_Count = items.Length Then ReDim Preserve items(items.Length * 2 - 1)
      items(m_Count) = value
      m_Count += 1
   End Sub

   Public Sub Insert(ByVal index As Integer, ByVal value As ItemType)
      If index < 0 OrElse index > m_Count Then Throw New ArgumentOutOfRangeException("Index was out of range.")
      If m_Count = items.Length Then ReDim Preserve items(items.Length * 2 - 1)
      Array.Copy(items, index, items, index + 1, m_Count - index)
      items(index) = value
      m_Count += 1
   End Sub

   Public Sub Remove(ByVal value As ItemType)
      Dim index As Integer = Me.IndexOf(value)
      If index >= 0 Then Me.RemoveAt(index)
   End Sub

   Default Public Property Item(ByVal index As Integer) As ItemType
      Get
         Return items(index)
      End Get
      Set(ByVal Value As ItemType)
         items(index) = Value
      End Set
   End Property

   Public Function IndexOf(ByVal value As ItemType) As Integer
      For i As Integer = 0 To m_Count - 1
         If items(i) = value Then Return i
      Next
      Return -1
   End Function

   Public Function Contains(ByVal value As ItemType) As Boolean
      Return IndexOf(value) <> -1
   End Function

   ' The IEnumerable interface

   Public Function GetEnumerator() As System.Collections.IEnumerator Implements IEnumerable.GetEnumerator
      ' Create an array that contains only the existing elements
      Dim res(m_Count - 1) As ItemType
      Array.Copy(items, res, m_Count)
      Return res.GetEnumerator()
   End Function

   ' The ICollection interface

   Private m_Count As Integer = 0

   Public ReadOnly Property Count() As Integer Implements ICollection.Count
      Get
         Return m_Count
      End Get
   End Property

   Public Sub CopyTo(ByVal array As Array, ByVal index As Integer) Implements ICollection.CopyTo
      array.Copy(items, 0, array, index, m_Count)
   End Sub

   Public ReadOnly Property IsSynchronized() As Boolean Implements ICollection.IsSynchronized
      Get
         Return False
      End Get
   End Property

   Public ReadOnly Property SyncRoot() As Object Implements ICollection.SyncRoot
      Get
         Return items
      End Get
   End Property

   Private Function ICollection_Add(ByVal value As Object) As Integer Implements IList.Add
      Me.Add(CType(value, ItemType))
   End Function

   Public Sub Clear() Implements IList.Clear
      ReDim items(15)
      m_Count = 0
   End Sub

   Private Function ICollection_Contains(ByVal value As Object) As Boolean Implements IList.Contains
      Return Me.Contains(CType(value, ItemType))
   End Function

   Private Function ICollection_IndexOf(ByVal value As Object) As Integer Implements IList.IndexOf
      Return Me.IndexOf(CType(value, ItemType))
   End Function

   Private Sub ICollection_Insert(ByVal index As Integer, ByVal value As Object) Implements IList.Insert
      Me.Insert(index, CType(value, ItemType))
   End Sub

   Public ReadOnly Property IsFixedSize() As Boolean Implements IList.IsFixedSize
      Get
         Return False
      End Get
   End Property

   Public ReadOnly Property IsReadOnly() As Boolean Implements IList.IsReadOnly
      Get
         Return False
      End Get
   End Property

   Private Property ICollection_Item(ByVal index As Integer) As Object Implements System.Collections.IList.Item
      Get
         Return Me.Item(index)
      End Get
      Set(ByVal Value As Object)
         Me.Item(index) = CType(Value, ItemType)
      End Set
   End Property

   Private Sub ICollection_Remove(ByVal value As Object) Implements IList.Remove
      Me.Remove(CType(value, ItemType))
   End Sub

   Public Sub RemoveAt(ByVal index As Integer) Implements IList.RemoveAt
      If index < 0 OrElse index > m_Count - 1 Then Throw New ArgumentOutOfRangeException("Index was out of range.")
      Array.Copy(items, index + 1, items, index, m_Count - index)
      m_Count -= 1
   End Sub
End Class

Se fate un po' di benchmark con grandi collection, vedrete che questa classe è decisamente più veloce di una collection class che deriva da CollectionBase.

Un'ultima nota, che vale sia per chi lavora con .NET 1.1 ma anche per chi è già passato armi e bagagli a .NET 2.0: questo trucchetto basato sugli alias in realtà ha alcuni vantaggi anche in VS 2005. Infatti, i generics non permettono di utilizzare il tipo argomento per specificare la classe base e inoltre non supportano le quattro operazioni aritmetiche se il tipo è numerico (come spiego in questo post). In altre parole, questo codice non compila:

// VB.NET 2005
Public Class MyGenericType(Of T)
   Inherits T ' <<<< errore

   Function Add(ByVal n1 As T, ByVal n2 As T) As T
      Return n1 + n2 ' <<<<< errore
   End Function
End Class

// C#
public class MyGenericType : T <<<<< errore
{
   public T Add(T n1, T n2)
   {
      return n1 + n2; // <<<<< errore
   }
}

E' evidente che entrambi i problemi si possono risolvere in modo "generico" usando la tecnica dell'alias. Uno sporco trucco, ma quando ce vo', ce vo' ! :-)

9/5/2005 7:04:44 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Oggi ho scoperto un'altra di quelle feature non documentate delle regex in .NET che invece dovrebbero essere un pubblicizzate a dovere, fosse solo per dimostrare che in Microsoft prendono la cosa molto seriamente. Considerate il seguente codice:

 

Sub Main()
  Dim text As String = "AA" & New String(" "c, 1000000) & "AA"
  Dim re As New Regex("A")
  Dim startTime As Date = Now

  For Each m As Match In re.Matches(text)
    Dim elapsed As TimeSpan = Now.Subtract(startTime)
    Console.WriteLine(elapsed.Milliseconds)
  Next
End Sub

 

Per come è fatta la stringa, le regular expression troverà un paio di match all'inizio e un paio alla fine, e in mezzo ci sono un milione di caratteri da saltare. Quello che mi sarei aspettato, e probabilmente sarei in buona compagnia, è che il metodo Matches avrebbe esaminato tutta la stringa in una volta per poi restituire una collection bella e completa. Ecco invece quello che appare nella finestra di debug:

 

Inizio del test

0

0

515

515

 

È facile immaginare cosa sia successo: il metodo Matches adotta una forma di lazy evaluation, per cui restituisce un oggetto Match al programma non appena trova un risultato, e riprende il parsing da dove lo aveva interrotto quando il programma esegue il comando Next e rientra nel ciclo.

 

Devo ammettere che ignoravo completamente questa feature, e quando usavo le regular expression su stringhe molto lunghe preferivo creare un loop basato sul metodo Match:

 

' Questo codice è equivalente all'esempio precedente.
Dim text As String = "AA" & New String(" "c, 1000000) & "AA"
Dim re As New Regex("A")
Dim startTime As Date = Now

Dim m As Match = re.Match(text)
Do While m.Success
  Dim elapsed As TimeSpan = Now.Subtract(startTime)
  Console.WriteLine(elapsed.Milliseconds)
  m = m.NextMatch()
Loop

 

Invece, è possibile usare il metodo Matches in un ciclo For Next (con uno stile decisamente più elegante) e allo stesso tempo evitare di "congelare" l'interfaccia utente anche quando si lavora con stringhe molto grandi. Se troviamo il match che stavamo cercando e vogliamo uscire dal ciclo, il resto della stringa non sarà valutata, con evidente risparmio di tempo.
8/9/2005 11:31:43 AM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Stasera vado in vacanza, o meglio, lascio la città e mi sposto in collina, dove si soffre meno il caldo e ho comunque il mare a cinque minuti di macchina. Non posso dire che vado realmente in vacanza, visto che passerò la maggior parte del tempo lavorando sul primo dei due libroni su VB 2005. Avrò accesso a Internet, ma con un lentissimo dialup, quindi non potrò frequentare il blog e la Rete come al solito. Ma non mi lamento, nè mi pesa vivere unplugged per un po' di tempo.

A proposito del libro, oggi sto rivedendo il capitolo sui generics, e ho fatto una piccola-grande scoperta che mi era sfuggita e che non ho trovato documentata da nessuna parte. Anzi è stata una doppia scoperta. Andiamo con ordine.

Come è noto, la classe System.Array ha sempre avuto il metodo IndexOf, che serve a restituire l'indice di un elemento nel vettore, oppure -1 se l'elemento cercato non esiste. In .NET 2.0 hanno affiancato un metodo generico, IndexOf<T>. Provate il seguente codice:

' VB
Dim
arr(999) As Integer
arr(999) = -1
' standard version
Dim
index As Integer = Array.IndexOf(arr, -1)
' geneeric version
Dim index2 As Integer = Array.IndexOf(Of T)(arr, -1)

// C#
int[] arr = int[1000];
arr[999] = -1;
// standard version
int
index = Array.IndexOf(arr, -1);
// generic version
int
index2 = Array.IndexOf<int>(arr, -1);

Secondo tutto quello che si può leggere in giro, la versione che usa IndexOf<int> dovrebbe andare molto più veloce della precedente, in quanto la ricerca è ottimizzata e perchè non viene eseguito il boxing del secondo argomento. Beh, ecco la prima sorpresa: le due sintassi producono esattamente lo stesso codice IL e quindi hanno la stessa velocità. Sia il compilatore VB che il compilatore C# si rendono conto che è possibile usare il metodo IndexOf<T> anzichè il meno efficiente IndexOf e quindi in entrambi i casi chiamano il metodo IndexOf<int> per produrre il codice più efficiente possibile. Ovviamente questo comportamento non è specifico per i soli metodi della classe Array, ma vale in tutti i casi in cui una classe espone due metodi con lo stesso nome, uno standard e uno generico: se il compilatore riesce a sfruttare il metodo generico lo farà, e voi ve ne potete accorgere solo esplorando l'assembly con ILDASM, come ho fatto io.

Tutto bene, quindi? Ovviamente no. Come ho detto, ho fatto anche una seconda scoperta, un po' meno piacevole della precedente. Il compilatore riesce ad usare il metodo generico solo se i tipi degli argomenti coincidono perfettamente! Attenzione, perchè - a differenza di quel che accade con gli overload dei metodi, in cui il compilatore è in grado di capire che un argomento Int32 può essere passato ad un argomento Int64 - quando cerca la corrispondenza dei metodi generici il compilatore non tenta neanche di eseguire una coercion degli argomenti per trovare il metodo generic più conveniente, e use direttamente il metodo standard.

' VB
' questo codice usa la versione nongeneric, meno efficiente
Dim
 search As Short = -1
Dim index2 As Integer = Array.IndexOf(arr, search)

// C#
// questo codice usa la versione nongeneric, meno efficiente
short
 search = -1;
int
index2 = Array.IndexOf(arr, search);

Buono a sapersi, direte voi, ma nella pratica non si capisce perchè dovremmo passare un Int16 quando stiamo cercando un elemento di un array Int32, quindi è giusto uno di quei dettagli teorici che non impattano sullo stile di scrittura del codice, tanto nella maggioranza dei casi se la vedrà il compilatore. Se credete questo, allora date una occhiata a questo codice dall'aria innocente:

' VB
Dim
arr(999) As Long
arr(999) = -1
' quale versione verrà usata, IndexOf oppure IndexOf(Of Long) ?
Dim
index As Integer = Array.IndexOf(arr, -1)

// C#
long[] arr = long[1000];
arr[999] = -1;
// quale versione verrà usata, IndexOf oppure IndexOf<long> ?
int
index = Array.IndexOf(arr, -1);

Soluzione del quesito: sia il compilatore VB che il compilatore C# userà la versione "tradizionale" di IndexOf, perchè tutte le costanti intere sono considerate essere degli Int32 (a meno di non specificare altrimenti), e come ho appena ribadito ambedue i compilatori usano la versione "generica" solo se il tipo corrisponde perfettamente. La soluzione è evidentemente quella di specificare la clausola (Of Long) (in VB) o <long> (in C#) oppure forzare il tipo del secondo argomento:

' VB
' forza il secondo argomento come Long per usare la versione "generica"
Dim
index As Integer = Array.IndexOf(arr, -1L)

// C#
// forza il secondo argomento come Long per usare la versione "generica"
int
index = Array.IndexOf(arr, -1L);

Sembra una cosa da nulla, ma con la Beta 2 quella "L" aggiunta in coda alla costante -1 fa andare il vostro codice circa 100 volte più veloce!!! Non mi pare di conoscere altri casi in cui un singolo carattere in più o in meno riesce ad aumentare l'efficienza del codice di circa 2 ordini di grandezza! :-)

Per essere più precisi, il guadagno di velocità dipende dal numero di volte che ci chiama il metodo IndexOf. Infatti, ad ogni chiamata avviene una operazione di boxing, che a sua volta crea un oggetto temporaneo che prima o poi dovrà essere "reclamato" dal GC. Eseguendo quel metodo 10mila o 100mila volte, il tempo complessivo della versione standard è circa 100 volte quello della versione che usa il metodo generico. Nella versione definitiva di .NET 2.0 i rapporti di velocità potrebbero essere differenti, e magari il divario potrebbe essere minore (o maggiore) a seconda della RAM installata e altri dettagli, ma certamente il comportamento dei compilatori rimarrà identico, quindi conviene abituarsi fin da ora a queste sottigliezze sintattiche dei metodi generici.

La cosa più buffa di tutta la questione è che ricordo di aver visto alcuni "benchmark" in giro sulla rete che confrontano la versione standard e la versione generics di IndexOf o di qualche altro metodo di .NET 2.0 che esiste in entrambe le versioni, e tutti i benchmark tentavano di dimostrare che la seconda delle due era il 10-15% più veloce dell'altra. Peccato che in realtà quei "benchmark" (le virgolette sono d'obbligo a questo punto) stavano in realtà testando lo stesso codice IL e che le differenze in performance erano da imputare soltanto alle solite fluttazioni statistiche di cui soffrono tutti i benchmark.

8/7/2005 11:41:05 AM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

In questi giorni sto lavorando al capitolo sulla serializzazione con .NET 2.0 e mi sono subito imbattuto in un problema imprevisto. La nuova versione del framework supporta un nuovo attributo chiamato OptionalField, che serve a marcare i campi aggiunti ad una nuova versione di una classe. Ecco in pratica come questo attributo dovrebbe funzionare (il condizionale è d'obbligo, come vedrete). Supponiamo di avere una classe Person nella versione 1.0.0.0 di un assembly BOLibrary (marcato con uno strong-name).

Class Person
   Public Name As String
   ' altri campi omessi...
End Class

Supponiamo ora di aggiornare il nostro assembly alla versione 1.1.0.0 e di aggiungere un nuovo campo ID. Se però tentiamo di deserializzare con la nuova versione uno stream serializzato con la prima versione avviene un SerializationException. Questo errore può essere aggirato proprio grazie al nuovo attributo:

Class Person
   Public Name As String
   <OptionalField()> Public ID As String
   ' altri campi omessi...
End Class

Il primo problema da risolvere, però, è che in realtà il .NET runtime tenterà comunque di deserializzare la versione 1.0.0.0 della classe, quindi l'unico modo per evitare un'altra SerializationException è di impostare la proprietà AssemblyFormat dell'oggetto BinaryFormatter al valore Simple, in questo modo:

Dim p As New Person()
Dim bf As New BinaryFormatter()
' non inserire il nome completo dell'assembly nello stream
bf.AssemblyFormat = FormatterAssemblyStyle.Simple
Dim fs As New FileStream("c:\person.dat", FileMode.Create)
bf.Serialize(fs, p)

Questo è almeno il codice che si trova in giro su Internet, in numerosi blog e articoli online. Peccato che con la Beta 2 questo codice non funziona più :-( Il motivo è semplice: in questa beta la proprietà AssemblyFormat viene ignorata per il BinaryFormatter, e viene onorata solo dal SoapFormatter (che però nel frattempo è stato dichiarato obsoleto e non si dovrebbe più usare). Quindi l'attributo OptionalField funziona come descritto nei vari esempi solo con gli assembly non segnati con uno strong-name, in cui il problema del versioning non esiste neanche. La cosa strana è che questo "dettaglio" non è documentato da nessuna parte e solo grazie a qualche "amicizia altolocata" a Redmond ho avuto la conferma che questo comportamento è by-design, non è un bug della Beta 2, e quindi lo ritroveremo pari pari nella versione definitiva (se non arrivano cambi di rotta nel frattempo).

Poichè è buona norma marcare con uno strong-name tutti i propri assembly - sia EXE che DLL - sia per applicare un numero di versione che per attivare la CAS che per lavorare con ClickOnce, è necessario quindi trovare un metodo per usare assembly con strong-name e allo stesso tempo poter deserializzare una istanza serializzata con una versione precedente. Altrimenti tutto il meccanismo della version tolerant deserialization (VTS) va a farsi benedire. Per fortuna la soluzione è a portata di mano, sotto forma della proprietà Binder.

La prima cosa da fare per poter usare questa proprietà è definire un tipo che deriva dalla classe astratta SerializationBinder. Questa classe ha un metodo astratto BindToType, che viene chiamato quando il BinaryFormatter deve deserializzare un tipo. Ecco una classe di questo tipo, che reindirizza tutti i tipi presenti nella versione 1.0.*.* della libreria BOLibrary alla versione 1.1.0.0 della stessa libreria:

Class MySerializationBinder
   Inherits SerializationBinder

   Public Overrides Function BindToType(ByVal assemblyName As String, ByVal typeName As String) As Type
      ' Read the version of the assembly.
      Dim an As New AssemblyName(assemblyName)
      If an.Name = "BOLibrary" AndAlso an.Version.Major = 1 AndAlso an.Version.Minor = 0 Then
         ' Replace the version number with the current version number.
         an.Version = New Version("1.1.0.0")
         ' Load the new assembly.
         Dim asm As Assembly = Assembly.Load(an.FullName)
         ' Return the type taken from that assembly
         Return asm.GetType(typeName)
      Else
        ' Otherwise, tell the runtime to apply the default binding policy.
        Return Nothing
      End If
   End Function
End Class

Ecco come si usa questa nuova classe:

Dim bf As New BinaryFormatter()
bf.Binder = New MySerializationBinder()

Gli oggetti SerializationBinder risolvono il problema in questione, ma in realtà sono molto più potenti di tanto. Ad esempio, potete usarli se avete spostato una classe in un assembly differente, oppure potete dire al .NET runtime di deserializzare lo stream in una classe con nome differente (ma con dei campi con lo stesso nome e tipo della classe originaria), ecc. E' un meccanismo molto potente che offre una enorme flessibilità durante la fase di deserializzazione. E senza il quale non potrete mai sfruttare il nuovo attributo OptionalField.

8/4/2005 11:02:32 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Come preannunciavo nel mio precedente post, mi sono messo al lavoro su una utility in grado di computare le principali metriche software su programmi Visual Basic .NET (versione 2003). Non è stato troppo complicato, soprattutto perchè ho deciso di scrivere un tool da linea di comando anzichè un programma con una bella interfaccia utente.

Il programma lavora su un file sorgente .vb alla volta, e mostra le seguenti informazioni:
  - numero totale di righe di codice, di righe di commenti, e di righe vuote, espresse anche come percentuale sul totale (a livello di file, tipo, e metodo)
  - numero di tipi per file
  - numero di metodi per ciascun tipo
  - numero di exit point in ciascun metodo
  - indice ciclomatico di ciascun metodo
  - massimo livello di nidificazione di if,loop, ecc. in ciascun metodo

Ovviamente non ho resistito alla curiosità di usare CodeMetrics su sè stesso. I risultati li potete vedere nella figura: solo 138 istruzioni eseguibili, tutto sommate davvero poche per un programmino che risolve un problema non banale. Potenza delle regular expression!

Cos'altro ho potuto scoprire? Ad esempio, che tutti i metodi hanno un solo exit point, hanno un livello di nidificazione massimo non superiore a 2, e un indice ciclomatico massimo di 2 (a parte un metodo, il cui I.C. è 12, a causa di una Select Case complessa). Quindi anche se ho scritto pochi commenti (appena il 10% del totale di righe di codice), posso quindi concludere che questo codice dovrebbe essere facilmente manutenibile. Forse non è vero, però questo è quello che dicono le cifre ufficiali! :-)

Non vi resta che scaricarlo, provarlo con i vostri programmi VB.NET, e farmi sapere cosa ne pensate: CodeMetricsVB v0.9.zip (4.89 KB)

Tenete presente che è una version "alpha" e che potrebbe fallire in qualche caso. Ad esempio la procedura di parsing non funziona bene con i tipi nidificati e con le istruzioni If..Then...Else su un'unica riga. Una delle prime aggiunte in programma è la possibilità di estrarre statistiche di un intero progetto VB e di calcolare i valori medi e massimi di alcuni indici. Se avete altri suggerimenti, lasciate pure un commento. Tempo permettendo scriverò anche una versione C#.

7/29/2005 8:18:59 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Ho finalmente finito il capitolo sui generics, che ha richiesto più tempo del previsto a causa di tanti impegni che si sono succeduti in questi giorni. Ci sono grandi novità in Code Architects e non sempre resta abbastanza tempo per scrivere. Comunque sono molto soddisfatto del risultato, perchè in queste 40 pagine circa sono riuscito a condensare non solo tutte le proprietà documentate dei generics ma anche e soprattutto un bel po' di tecniche di programmazioni meno banali. In questo post mostro ad esempio come risolvere un problema abbastanza comune con i generics. (Per la gioia di coloro che sono allergici alle parole chiave Me e Of, ho tradotto tutti gli esempi in C#.)

 

Una delle limitazioni dei generics in cui ci si imbatte quasi subito è il fatto che un generic non può usare alcun operator sugli elementi del tipo passato come argomento. Mi spiego meglio. Supponiamo di voler creare una classe che deriva da List<T> e che aggiunge un metodo che restituisce la somma degli elementi: 

' VB
Public Class StatsList(Of T)
   Inherits List(Of T)

   Public Function Sum() As T
      Dim result As T = Nothing
      For Each item As T In Me
         result += item ' *** Compile error
      Next
      Return result
   End Function
End Class

// C#
class StatsList<T> : List<T>
{
   public T Sum()
   {
      T result = default(T);
      foreach (T item in this)
         result += item; // *** Compile error
      return result;
   }
}

I commenti indicano quali istruzioni causano un errore di compilazione, che è ovviamente causato dal fatto che il compilatore non può essere sicuro che il tipo T supporti l'operatore +. Questo è un serio limite nella applicazione dei generics, come mi sono reso conto quasi immediatamente quando ho cominciato a sviluppare con Visual Studio 2005. Ricordo di aver letto alcuni blog sulla rete che lamentavano lo stesso problema, e non mi pare di aver trovato una soluzione davvero soddisfacente.

 

In realtà, a mio avviso si tratta di una svista a cui Microsoft avrebbe potuto rimediare abbastanza facilmente quando .NET 2.0 era in alpha. Dato che siamo nelle fasi finali della Beta 2 dubito fortemente che faranno qualcosa in proposito prima del rilascio ufficiale. Per rimediare a questo problema, infatti, sarebbe bastato che Microsoft avesse definito una interfaccia contenente i metodi per le quattro operazioni, ad esempio:

 

' VB
Public Interface IOperators(Of T)
   Function Add(ByVal n1 As T) As T
   Function Subtract(ByVal n As T) As T
   Function Multiply(ByVal n As T) As T
   Function Divide(ByVal n As T) As T
End Interface

// C#
public interface IOperators<T>
{
   T Add(T n);
   T Subtract(T n);
   T Multiply(T n);
   T Divide(T n);
}

Se questa interfaccia fosse esposta da tutti i tipi numerici base del .NET Framework, sarebbe molto semplice aggiungere un constraint alla classe StatsList, in modo che il codice nella classe possa eseguire le operazioni aritmetiche sul tipo T per mezzo della interfaccia:

' VB
Public Class StatsList(Of T As IOperators(Of T))
   Inherits List(Of T)

   Public Function Sum() As T
      Dim result As T = Nothing
      For Each item As T In Me
         result = result.Add(item)
      Next
      Return result
   End Function
End Class

// C#
class StatsList<T> : List<T> where T:IOperators<T>
{
   public T Sum()
   {
      T result = default(T);
      foreach (T item in this)
         result = result.Add(item);
      return result;
   }
}

Questo concetto non è completamente nuovo. Ad esempio, tutti i tipi numerici (e altri tipi primitivi, come String) supportano l'interfaccia IComparable<T>, che infatti permette di supplire alla impossibilità di usare gli operatori di confronto nelle classi generic. Avrebbero potuto aggiungere allo stesso modo una interfaccia come IOperators, ma la realtà è che in Microsoft non ci hanno pensato e adesso, per bene che ci vada, dovremo aspettare la prossima versione del .NET Framework.

 

Tuttavia, anche se non possiamo modificare l'implementazione dei tipi primitivi come Int32 o Double, possiamo però creare una classe ausiliaria che esegua per noi queste operazioni. Questo concetto è molto simile a quello che accade con le interfacce IComparable<T> e IComparer<T>. Se una classe implementa IComparable<T> essa è in grado di confrontare i suoi elementi in modo autonomo; se però non è questo il caso, possiamo comunque definire una classe ausiliaria che implementa IComparer<T> e che dichiara quindi di essere in grado di confrontare due valori di tipo T.

 

Se spostiamo il concetto sulle quattro operazioni aritmetiche, possiamo quindi definire una interfaccia ICalculator<T>; una classe può implementare tale interfaccia per dichiarare che è in grado di eseguire queste operazioni su elementi di tipo T: 

' VB
Public Interface ICalculator(Of T)
   Function Add(ByVal n1 As T, ByVal n2 As T) As T
   Function Subtract(ByVal n1 As T, ByVal n2 As T) As T
   Function Multiply(ByVal n1 As T, ByVal n2 As T) As T
   Function Divide(ByVal n1 As T, ByVal n2 As T) As T
   Function ConvertTo(ByVal n As Object) As T
End Interface

// C#
public interface ICalculator<T>
{
   T Add(T n1, T n2);
   T Subtract(T n1, T n2);
   T Multiply(T n1, T n2);
   T Divide(T n1, T n2);
   T ConvertTo(object n);
}

Non resta ora che implementare una classe che implementi ICalculator<Int32>, un'altra classe che implementa ICalculator<Double>, e così via. Per ridurre il numero delle classi con cui avremo a che fare, possiamo fare anche di meglio: possiamo definire una unica classe che implementa l'interfaccia ICalculator<T> per tutte le classi numeriche che ci interessano. È un bel po' di codice, ma la sua struttura è davvero semplice:  

' VB
Public Class NumericCalculator
   Implements ICalculator(Of Int32)
   Implements ICalculator(Of Double)

   ' The ICalculator(Of Int32) interface
   Public Function AddInt32(ByVal n1 As Int32, ByVal n2 As Int32) As Int32 _
         Implements ICalculator(Of Int32).Add
      Return n1 + n2
   End Function
   Public Function SubtractInt32(ByVal n1 As Int32, ByVal n2 As Int32) As Int32 _
         Implements ICalculator(Of Int32).Subtract
      Return n1 - n2
   End Function
   Public Function MultiplyInt32(ByVal n1 As Int32, ByVal n2 As Int32) As Int32 _
         Implements ICalculator(Of Int32).Multiply
      Return n1 * n2
   End Function
   Public Function DivideInt32(ByVal n1 As Int32, ByVal n2 As Int32) As Int32 _
         Implements ICalculator(Of Int32).Divide
      Return n1 \ n2
   End Function
   Public Function ConvertToInt32(ByVal n As Object) As Int32 _
         Implements ICalculator(Of Int32).ConvertTo
      Return CInt(n)
   End Function

   ' The ICalculator(Of Double) interface
   Public Function AddDouble(ByVal n1 As Double, ByVal n2 As Double) As Double _
         Implements ICalculator(Of Double).Add
      Return n1 + n2
   End Function
   Public Function SubtractDouble(ByVal n1 As Double, ByVal n2 As Double) As Double _
         Implements ICalculator(Of Double).Subtract
      Return n1 - n2
   End Function
   Public Function MultiplyDouble(ByVal n1 As Double, ByVal n2 As Double) As Double _
         Implements ICalculator(Of Double).Multiply
      Return n1 * n2
   End Function
   Public Function DivideDouble(ByVal n1 As Double, ByVal n2 As Double) As Double _
         Implements ICalculator(Of Double).Divide
      Return n1 / n2
   End Function
   Public Function ConvertToDouble(ByVal n As Object) As Double _
         Implements ICalculator(Of Double).ConvertTo
      Return CDbl(n)
   End Function
End Class

// C#
public class NumericCalculator 
   : ICalculator<Int32>, ICalculator<Double>
{
   // The ICalculator<Int32> interface
   Int32 ICalculator<Int32>.Add(Int32 n1, Int32 n2)
   {
      return n1 + n2;
   }
   Int32 ICalculator<Int32>.Subtract(Int32 n1, Int32 n2)
   {
      return n1 - n2;
   }
   Int32 ICalculator<Int32>.Multiply(Int32 n1, Int32 n2)
   {
      return n1 * n2;
   }
   Int32 ICalculator<Int32>.Divide(Int32 n1, Int32 n2)
   {
      return n1 / n2;
   }
   Int32 ICalculator<Int32>.ConvertTo(object n)
   {
      return Convert.ToInt32(n);
   }

   // The ICalculator<Double> interface
   Double ICalculator<Double>.Add(Double n1, Double n2)
   {
      return n1 + n2;
   }
   Double ICalculator<Double>.Subtract(Double n1, Double n2)
   {
      return n1 - n2;
   }
   Double ICalculator<Double>.Multiply(Double n1, Double n2)
   {
      return n1 * n2;
   }
   Double ICalculator<Double>.Divide(Double n1, Double n2)
   {
      return n1 / n2;
   }
   Double ICalculator<Double>.ConvertTo(object n)
   {
      return Convert.ToDouble(n);
   }
}

Ora finalmente possiamo riscrivere la classe StatsList correttamente; in questa nuova versione la classe accetta due argomenti generici: il tipo T degli elementi della list e il tipo C che è in grado di eseguire operazioni aritmetiche sul tipo T. Un accorgimento importante è nel definire correttamente i constraint del tipo C, che deve essere instanziabile e deve implementare l'interfaccia generica ICalculator<T>: 

' VB
Public Class StatsList(Of T, C As {New, ICalculator(Of T)})
   Inherits List(Of T)

   ' The object used as a calculator
   Dim calc As New C

   Public Function Sum() As T
      Dim result As T
      For Each elem As T In Me
         result = calc.Add(result, elem)
      Next
      Return result
   End Function

   Public Function Avg() As T
      Return calc.Divide(Me.Sum, calc.ConvertTo(Me.Count))
   End Function
End Class

// C#
class StatsList<T, C> : List<T> where C : ICalculator<T>, new()
{
   // an instance of the number calculator
   private C calc = new C();

   // return the sum of all elements in the list
   public T Sum()
   {
      T result = default(T);
      foreach (T item in this)
         result = calc.Add(result, item);
      return result;
   }

   // return the average of all elements in the list
   public T Avg()
   {
      return calc.Divide(Sum(), calc.ConvertTo(this.Count));
   }
}

Ecco come si usa la nuova versione della classe StatsList: 

' VB
Dim list As New StatsList(Of Double, NumericCalculator)
' fill the list with some values
For i As Integer = 1 To 10
   list.Add(i)
Next
Console.WriteLine("Sum = {0}", list.Sum())
Console.WriteLine("Average = {0}", list.Avg())

// C#
StatsList<Double, NumericCalculator> list;
list = new StatsList<double, NumericCalculator>();
// fill the list with some values
for ( int i = 1; i <= 10; i++ )
   list.Add(i);
Console.WriteLine("Sum = {0}", list.Sum());
Console.WriteLine("Average = {0}", list.Avg());

Per una soluzione davvero completa occorre estendere la classe NumericCalculator per supportare tutti i tipi numerici di .NET. Per farlo occorre fare un po' di copia-e-incolla, oppure potete utilizzare le versioni VB e C# che ho preparato per voi e che potete scaricare qui.

 

7/21/2005 1:19:19 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Con mia sorpresa - e un po' di delusione - devo constatare che questa volta nessuno ha risposto il quizzettino che avevo proposto sui generics, se si eccettua il "solito" Stefano. Mi piacerebbe leggere qualche vostro commento, per capire se il quizzettino era troppo difficile, oppure era su un argomento che ancora in pochi conoscono, o semplicemente se siete già tutti al mare :-)

Comunque, nella sua semplicità il quiz era abbastanza interessante. Se con l'object browser si dà una occhiata ai metodi dell'oggetto List<T> si nota un metodo generico che potrebbe fare al caso nostro. Il metodo FindAll, infatti, restituisce un altro oggetto List<T> che contiene solo gli elementi della lista che soddisfano l'azione indicata da un delegate di tipo Predicate<T>. Allora potrei usare il metodo FindAll sull'oggetto List2 e nel predicato testo la condizione che l'elemento sia contenuto anche in List1, ossia qualcosa del genere:

Dim list1 As New List(Of Double)(New Double() {1, 2, 3, 4, 5, 6, 7, 8, 9})
Dim list2 As New List(Of Double)(New Double() {0, 3, 6, 9, 12})
Dim list3 As List(Of Double)

Sub SolveQuiz
   ' Find all eleemnts of list2 that appear also in list2, and remove them from list1
   Dim list3 As List(Of Double) = list2.FindAll(AddressOf FindInList1)
   ' Remove all the elements in list1/list2 that appear also in list3
   list1.RemoveAll(AddressOf FindInList3)
   list2.RemoveAll(AddressOf FindInList3)
End Sub

Function FindInList1(ByVal n As Double) As Boolean
   Return list1.Contains(n)
End Function

Function FindInList3(ByVal n As Double) As Boolean
   Return list3.Contains(n)
End Function

Questa soluzione è poco elegante, anche perchè costringe a dichiarare le tre liste a livello di classe, per metterle a disposizione delle tre procedure. In C# si può aggirare il problema usando gli anonymous methods:

List<double> list1 = new List<double>(new double[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
List<double> list2 = new List<double>(new double[] { 0, 3, 6, 9, 12});

List<double> list3 = list2.FindAll( delegate(double n)
                                  { return list1.Contains(n);});
list1.RemoveAll(delegate(double n)
                                  { return list3.Contains(n); });
list2.RemoveAll(delegate(double n)
                                  { return list3.Contains(n); });

Se contiamo le istruzioni negli anonymous methods (ma non le dichiarazioni di list1 e list2), anche la soluzione C# non riesce a scendere sotto le 6 istruzioni. Quindi probabilmente vi stupirete se vi dico che si può fare con soltanto DUE istruzioni?

La tecnica è molto semplice, una volta che si comprende il trucco. Tutti i libri e gli articoli che ho letto mostrano come usare i metodi generici di Array, List, Dictionary e altri classe con dei delegate che puntano a dei metodi definiti nella propria applicazione o, nel caso di C#, che puntano a degli anonymous methods. Ma i delegate possono puntare a metodi definiti nel framework, purchè ovviamente tali metodi rispettino la signature del delegate. In altre parole, è possibile scrivere del codice molto più compatto come segue:

' VB
Dim list3 As List(Of Double) = list2.FindAll(AddressOf list1.Contains)
list1.RemoveAll(AddressOf list3.Contains)
list2.RemoveAll(AddressOf list3.Contains)

// C#
List<double> list3 = list2.FindAll(new Predicate<double>(list1.Contains));
list1.RemoveAll(new Predicate<double>(list3.Contains));
list2.RemoveAll(new Predicate<double>(list3.Contains));

E così siamo riusciti a ridurre le istruzioni da 6 a 3. Come fare per eliminarne ancora una? Basta ricordare che il metodo Remove di List<T> restituisce True se l'elemento specificato era effettivamente presente nella lista, quindi possiamo passarlo direttamente come predicato per il metodo FindAll:

' VB
Dim list3 As List(Of Double) = list2.FindAll(AddressOf list1.Remove)
list2.RemoveAll(AddressOf list3.Contains)

// C#
List<double> list3 = list2.FindAll(list1.Remove);
list2.RemoveAll(list3.Contains);

... dove in C# ho approfittato della nuova feature del delegate inference (che il VB.NET ha da qualche anno, :-) ).

Certo non si può dire che questo codice sia il massimo della leggibilità, ma di certo è un buon esempio di quello che si riesce a fare con i generics. Che ne dite?


Ora che vi ho spiegato il meccanismo, vi lascio con un piccolo quizzettino accessorio, la cui soluzione si basa sulla medesima tecnica:

Data un oggetto List1 di tipo List<int> che contiene un numero qualsiasi di interi (es 10,16,32), ottenere una nuova lista List2<string> che contiene la rapresentazione esadecimale dei numeri in List1 (es. "A", "10", "20") con una sola istruzione e senza usare anonymous methods.


 

6/30/2005 1:10:23 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Con l'uscita di Whidbey arriveranno sul mercato, anzi stanno già arrivando, tonnellate di libri che spiegheranno tutte le nuove feature di Visual Studio 2005. Come ho spiegato in un precedente post non credo che i libri basati sulle versioni beta siano molto affidabili o utili, perchè si basano su feature non ancora stabili ma soprattuto perchè se si tratta di feature veramente nuove neanche l'autore ha avuto probabilmente il tempo di approfondire davvero certi temi, pressato dall'editore che vuole pubblicare al più presto (...e dalla moglie e i figli che rivogliono indietro il marito/padre :-) )

Tutte queste considerazioni continuano a frullarmi in mente in questi giorno che mi sto "spazzolando" per bene i generics, che sono davvero la cosa migliore che poteva capitare al Visual Basic e al C#. Anche se i generics sono spesso in grado di generare codice più efficiente delle tecniche old-styled, la loro vera potenza è nella eleganza e nella concisione, ovvero nella possibilità di ottenere tanto scrivendo pochissimo codice. Gli articoli e i primi libri che sto vedendo in giro non mi pare rendano giustizia a questa nuova feature.

Ero pronto a mostrare qualche esempio particolarmente interessante preso dal mio prossimo libro, ma la tentazione di presentare l'ennesimo quizzettino è stata troppo forte. Partiamo con due liste di numeri in virgola mobile, ad esempio:

Dim list1 As New List(Of Double)(New Double(){1, 2, 3, 4, 5, 6, 7, 8, 9})
Dim list2 As New List(Of Double)(New Double(){0, 3, 6, 9, 12})

Il quizzettino consiste nello scrivere il più breve programma che genera una nuova lista list3 che contiene tutti gli elementi di list2 che compaiono anche in list1 e contemporaneamente rimuove tutti gli elementi di list3 da list1 e list2. Per intenderci, nell'esempio di cui sopra al termine dell'esecuzione list1 dovrà contenere {1,2,4,5,7,8}, list2 dovrà contenere {0,12} e list3 dovrà contenere {3,6,9}. Ecco un esempio di codice che risolve il problema ma non sfrutta adeguatamente i generics:

Dim list3 As New List(Of Double)
For i As Integer = list1.Count - 1 To 0 Step -1
   If list2.Contains(list1(i)) Then
      list3.Add(list1(i))
      list2.Remove(list1(i))
      list1.RemoveAt(i)
   End If
Next

La soluzione deve essere il più generale possibile, ma per semplicità possiamo supporre che nè list1 nè list2 contengano elementi duplicati. Si accettano soluzioni sia in VB2005 che C# 2.0. Non è importante che la soluzione sia la più efficiente, ma solo la più concisa in termini di numero di istruzioni. (Nel conto totale delle istruzioni si includono quelle che fanno parte di un anonymous method.)

6/21/2005 7:56:14 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 

Il sistema più semplice per eseguire una shallow copy di un oggetto (ossia, creare una copia dell'oggetto ma non dei suoi oggetti "figli") è chiamare il metodo MemberwiseClone, che tutti i tipi .NET ereditano da System.Object. Ecco ad esempio come implementare il metodo Clone della interfaccia IClonable in una propria classe:

' VB.NET
Class TestClass
   Implements ICloneable

   Public Function Clone() As Object Implements ICloneable.Clone
      Return Me.MemberwiseClone()
   End Function
End Class

// C#
class TestClass : ICloneable
{
   public object Clone()
   {
      return this.MemberwiseClone();
   }
}

Come fare quando vogliamo clonare una istanza di un tipo che NON abbiamo definito nel programma? Proprio oggi ho dovuto risolvere questo problema con un Data Adapter. La soluzione è davvero semplice, almeno se l'applicazione è full trusted o almeno ha le permission per usare reflection: è sufficiente richiamare MemberwiseClone via reflection, no? Ecco un metodo riutilizzabile che esegue la copia di qualsiasi oggetto:

' VB.NET
Public Function CloneObject(ByVal obj As Object) As Object
   Return obj.GetType().InvokeMember("MemberwiseClone", BindingFlags.NonPublic Or BindingFlags.InvokeMethod Or BindingFlags.Instance, Nothing, obj, Nothing)
End Function

// C#
public object CloneObject(object obj)
{
   return obj.GetType().InvokeMember("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.InvokeMethod | BindingFlags.Instance, null, obj, null);
}

Attenzione a un dettaglio: quando si usa MemberwiseClone per creare un nuovo oggetto, il costruttore dell'oggetto non viene eseguito. Se il costruttore contiene del codice che esegue una operazione che non sia la semplice inizializzazione di un field - ad es. incrementa un contatore globale o apre un file - occorre adottare una tecnica differente.

6/17/2005 1:05:04 PM (GMT Daylight Time, UTC+01:00) #  | Comments [0] | 
 
Feed di Blog2theMax
RSS 2.0 | Atom 0.2
Cerca nel blog
Archivio
<March 2010>
SunMonTueWedThuFriSat
28123456
78910111213
14151617181920
21222324252627
28293031123
45678910
Categorie

Powered by: newtelligence dasBlog 1.7.5016.2

 ©2004-2005 Code Architects S.r.l. - All rights reserved  Sign In