Le novità di Visual Basic 9.0 - Parte 2
a cura di Antonio Catucci (requisiti: livello intermedio)

Nel precedente articolo abbiamo visto alcune delle novità introdotte con il .NET Framework 3.5 Beta 2 come l'inferenza, gli inizializzatori e i tipi anonimi.
In questa seconda parte affronteremo due argomenti molto interessanti: gli extension method e le lambda expression.

Extension Method
La novità certamente più interessante del .NET Framework 3.5 è costituita dagli Extension Method. Si tratta della possibilità di estendere tipi esistenti con metodi supplementari, senza che il tipo originario venga modificato e senza necessità di ricompilare l'assembly in cui risiede.
Ad esempio, ipotizziamo di avere questo codice:

    Dim x As Integer
    If x Mod 2 <> 0 Then
      ' codice per x dispari
    Else
      ' codice per x pari
    End If

Si potrebbe migliorare o rimaneggiare (con lo strumento Refactor, per esempio), in questo modo:

  Public Function IsOdd(ByVal number As Integer) As Boolean
    Return (number Mod 2) <> 0
  End Function

  Private Sub Qualsiasi()

    Dim x As Integer
    If IsOdd(x) Then
      ' codice per x dispari
    Else
      ' codice per x pari
    End If

  End Sub

Creando, così, un metodo riutilizzabile nei nostri progetti.

La soluzione offerta dagli Extension Method consiste nel rendere il metodo IsOdd() una estensione del tipo Integer mediante il nuovo attributo <Extension()>.

Vediamo più in dettaglio quali sono i requisiti per realizzare degli extension method personalizzati:

  1. Un extension method può essere soltanto una Function o una Sub
  2. Il metodo deve essere decorato con l'attributo <Extension()> che si trova nel namespace System.Runtime.CompilerServices
  3. E' possibile definire gli extension method solo all'interno di un modulo (questo perché il modulo altro non è che una classe statica con tutti metodi statici che in VB non è supportato)
  4. Ogni metodo deve avere sempre almeno un parametro che indica il tipo che estende (nell'esempio precedente essendo il tipo un Integer, l'estensione viene applicata a tutti i tipi Integer).
Imports System.Runtime.CompilerServices

Module CustomExtensions

  <Extension()> _
  Public Function IsOdd(ByVal number As Integer) As Boolean
    Return (number Mod 2) <> 0
  End Function

End Module

Da usare così:

  Private Sub Qualsiasi()

    Dim x As Integer
    If x.IsOdd Then
      ' codice per x dispari
    Else
      ' codice per x pari
    End If

  End Sub

con tutto il supporto dell'Intellisense:


   Figura 3 - Extension method supportate dall'IDE

Come vedete, se avete dei moduli con una raccolta di metodi di uso generale, diventa molto semplice trasformarla in una libreria di extension method.
I tipi che è possibile estendere (e quindi il tipo del primo parametro) sono i seguenti:

Inoltre, il parametro che specifica il tipo (il primo) può essere passato sia per valore che per riferimento.
Un esempio di metodo con un parametro ByRef potrebbe essere:

  <Extension()> _
  Public Function Increase(ByRef value As Integer, _
                           ByVal [step] As Integer) As Integer
    value += [step]
    Return value
  End Function

Dove le parentesi quadre indicano che step è un nome di variabile e non la parola chiave.
Il metodo Increase appena mostrato è utilizzabile in diversi modi:

    Dim x As Integer = 7
    x.Increase(1)         ' x = 8
    x.Increase(2)         ' x = 10

E' anche possibile avere metodi overload di extension method:

  <Extension()> _
  Public Function Increase(ByRef value As Integer) As Integer
    Return Increase(value, 1)
  End Function

Nel caso di estensione di una classe, questa vale anche per le classi derivate. Quindi, se vogliamo una estensione per tutti i controlli WinForms, avremo:

  <Extension()> _
  Public Sub MyExt(ByVal value As System.Windows.Forms.Control)
    '...
  End Sub

Una eccezione a questa regola è un'estensione per il tipo Object, che è disponibile per tutti i tipi (essendo il tipo base del .NET Framework) tranne il tipo Object stesso. Questo per evitare pericolose sovrapposizioni in caso di late binding con estensioni con nome identico ad un metodo definito nel tipo che si sta estendendo.
Con un esempio è più facile capirlo. Supponiamo di avere:

    Dim obj As Object = New Person()
    obj.Foo()   ' dove Foo() è un metodo di Person

Il metodo Foo() viene determinato a runtime, visto che non è noto il tipo di obj prima della sua creazione. Se implementassimo un extension method omonimo:

  <Extension()> _
  Public Sub Foo(ByVal o As Object)
    '...
  End Sub

e se fosse possibile chiamarlo sul tipo Object, eseguiremmo un codice differente (potenzialmente dannoso) senza rendercene conto (cioè eseguiremmo l'estensione Foo() anziché l'omonimo metodo di Person), perché l'extension method avrebbe la precedenza sul metodo di istanza, non essendo late binding.

Dunque, la chiamata obj.Foo() è possibile solo in late binding, perché si riferisce al metodo di Person e non all'estensione. Da qui, la regola che i metodi di istanza hanno la precedenza su metodi estesi omonimi.
Non è così, però, se il metodo Foo() di Person è statico, nel qual caso viene chiamata sempre l'estensione.

Sempre in merito alle precedenze, nel caso ci siano estensioni con lo stesso nome, viene utilizzato lo stesso criterio per evitare i conflitti tra metodi omonimi: hanno la precedenza le estensioni dichiarate nei moduli locali su quelle esterne. Maggiori dettagli si trovano nella documentazione MSDN.

Nel .Net Framework 3.5 viene inclusa una libreria contenente diversi Extension Method, sotto il nome di LINQ (Language INtegrated Query), che si trova nel nuovo assembly System.Core.dll.

Lambda Expression
Una Lambda Expression è una funzione che si può usare sia come espressione creata al volo sia laddove è richiesto l'uso di un delegate senza doverlo dichiarare esplicitamente.
Per chi programma in C#, si tratta di una cosa nota: ovvero i delegate anonimi, che sono finalmente disponibili pure in VB (anche se con delle limitazioni) :).

Per definire un'espressione lambda si usa la parola chiave Function:

  Function(ByVal s As String) s.Length()

Questa funzione restituisce la lunghezza della stringa passata con il parametro "s". Così come si legge, non rende l'idea, ma un esempio del suo uso sì:

    Console.WriteLine((Function(s As String) s.Length())("Antonio"))

Viene visualizzato 7 cioè la lunghezza della stringa "Antonio" passata come argomento.
E' possibile riutilizzare una funzione lambda assegnandolo ad una variabile:

    Dim f = Function(s As String) s.Length()
    Console.WriteLine(f("Antonio"))

Vediamo qual è la sintassi di una espressione lambda:

  1. Si definisce mediante la parola chiave Function senza un nome
  2. I parametri (opzionali) devono essere tipizzati
  3. Il tipo restituito dall'espressione non deve essere specificato, perché dedotto per inferenza dal corpo della funzione stessa (nell'esempio precedente si tratta di una funzione che restituisce un Integer)
  4. Il corpo deve essere una espressione e non è ammesso l'uso di Return, è possibile richiamare altri metodi
  5. Non sono ammessi parametri Optional, ParamArray e generici

Un esempio pratico sull'uso di una lambda expression è:

    Dim list = New Integer() {1, 2, 3, 4, 5}
    Dim OnlyOdd = Array.FindAll(list, Function(x As Integer) x Mod 2 <> 0)

che in .NET 2 si potrebbe scrivere così:

  Private Sub Qualsiasi()
    Dim list = New Integer() {1, 2, 3, 4, 5}
    Dim OnlyOdd = Array.FindAll(list, AddressOf FindOdd)
  End Sub

  Public Function FindOdd(ByVal x As Integer) As Boolean
    Return x Mod 2 <> 0
  End Function

Ed è quello che, grosso modo, fa il compilatore VB. Un bel passo avanti direi!

Il fatto di poter usare solo espressioni è certamente una limitazione, ma, grazie al nuovo operatore ternario If() è possibile creare espressioni lambda un po' più sofisticate:

    Dim f = Function(s As String) If(s IsNot Nothing, s.Length(), 0)

Si tratta di un operatore simile alla funzione IIF() di VB6, ma tipizzato e nativo.

Una ulteriore potenzialità delle espressioni lambda è la possibilità di sfruttare l'inferenza dei tipi evitando, cioè, di dichiarare il tipo del valore dei parametri e del risultato.
Riprendendo l'esempio precedente, si può scrivere:

    Dim list = New Integer() {1, 2, 3, 4, 5}
    Dim OnlyOdd = Array.FindAll(list, Function(x) x Mod 2 <> 0)

Il tipo del parametro 'x' viene dedotto dal compilatore in base al contesto. Infatti, dovendolo applicare ad un array di Integer, "capisce" che si tratta di un Integer. Analogo discorso si può fare per il valore di ritorno, che, in questo caso, sarà un Boolean, trattandosi di una diseguaglianza.

Volendo spingersi più oltre, si può usare anche il late binding e lavorare con espressioni lambda come se fossero delegate anonimi.
Consideriamo questa espressione lambda già vista precedentemente:

    Dim f = Function(s As String) s.Length()

Che restituisce la lunghezza di una stringa.
Ora, eliminiamo il tipo ed sfruttiamo il late binding, disattivando l'opzione di congruenza dei tipi:

Option Strict Off

Dim GetLength = Function(s) s.Length()

Abbiamo definito una funzione "anonima", in grado di funzionare con tutti gli oggetti che hanno una proprietà Length(), che potremmo usare in diversi modi:

Option Strict Off

Public Class AnyClass

  Private Sub Qualsiasi()
    Dim GetLength = Function(s) s.Length()
    Dim stringa = "Antonio"  ' stringa è di tipo String (dedotto per inferenza)
    Console.WriteLine(GetLength(stringa)) ' stampa 7

    Dim list = New Integer() {1, 2, 3, 4, 5}
    Console.WriteLine(GetLength(list))    ' stampa 5

  End Sub

Le espressioni lambda, oltre ad essere usate localmente, possono essere usate come parametri di metodo, consentendoci la creazione di metodi custom:

  Public Function Sum(ByVal source As IEnumerable(Of Integer), _
                      ByVal func As Func(Of Integer, Boolean)) As Integer
    Dim s = 0
    For Each n In source
      If func(n) Then
        s = s + n
      End If
    Next
    Return s
  End Function

Utilizzabile, considerando l'array di interi del precedente esempio, così:

    Dim Result = Sum(list, Function(x) x Mod 2 <> 0)

Ottenendo la somma dei numeri dispari.
Questo è possibile grazie al nuovo Delegate System.Func(T, TResult), che prevede fino a quattro parametri di input (T1, T2, T3 e T4) e uno di ritorno (TResult), con il quale si possono gestire le espressioni lambda.
Infatti possiamo scrivere:

    Dim f As Func(Of String, Integer) = Function(s As String) s.Length()

Questo ci consente non solo di avere parametri di tipo System.Func(), ma di usarli anche come valori di ritorno di un metodo.

L'introduzione delle espressioni lambda migliora e velocizza la scrittura del codice, ma il motivo della loro introduzione è importante soprattutto per LINQ, un linguaggio integrato molto potente, che consente la manipolazione di oggetti in memoria.

Conclusioni
In definitiva, le novità non sono così tante come è avvenuto con il .NET 2.0, ma sono sicuramente molto interessanti (come gli extension method); velocizzano in alcuni casi la scrittura di codice (come gli inizializzatori) e rendono VB più allineato con C# (grazie alle espressioni lambda). Ma, come già detto diverse volte, tutte queste novità sono importanti per comprendere ed utilizzare il nuovo linguaggio di query LINQ.
In merito a quest'articolo, potete scrivere all'autore Antonio Catucci, del quale potete visitare il blog.