Сброс System.Lazy

#c#-4.0

#c #-4.0

Вопрос:

В бизнес-классе у меня есть :

  class Employee{

      public Employee() {
          m_Manager = new Lazy<Manager>( () => return myRepository.GetManager(ManagerId); );
      }
      public int ManagerId { get; set;}
      private Lazy<Manager> m_Manager;
      public Manager Manager { 
          get {
               return m_Manager.Value;
          }
      }
 }
  

Это работает правильно, пользовательский репозиторий вызывается только при доступе к свойству Manager.
Теперь я хочу «сбросить» свое свойство manager, если ManagerID изменился. Как это сделать?

Я могу сделать :

  class Employee{

      public Employee() {
          m_Manager = new Lazy<Manager>( () => return myRepository.GetManager(ManagerId); );
      }
      private int m_ManagerId;
      public int ManagerId { 
          get { return m_ManagerId;}
          set { 
               if(m_ManagerId != value)
               {
                    m_ManagerId = value;
                    m_Manager = new Lazy<Manager>( () => return myRepository.GetManager(ManagerId); );
               }
          }
      }
      private Lazy<Manager> m_Manager;
      public Manager Manager { 
          get {
               return m_Manager.Value;
          }
      }
 }
  

Есть ли более чистый способ сделать это? Разве не существует m_Manager.Reset() или чего-то подобного этому?

Ответ №1:

Lazy<T> не определяет метод Reset(). Я думаю, то, что вы реализовали, выглядит нормально.

Комментарии:

1. Я согласен, что общий подход хорош, но предлагаю извлечь дублированный код инициализации Lazy<Manager> в общий метод.

Ответ №2:

Если вас устраивает использование недокументированного поведения и закрытых полей, вот способ сделать это:

 public static void ResetPublicationOnly<T>(this Lazy<T> lazy)
{
    const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;

    LazyThreadSafetyMode mode = (LazyThreadSafetyMode)typeof(Lazy<T>).GetProperty("Mode", flags).GetValue(lazy, null);
    if (mode != LazyThreadSafetyMode.PublicationOnly)
        throw new InvalidOperationException("ResetPublicationOnly only works for Lazy<T> with LazyThreadSafetyMode.PublicationOnly");

    typeof(Lazy<T>).GetField("m_boxed", flags).SetValue(lazy, null); 
}
  

И какой-нибудь тест на использование:

 Lazy<string> val = new Lazy<string>(() => "hola"   DateTime.Now.Ticks, LazyThreadSafetyMode.PublicationOnly);

val.ResetPublicationOnly(); //reset before initialized
var str1 = val.Value;
val.ResetPublicationOnly(); //reset after initialized

var str2 = val.Value;

Assert.AreNotEqual(str1, str2); 
  

РЕДАКТИРОВАТЬ: Устарело!Это решение больше не работает так, как указал Кит. Мы создали нашу собственную систему сброса в Signum Framework

 public interface IResetLazy
{
    void Reset();
    void Load();
    Type DeclaringType { get; }
}

[ComVisible(false)]
[HostProtection(Action = SecurityAction.LinkDemand, Resources = HostProtectionResource.Synchronization | HostProtectionResource.SharedState)]
public class ResetLazy<T>: IResetLazy
{
    class Box
    {
        public Box(T value)
        {
            this.Value = value;
        }

        public readonly T Value;
    }

    public ResetLazy(Func<T> valueFactory, LazyThreadSafetyMode mode = LazyThreadSafetyMode.PublicationOnly, Type declaringType = null)
    {
        if (valueFactory == null)
            throw new ArgumentNullException("valueFactory");

        this.mode = mode;
        this.valueFactory = valueFactory;
        this.declaringType = declaringType ?? valueFactory.Method.DeclaringType;
    }

    LazyThreadSafetyMode mode; 
    Func<T> valueFactory;

    object syncLock = new object();

    Box box;

    Type declaringType; 
    public Type DeclaringType
    {
        get { return declaringType; }
    }

    public T Value
    {
        get
        {
            var b1 = this.box;
            if (b1 != null)
                return b1.Value;

            if (mode == LazyThreadSafetyMode.ExecutionAndPublication)
            {
                lock (syncLock)
                {
                    var b2 = box;
                    if (b2 != null)
                        return b2.Value;

                    this.box = new Box(valueFactory());

                    return box.Value;
                }
            }

            else if (mode == LazyThreadSafetyMode.PublicationOnly)
            {
                var newValue = valueFactory(); 

                lock (syncLock)
                {
                    var b2 = box;
                    if (b2 != null)
                        return b2.Value;

                    this.box = new Box(newValue);

                    return box.Value;
                }
            }
            else
            {
                var b = new Box(valueFactory());
                this.box = b;
                return b.Value;
            }
        }
    }


    public void Load()
    {
        var a = Value;
    }

    public bool IsValueCreated
    {
        get { return box != null; }
    }

    public void Reset()
    {
        if (mode != LazyThreadSafetyMode.None)
        {
            lock (syncLock)
            {
                this.box = null;
            }
        }
        else
        {
            this.box = null;
        }
    }
}
  

Комментарии:

1. надеюсь, вы не в замкнутом цикле

2. похоже, они внесли некоторые изменения в исходный код Lazy<T>. Этот метод больше не работает, и будет выдано исключение

3. Отличный пример того, почему не следует использовать недокументированное поведение и закрытые поля!

4. Гораздо лучше просто переназначить переменную в соответствии с кодом в вопросе… простой И надежный, легко сделать потокобезопасным.

5. Я думаю, что одному и тому же потоку разрешено дважды использовать одну и ту же блокировку, это то, что вы имеете в виду, но есть фундаментальные различия в том, как работает ResetLazy по сравнению с referencesource.microsoft.com/#mscorlib/system / … и это заставляет меня нервничать. Во всяком случае, я часто использую его без проблем.

Ответ №3:

Перед повторным созданием экземпляра Lazy вы также можете добавить условие для сброса, только если оно было инициализировано «if(m_Manager.IsValueCreated)».

Комментарии:

1. Это создало бы условие гонки — если бы другой поток начал создавать значение и уже прочитал (исходный) ManagerID, и в этот момент ManagerID изменен, тогда вы бы увидели, что IsValueCreated равен false , и не создавали бы повторно экземпляр Lazy<> . Несколько мгновений спустя другой поток успешно создает объект Manager, и все последующие вызывающие пользователи получают неправильный объект manager. В исходном коде только вызывающие, которые, по крайней мере, начали обращаться к объекту Employee до изменения идентификатора ManagerID, будут видеть старый объект Manager.