Можно ли кэшировать значение, вычисленное в лямбда-выражении?
Вопрос
В методе containsIngredients в следующем коде можно кэшировать п.Ингредиенты value вместо того, чтобы явно ссылаться на него несколько раз?Это довольно тривиальный пример, который я только что придумал для иллюстративных целей, но код, над которым я работаю, ссылается на значения глубоко внутри. п например. 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);
}
}
Решение
Не можете ли вы просто написать свое логическое выражение в отдельной статической функции, которую вы вызываете из своей лямбды, передав 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;
}
Другие советы
Вы рассмотрели Мемоизация?
Основная идея такова;если у вас есть дорогостоящий вызов функции, есть функция, которая вычисляет дорогостоящее значение при первом вызове, но после этого возвращает кэшированную версию.Функция выглядит следующим образом;
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());
В качестве общей стратегии это может помочь.
Что ж, в этом случае, если вы не можете использовать мемоизацию, вы довольно ограничены, поскольку на самом деле вы можете использовать стек только в качестве кеша:У вас нет возможности объявить новую переменную в той области, которая вам нужна.Все, что я могу придумать (и я не претендую на то, что это будет красиво), которое будет делать то, что вы хотите, но сохранит необходимую вам компоновку, будет что-то вроде...
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, улучшится ли это производительность, будет зависеть от того, насколько дорогим был ваш ExpensiveCall().
Обратите внимание: в исходном примере не будет никакой встраивания, как было предложено в другом ответе, поскольку, насколько мне известно, компилятор выражений не выполняет такой уровень оптимизации.
Я бы сказал нет в данном случае.Я предполагаю, что компилятор может понять, что он использует p.Ingredients
переменную 3 раза и сохранит переменную рядом в стеке, регистрах или чем-то еще, что она использует.
У Турбулентного Интеллекта есть совершенно правильный ответ.
Я просто хочу посоветовать вам удалить некоторые значения null и исключения из используемых вами типов, чтобы сделать их более удобным для использования.
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, почему только чтение?