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

Предположим, что я создал класс обертки как следующее:

public class Foo : IFoo
{
    private readonly IFoo innerFoo;

    public Foo(IFoo innerFoo)
    {
        this.innerFoo = innerFoo;
    }

    public int? Bar { get; set; }
    public int? Baz { get; set; }
}

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

Достаточно просто сделать это, конечно:

int IFoo.GetBar()
{
    if ((Bar == null) && (innerFoo != null))
        Bar = innerFoo.GetBar();
    return Bar ?? 0;
}

int IFoo.GetBaz()
{
    if ((Baz == null) && (innerFoo != null))
        Baz = innerFoo.GetBaz();
    return Baz ?? 0;
}

Но это становится довольно повторяющимся, если я делаю это с 10 различными свойствами и 30 различными обертками. Таким образом, я фигурировал, эй, давайте сделаем этот дженерик:

T LazyLoad<T>(ref T prop, Func<IFoo, T> loader)
{
    if ((prop == null) && (innerFoo != null))
        prop = loader(innerFoo);
    return prop;
}

Который почти получает меня, где я хочу, но не совсем, потому что Вы не можете ref автосвойство (или любое свойство вообще). Другими словами, я не могу записать это:

int IFoo.GetBar()
{
    return LazyLoad(ref Bar, f => f.GetBar());  // <--- Won't compile
}

Вместо этого я должен был бы измениться Bar иметь явное поле поддержки и записать явные методы считывания и методы set. Который прекрасен, за исключением того, что я заканчиваю тем, что писал еще больше избыточного кода, чем я писал во-первых.

Затем я рассмотрел возможность использования деревьев выражений:

T LazyLoad<T>(Expression<Func<T>> propExpr, Func<IFoo, T> loader)
{
    var memberExpression = propExpr.Body as MemberExpression;
    if (memberExpression != null)
    {
        // Use Reflection to inspect/set the property
    }
}

Это играет по правилам с рефакторингом - он будет работать отлично, если я сделаю это:

return LazyLoad(f => f.Bar, f => f.GetBar());

Но это не на самом деле безопасно, потому что кто-то менее умный (т.е. я через 3 дня с этого времени, когда я неизбежно забываю, как это реализовано внутренне) мог решить записать это вместо этого:

return LazyLoad(f => 3, f => f.GetBar());

Который или собирается отказать или привести к неожиданному/неопределенному поведению, в зависимости от того, как оборонительно я пишу LazyLoad метод. Таким образом, мне действительно не нравится этот подход также, потому что он приводит к возможности ошибок периода выполнения, которые были бы предотвращены в первой попытке. Это также полагается, по размышлении, какие чувства, немного грязные здесь, даже при том, что этот код по общему признанию не чувствителен к производительности.

Теперь я мог также решить полностью выложиться и использовать DynamicProxy, чтобы сделать перехват метода и не иметь для написания любого кода, и на самом деле я уже делаю это в некоторых приложениях. Но этот код находится в оперативной библиотеке, от которой зависят много других блоков, и кажется ужасно неправильным представить этот вид сложности на таком низком уровне. Разделение основанной на перехватчике реализации от IFoo интерфейс путем помещения его в его собственный блок действительно не помогает; факт - то, что этот самый класс все еще будет используемым повсеместно, должен использоваться, таким образом, это не одна из тех проблем, которые могли быть тривиально решены с небольшим волшебством DI.

Последняя опция, о которой я уже думал, состояла бы в том, чтобы иметь метод как:

 T LazyLoad<T>(Func<T> getter, Action<T> setter, Func<IFoo, T> loader) { ... }

Эта опция очень "meh" также - она избегает Отражения, но все еще подвержена ошибкам, и она действительно не уменьшает повторение так очень. Это должно почти настолько же плохо как записать явные методы считывания и методы set для каждого свойства.

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

Нижняя строка: я в тупике, ища другие идеи.

Вопрос:

Есть ли любой способ очистить лениво загружающийся код наверху, такой, что реализация будет:

  • Гарантируйте безопасность времени компиляции, как ref версия;
  • На самом деле уменьшите повторение объема кода, как Expression версия; и
  • Не берут какие-либо значительные дополнительные зависимости?

Другими словами, существует ли способ сделать это просто использование регулярных функций языка C# и возможно нескольких маленьких классов помощника? Или я просто оказывающийся перед необходимостью признавать, что существует компромисс здесь, и ударьте одно из указанных выше требований из списка?

9
задан Aaronaught 16 April 2010 в 18:39
поделиться

3 ответа

В итоге я реализовал нечто своего рода похожее на класс Lazy в .NET 4, но более приспособленное к определенному понятию «кеширование», а не к «ленивой загрузке». ".

Квазиленивый класс выглядит так:

public class CachedValue<T>
{
    private Func<T> initializer;
    private bool isValueCreated;
    private T value;

    public CachedValue(Func<T> initializer)
    {
        if (initializer == null)
            throw new ArgumentNullException("initializer");
        this.initializer = initializer;
    }

    public CachedValue(T value)
    {
        this.value = value;
        this.isValueCreated = true;
    }

    public static implicit operator T(CachedValue<T> lazy)
    {
        return (lazy != null) ? lazy.Value : default(T);
    }

    public static implicit operator CachedValue<T>(T value)
    {
        return new CachedValue<T>(value);
    }

    public bool IsValueCreated
    {
        get { return isValueCreated; }
    }

    public T Value
    {
        get
        {
            if (!isValueCreated)
            {
                value = initializer();
                isValueCreated = true;
            }
            return value;
        }
    }
}

Идея состоит в том, что, в отличие от класса Lazy , он также может быть инициализирован с помощью определенного значения . Я также реализовал некоторые операторы неявного преобразования, чтобы можно было напрямую назначать значения свойствам CachedValue , как если бы они были просто T . Я не реализовал функции безопасности потоков Lazy - эти экземпляры не предназначены для передачи.

Затем, поскольку создание экземпляров этих вещей очень многословно, я воспользовался некоторыми универсальными функциями вывода типов, чтобы создать более компактный синтаксис lazy-init:

public static class Deferred
{
    public static CachedValue<T> From<TSource, T>(TSource source, 
        Func<TSource, T> selector)
    {
        Func<T> initializer = () =>
            (source != null) ? selector(source) : default(T);
        return new CachedValue<T>(initializer);
    }
}

В конце концов, это дает мне почти -POCO класс, использующий автоматические свойства, которые инициализируются отложенными загрузчиками в конструкторе (которые объединяются с нулевым значением из Deferred ):

public class CachedFoo : IFoo
{
    public CachedFoo(IFoo innerFoo)
    {
        Bar = Deferred.From(innerFoo, f => f.GetBar());
        Baz = Deferred.From(innerFoo, f => f.GetBaz());
    }

    int IFoo.GetBar()
    {
        return Bar;
    }

    int IFoo.GetBaz()
    {
        return Baz;
    }

    public CachedValue<int> Bar { get; set; }
    public CachedValue<int> Baz { get; set; }
}

Я не абсолютно в восторге от этого, но я очень доволен им. Что хорошо, так это то, что он также позволяет посторонним заполнять свойства способом, не зависящим от реализации, что очень полезно, когда я хочу переопределить поведение ленивой загрузки, то есть предварительно загрузить кучу записей из большого SQL-запроса:

CachedFoo foo = new CachedFoo(myFoo);
foo.Bar = 42;
foo.Baz = 86;

I Я придерживаюсь этого пока. При таком подходе довольно сложно испортить класс-оболочку.Поскольку используется оператор неявного преобразования, он безопасен даже против экземпляров null .

Это все еще несколько хакерское чувство, и я все еще открыт для лучших идей.

1
ответ дан 4 December 2019 в 23:38
поделиться

Если вы можете использовать .NET 4, вам следует просто использовать Lazy .

Он обеспечивает нужную вам функциональность, и кроме того, он полностью потокобезопасен.

Если вы не можете использовать .NET 4, я все равно рекомендую взглянуть на него и «украсть» его дизайн и API. Это делает ленивое создание экземпляров довольно простым.

4
ответ дан 4 December 2019 в 23:38
поделиться

Если все, что вы пытаетесь сделать, это не повторять этот код 300 раз:

private int? bar;
public int Bar
{
    get
    {
        if (bar == null && innerFoo != null)
           bar = innerFoo.GetBar();
        return bar ?? 0;           
    }
    set
    {
        bar = value;
    }
}

Тогда вы всегда можете просто создать индексатор.

enum FooProperties
{
    Bar,
    Baz,
}

object[] properties = new object[2];

public object this[FooProperties property]
{
    get
    {
        if (properties[property] == null)
        {
           properties[property] = GetProperty(property);
        }
        return properties[property];           
    }
    set
    {
        properties[property] = value;
    }
}

private object GetProperty(FooProperties property)
{
    switch (property)
    {
         case FooProperties.Bar:
              if (innerFoo != null)
                  return innerFoo.GetBar();
              else
                  return (int)0;

         case FooProperties.Baz:
              if (innerFoo != null)
                  return innerFoo.GetBaz();
              else
                  return (int)0;

         default:
              throw new ArgumentOutOfRangeException();
    }
}

Это потребует преобразования значения при чтении:

int myBar = (int)myFoo[FooProperties.Bar];

Но это позволяет избежать большинства других проблем.

ИЗМЕНЕНО, ЧТОБЫ ДОБАВИТЬ:

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

int IFoo.GetBar()
{
    return LazyLoad(ref Bar, f => f.GetBar());  // <--- Won't compile
}

Создание Bar , Baz и друзей общедоступных полей вместо свойств. Это должно быть именно то, что вы ищете.

Но, опять же, никому не говорите, что вы это сделали!

1
ответ дан 4 December 2019 в 23:38
поделиться
Другие вопросы по тегам:

Похожие вопросы: