Parsing di un file .ini in un solo statement
Questo post è un po' una prosecuzione del precedente, in cui ho dato una rapida occhiata alla possibilità di approcciarsi al paradigma funzionale con LINQ. Il codice che vi propongo di seguito è in grado di eseguire il parsing completo di un file di configurazione usando solo due variabili di appoggio e condensando tutta l'elaborazione in una cascata di metodi di estensione linq. Ogni funzione lavora sull'output della precedente e fornisce l'input alla successiva. In questi casi l'inferenza di tipo diventa un'alleata preziosa:
static void Main(string[] args)
{
if (args.Length < 2)
return;
String fileName = args[1];
Regex sectionRegex = new Regex(@"\[(?<Section>\w+)\]");
Regex fieldRegex = new Regex(@"(?<Field>\w+)\s*\=\s*(?<Value>.*)");
Match match;
String lastSection = "<No Section>";
var ini = File.ReadAllLines(fileName)
.Select (line => line.Contains(';') ?
line.Remove(line.IndexOf(';')) :
line)
.Select (line => line.Trim())
.Where (line => !String.IsNullOrEmpty(line))
.GroupBy(line => (match = sectionRegex.Match(line)).Success ?
lastSection = match.Groups["Section"].Value :
lastSection)
.Select (group => new
{
SectionName = group.Key,
Fields = group
.Select(line => (match = fieldRegex.Match(line)).Success ?
new { Name = match.Groups["Field"].Value,
Value = match.Groups["Value"].Value } :
null)
.Where(element => element != null)
});
foreach (var section in ini)
{
Console.WriteLine("Section: {0}", section.SectionName);
foreach (var field in section.Fields)
Console.WriteLine(" Field {0} = {1}", field.Name, field.Value);
}
}
Un'altra funzionalità che ho sfruttato pesantemente è una costante di tutti i linguaggi C-like dagli albori della programmazione letterale imperativa: il valore di ritorno dell'operatore di assegnazione. Notate come non sarebbe stato possibile definire la chiave di raggruppamento se l'assegnazione di lastSection non avesse di fatto restituito il nuovo valore della variabile e come sarebbe stato necessario usare dei metodi anonimi anziché delle espressioni lambda se non avessi potuto richiamare la proprietà Success dall'oggetto esposto dall'assegnazione della variabile match.
Funzioni lambda e metodi di estensione
Dalla versione 3.5 del framework, il .NET supporta ufficialmente il lambda calcolo e i metodi di estensione, mentre i generics sono amici affezionati già dalla release 2.0. Negli ultimi tempi l’impulso a scrivere del codice più elegante, funzionale e generale è stato quindi fortemente favorito. In questo breve e semplice prospetto vorrei mostrare come usare queste tecniche per migliorare la riusabilità e l’utilità del codice.
N.B.: la conoscenza dei generics è data per scontata.
Metodi di estensione
Un metodo di estensione viene dichiarato come un metodo statico, ma viene utilizzato come un metodo d’istanza. Grazie a questa peculiare caratteristica, è possibile aggiungere ai tipi già esistenti – ad esempio String, Int32, Point, eccetera… – altri metodi, pur non avendo accesso al codice che originariamente ne contiene la definizione. Sfortunatamente non è possibile fare lo stesso con gli operatori, le proprietà o i metodi sottoposti a polimorfismo, mentre in altri linguaggi, come ad esempio in Ruby, è possibile anche fare questo.
Dato che i metodi di estensione si comportano in maniera atipica, anche la loro dichiarazione non segue le normali regole. Poiché agiscono come se fossero metodi d’istanza, è necessario specificare da qualche parte su quale istanza si debba lavorare. Si fa questo passando un parametro del tipo che si vuole estendere nella dichiarazione della signature, facendolo precedere dal modificator “this”. E’ inoltre necessario inserire questa dichiarazione in una classe pubblica e statica, in modo che sia accessibile da ogni parte del codice client, il quale dovrà a usa volta eventualmente importare il namespace in cui essa è scritta. Ecco un esempio:
namespace ExtensionMethods
{
// Classe statica e pubblica
public static class Methods
{
// Il metodo di estensione è pure pubblico e statico
public static Int32 Double(this Int32 instance)
{
return instance * 2;
}
}
}
namespace Esempio
{
// Importa il namespace in cui è definita Double
using ExtensionMethods;
class Program
{
static void Main(string[] args)
{
Int32 a = 32;
// Richiama Double come se fosse un metodo d'istanza della classe Int32
Int32 b = a.Double();
Console.WriteLine(a); // 32
Console.WriteLine(b); // 64
Console.ReadKey();
}
}
}
Espressioni lambda
Il lambda calcolo si basa sulla manipolazione di funzioni definite inline. In .NET, esiste un tipo deputato a contenere una funzione lambda. Invece che contenere un valore, variabili di quel tipo contengono invece quelle funzioni che, a seconda dei punti di vista, si possono chiamare inline, anonime o lambda. Tale tipo è Func, ed è dichiarato usando svariati tipi generics collegati. Una funzione lambda si dichiara in questo modo:
parametri => valore_restituito
Se i parametri sono più di uno, vanno specificati tra parentesi e separati da virgole, opzionalmente preceduti dal tipo.
Ad esempio:
// Rappresenta una funzione che, dato x, restituisce x al quadrato
x => Math.Pow(x, 2)[/code]
Detto questo, un semplice codice dimostrativo:
[code]namespace Esempio
{
class Program
{
static void Main(string[] args)
{
// Func indica una funzione che accetta un parametro double e
// restituisce un risultato di tipo double
Func square = x => x * x;
// Analogamente la prossima accetta in input due double e restituisce un altro double
Func weightedAverage = (x, y) => Math.Sqrt(x * y);
// Richiama la funzione contenuta in square
Console.WriteLine(square(4)); // 2.0
Console.WriteLine(weightedAverage(2, 8)); // 4.0
Console.ReadKey();
}
}
}
Mettere tutto insieme
Se l'utilità dei metodi di estensioni può essere lampante, forse lo è di meno quella delle espressioni lambda. Perciò ecco degli esempi pratici.
Nel prossimo codice scriverò una funzione che, data una qualsiasi collezione, applica a tutti gli elementi una certa funzione e restituisce una lista di tutti i risultati. Ad esempio, data una lista di numeri interi, restituisce una lista che ne contiene i quadrati, oppure dato un array di stringhe, restituisce un array che ne contiene le lunghezze. Questi due compiti sembrano molto diversi, ma è possibile implementarli usando lo stesso codice e modificando solo i parametri:
using System;
using System.Linq;
using System.Collections.Generic;
namespace ExtensionMethods
{
public static class Methods
{
/* Transform è un metodo di estensione, perciò lo potremo usare su di ogni tipo o derivato di
IEnumerable, il che equivale a dire che lo potremo usare su ogni collezione di qualsiasi tipo.
transformation è una funzione che accetta come input un parametro di tipo T e restituisce come output
un valore di tipo TResult, quindi la collezione da restituire sarà una IEnumerable di TResult. */
public static IEnumerable
Transform(this IEnumerable instance, Func transformation)
{
// Come vedete il codice è semplice. Crea una lista vuota
List
result = new List
();
// La popola con i risultati della funzione
foreach (T element in instance)
result.Add(transformation(element));
// E la restituisce come IEnumerable
return result.AsEnumerable();
}
}
}
namespace Esempio
{
using ExtensionMethods;
class Program
{
static void Main(string[] args)
{
Int32[] numbers = new Int32[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
String[] strings = new String[] { "tizio", "caio", "sempronio" };
// numbers.Transform(x => x * x) prende la collezione numbers e restituisce una collezione
// contenente i quadrati dei numeri in numbers
foreach (Int32 square in numbers.Transform(x => x * x))
Console.WriteLine(square);
// Allo stesso modo questa ottiene l'insieme delle lunghezze delle stringhe in strings
foreach (Int32 length in strings.Transform(s => s.Length))
Console.WriteLine(length);
Console.ReadKey();
}
}
}
E' talmente utile questa funzione che è implementata di default nel namespace System.Linq e si chiama Select.
Ora, avete presenta la funzione Array.IndexOf? Essa restituisce l'indice del primo elemento dell'array che risulta uguale al parametro specificato. Volendo scrivere una funzione simile, ma che funzioni per tutte le collezioni, potremmo usare un metodo di estensione e qualche vincolo generics:
using System;
using System.Linq;
using System.Collections.Generic;
namespace ExtensionMethods
{
public static class Methods
{
/* Notate il vincolo di interfaccia IComparable. Se il tipo di dato degli elementi della collezione non
rappresenta qualcosa di "comparabile", allora non c'è modo di stabilire se due elementi sono o meno
uguali, o meglio non c'è modo di farlo con le informazioni che si hanno ora sul tipo. Usando il vincolo
stiamo imponendo che questa condizione sia verificata. E' come usare le ipotesi di un teorema. */
public static IEnumerable IndicesOf(this IEnumerable instance, T seed) where T : IComparable
{
List indices = new List();
Int32 i = 0;
foreach(T element in instance)
{
if (element.CompareTo(seed) == 0)
indices.Add(i);
i++;
}
return indices.AsEnumerable();
}
}
}
namespace Esempio
{
using ExtensionMethods;
class Program
{
static void Main(string[] args)
{
String[] strings = new String[] { "mela", "pera", "banana", "mela", "uva", "arancia", "mela" };
// Scrive gli indici a cui compare la parola "mela", ossia 0, 3 e 6
foreach (Int32 index in strings.IndicesOf("mela"))
Console.WriteLine(index);
Console.ReadKey();
}
}
}
Come avete visto, usando i metodi di estensione, le espressioni lambda e poco codice è possibile porre le basi per implementare un'enormità di funzionalità differenti. Visto questo trend verso la programmazione funzionale che si sta insinuando in C# tramite linq, consiglio vivamente di studiare linq to objects e linq to entities.
