juillet
2010
Pour ce premier article de blog sur developpez.com, j’ai décidé de m’attaquer à un problème qui m’a tenu en haleine une bonne heure : l’instanciation d’une Func via une lambda expression dans un Foreach.
Soit le code ci-dessous :
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.Reflection;
6:
7: namespace FunctionalForeach
8: {
9: public class Program
10: {
11: private static Dictionary<string, Func<string>> methods;
12:
13: public static void Main(string[] args)
14: {
15: methods = new Dictionary<string, Func<string>>();
16: Type type = typeof(Program);
17: foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public).Where(m => m.ReturnType == typeof(string)))
18: {
19: methods.Add(method.Name, () => (string)method.Invoke(null, null));
20: }
21:
22: Func<string> getPseudo = methods["GetPseudo"];
23: Console.WriteLine(string.Format("Pseudo : {0}", getPseudo()));
24: Func<string> getWebsite = methods["GetWebsite"];
25: Console.WriteLine(string.Format("WebSite : {0}", getWebsite()));
26: }
27:
28: public static string GetPseudo() { return "Mattk"; }
29:
30: public static string GetWebsite() { return "http://mattk.developpez.com"; }
31: }
32: }
A première vue, le résultat auquel on peut s’attendre serait le suivant :
Pseudo : Mattk
WebSite : http://mattk.developpez.com
Malheureusement ce ne sera pas le cas. L’exécution affichera ceci :
Pseudo: http://mattk.developpez.com
WebSite : http://mattk.developpez.com
Pour comprendre un peu ce qui se passe, regardons l’IL de la fonction Main :
Variables locales :
1: .locals init ([0] class [mscorlib]System.Type 'type',
2: [1] class [mscorlib]System.Func`1<string> 'CS$<>9__CachedAnonymousMethodDelegate3',
3: [2] class FunctionalForeach.Program/'<>c__DisplayClass4' 'CS$<>8__locals5',
4: [3] class [mscorlib]System.Func`1<string> getPseudo,
5: [4] class [mscorlib]System.Func`1<string> getWebsite,
6: [5] class [mscorlib]System.Collections.Generic.IEnumerator`1<class [mscorlib]System.Reflection.MethodInfo> CS$5$0000,
7: [6] bool CS$4$0001)
Corps du Foreach :
1: IL_0057: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<class [mscorlib]System.Reflection.MethodInfo>::get_Current()
2: IL_005c: stfld class [mscorlib]System.Reflection.MethodInfo FunctionalForeach.Program/'<>c__DisplayClass4'::'method'
3: IL_0061: nop
4: IL_0062: ldsfld class [mscorlib]System.Collections.Generic.Dictionary`2<string,class [mscorlib]System.Func`1<string>> FunctionalForeach.Program::methods
5: IL_0067: ldloc.2
6: IL_0068: ldfld class [mscorlib]System.Reflection.MethodInfo FunctionalForeach.Program/'<>c__DisplayClass4'::'method'
7: IL_006d: callvirt instance string [mscorlib]System.Reflection.MemberInfo::get_Name()
8: IL_0072: ldloc.1
9: IL_0073: brtrue.s IL_0084
10: IL_0075: ldloc.2
11: IL_0076: ldftn instance string FunctionalForeach.Program/'<>c__DisplayClass4'::'<Main>b__1'()
12: IL_007c: newobj instance void class [mscorlib]System.Func`1<string>::.ctor(object,
13: native int)
14: IL_0081: stloc.1
15: IL_0082: br.s IL_0084
16: IL_0084: ldloc.1
17: IL_0085: callvirt instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string,class [mscorlib]System.Func`1<string>>::Add(!0,
18: !1)
19: IL_008a: nop
20: IL_008b: nop
21: IL_008c: ldloc.s CS$5$0000
22: IL_008e: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
Une classe a été générée pour notre Lambda Expression, et une seule et unique variable locale (la [1]) a été déclarée (ligne 2 des variables locales).
Dans le Foreach, on peut constater qu’à chaque nouvelle instanciation d’une Func<string>, on a un stloc.1 (ligne 14) soit une assignation de la variable locale 1. Ainsi, à chaque iteration, on écrase la variable locale tout en conservant sa référence. Au final, on insére donc toujours le même objet dans notre dictionnaire methods, mais dont la valeur change à chaque itération. Ce qui provoque le comportement indésiré.
Enfin, une lambda expression entre dans le cadre de la programmation fonctionnelle. Une des règles à respecter sur une fonction (et donc une lambda) est la closure : une fonction possède son propre contexte d’exécution. L’exemple ci-dessus viole cette règle puisqu’on change le contexte (le MethodInfo passée dans la lambda) à chaque itération.
Voilà