VB.NET e C# - Risposta ad un vecchio quiz e rilancio
Un paio di giorni fa Adrian Florea ha scritto un post (peraltro davvero interessante) che rimanda ad un quiz che avevo scritto parecchi mesi fa.
La cosa curiosa è che ho letto il post di Adrian e non mi ricordavo più il mio :-(
E quella ancora più curiosa (sostituite pure curiosa con "preoccupante") è che andando a rileggere il mio quiz non mi ricordavo la soluzione :-(((
Che poi mi è venuta in mente e scrivo ora, in *lievissimo* ritardo. Però è abbastanza interessante lo stesso.
Recall
Riassumendo la questione e riformulandola un minimo, il succo è:
La keyword C# foreach e la keyword VB.NET For Each fanno proprio la stessa identica cosa ?
In entrambi i casi si tratta di abbreviazioni sintattiche che consentono l'iterazione su oggetti IEnumerable, il tutto contornato da un bel try...finally con tanto di Dispose dell'oggetto IEnumerator utilizzato durante il ciclo.
Il che porta a dimenticare un piccolo (ma evidentemente non trascurabile) particolare.
Pensate alla sintassi (VB.NET):
For Each item As String In list
System.Console.WriteLine(item)
Next
o all'analogo in C#:
foreach (string item in list)
{
System.Console.WriteLine(item);
}
Il particolare è la conversione di tipo.
Soluzione
Dato list come oggetto di tipo IEnumerable, il costrutto foreach si traduce (anche) nell'invocazione di get_Current su tale istanza (accessor della property Current).
get_Current ritorna Object. E le istruzioni di cui sopra segnalano implicitamente al compilatore la necessità di conversione da Object a System.String.
Qui i due compilatori divergono. E si fa presto a vederlo (non posto tutto il codice ma solo le parti interessate).
Questo il codice IL generato da csc.exe (csc.exe /out:Quiz.exe /r:Componente.dll):
IL_0011: ldloc.2
IL_0012: callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current()
IL_0017: castclass [mscorlib]System.String
IL_001c: stloc.1
Questo invece il codice IL generato da vbc.exe (vbc /out:Quiz.exe /r:Componente.dll)
IL_000f: ldloc.2
IL_0010: callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current()
IL_0015: call string [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Conversions::ToString(object)
IL_001a: stloc.1
Dopo l'invocazione di get_Current, il compilatore C# emette l'istruzione IL castclass (che effettua un cast, generando una InvalidCastException in caso di errore).
Al contrario, il compilatore VB.NET si affida alla classe Microsoft.VisualBasic.CompilerServices.Conversions.
Un po' di ILDASM ?
Ecco la parte interessante di Conversions::ToString(object):
// Se mi passano null, torno null.
IL_0000: ldarg.0
IL_0001: brtrue.s IL_0005
IL_0003: ldnull
IL_0004: ret
// Se mi passano una stringa, torno quella stringa.
IL_0005: ldarg.0
IL_0006: isinst [mscorlib]System.String
IL_000b: stloc.3
IL_000c: ldloc.3
IL_000d: brfalse.s IL_0011
IL_000f: ldloc.3
IL_0010: ret
// Se mi passano un IConvertible, lo converto :-)
IL_0011: ldarg.0
IL_0012: isinst [mscorlib]System.IConvertible
IL_0017: stloc.1
IL_0018: ldloc.1
IL_0019: brfalse IL_0132
IL_001e: ldloc.1
IL_001f: callvirt instance valuetype [mscorlib]System.TypeCode [mscorlib]System.IConvertible::GetTypeCode()
... [CUT: mega switch su typecode. Qui sotto un esempio con parametro Int32] ...
IL_00b3: ldloc.1
IL_00b4: ldnull
IL_00b5: callvirt instance int32 [mscorlib]System.IConvertible::ToInt32(class [mscorlib]System.IFormatProvider)
IL_00ba: call string Microsoft.VisualBasic.CompilerServices.Conversions::ToString(int32)
IL_00bf: ret
... [e se non va bene ?]
// Ci ho provato, più di tanto ... throw InvalidCastException :-(
IL_016d: newobj instance void [mscorlib]System.InvalidCastException::.ctor(string)
IL_0172: throw
Quindi, ufficialmente, la risposta al QUIZ è:
Il metodo GetList deve ritornare un' ArrayList contenente almeno un oggetto non string e di tipo IConvertible.
Perchè:
- Se l'oggetto è null, in entrambi i casi (C# e VB) viene richiamato Console.WriteLine(null)
- Se l'oggetto è String, in entrambi i casi viene richiamato Console.WriteLine(String)
- Se l'oggetto non è nè null, nè String, nè IConvertible, in entrambi i casi viene generata un'eccezione di tipo InvalidCastException (anche se in punti diversi !!!)
In altre parole, questo è uno snippet di codice che risponde al quiz in questione (scritto in C# ma qui è indifferente)
//
// csc.exe /out:Componente.dll /t:library
//
using System;
using System.Collections;
public class Libreria
{
public static ArrayList GetList()
{
ArrayList al = new ArrayList();
al.Add("Ciao");
al.Add(42);
return al;
}
}
Considerazioni
Personalmente trovo l'operato del compilatore C# più intuitivo.
In realtà penso la stessa cosa più o meno nel 90% dei casi in cui vengono invocate le funzionalità esposte da Microsoft.VisualBasic.dll :-)
E' altrettanto vero che non ho un background VB, quindi non sono abituato ai comportamenti del linguaggio VB a proposito di conversioni di tipo e via dicendo.
Sapete quante volte ho preso delle facciate contro ******* perchè credevo che facesse un semplicissimo cast ?
Ah ... ci ho messo le stelline perchè il rilancio di cui nel titolo è il seguente:
Contro quale keyword ho tirato mille imprecazioni dopo aver capito cosa faceva internamente ? E quale keyword uso invece di solito ?
Oppure, seriamente:
Quale keyword VB.NET genera codice di conversione analogo a quello visto durante questo lungo post ? E quale invece genera codice analogo a quello che genera il compilatore C# ?