Можно ли кэшировать значение, вычисленное в лямбда-выражении? - PullRequest
4 голосов
/ 16 сентября 2008

В методе ContainsIngredients в следующем коде возможно ли кэшировать значение p.Ingredients вместо явной ссылки на него несколько раз? Это довольно тривиальный пример, который я только что подготовил для иллюстративных целей, но код, над которым я работаю, ссылается на значения глубоко внутри p , например. p.InnerObject.ExpensiveMethod (). Значение

редактирование: Я использую PredicateBuilder из http://www.albahari.com/nutshell/predicatebuilder.html

public class IngredientBag
{
    private readonly Dictionary<string, string> _ingredients = new Dictionary<string, string>();

    public void Add(string type, string name)
    {
        _ingredients.Add(type, name);
    }

    public string Get(string type)
    {
        return _ingredients[type];
    }

    public bool Contains(string type)
    {
        return _ingredients.ContainsKey(type);
    }
}

public class Potion
{
    public IngredientBag Ingredients { get; private set;}
    public string Name {get; private set;}        

    public Potion(string name) : this(name, null)
    {

    }

    public Potion(string name, IngredientBag ingredients)
    {
        Name = name;
        Ingredients = ingredients;
    }

    public static Expression<Func<Potion, bool>> 
        ContainsIngredients(string ingredientType, params string[] ingredients)
    {
        var predicate = PredicateBuilder.False<Potion>();
        // Here, I'm accessing p.Ingredients several times in one 
        // expression.  Is there any way to cache this value and
        // reference the cached value in the expression?
        foreach (var ingredient in ingredients)
        {
            var temp = ingredient;
            predicate = predicate.Or (
                p => p.Ingredients != null &&
                p.Ingredients.Contains(ingredientType) &&
                p.Ingredients.Get(ingredientType).Contains(temp));
        }

        return predicate;
    }

}


[STAThread]
static void Main()
{
    var potions = new List<Potion>
    {
        new Potion("Invisibility", new IngredientBag()),
        new Potion("Bonus"),
        new Potion("Speed", new IngredientBag()),
        new Potion("Strength", new IngredientBag()),
        new Potion("Dummy Potion")
    };

    potions[0].Ingredients.Add("solid", "Eye of Newt");
    potions[0].Ingredients.Add("liquid", "Gall of Peacock");
    potions[0].Ingredients.Add("gas", "Breath of Spider");

    potions[2].Ingredients.Add("solid", "Hair of Toad");
    potions[2].Ingredients.Add("gas", "Peacock's anguish");

    potions[3].Ingredients.Add("liquid", "Peacock Sweat");
    potions[3].Ingredients.Add("gas", "Newt's aura");

    var predicate = Potion.ContainsIngredients("solid", "Newt", "Toad")
        .Or(Potion.ContainsIngredients("gas", "Spider", "Scorpion"));

    foreach (var result in 
                from p in potions
                where(predicate).Compile()(p)
                select p)
    {
        Console.WriteLine(result.Name);
    }
}

Ответы [ 5 ]

10 голосов
/ 16 сентября 2008

Рассматривали ли вы Памятка ?

Основная идея заключается в следующем; если у вас дорогой вызов функции, есть функция, которая вычислит дорогое значение при первом вызове, но затем вернет кэшированную версию. Функция выглядит так:

static Func<T> Remember<T>(Func<T> GetExpensiveValue)
{
    bool isCached= false;
    T cachedResult = default(T);

    return () =>
    {
        if (!isCached)
        {
            cachedResult = GetExpensiveValue();
            isCached = true;
        }
        return cachedResult;

    };
}

Это означает, что вы можете написать это;

    // here's something that takes ages to calculate
    Func<string> MyExpensiveMethod = () => 
    { 
        System.Threading.Thread.Sleep(5000); 
        return "that took ages!"; 
    };

    // and heres a function call that only calculates it the once.
    Func<string> CachedMethod = Remember(() => MyExpensiveMethod());

    // only the first line takes five seconds; 
    // the second and third calls are instant.
    Console.WriteLine(CachedMethod());
    Console.WriteLine(CachedMethod());
    Console.WriteLine(CachedMethod());

Как общая стратегия, это может помочь.

2 голосов
/ 16 сентября 2008

Разве вы не можете просто написать свое логическое выражение в отдельной статической функции, которую вы вызываете из своей лямбды - передавая p.Ingredients в качестве параметра ...

private static bool IsIngredientPresent(IngredientBag i, string ingredientType, string ingredient)
{
    return i != null && i.Contains(ingredientType) && i.Get(ingredientType).Contains(ingredient);
}

public static Expression<Func<Potion, bool>>
                ContainsIngredients(string ingredientType, params string[] ingredients)
{
    var predicate = PredicateBuilder.False<Potion>();
    // Here, I'm accessing p.Ingredients several times in one 
    // expression.  Is there any way to cache this value and
    // reference the cached value in the expression?
    foreach (var ingredient in ingredients)
    {
        var temp = ingredient;
        predicate = predicate.Or(
            p => IsIngredientPresent(p.Ingredients, ingredientType, temp));
    }

    return predicate;
}
1 голос
/ 16 сентября 2008

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

private static bool TestWith<T>(T cached, Func<T, bool> predicate)
{
    return predicate(cached);
}

public static Expression<Func<Potion, bool>>
                ContainsIngredients(string ingredientType, params string[] ingredients)
{
    var predicate = PredicateBuilder.False<Potion>();
    // Here, I'm accessing p.Ingredients several times in one 
    // expression.  Is there any way to cache this value and
    // reference the cached value in the expression?
    foreach (var ingredient in ingredients)
    {
        var temp = ingredient;
        predicate = predicate.Or (
            p => TestWith(p.Ingredients,
                i => i != null &&
                     i.Contains(ingredientType) &&
                     i.Get(ingredientType).Contains(temp));
    }

    return predicate;
}

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

Хотя было бы довольно сложно читать код, и, поскольку вы могли бы вводить больше стековых переходов со всеми вызовами TestWith, от того, повысит ли это производительность, будет зависеть только то, насколько дорогим был ваш ExорогоCall ().

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

0 голосов
/ 16 сентября 2008

Турбулентный интеллект имеет абсолютно правильный ответ.

Я просто хочу посоветовать вам исключить некоторые пустые значения и исключения из типов, которые вы используете, чтобы сделать их использование более удобным.

    public class IngredientBag
    {
      private Dictionary<string, string> _ingredients = 
new Dictionary<string, string>();
      public void Add(string type, string name)
      {
        _ingredients[type] = name;
      }
      public string Get(string type)
      {
        return _ingredients.ContainsKey(type) ? _ingredients[type] : null;
      }
      public bool Has(string type, string name)
      {
        return name == null ? false : this.Get(type) == name;
      }
    }

    public Potion(string name) : this(name, new IngredientBag())    {    }

Тогда, если у вас есть параметры запроса в этой структуре ...

Dictionary<string, List<string>> ingredients;

Вы можете написать запрос следующим образом.

from p in Potions
where ingredients.Any(i => i.Value.Any(v => p.IngredientBag.Has(i.Key, v))
select p;

PS, почему только для чтения?

0 голосов
/ 16 сентября 2008

Я бы сказал нет в этом случае. Я предполагаю, что компилятор может выяснить, что он использует переменную p.Ingredients 3 раза и будет держать переменную близко к стеку или регистрам или тому, что использует.

...