Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Divertirsi con C# - Parte 2
Bentornato. Nel mio ultimo articolo, "divertimento con c#" (msdn.microsoft.com/magazine/dn754595), ha parlato brevemente come familiarità con altri linguaggi di programmazione può aiutare a chiarire il tuo pensiero intorno a un problema di progettazione che altrimenti possa sembrare insolubile. Introdotto un problema che ho riscontrato anni fa durante un consulenza fidanzamento in cui mi è stato richiesto di conciliare un elenco delle transazioni memorizzate localmente con un elenco presumibilmente uguale delle transazioni memorizzate in remoto. Ho dovuto farli corrispondere sia di importo della transazione — nient'altro era garantita da abbinare anche della transazione stessa esatta — o contrassegnare gli elementi senza pari nell'elenco risultante.
Ho scelto di utilizzare F # per tale esercizio perché è un linguaggio con cui sono a conoscenza. Francamente, avrebbe facilmente potuto essere un altro linguaggio come Scala, Clojure o Haskell. Qualsiasi linguaggio funzionale avrebbe funzionato in modo simile. La chiave qui non è il linguaggio stesso o la piattaforma su cui girava, ma i concetti coinvolti in un linguaggio funzionale. Questo era un problema abbastanza funzionale-amichevole.
La soluzione di F #
Basta rivisitare, guardate la soluzione F # in Figura 1 prima di guardare come tradurrebbe in c#.
Esaminare la colonna precedente per vedere un breve riepilogo di F # sintassi utilizzata Figura 1, soprattutto se non si ha familiarità con F #. Inoltre ho intenzione di andare su di essa come trasformarlo in C#, quindi potrebbe anche solo tuffarsi.
Figura 1 la soluzione di F # per risolvere le più Disparate operazioni
type Transaction =
{
amount : float32;
date : DateTime;
comment : string
}
type Register =
| RegEntry of Transaction * Transaction
| MissingRemote of Transaction
| MissingLocal of Transaction
let reconcile (local : Transaction list)
(remote : Transaction list) : Register list =
let rec reconcileInternal outputSoFar local remote =
match (local, remote) with
| [], _
| _, [] -> outputSoFar
| loc :: locTail, rem :: remTail ->
match (loc.amount, rem.amount) with
| (locAmt, remAmt) when locAmt = remAmt ->
reconcileInternal (RegEntry(loc, rem) ::
outputSoFar) locTail remTail
| (locAmt, remAmt) when locAmt < remAmt ->
reconcileInternal (MissingRemote(loc) ::
outputSoFar) locTail remote
| (locAmt, remAmt) when locAmt > remAmt ->
reconcileInternal (MissingLocal(rem) ::
outputSoFar) local remTail
| _ ->
failwith "How is this possible?"
reconcileInternal [] local remote
La soluzione C#
Presso il punto di partenza, è necessario i tipi di transazione e registro. Il tipo di transazione è facile. È un tipo strutturale semplice con tre elementi denominati, rendendo facile al modello come una classe c#:
class Transaction
{
public float Amount { get; set; }
public DateTime Date { get; set; }
public String Comment { get; set; }
}
Tali proprietà automatica rendono questa classe breve quasi come suo cugino F #. In tutta onestà, se stavo per rendere davvero una traduzione univoca di quale la versione F # fa, devo introdurre sottoposta a override di Equals e GetHashCode metodi. Ai fini di questa colonna, però, questo lavoro.
Le cose si fanno difficili con l'unione discriminata tipo registro. Come un'enumerazione in C#, un'istanza di un tipo di registro può essere solo uno dei tre valori possibili (voce, MissingLocal o Missingremoto). A differenza di un'enumerazione di c#, ciascuno di quei valori a sua volta può contenere dati (le due operazioni corrispondenti per voce, o la transazione manca per MissingLocal o Missingremoto). Mentre sarebbe facile creare tre distinte classi in c#, queste tre classi devono essere collegate in qualche modo. Abbiamo bisogno di una lista che può contenere uno qualsiasi dei tre — ma solo quei tre — per l'output restituito, come in Figura 2. Ciao, ereditarietà.
Figura 2 utilizzare l'ereditarietà per includere tre distinte classi
class Register { }
class RegEntry : Register
{
public Transaction Local { get; set; }
public Transaction Remote { get; set; }
}
class MissingLocal : Register
{
public Transaction Transaction { get; set; }
}
class MissingRemote : Register
{
public Transaction Transaction { get; set; }
}
Esso è non ridicolmente complicato, appena più dettagliato. E se questo fosse rivolto a produzione codice, ci sono alcuni metodi più dovrei aggiungere — è uguale, GetHashCode e, quasi certamente, ToString. Anche se ci potrebbero essere alcuni modi per renderlo più idiomatico c#, scriverò il metodo Reconcile abbastanza vicino alla sua ispirazione F #. Guarderò per ottimizzazioni idiomatiche più tardi.
La versione F # ha un esterno"," funzione pubblicamente accessibile si ripresentanosively chiamato in un interno, incapsulato funzione. Tuttavia, c# non ha alcun concetto di metodi annidati. È la più vicina che io posso approssimare con due metodi — uno dichiarata pubblica e una privata. Anche allora, questo non è abbastanza lo stesso. Nella versione F #, la funzione nidificata viene incapsulata da tutti, anche altre funzioni nel modulo stesso. Ma è il meglio che possiamo ottenere, come si può vedere Figura 3.
Figura 3 che la funzione nidificata viene incapsulata qui
class Program
{
static List<Register> ReconcileInternal(List<Register> Output,
List<Transaction> local,
List<Transaction> remote)
{
// . . .
}
static List<Register> Reconcile(List<Transaction> local,
List<Transaction> remote)
{
return ReconcileInternal(new List<Register>(), local, remote);
}
}
Come nota laterale, ora posso realizzare il "nascosto da tutti" approccio ricorsivo scrivendo interamente come un'espressione lambda viene fatto riferimento come una variabile locale Reconcile funzione interna. Detto questo, che è probabilmente una poco troppo pedissequa aderenza all'originale e interamente non idiomatico per c#.
Non è qualcosa che farebbe la maggior parte degli sviluppatori c#, ma avrebbe quasi lo stesso effetto come la versione di F #. All'interno della riconciliazioneinterna, devo estrarre in modo esplicito gli elementi di dati utilizzati. Poi scrivo in modo esplicito in un albero se/altro-se, al contrario la più succinta e laconico F # corrispondenza modello. Tuttavia, è davvero lo stesso codice esatto. Se l'elenco sia locale o remoto è vuoto, ho finito recursing. Basta restituire l'output e chiamare un giorno, in questo modo:
static List<Register> ReconcileInternal(List<Register> Output,
List<Transaction> local,
List<Transaction> remote)
{
if (local.Count == 0)
return Output;
if (remote.Count == 0)
return Output;
Allora, ho bisogno di estrarre le "teste" di ogni lista. Inoltre è necessario mantenere un riferimento alla parte restante di ogni lista (la "coda"):
Transaction loc = local.First();
List<Transaction> locTail = local.GetRange(1, local.Count - 1);
Transaction rem = remote.First();
List<Transaction> remTail = remote.GetRange(1, remote.Count - 1);
Questo è un posto dove posso presentarvi una prestazione enorme ha colpita se non sto attento. Liste non sono modificabili in F #, quindi prendendo la coda di una lista è semplicemente prendendo un riferimento alla seconda voce dell'elenco. No le copie vengono effettuate.
C#, non ha tuttavia, nessuna di tali garanzie. Ciò significa che io potrei finiscono per fare copie complete di lista ogni volta. Il metodo dice che fa "copie fittizie" GetRange, significa che creerà un nuovo elenco. Tuttavia, esso indicherà gli elementi originali delle transazioni. Questo è probabilmente il meglio che posso sperare per senza ottenere troppo esotici. Dopo aver detto che, se il codice diventa un collo di bottiglia, ottenere esotici come necessario.
Guardando nuovamente la versione F #, ciò che realmente sto esaminando nel secondo modello è gli importi della transazione locale e remoto, come illustrato nel Figura 4. Quindi estrarre quei valori pure e iniziare il confronto.
Figura 4 la F # versione esamina la quantità di locali e Remote
float locAmt = loc.Amount;
float remAmt = rem.Amount;
if (locAmt == remAmt)
{
Output.Add(new RegEntry() { Local = loc, Remote = rem });
return ReconcileInternal(Output, locTail, remTail);
}
else if (locAmt < remAmt)
{
Output.Add(new MissingRemote() { Transaction = loc });
return ReconcileInternal(Output, locTail, remote);
}
else if (locAmt > remAmt)
{
Output.Add(new MissingLocal() { Transaction = rem });
return ReconcileInternal(Output, local, remTail);
}
else
throw new Exception("How is this possible?");
}
Ogni ramo dell'albero è abbastanza facile da capire a questo punto. Ho aggiunto il nuovo elemento all'elenco di Output, poi recurse con elementi non trasformati delle liste locali e remoti.
Il confezionamento
Se è veramente questo elegante la soluzione C#, perché preoccuparsi di prendere alla fermata attraverso F # in primo luogo? È difficile da spiegare se non si passa attraverso lo stesso processo. Essenzialmente, mi serviva la fermata attraverso F # per rimpolpare l'algoritmo in primo luogo. Il mio primo tentativo in questo è stato un disastro assoluto. Ho iniziato scorrendo le due liste utilizzando il doppio "foreach" loop. Stavo cercando di tenere traccia dello stato lungo la strada, e ho finito con un pasticcio enorme, fumante, che non sarebbe mai stato in grado di eseguire il debug in 1 milione di anni.
Imparare a "pensare diversamente" (prendere in prestito un famoso computer marketing linea società da alcuni decenni fa) produce risultati, non la scelta del linguaggio stesso. Potrei aver facilmente detto questa storia attraversando la Scala, Haskell o Clojure. Il punto non è stato il set di funzionalità del linguaggio, ma i concetti di base lingue più funzionale — ricorsione, in particolare. Questo è quello che ha aiutato a superare l'impasse mentale.
Questa è parte della ragione gli sviluppatori dovrebbero imparare un nuovo linguaggio di programmazione di ogni anno, come suggerito da uno dei programmatori pragmatico, Dave Thomas, della fama di rubino. La mente non può aiutare ma essere esposta a nuove idee e nuove opzioni. Simili tipi di idee emergono quando un programmatore trascorre qualche tempo con schema, Lisp o con un linguaggio basato su stack come Forth — o con un linguaggio basato su prototipo come Io.
Se vuoi un leggero introduzione a un numero di lingue differenti, tutti fuori la piattaforma Microsoft .NET Framework, raccomando caldamente il libro di Bruce Tate, "Sette lingue in sette settimane" (pragmatico Bookshelf, 2010). Alcuni di loro non sarebbe utilizzare direttamente sulla piattaforma .NET. Poi di nuovo, a volte la vittoria è in quanto pensiamo che il problema e inquadrare la soluzione, il codice non necessariamente riutilizzabile. Codificazione felice!
Ted Neward è CTO di iTrellis, una società di consulenza servizi. Ha scritto più di 100 articoli e autore e coautore di una dozzina di libri, tra cui "Professional F # 2.0" (Wrox, 2010). Egli è un MVP di c# e parla a conferenze in tutto il mondo. Egli consulta e mentors regolarmente — contattarlo al ted@tedneward.com o ted@itrellis.com se siete interessati ad avere lui venire a lavorare con il vostro team, e leggere il suo blog a blogs.tedneward.com.
Grazie al seguente Microsoft esperto tecnico per la revisione di questo articolo: Lincoln Atkinson