É possível armazenar em cache um valor avaliado em uma expressão lambda?
Pergunta
No método ContainsIngredients no código a seguir, é possível armazenar em cache o p.Ingredientes valor em vez de referenciá-lo explicitamente várias vezes?Este é um exemplo bastante trivial que acabei de criar para fins ilustrativos, mas o código no qual estou trabalhando faz referência a valores profundos p por exemplo. p.InnerObject.ExpensiveMethod().Value
editar:Estou usando o PredicateBuilder de 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);
}
}
Solução
Você não pode simplesmente escrever sua expressão booleana em uma função estática separada que você chama de seu lambda - passando p.Ingredients como parâmetro ...
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;
}
Outras dicas
Você considerou Memoização?
A ideia básica é esta;se você tiver uma chamada de função cara, existe uma função que calculará o valor caro na primeira chamada, mas retornará uma versão em cache depois disso.A função fica assim;
static Func<T> Remember<T>(Func<T> GetExpensiveValue)
{
bool isCached= false;
T cachedResult = default(T);
return () =>
{
if (!isCached)
{
cachedResult = GetExpensiveValue();
isCached = true;
}
return cachedResult;
};
}
Isso significa que você pode escrever isso;
// 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());
Como estratégia geral, pode ajudar.
Bem, neste caso, se você não pode usar o Memoization, você está bastante restrito, pois só pode usar a pilha como cache:Você não tem como declarar uma nova variável no escopo necessário.Tudo o que consigo pensar (e não estou afirmando que será bonito) que fará o que você deseja, mas manterá a capacidade de composição necessária, seria algo como ...
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;
}
Você pode combinar os resultados de várias chamadas TestWith em uma expressão booleana mais complexa quando necessário - armazenando em cache o valor caro apropriado com cada chamada - ou pode aninhá-los dentro dos lambdas passados como o segundo parâmetro para lidar com suas hierarquias profundas e complexas.
Seria muito difícil ler o código e, como você pode introduzir mais transições de pilha com todas as chamadas TestWith, a melhoria do desempenho dependeria de quão caro era seu ExpensiveCall().
Como observação, não haverá nenhum inlining no exemplo original, conforme sugerido por outra resposta, já que o compilador de expressão não faz esse nível de otimização, até onde eu sei.
Eu diria não neste caso.Presumo que o compilador possa descobrir que ele usa o p.Ingredients
variável 3 vezes e manterá a variável próxima na pilha ou nos registros ou o que quer que ela use.
O Turbulent Intellect tem a resposta exatamente certa.
Eu só quero avisar que você pode retirar alguns dos nulos e exceções dos tipos que está usando para torná-los mais fáceis de usar.
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()) { }
Então, se você tiver os parâmetros de consulta nesta estrutura...
Dictionary<string, List<string>> ingredients;
Você pode escrever a consulta assim.
from p in Potions
where ingredients.Any(i => i.Value.Any(v => p.IngredientBag.Has(i.Key, v))
select p;
PS, por que somente leitura?