CreateCzęść NR.6 Pomówmy o wyrażeniach drzewiastych raz jeszcze. Wyrażenia drzewiaste są naprawdę potężnym narzędziem, ponieważ traktują kod jak dane. W poprzednim wpisie przyjrzeliśmy się jak wyrażenia drzewiaste są zbudowane i jak je wykorzystać.
W tym wpisie skoncentrujemy się na tworzeniu wyrażeń, jak i ich zmianie.
Tematyka ta jest dosyć obszerna dlatego postanowiłem przygotować tylko parę ciekawych przykładów, które wykazują pewną użyteczność tworzenia dynamicznych wyrażeń.
1. Wyrażenie drzewiaste, które wywołuje delegate
Zacznijmy od czegoś prostego. Oto wyrażenie drzewiaste, które wywołuje przygotowaną wcześniej delegatę.
Action<int> func = i => Console.WriteLine(i * i);
var callExpr = Expression.Call(Expression.Constant(func.Target),
func.Method, Expression.Constant(3));
var lambdaExpr = Expression.Lambda<Action>(callExpr);
var fun = lambdaExpr.Compile();
fun(); //9
Stworzyłem więc wyrażenie, które jest wywołaniem danej delegaty z określonymi już parametrami. Później to wyrażenie mogę skompilować i wykonać.
Action<int,int> func2 = (i,j) => Console.WriteLine(i * j);
int a = 3;
var callExpr2 = Expression.Call(Expression.Constant(func2.Target),
func2.Method, Expression.Constant(a), Expression.Constant(7));
var lambdaExpr2 = Expression.Lambda<Action>(callExpr2);
var fun2 = lambdaExpr2.Compile();
fun2();
Oto jak dostarczyć większą liczbę parametrów, jeśli delegata tego wymaga.
2. Generowanie wyrażenia drzewiastego z wyrażeniem Where
Innym ciekawym zastosowaniem jest stworzenie wyrażenia drzewiastego, które będzie reprezentowało wyrażenie wewnątrz metody Where przy filtrowaniu kolekcji.
public class PersonEntity
{
public string Name { get; set; }
public Hair PersonHair { get; set; }
}
Na potrzeby przykładu mam taką encję.
public static Expression<Func<T, bool>>
DynamicWhere<T>(object value, string nameProperty)
{
var item = Expression.Parameter(typeof(T), "item");
var prop = Expression.Property(item, nameProperty);
var consVal = Expression.Constant(value);
var equal = Expression.Equal(prop, consVal);
var lambda = Expression.Lambda<Func<T, bool>>(equal, item);
return lambda;
}
A oto funkcja, która krok po kroku tworzy dynamicznie wyrażenie Lambda, które będzie można wkleić do metody LINQ Where.
W tym wypadki sprawdzam, czy wartość jest równa, ale równie dobrze mógłbym tutaj wstawić coś innego niż wyrażenie przyrównania. Chociaż oczywiście łatwo jest porównywać, gdy nie wiesz co dokładnie będzie porównane w tym wyrażeniu.
Przykładowo wyrażenie mniejsze, równe nie ma sensu dla typu String.
IQueryable<PersonEntity> list = new List<PersonEntity>()
{
new PersonEntity() {Name ="Kl" },
new PersonEntity() {Name ="Cez" },
}.AsQueryable(); ;
var lambda = DynamicWhere<PersonEntity>("Cez", "Name");
var result = list.Where(lambda);
var listre = result.ToList();
Ten kod zwróci mi wszystkie osoby, które mają imię Cez.
3. Wyrażenie z string.contains
Chciałbyś utworzyć dynamicznie wyrażenie używając metody string.contains, ale nie wiesz do jakiej klasy i do jakiej właściwości. No cóż, teraz już możesz przy pomocy tego kodu.
static Expression<Func<T, bool>> GetStringContainsExpression<T>
(string propertyName, string propertyValue)
{
var parameterExp = Expression.Parameter(typeof(T), "type");
var propertyExp = Expression.Property(parameterExp, propertyName);
MethodInfo method = typeof(string).GetMethod("Contains", new[] { typeof(string) });
var someValue = Expression.Constant(propertyValue, typeof(string));
var containsMethodExp = Expression.Call(propertyExp, method, someValue);
return Expression.Lambda<Func<T, bool>>(containsMethodExp, parameterExp);
}
Użycie jest następujące. Deklaruję, że chcę stworzyć wyrażenie odnoszące się do klasy PersonEnity i do właściwości Name.
To wrażenie sprawdzi, czy dany obiekt PersonEnity zawiera w swojej nazwie słowo “Cez”.
var ex = GetStringContainsExpression<PersonEntity>("Name", "Cez");
var per1 = new PersonEntity() { Name = "Cez" };
var per2 = new PersonEntity() { Name = "Kat" };
var per3 = new PersonEntity() { Name = "KatCez" };
var method = ex.Compile();
var r1 = method.Invoke(per1);
var r2 = method.Invoke(per2);
var r3 = method.Invoke(per3);
Jak widać tylko 1 i 3 osoba zawierają słowo Cez.
Bardzo ciekawy przykład, zwłaszcza że nic nie stoi na przeszkodzie, abyś mógł określić wywołanie innej metody niż string.contains.
4.Zmiana typu wyrażenia z jednego typu w drugi przy pomocy konwersji
Czas na bardziej zaawansowany przykład. Mamy dwie klasy. Jednak z nich reprezentuje osobę, a druga zwierzę. Obie klasy mają wiek.
public class PersonEntity
{
public int Age { get; set; }
}
public class AnimalEnity
{
public int Age { get; set; }
}
Mam do dyspozycji dwa wyrażenia. Pierwsza z nich określa wyrażenie sprawdzające, czy dana osoba ma więcej niż 18 lat. Drugie wyrażenie konwertuje zwierzę na osobę mnożąc jego wiek czterokrotnie.
Expression<Func<PersonEntity, bool>> predicate =
per => per.Age > 18;
Expression<Func<AnimalEnity,PersonEntity>> convert =
animal => new PersonEntity() { Age = animal.Age * 4 };
Mając te dwa wyrażenia chcę je razem połączyć w następujący sposób. Chcę stworzyć wyrażenie, które będzie sprawdzało czy dane zwierzę ma 18 ludzkich lat. Aby to zrobić muszę stworzyć wyrażenie, które da mi możliwość umieszczania zwierząt.
var param = Expression.Parameter(typeof(AnimalEnity), "AnimalEnity");
var body = Expression.Invoke(predicate,
Expression.Invoke(convert, param));
var lambda = Expression.Lambda<Func<AnimalEnity, bool>>(body, param);
var func_ = lambda.Compile();
bool with1= func_(new AnimalEnity() { Age = 1 }),
with5 = func_(new AnimalEnity() { Age = 5 }),
with4 = func_(new AnimalEnity() { Age = 4 });
Wewnątrz tego wyrażenia nastąpi konwersja z encji zwierzęcia na człowieka z pomnożonym wiekiem.
Czas sprawdzić czy nowa funkcja działa poprawienie. Zwierzę o wieku 1 jako człowiek będzie miało 4 lata, więc warunek osiemnastu lat nie jest spełniony. Zwierzę o wieku 5 lat jednak ma już 20 ludzkich lat i warunek zostaje już spełniony
5. Łączenie dwóch wrażeń lambda z różnymi parametrami
Jak połączyć dwa wyrażenie logiczne w jedno nowe.
Jedyny problem związany ze scalaniem dwóch wyrażeń polega na braku spójności dwóch parametrów lambda.
Dlatego będzie nam potrzebny mały pomocnik – odwiedzający wyrażenia, który rozwiąże ten problem i stworzy jeden wspólny parametr lambda.
public class ParameterReplacer : ExpressionVisitor
{
private readonly ParameterExpression _parameter;
protected override Expression VisitParameter(ParameterExpression node)
{
return base.VisitParameter(_parameter);
}
public ParameterReplacer(ParameterExpression parameter)
{
_parameter = parameter;
}
}
Mając wspólny parametr lambda teraz oba wyrażenia logiczne może przykładowo złączyć operacja logiczna LUB.
Expression<Func<PersonEntity, bool>> expr1 = s2 => s2.Age > 18;
Expression<Func<PersonEntity, bool>> expr2 = s3 => s3.Name.Length > 5;
var paramExpr_ = Expression.Parameter(typeof(PersonEntity));
var exprBody_ = Expression.Or(expr1.Body, expr2.Body);
exprBody_ = (BinaryExpression)new ParameterReplacer(paramExpr_).Visit(exprBody_);
var finalExpr = Expression.Lambda<Func<PersonEntity, bool>>(exprBody_, paramExpr_);
var methodCombine = finalExpr.Compile();
Sprawdźmy czy to działa. Warunek zostanie spełniony, jeśli imię jest dłuższe niż 5 znaków lub osoba ma więcej niż 18 lat.
var pers1 = new PersonEntity() { Name = "Cez", Age =21 };
var pers2 = new PersonEntity() { Name = "Kat", Age = 17 };
var pers3 = new PersonEntity() { Name = "KatCez", Age = 10 };
var re1 = methodCombine(pers1);
var re2 = methodCombine(pers2);
var re3 = methodCombine(pers3);
Tak wszystko jest okej,
6. Utworzenie wyrażenia, które ustawia pole i właściwość
Na koniec zostawiłem utworzenie wyrażenia, które będzie ustawiało dane pole i właściwość. Oczywiście zabawa polega na tym, że to wyrażenie tworzone jest dynamicznie i to my określamy, od jakiej klasy i od jakiego pola wartość ma być ustawiona.
Analogicznie później to zrobimy z właściwością.
public static Expression<Action<T, string>> GetExpressionThatSettersField<T>
(string fieldName, Type type)
{
FieldInfo field = typeof(T).GetField(fieldName);
ParameterExpression targetExp = Expression.Parameter(typeof(T), "target");
ParameterExpression valueExp = Expression.Parameter(type, "value");
// Expression.Property can be used here as well
MemberExpression fieldExp = Expression.Field(targetExp, field);
BinaryExpression assignExp = Expression.Assign(fieldExp, valueExp);
var setter = Expression.Lambda<Action<T, string>>
(assignExp, targetExp, valueExp);
return setter;
}
Do testu będzie potrzebna mi pewna klasa.
public class FieldTest
{
public string field;
public string Prop { get; set; }
}
Teraz mogę ustawić pole przy pomocy wyrażenia
var expression = GetExpressionThatSettersField<FieldTest>("field", typeof(string));
var ft = new FieldTest();
expression.Compile().Invoke(ft, "nowa wartosc");
var letsee = ft.field;
Po małych zmianach taki sam kod mogę napisać do tworzenia wyrażenia, które będzie ustawiało daną właściwość określoną wartością.
public static Expression<Action<T, string>> GetExpressionThatSettersProperty<T>
(string propertyName, Type type)
{
PropertyInfo field = typeof(T).GetProperty(propertyName);
ParameterExpression targetExp = Expression.Parameter(typeof(T), "target");
ParameterExpression valueExp = Expression.Parameter(type, "value");
MemberExpression fieldExp = Expression.Property(targetExp, field);
BinaryExpression assignExp = Expression.Assign(fieldExp, valueExp);
var setter = Expression.Lambda<Action<T, string>>
(assignExp, targetExp, valueExp);
return setter;
}
Przetestujmy kod ponownie.
var expression2 = GetExpressionThatSettersProperty<FieldTest>
("Prop", typeof(string));
var pt = new FieldTest();
expression2.Compile().Invoke(pt, "nowa era");
var letsee2 = pt.Prop;
Oba przypadki działają poprawnie.
To byłoby na tyle, jeśli chodzi o wyrażenia drzewiaste. W końcu możemy przejść do prawdziwych smakołyków języków funkcjonalnych, które też są w C#.
Do zobaczenia w Currying-u.