Ieri stavo debuggando un componente per risolvere una serie di bugs, quando, dopo un buon numero di F11 arrivo a un metodo che prende in input una collection e restituisce una collection dello stesso tipo. La signature è più o meno come questa:
private static List<int> ApplyCustomFilter1(List<int> ints)
E questo assomiglia al codice che lo usa:
List<int> ints = new List<int> { 0, 1, 2, 3 };
var filteredInts = ApplyCustomFilter1(ints);
// altro codice che lavora su ints e filteredInts
Ok, quelle tre righe mi hanno fatto perdere mezz'ora :(
Il problema è che non posso assumere nulla sulla lista ‘ints’ dopo che il metodo ‘ApplyCustomFilter1’ è stato eseguito. Quale di questi assert sarà soddisfatto?
Debug.Assert(object.ReferenceEquals(ints, filteredInts));
Debug.Assert(!object.ReferenceEquals(ints, filteredInts));
Debug.Assert(ints.Count == 4);
Debug.Assert(ints[0] == 0 && ints[1] == 1 && ints[2] == 2 && ints[3] == 3);
Non lo so! E non lo posso sapere senza andare a vedere la sua implementazione interna, o, per essere più precisi: guardando l’implementazione, posso capire cosa succede in determinate circostanze, ma non sono sicuro di poter garantire il determinismo degli asserts.
E se il metodo fosse stato un po’ più esplicito:
private static void ApplyCustomFilter2(List<int> ints)
Mmm, in questo modo non mi devo porre il problema di una seconda lista. Sono quasi sicuro che il metodo modificherà l’input. Per essere sicuro al 100% avrei potuto usare ref:
private static void ApplyCustomFilter3(ref List<int> ints)
Ok, “ApplyCustomFilter3” è più esplicito sulle sue intenzioni e su come le esegue, mi garantisce che le modifiche verranno fatte sulla mia lista in input. Potrei accontentarmi, ma oltre a non essere un gran fan di ref/out, preferirei che il filtering sulla lista venisse applicato senza side effects, cioè senza modificare nessuno degli input parameters.
Se provo a seguire una delle regole della programmazione funzionale, e cioè che una funzione non può avere side effects e deve sempre ritornare un risultato, allora:
private static List<int> ApplyCustomFilter4(IEnumerable<int> ints)
A questo punto posso assumere qualcosa:
ints = new List<int> { 0, 1, 2, 3 };
newFilteredInts = ApplyCustomFilter4(ints);
Debug.Assert(!object.ReferenceEquals(ints, newFilteredInts));
Debug.Assert(ints.Count == 4);
Debug.Assert(ints[0] == 0 && ints[1] == 1 && ints[2] == 2 && ints[3] == 3);
’ApplyCustomFilter4’ è esplicita sulle intenzioni e mi garantisce che l’input non verrà modificato.
In realtà la garanzia sul fatto che l’input non venga modificato è vera in questo particolare esempio dove l’input è IEnumerable<T> con T che è un value type (quindi immutabile). Se T fosse un reference type, quindi mutable, avrei solo la garanzia che la sequenza non possa essere modificata, ma non potrei assumere nulla sul valore degli oggetti T contenuti (vedi immutable facades). In altre parole l’ultimo Assert non è garantito se T è un reference type.