2009-08-14 6 views
5

Disons que j'ai le code suivant:Les méthodes anonymes, la portée et la sérialisation

public class Foo 
{ 
    private int x; 
    private int y; 

    public Bar CreateBar() 
    { 
     return new Bar(x,() => y); 
    } 
} 

[Serializable] 
public class Bar 
{ 
    private int a; 
    private Func<int> b; 

    public Bar(int a, Func<int> b) 
    { 
     this.a = a; 
     this.b = b; 
    } 
} 

Que se passe avec la portée des objets et des valeurs dans ce scénario? Puisque x est un type de valeur, il est passé à Bar par valeur, et par conséquent, rien ne doit arriver à sa portée. Mais qu'arrive-t-il à y? La valeur de y doit rester affichée quand b est réellement évalué. Est-ce que tout Foo est resté là pour l'évaluer plus tard? Je peux seulement supposer que Foo n'est pas GC'ed.

Maintenant, disons que nous sérialisons Bar sur disque, puis désérialisons-le plus tard. Qu'est-ce qui a réellement été sérialisé? A-t-il aussi sériel Foo? Quelle magie a continué pour que b puisse être évalué après la désérialisation de Bar? Pouvez-vous expliquer ce qui se passe dans l'IL?

Répondre

5

Mise à jour: pour voir ce qui se passe réellement sans avoir à recourir à l'IL: Using reflector to understand anonymous methods and captured variables


Lorsque vous utilisez:

public Bar CreateBar() 
{ 
    return new Bar(x,() => y); 
} 

Vous implicitement this.y qui signifie; donc en termes de délégué, c'est la référence à Foo qui est inclus. En tant que tel, l'instance de Bar (via le délégué) conserve l'intégralité de Foo en vie (non récupérée) jusqu'à ce que le Bar soit disponible pour la collecte.

En particulier, il n'est pas nécessaire (dans ce cas) que le compilateur génère une classe supplémentaire pour gérer les variables capturées; la seule chose requise est l'instance Foo, donc une méthode peut être générée sur Foo. Cela serait plus complexe si le délégué impliquait des variables locales (autres que this).

En termes de sérialisation ... eh bien, la première chose que je dirais, c'est que sérialiser les délégués est une très très mauvaise idée. Cependant, BinaryFormattersera délégués à pied, et vous pouvez (en théorie) finissent avec une sérialisé Bar, un sérialisé Foo, et un délégué sérialisé pour les relier - mais seulement si vous marquez Foo comme [Serializable]. Mais je souligne - c'est mauvaise idée. J'utilise rarement BinaryFormatter (pour une variété de raisons), mais une question commune que je vois par les gens qui l'utilisent est "pourquoi essaie-t-il de sérialiser (un certain type aléatoire)". Habituellement, la réponse est "vous publiez un événement, et il essaie de sérialiser l'abonné", auquel cas le correctif le plus courant serait de marquer le champ de l'événement comme [NonSerialized].


Plutôt que de regarder IL; une autre façon d'étudier ceci est d'utiliser un réflecteur en mode .NET 1.0 (c'est-à-dire sans échange de données dans des méthodes anonymes); alors vous pouvez voir:

public Bar CreateBar() 
{ 
    return new Bar(this.x, new Func<int>(this.<CreateBar>b__0)); 
} 
[CompilerGenerated] 
private int <CreateBar>b__0() 
{ 
    return this.y; 
} 

Comme vous pouvez le voir; la chose transmise à Bar est un délégué à une méthode cachée (appelée <CreateBar>b__0()) sur l'instance en cours (this). Donc, il est l'instance à l'actuelle Foo qui est transmise au Bar.

+0

Dans la section 6.5.3 du langage de programmation C# (3ème édition), il y a un exemple très similaire à ce cas et il est manipulé comme Marc l'a expliqué par une méthode d'instance générée par le compilateur sur Foo. –

+0

Réponse fantastique Marc! Merci! –

0

Créez un projet de test rapide pour afficher les valeurs, puis regardez-les. Il devrait répondre aux questions, et probablement vous faire apprendre quelque chose de plus dans le processus. (C'est ce que la plupart des gens qui répondront à votre question ont fait.)

+0

Je suis également curieux au sujet de l'IL et de sa théorie. J'ai écrit des tests pour répondre à certaines de mes questions, mais j'aimerais mieux comprendre ce qui se passe réellement. Je pensais que certaines personnes super intelligentes sur SO pourraient jeter un peu plus de lumière. –

+0

Vous pourriez utiliser Reflector pour voir l'IL produit par votre projet de test, mais bien sûr les commentaires d'un expert seraient plus immédiatement compréhensibles. –

1

J'ai eu une erreur en essayant de sérialiser quand il reflétait l'objet à sérialiser.

mon exemple:

[Serializable] 
    public class SerializeTest 
    { 
     //public SerializeTest(int a, Func<int> b) 
     //{ 
     // this.a = a; 
     // this.b = b; 
     //} 

     public SerializeTest() 
     { 

     } 

     public int A 
     { 
      get 
      { 
       return a; 
      } 

      set 
      { 
       a = value; 
      } 
     } 
     public Func<int> B 
     { 
      get 
      { 
       return b; 
      } 
      set 
      { 
       b = value; 
      } 
     } 


     #region properties 

     private int a; 
     private Func<int> b; 



     #endregion 

     //serialize itself 
     public string Serialize() 
     { 
      MemoryStream memoryStream = new MemoryStream(); 

      XmlSerializer xs = new XmlSerializer(typeof(SerializeTest)); 
      using (StreamWriter xmlTextWriter = new StreamWriter(memoryStream)) 
      { 
       xs.Serialize(xmlTextWriter, this); 
       xmlTextWriter.Flush(); 
       //xmlTextWriter.Close(); 
       memoryStream = (MemoryStream)xmlTextWriter.BaseStream; 
       memoryStream.Seek(0, SeekOrigin.Begin); 
       StreamReader reader = new StreamReader(memoryStream); 

       return reader.ReadToEnd(); 
      } 
     } 

     //deserialize into itself 
     public void Deserialize(string xmlString) 
     { 
      String XmlizedString = null; 

      using (MemoryStream memoryStream = new MemoryStream()) 
      { 
       using (StreamWriter w = new StreamWriter(memoryStream)) 
       { 
        w.Write(xmlString); 
        w.Flush(); 

        XmlSerializer xs = new XmlSerializer(typeof(SerializeTest)); 
        memoryStream.Seek(0, SeekOrigin.Begin); 
        XmlReader reader = XmlReader.Create(memoryStream); 

        SerializeTest currentConfig = (SerializeTest)xs.Deserialize(reader); 

        this.a = currentConfig.a; 
        this.b = currentConfig.b; 

        w.Close(); 
       } 
      } 
     } 

    } 

class Program 
    { 
     static void Main(string[] args) 
     { 

      SerializeTest test = new SerializeTest() { A = 5, B =()=>67}; 
      string serializedString = test.Serialize(); 


} 
} 

Voici un lien pour le faire ... peu plus complexe: Serializing Anon Delegates

+0

Vous avez raison, dans mon exemple, Foo doit également être marqué comme Serializable. Donc, cela signifie qu'il est en série tout Foo. –

+0

@Stefan: non, aucune de vos classes n'a besoin de l'attribut Serializable (du moins pour la sérialisation XML). Cet attribut est pour la sérialisation avec les formateurs (BinaryFormatter, SoapFormatter ...) –

+0

@ Thomas Right, j'utilise le BinaryFormatter. –

0

Je pense que x et y dans l'objet Foo seront capturés comme les types de valeur. La fermeture créée pour cette expression lambda ne doit donc pas conserver de référence à l'objet Foo. Ainsi, le compilateur peut créer une classe pour cette fermeture comme:

internal class CompilerGeneratedClassName 
{ 
    private int x; 
    private int y; 
    public CompilerGeneratedClassName(int x, int y) 
    { 
    this.x = x; 
    this.y = y; 
    } 

    public int CompilerGeneratedMethodName() 
    { 
    return this.y; 
    }  
} 

et

return new Bar(x,() => y); 

peut être remplacé par

return new Bar(x,new CompilerGeneratedClassName(x,y).CompilerGeneratedMethodName); 

Je ne pense pas que qu'il y ait référence à l'objet Foo à la suite de cette fermeture. Donc l'objet Foo pourrait être GCed. Je peux me tromper. Une chose que vous pouvez faire est d'écrire un petit programme, de le compiler et d'inspecter l'IL généré dans l'outil ILDASM.

+0

J'ai inspecté l'IL, mais je ne suis pas très familier avec IL et ne peux pas comprendre quelque chose de spécial que le compilateur a fait. –

+1

Le compilateur n'a pas besoin d'une classe de fermeture pour cela; c'est un champ, pas une variable de méthode locale. C'est le 'this' qui est passé. Voir ma réponse pour plus. –

Questions connexes