Ho un sistema di costruzione di alberi di espressioni binarie eccessivamente complicato. Include una stringa e una coppia di oggetti (Player e World)
Ogni nodo dell'albero rappresenta una funzione esterna che accetta una stringa, un giocatore e un mondo e restituisce un bool (per i test) una stringa (per l'output) o void (per le azioni)
Il mio problema è triplice: in primo luogo ho bisogno di usare qualcosa come Expression.Condition
o Expression.IfThenElse
dove l'espressione di prova è nella forma Expression<func<string, Player, World, bool>>
piuttosto che Expresson<bool>
(come Expression.And
produrrebbe)
In secondo luogo, devo essere sicuro che il riferimento di memoria per Player e World rimanga lo stesso ovunque - in modo che se uno dei nodi nella struttura aggiorna qualcosa all'interno di Player, verrà comunque aggiornato al nodo successivo.
Alla fine ho bisogno di aggiungere tutte le stringhe, l'una all'altra.
Se potessi scrivere un codice difficile sull'albero, potrebbe sembrare qualcosa del genere:
class Main
{
string Foo(string text, World world, Player player)
{
string output;
output += SomeClass.PrintStarting();
if (SomeClass.Exists(text, world, player))
{
output += SomeClass.PrintName(text, world, player);
SomeClass.KillPlayer(text, world, player);
if (SomeClass.Exists(text, world, player))
output += SomeClass.PrintSurvived(text, world, player);
}
else
output += SomeClass.PrintNotExists(text, world, player);
return output;
}
}
public class SomeClass
{
string PrintStart(string text, World world, Player player)
{
return "Starting.\n";
}
bool Exists(string text, World world, Player player)
{
player.Lives;
}
string PrintName(string text, World world, Player player)
{
return player.Name + ".\n";
}
string PrintSurvived(string text, World world, Player player)
{
return player.Name + "died.\n";
}
string PrintNotExists(string text, World world, Player player)
{
return "This person does not exist.\n";
}
void KillPlayer(string text, World world, Player player)
{
if (text != "kidding")
player.Lives = false;
}
}
Per approfondire: ho un'istanza di SomeClass con tutti i suoi metodi test / assegnazione / stringa. Vado quindi a creare un elenco di Expression<func<string[], World, Player, bool>>
, Expression<Action<string[], World, Player>>
ed Expression<func<string[], World, Player, string>>
e inizia a gettarli insieme in un albero di espressioni. L'effettivo ordinamento di ciò che va dove mi sono lasciato con (ad esempio):
public string Foo2(string text, World world, Player player)
{
ParameterExpression result = Expression.Parameter(typeof(string), "result");
ParameterExpression inputString = Expression.Parameter(typeof(string[]), "inputString");
ParameterExpression inputWorld = Expression.Parameter(typeof(World), "inputWorld");
ParameterExpression inputPlayer = Expression.Parameter(typeof(Player), "inputPlayer");
System.Reflection.MethodInfo methodInfo = typeof(string).GetMethod("Concat", new Type[] { typeof(string), typeof(string) });
Expression textPrintStarting = (Expression<Func<string, World, Player, string>>)((Text, World, Player) => SomeClass.PrintStarting(Text, World, Player));
Expression testExists = (Expression<Func<string, World, Player, bool>>)((Text, World, Player) => SomeClass.Exists(Text, World, Player));
Expression textPrintName = (Expression<Func<string, World, Player, string>>)((Text, World, Player) => SomeClass.PrintName(Text, World, Player));
Expression killPlayer = (Expression<Action<string, World, Player>>)((Text, World, Player) => SomeClass.KillPlayer(Text, World, Player));
Expression textPrintSurvived = (Expression<Func<string, World, Player, string>>)((Text, World, Player) => SomeClass.PrintSurvived(Text, World, Player));
Expression textPrintNotExist = (Expression<Func<string, World, Player, string>>)((Text, World, Player) => SomeClass.PrintNotExists(Text, World, Player));
Expression innerTest =
Expression.Condition(
Expression.Invoke(Expression.Lambda<Func<string, World, Player, bool>>(testExists, inputString, inputWorld, inputPlayer)),
Expression.Assign(result, Expression.Call(methodInfo, result, Expression.Lambda<Func<string, World, Player, string>>(textPrintSurvived, inputString, inputWorld, inputPlayer))),
Expression.Empty());
Expression success =
Expression.Block(
Expression.Assign(result, Expression.Call(methodInfo, result, Expression.Lambda<Func<string, World, Player, string>>(textPrintName, inputString, inputWorld, inputPlayer))),
Expression.Lambda<Action<string, World, Player>>(killPlayer, inputString, inputWorld, inputPlayer),
innerTest);
Expression failure =
Expression.Assign(result, Expression.Call(methodInfo, result, Expression.Lambda<Func<string, World, Player, string>>(textPrintNotExist, inputString, inputWorld, inputPlayer)));
Expression outerTest =
Expression.Condition(
Expression.Invoke(Expression.Lambda<Func<string, World, Player, bool>>(testExists, inputString, inputWorld, inputPlayer)),
success,
failure);
Expression finalExpression =
Expression.Block(
Expression.Assign(result, Expression.Call(methodInfo, result, Expression.Lambda<Func<string, World, Player, string>>(textPrintStarting, inputString, inputWorld, inputPlayer))),
outerTest);
return Expression.Lambda<Func<string, World, Player, string>>(
Expression.Block(new[] { result },
finalExpression)).Compile()(text, world, player);
}
Il problema è con le istruzioni Condition
che generano un errore perché non può convertire da Func a bool. Non sono sicuro che i parametri vengano inoltrati o meno (dato che non sono riuscito a eseguire il debug)
Dopo aver dilettato molto con MethodInfo, ho scoperto che quando stavo scrivendo:
Expression innerTest =
Expression.Condition(
Expression.Invoke(Expression.Lambda<Func<string, World, Player, bool>>(testExists, inputString, inputWorld, inputPlayer)),
Expression.Assign(result, Expression.Call(methodInfo, result, Expression.Lambda<Func<string, World, Player, string>>(textPrintSurvived, inputString, inputWorld, inputPlayer))),
Expression.Empty());
the Expression.Lambda
stava aggiungendo un livello di complessità al mio codice trasformando Func<string, World, Player, string>
in Func<string, World, Player, Func<string, World, Player, string>>
The Expression.Invoke
strappato via questo strato aggiuntivo che inizialmente mi ha confuso. Con questa sorprendente rivelazione l'ho aggiornato per:
Expression innerTest =
Expression.IfThen(
Expression.Invoke(testExists, inputString, inputWorld, inputPlayer),
Expression.Assign(result, Expression.Call(methodInfo, result, Expression.Lambda<Func<string, World, Player, string>>(textPrintSurvived, inputString, inputWorld, inputPlayer))));
Ho provato a esprimere quel codice come espressione. Questo è quello che mi è venuto in mente. Non so se funzioni come previsto per tutti i casi ma compila e sembra funzionare nei miei test.
// reference method
static string Foo(string text, World world, Player player)
{
string output = SomeClass.PrintStarting();
if (SomeClass.Exists(text, world, player))
{
output += SomeClass.PrintName(text, world, player);
SomeClass.KillPlayer(text, world, player);
if (SomeClass.Exists(text, world, player))
output += SomeClass.PrintSurvived(text, world, player);
}
else
output += SomeClass.PrintNotExists(text, world, player);
return output;
}
// helper method
static Expression AddAssignStrings(ParameterExpression left, Expression right)
{
var stringType = typeof(string);
var concatMethod = stringType.GetMethod("Concat", new[] { stringType, stringType });
return Expression.Assign(
left,
Expression.Call(concatMethod, left, right)
);
}
var text = Expression.Parameter(typeof(string), "text");
var world = Expression.Parameter(typeof(World), "world");
var player = Expression.Parameter(typeof(Player), "player");
var output = Expression.Variable(typeof(string), "output");
// looks safe to reuse this array for the expressions
var arguments = new ParameterExpression[] { text, world, player };
var someClassType = typeof(SomeClass);
// assuming the methods are all publicly accessible
var printStartingMethod = someClassType.GetMethod("PrintStarting");
var existsMethod = someClassType.GetMethod("Exists");
var printNameMethod = someClassType.GetMethod("PrintName");
var killPlayerMethod = someClassType.GetMethod("KillPlayer");
var printSurvivedMethod = someClassType.GetMethod("PrintSurvived");
var printNotExistsMethod = someClassType.GetMethod("PrintNotExists");
var ifTrueBlockContents = new Expression[]
{
AddAssignStrings(output, Expression.Call(printNameMethod, arguments)),
Expression.Call(killPlayerMethod, arguments),
Expression.IfThen(
Expression.Call(existsMethod, arguments),
AddAssignStrings(output, Expression.Call(printSurvivedMethod, arguments))
),
};
var blockContents = new Expression[]
{
Expression.Assign(output, Expression.Call(printStartingMethod)),
Expression.IfThenElse(
Expression.Call(existsMethod, arguments),
Expression.Block(ifTrueBlockContents),
AddAssignStrings(output, Expression.Call(printNotExistsMethod, arguments))
),
output,
};
var body = Expression.Block(typeof(string), new ParameterExpression[] { output }, blockContents);
var lambda = Expression.Lambda<Func<string, World, Player, string>>(body, arguments);
Ecco la vista di debug dell'espressione:
.Lambda #Lambda1<System.Func`4[System.String,Test.World,Test.Player,System.String]>(
System.String $text,
Test.World $world,
Test.Player $player) {
.Block(System.String $output) {
$output = .Call Test.SomeClass.PrintStarting();
.If (
.Call Test.SomeClass.Exists(
$text,
$world,
$player)
) {
.Block() {
$output = .Call System.String.Concat(
$output,
.Call Test.SomeClass.PrintName(
$text,
$world,
$player));
.Call Test.SomeClass.KillPlayer(
$text,
$world,
$player);
.If (
.Call Test.SomeClass.Exists(
$text,
$world,
$player)
) {
$output = .Call System.String.Concat(
$output,
.Call Test.SomeClass.PrintSurvived(
$text,
$world,
$player))
} .Else {
.Default(System.Void)
}
}
} .Else {
$output = .Call System.String.Concat(
$output,
.Call Test.SomeClass.PrintNotExists(
$text,
$world,
$player))
};
$output
}
}