Подходы к универсальным безопасным методам отложенной загрузки во время компиляции - PullRequest
9 голосов
/ 16 апреля 2010

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

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, чтобы иметь явное поле поддержки и писать явные методы получения и установки. Это нормально, за исключением того факта, что я пишу еще более избыточный код, чем прежде.

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

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. Поэтому мне не очень нравится этот подход, потому что он приводит к возможности ошибок во время выполнения, которые были бы предотвращены с первой попытки. Он также опирается на Reflection, которая здесь выглядит немного грязной, хотя этот код, по общему признанию, не чувствителен к производительности.

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

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

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

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

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

Итог: я в тупике, ищу другие идеи.

Вопрос:

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

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

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

Ответы [ 3 ]

4 голосов
/ 16 апреля 2010

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

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

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

1 голос
/ 19 апреля 2010

В итоге я реализовал нечто вроде своего рода , похожее на класс 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<T>, он также может быть инициализирован из определенного значения . Я также реализовал некоторые операторы неявного преобразования, чтобы можно было напрямую присваивать значения CachedValue<T> свойствам, как если бы они были просто T. Я не реализовал функции безопасности потоков Lazy<T> - эти экземпляры не предназначены для передачи.

Затем, поскольку создание этих вещей очень многословно, я воспользовался некоторыми функциями вывода обобщенных типов, чтобы создать более компактный синтаксис 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;

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

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

1 голос
/ 16 апреля 2010

Если все, что вы пытаетесь сделать, это избегать повторения этого кода 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 и друзей в открытых полях вместо свойств. Это должно быть именно то, что вы ищете.

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

...