2009-07-15 2 views

ответ

30

Это довольно рискованная вещь.

Хотя верно, что вы можете сериализовать и десериализовать делегат точно так же, как любой другой объект, делегат является указателем на метод внутри программы, который его сериализовал. Если вы десериализуете объект в другой программе, вы получите SerializationException - если вам повезет.

Например, давайте изменим программу Дарин немного:

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; 
    } 
} 

запустить его, и вы увидите, что он создает объект, упорядочивает его, преобразовывает его в новый объект, а при вызове Del на новом объекте он возвращает «a». Отлично. Хорошо, теперь закомментируйте звонок WriteFoo, так что программа просто десериализует объект. Запустите программу еще раз, и вы получите тот же результат.

Теперь замените объявление a и b и запустите программу. Хлоп. Теперь десериализованный объект возвращает «b».

Это происходит потому, что то, что на самом деле сериализуется, - это имя, которое компилятор присваивает выражению лямбда. И компилятор присваивает имена лямбда-выражениям в том порядке, в котором они находятся.

И вот что опасно в этом: вы не сериализуете делегата, вы сериализуете символ. Это значение значения символа, а не то, что представляет символ, который сериализуется. Поведение десериализованного объекта зависит от того, какое значение представляет этот символ в программе, которая десериализует его.

В определенной степени это верно при всей сериализации. Уничтожьте объект в программе, которая реализует класс объекта иначе, чем программа сериализации, и начинается самое интересное. Но сериализация делегатов связывает сериализованный объект с таблицей символов программы, которая сериализовала его, а не для реализации класса объекта.

Если бы это был я, я бы подумал о том, чтобы сделать эту связь явной. Я бы создал статическое свойство Foo, которое было Dictionary<string, Func<string>>, заполнить его с помощью клавиш и функций и сохранить ключ в каждом экземпляре, а не в функции. Это делает программу десериализации ответственной за заполнение словаря, прежде чем он начнет десериализацию Foo объектов. В какой-то степени это то же самое, что и использование BinaryFormatter для сериализации делегата; разница в том, что этот подход делает работу десериализационной программы по назначению функций символам намного более очевидной.

+2

Я решил не сохранять делегатов в файлах Сохранение делегатов в файлах приводит к другой проблеме: Несколько копий одной и той же функции, которая будет храниться в файле. Скорее (как говорит Роберт), я думаю, что лучше определить массив делегатов и сохранить индекс каждого делегата в файле. –

+0

+1 это ответ спасло меня от боли, связанной с такой ошибкой. Другая причина, по которой делегат может измениться, - это использовать другую версию компилятора: http://stackoverflow.com/a/40780504/66372 – eglasius

15

Фактически вы можете с BinaryFormatter, поскольку он сохраняет информацию о типе. И вот доказательство:

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"; 
    } 

} 

Важной вещь, которую вы должны знать, если вы решили использовать BinaryFormatter в том, что его формат не хорошо документирован и реализация может иметь отличия между .NET и/или CLR версией.

+1

Вы уверены, что это работает, когда делегат ссылается на нестатический метод? Я вижу, что он работает со статическими методами, так как не нужно определять Трагета, но, например, методы, что он делает? Потенциально, он мог бы сериализовать граф экземпляра Target (предполагая, что он сериализуется), но тогда, когда десериализован и вызван, он будет на другом экземпляре с потенциально устаревшими данными. Я лично был бы очень осторожен в выборе постоянных делегатов таким образом, поскольку это могло бы легко привести к неожиданным и трудным для отладки/исправления поведения. – LBushkin

+1

Он также работает с нестационарными методами. Он сериализует граф экземпляра Target, а также предполагает, что он сериализуется (помечается с помощью SerializableAttribute). –

2

Делегат - это указатель на метод, я могу неправильно понять, когда вы скажете «Сохранить», но местоположение, добавленное в делегат во время выполнения, может не существовать больше, если вы попытаетесь сохранить и восстановить адрес.

+0

Спасибо Квинтин. Вы правы, как указатель, который мы не можем. Но как насчет их содержания? что-то вроде оператора C++ *. –

+4

Если вы используете двоичный сериализатор, делегат будет сериализован, а также весь граф объектов, на который он ссылается. Это гарантирует, что делегат может быть вызван после десериализации. –

1

Итак, я понимаю, что вы хотите «сохранить» указатель на функцию (делегат). Теперь, если вы поместили все свои функции делегата в библиотеку, вы могли бы использовать отражение системы для создания ссылки во время выполнения, а затем иметь возможность передать делегата определенному делегату-компилятору (который снова будет в библиотеке). Единственное падение к этому состоит в том, что целевой метод должен быть четко определенным местом, поэтому никакие анонимные методы, так как там местоположение определяется во время компиляции каждый раз при компиляции. Вот код, который я разработал, чтобы иметь возможность воссоздать делегата во время выполнения, использовать на свой страх и риск и не документировать его комментариями.

Обновление: Другое, что вы можете сделать, это создать настраиваемый атрибут и применить его ко всем и всем методам, которые вы хотите создать в делегате. Во время выполнения, используя системное отражение, перемещайте найденные типы экспорта, а затем выберите все методы из тех типов, у которых есть настраиваемый атрибут. Это может быть больше того, что вы хотели и будет полезно, только если вы также указали значение «ID», чтобы был логический способ связывания идентификатора с желаемым делегатом через основную таблицу поиска.

Я также заметил комментарий, который вы отказались от этого подхода из-за фактора риска. Я оставлю это здесь, чтобы предоставить еще один способ сделать что-то.

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); 
      } 
     } 
    } 
Смежные вопросы