Предположим, что я создал класс обертки как следующее:
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# и возможно нескольких маленьких классов помощника? Или я просто оказывающийся перед необходимостью признавать, что существует компромисс здесь, и ударьте одно из указанных выше требований из списка?
В итоге я реализовал нечто своего рода похожее на класс 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
.
Это все еще несколько хакерское чувство, и я все еще открыт для лучших идей.
Если вы можете использовать .NET 4, вам следует просто использовать Lazy
.
Он обеспечивает нужную вам функциональность, и кроме того, он полностью потокобезопасен.
Если вы не можете использовать .NET 4, я все равно рекомендую взглянуть на него и «украсть» его дизайн и API. Это делает ленивое создание экземпляров довольно простым.
Если все, что вы пытаетесь сделать, это не повторять этот код 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
и друзей общедоступных полей вместо свойств. Это должно быть именно то, что вы ищете.
Но, опять же, никому не говорите, что вы это сделали!