2009-07-15 14 views

risposta

30

Questa è una cosa piuttosto rischiosa da fare.

Mentre è vero che è possibile serializzare e deserializzare un delegato come qualsiasi altro oggetto, il delegato è un puntatore a un metodo all'interno del programma che lo serializza. Se deserializzi l'oggetto in un altro programma, riceverai un SerializationException - se sei fortunato.

Per esempio, cerchiamo di modificare il programma di Darin un po ':

class Program 
{ 
    [Serializable] 
    public class Foo 
    { 
     public Func<string> Del; 
    } 

    static void Main(string[] args) 
    { 
     Func<string> a = (() => "a"); 
     Func<string> b = (() => "b"); 

     Foo foo = new Foo(); 
     foo.Del = a; 

     WriteFoo(foo); 

     Foo bar = ReadFoo(); 
     Console.WriteLine(bar.Del()); 

     Console.ReadKey(); 
    } 

    public static void WriteFoo(Foo foo) 
    { 
     BinaryFormatter formatter = new BinaryFormatter(); 
     using (var stream = new FileStream("test.bin", FileMode.Create, FileAccess.Write, FileShare.None)) 
     { 
      formatter.Serialize(stream, foo); 
     } 
    } 

    public static Foo ReadFoo() 
    { 
     Foo foo; 
     BinaryFormatter formatter = new BinaryFormatter(); 
     using (var stream = new FileStream("test.bin", FileMode.Open, FileAccess.Read, FileShare.Read)) 
     { 
      foo = (Foo)formatter.Deserialize(stream); 
     } 

     return foo; 
    } 
} 

Run, e vedrete che si crea l'oggetto, serializza esso, deserializza in un nuovo oggetto, e quando si chiama Del sul nuovo oggetto restituisce "a". Eccellente. Ok, ora commenta la chiamata a WriteFoo, in modo che il programma stia semplicemente deserializzando l'oggetto. Esegui nuovamente il programma e ottieni lo stesso risultato.

Ora scambiare la dichiarazione di aeb ed eseguire il programma. Yikes. Ora l'oggetto deserializzato sta restituendo "b".

Questo sta accadendo perché ciò che viene effettivamente serializzato è il nome che il compilatore sta assegnando all'espressione lambda. E il compilatore assegna i nomi alle espressioni lambda nell'ordine in cui li trova.

E questo è ciò che è rischioso: non si sta serializzando il delegato, si sta serializzando un simbolo. È il valore del simbolo e non quello che rappresenta il simbolo, che viene serializzato. Il comportamento dell'oggetto deserializzato dipende da cosa il valore di quel simbolo rappresenta nel programma che lo sta deserializzando.

In una certa misura, questo è vero con tutte le serializzazioni. Deserializza un oggetto in un programma che implementa la classe dell'oggetto in modo diverso rispetto al programma di serializzazione, e inizia il divertimento. Ma la serializzazione dei delegati accoppia l'oggetto serializzato alla tabella dei simboli del programma che lo serializzava, non all'implementazione della classe dell'oggetto.

Se fossi in me, considererei esplicito questo accoppiamento. Vorrei creare una proprietà statica di che era un Dictionary<string, Func<string>>, popolare con chiavi e funzioni e memorizzare la chiave in ogni istanza anziché nella funzione. Ciò rende il programma di deserializzazione responsabile della compilazione del dizionario prima che inizi la deserializzazione degli oggetti . In una certa misura, questa è esattamente la stessa cosa che sta usando il serializzatore BinaryFormatter; la differenza è che questo approccio rende molto più evidente la responsabilità del programma di deserializzazione per l'assegnazione di funzioni ai simboli.

+2

Alla fine ho deciso di non salvare i delegati nei file Salvare i delegati nei file porta a un altro problema: Diverse copie di una stessa funzione da memorizzare su un file. Piuttosto, come dice Robert, ritengo sia meglio definire un array di delegati e memorizzare l'indice di ciascun delegato nel file. –

+0

+1 questa risposta mi ha risparmiato un sacco di dolore nel trattare un tale errore. Un altro motivo per cui il delegato può cambiare è quando si utilizza una versione del compilatore diversa: http://stackoverflow.com/a/40780504/66372 – eglasius

15

In realtà è possibile con BinaryFormatter poiché conserva le informazioni sul tipo. Ed ecco la prova:

class Program 
{ 
    [Serializable] 
    public class Foo 
    { 
     public Func<string> Del; 
    } 

    static void Main(string[] args) 
    { 
     Foo foo = new Foo(); 
     foo.Del = Test; 
     BinaryFormatter formatter = new BinaryFormatter(); 
     using (var stream = new FileStream("test.bin", FileMode.Create, FileAccess.Write, FileShare.None)) 
     { 
      formatter.Serialize(stream, foo); 
     } 

     using (var stream = new FileStream("test.bin", FileMode.Open, FileAccess.Read, FileShare.Read)) 
     { 
      foo = (Foo)formatter.Deserialize(stream); 
      Console.WriteLine(foo.Del()); 
     } 
    } 

    public static string Test() 
    { 
     return "test"; 
    } 

} 

Una cosa importante che si dovrebbe essere a conoscenza se si decide di utilizzare BinaryFormatter è che il suo formato non è ben documentato e l'attuazione potrebbe avere rottura cambiamenti tra le versioni .NET e/o CLR.

+1

Sei sicuro che funzioni quando il delegato fa riferimento a un metodo non statico? Posso vederlo lavorare con metodi statici poiché non è necessario definire Traget, ma ad esempio i metodi che cosa fa? Potenzialmente, potrebbe serializzare il grafico dell'istanza di Target (supponendo che sia serializzabile), ma poi quando deserializzato e invocato si troverebbe su un'istanza diversa con dati potenzialmente obsoleti. Personalmente sarei molto attento a scegliere di mantenere i delegati in questo modo in quanto potrebbe facilmente portare a comportamenti inattesi e difficili da correggere/correggere. – LBushkin

+1

Funziona anche con metodi non statici. Serializza anche il grafico dell'istanza di Target assumendo che sia serializzabile (Contrassegnato con SerializableAttribute). –

2

Un delegato è un puntatore del metodo, potrei fraintendere quando dici save, ma la posizione aggiunta al delegato in fase di runtime potrebbe non esistere più se si tenta di salvare e ripristinare l'indirizzo.

+0

Grazie Quintin. Hai ragione, come puntatore non possiamo. Ma per quanto riguarda i loro contenuti? qualsiasi cosa come l'operatore C++ *. –

+4

Se si utilizza un serializzatore binario, il delegato verrà serializzato, così come l'intero grafico dell'oggetto a cui si riferisce. Ciò garantisce che il delegato possa essere invocato dopo la deserializzazione. –

1

Quindi, è a mia conoscenza che si desidera "salvare" un puntatore a funzione (delegato). Ora, se si mettono tutte le funzioni delegate in una libreria, è possibile utilizzare il reflection del sistema per creare il collegamento in fase di esecuzione e quindi avere la possibilità di eseguire il cast del delegato su un delegato definito dal compilatore (che, di nuovo, si troverà nella libreria). L'unica rovina di questo è che il metodo di destinazione deve essere una posizione ben definita, quindi nessun metodo anonimo poiché lì la posizione è definita in fase di compilazione ogni volta che si compila. Ecco il codice che ho elaborato per essere in grado di ricreare un delegato in fase di runtime, utilizzare a proprio rischio e non documentato con commenti.

Aggiornamento: Un'altra cosa che si potrebbe fare è creare un attributo personalizzato e applicarlo a tutti i metodi che si desidera creare in un delegato. Durante il runtime, utilizzando il sistema reflect, attraversa i tipi esportati trovati e quindi seleziona tutti i metodi da quei tipi che hanno l'attributo personalizzato. Potrebbe essere più di ciò che volevi e sarebbe utile solo se fornissi anche un valore 'ID', quindi c'era un modo logico di collegare l'id al delegato desiderato tramite una tabella di ricerca principale.

Ho anche notato il commento che avevate rinunciato a questo approccio a causa del fattore di rischio, lascerò questo qui per fornire un altro modo di fare le cose.

using System; 
    using System.Collections.Generic; 
    using System.Linq; 
    using System.Text; 
    using System.Runtime.Serialization; 
    using System.Reflection; 

    namespace RD.Runtime 
    { 
     [Serializable] 
     public struct RuntimeDelegate 
     { 
      private static class RuntimeDelegateUtility 
      { 
       public static BindingFlags GetSuggestedBindingsForMethod(MethodInfo method) 
       { 
        BindingFlags SuggestedBinding = BindingFlags.Default; 

        if (method.IsStatic) 
         SuggestedBinding |= BindingFlags.Static; 
        else 
         SuggestedBinding |= BindingFlags.Instance; 

        if (method.IsPublic) 
         SuggestedBinding |= BindingFlags.Public; 
        else 
         SuggestedBinding |= BindingFlags.NonPublic; 

        return SuggestedBinding; 
       } 

       public static Delegate Create(RuntimeDelegate link, Object linkObject) 
       { 
        AssemblyName ObjectAssemblyName = null; 
        AssemblyName DelegateAssemblyName = null; 
        Assembly ObjectAssembly = null; 
        Assembly DelegateAssembly = null; 
        Type ObjectType = null; 
        Type DelegateType = null; 
        MethodInfo TargetMethodInformation = null; 

        #region Get Assembly Names 
        ObjectAssemblyName = GetAssemblyName(link.ObjectSource); 
        DelegateAssemblyName = GetAssemblyName(link.DelegateSource); 
        #endregion 
        #region Load Assemblys 
        ObjectAssembly = LoadAssembly(ObjectAssemblyName); 
        DelegateAssembly = LoadAssembly(DelegateAssemblyName); 
        #endregion 
        #region Get Object Types 
        ObjectType = GetTypeFromAssembly(link.ObjectFullName, ObjectAssembly); 
        DelegateType = GetTypeFromAssembly(link.DelegateFullName, DelegateAssembly); 
        #endregion 
        #region Get Method 
        TargetMethodInformation = ObjectType.GetMethod(link.ObjectMethodName, link.SuggestedBinding); 
        #endregion 

        #region Create Delegate 
        return CreateDelegateFrom(linkObject, ObjectType, DelegateType, TargetMethodInformation); 
        #endregion 
       } 

       private static AssemblyName GetAssemblyName(string source) 
       { 
        return GetAssemblyName(source, source.ToUpper().EndsWith(".DLL") || source.ToUpper().EndsWith(".EXE")); 
       } 
       private static AssemblyName GetAssemblyName(string source, bool isFile) 
       { 
        AssemblyName asmName = null; 

        try 
        { 
         if (isFile) 
          asmName = GetAssemblyNameFromFile(source); 
         else 
          asmName = GetAssemblyNameFromQualifiedName(source); 
        } 
        catch (Exception err) 
        { 
         string ErrorFormatString = "Invalid Call to utility method 'GetAssemblyNameOrThrowException'\n" + 
                "Arguments passed in:\n" + 
                "=> Source:\n[{0}]\n" + 
                "=> isFile = {1}\n" + 
                "See inner exception(s) for more detail."; 
         throw new InvalidOperationException(string.Format(ErrorFormatString, source, isFile), err); 
        } 

        if (asmName == null) 
         throw new InvalidOperationException(asmName.Name + " Assembly Name object is null, but no other error was encountered!"); 

        return asmName; 
       } 
       private static AssemblyName GetAssemblyNameFromFile(string file) 
       { 
        #region Validate parameters 
        if (string.IsNullOrWhiteSpace(file)) 
         throw new ArgumentNullException("file", "given a null or empty string for a file name and path"); 
        if (!System.IO.File.Exists(file)) 
         throw new ArgumentException("File does not exsits", "file"); 
        #endregion 

        AssemblyName AssemblyNameFromFile = null; 

        try 
        { 
         AssemblyNameFromFile = AssemblyName.GetAssemblyName(file); 
        } 
        catch (Exception err) 
        { 
         throw err; 
        } 

        return AssemblyNameFromFile; 
       } 
       private static AssemblyName GetAssemblyNameFromQualifiedName(string qualifiedAssemblyName) 
       { 
        #region Validate parameters 
        if (string.IsNullOrWhiteSpace(qualifiedAssemblyName)) 
         throw new ArgumentNullException("qualifiedAssemblyName", "given a null or empty string for a qualified assembly name"); 
        #endregion 

        AssemblyName AssemblyNameFromQualifiedAssemblyName = null; 

        try 
        { 
         AssemblyNameFromQualifiedAssemblyName = new AssemblyName(qualifiedAssemblyName); 
        } 
        catch (Exception err) 
        { 
         throw err; 
        } 

        return AssemblyNameFromQualifiedAssemblyName; 
       } 

       private static Assembly LoadAssembly(AssemblyName assemblyName) 
       { 
        Assembly asm = LoadAssemblyIntoCurrentAppDomain(assemblyName); 
        if (asm == null) 
         throw new InvalidOperationException(assemblyName.Name + " Assembly is null after loading but no other error was encountered!"); 

        return asm; 
       } 
       private static Assembly LoadAssemblyIntoCurrentAppDomain(AssemblyName assemblyName) 
       { 
        #region Validation 
        if (assemblyName == null) 
         throw new ArgumentNullException("assemblyName", "Assembly name is null, must be valid Assembly Name Object"); 
        #endregion 

        return LoadAssemblyIntoAppDomain(assemblyName, AppDomain.CurrentDomain); 
       } 
       private static Assembly LoadAssemblyIntoAppDomain(AssemblyName assemblyName, AppDomain appDomain) 
       { 
        #region Validation 
        if (assemblyName == null) 
         throw new ArgumentNullException("assemblyName", "Assembly name is null, must be valid Assembly Name Object"); 
        if (appDomain == null) 
         throw new ArgumentNullException("appDomain", "Application Domain is null, must be a valid App Domain Object"); 
        #endregion 

        return appDomain.Load(assemblyName); 
       } 

       private static Type GetTypeFromAssembly(string targetType, Assembly inAssembly) 
       { 
        #region Validate 
        if (string.IsNullOrWhiteSpace(targetType)) 
         throw new ArgumentNullException("targetType", "Type name is null, empty, or whitespace, should be type's display name."); 
        if (inAssembly == null) 
         throw new ArgumentNullException("inAssembly", "Assembly is null, should be valid assembly"); 
        #endregion 

        try 
        { 
         return inAssembly.GetType(targetType, true); 
        } 
        catch (Exception err) 
        { 
         string ErrorFormatMessage = "Unable to retrive type[{0}] from assembly [{1}], see inner exception."; 
         throw new InvalidOperationException(string.Format(ErrorFormatMessage, targetType, inAssembly), err); 
        } 
       } 

       private static Delegate CreateDelegateFrom(Object linkObject, Type ObjectType, Type DelegateType, MethodInfo TargetMethodInformation) 
       { 
        if (TargetMethodInformation.IsStatic & linkObject == null) 
        { 
         return CreateStaticMethodDelegate(DelegateType, TargetMethodInformation); 
        } 

        if (linkObject != null) 
        { 
         ValidateLinkObjectType(linkObject, ObjectType); 
        } 
        else 
        { 
         linkObject = CreateInstanceOfType(ObjectType, null); 
        } 

        return CreateInstanceMethodDelegate(linkObject, DelegateType, TargetMethodInformation); 
       } 

       private static Delegate CreateStaticMethodDelegate(Type DelegateType, MethodInfo TargetMethodInformation) 
       { 
        return Delegate.CreateDelegate(DelegateType, TargetMethodInformation); 
       } 

       private static void ValidateLinkObjectType(object linkObject, Type ObjectType) 
       { 
        if (!ObjectType.IsInstanceOfType(linkObject)) 
        { 
         throw new ArgumentException(
          string.Format("linkObject({0}) is not of type {1}", linkObject.GetType().Name, ObjectType.Name), 
          "linkObject", 
          new InvalidCastException(
           string.Format("Unable to cast object type {0} to object type {1}", linkObject.GetType().AssemblyQualifiedName, ObjectType.AssemblyQualifiedName), 
           new NotSupportedException(
            "Conversions from one delegate object to another is not support with this version" 
           ) 
          ) 
         ); 
        } 
       } 

       private static Object CreateInstanceOfType(Type targetType, params Object[] parameters) 
       { 
        #region Validate 
        if (targetType == null) 
         throw new ArgumentNullException("targetType", "Target Type is null, must be valid System type."); 
        #endregion 

        try 
        { 
         return System.Activator.CreateInstance(targetType, parameters); 
        } 
        catch (Exception err) 
        { 
         string ErrorFormatMessage = "Invalid call to CreateInstanceOfType({0}, Object[])\n" + 
                "parameters found:\n" + 
                "{1}" + 
                "See inner exception for further information."; 
         string ParamaterInformationLine = GetParamaterLine(parameters); 

         throw new NotSupportedException(
          string.Format(ErrorFormatMessage, targetType.Name, ParamaterInformationLine), err); 
        } 

       } 
       private static string GetParamaterLine(Object[] parameters) 
       { 
        if (parameters == null) 
         return "NONE\n"; 

        string ParamaterFormatLine = "==> Paramater Type is {0} and object is {1}\n"; 
        string ParamaterInformationLine = string.Empty; 

        foreach (object item in parameters) 
        { 
         ParamaterInformationLine += string.Format(ParamaterFormatLine, item.GetType().Name, item); 
        } 

        return ParamaterInformationLine; 
       } 

       private static Delegate CreateInstanceMethodDelegate(Object linkObject, Type DelegateType, MethodInfo TargetMethodInformation) 
       { 
        return Delegate.CreateDelegate(DelegateType, linkObject, TargetMethodInformation); 
       } 
      } 

      public string ObjectSource; 
      public string ObjectFullName; 
      public string ObjectMethodName; 
      public string DelegateSource; 
      public string DelegateFullName; 
      public BindingFlags SuggestedBinding; 

      public RuntimeDelegate(Delegate target) 
       : this(target.Method.DeclaringType.Assembly.FullName, 
         target.Method.DeclaringType.FullName, 
         target.Method.Name, 
         target.GetType().Assembly.FullName, 
         target.GetType().FullName, 
         RuntimeDelegateUtility.GetSuggestedBindingsForMethod(target.Method)) { } 

      public RuntimeDelegate(
       string objectSource, 
       string objectFullName, 
       string objectMethodName, 
       string delegateSource, 
       string delegateFullName, 
       BindingFlags suggestedBinding) 
       :this() 
      { 
       #region Validate Arguments 
       if (string.IsNullOrWhiteSpace(objectSource)) 
        throw new ArgumentNullException("ObjectSource"); 
       if (string.IsNullOrWhiteSpace(objectFullName)) 
        throw new ArgumentNullException("ObjectFullName"); 
       if (string.IsNullOrWhiteSpace(objectMethodName)) 
        throw new ArgumentNullException("ObjectMethodName"); 
       if (string.IsNullOrWhiteSpace(delegateSource)) 
        throw new ArgumentNullException("DelegateSource"); 
       if (string.IsNullOrWhiteSpace(delegateFullName)) 
        throw new ArgumentNullException("DelegateFullName"); 
       #endregion 
       #region Copy values for properties 
       this.ObjectSource = objectSource; 
       this.ObjectFullName = objectFullName; 
       this.ObjectMethodName = objectMethodName; 
       this.DelegateSource = delegateSource; 
       this.DelegateFullName = delegateFullName; 
       this.SuggestedBinding = suggestedBinding; 
       #endregion 
      } 

      public Delegate ToDelegate() 
      { 
       return ToDelegate(null); 
      } 
      public Delegate ToDelegate(Object linkObject) 
      { 
       return RD.Runtime.RuntimeDelegate.RuntimeDelegateUtility.Create(this, linkObject); 
      } 
     } 
    }