Построение предиката через Expression tree
От: rFLY  
Дата: 01.03.23 14:04
Оценка:
Хочу создать аналог предиката как в list.Where(x => ReferenceEquals(x.barObj, likeObj.barObj));

class Bar {...}
class Baz {...}
class Foo
{
    // Разные экземпляры Foo могут содержать barObj или bazObj ссылающиеся на один и тот же объект
    public Bar barObj = new Bar();
    public Baz bazObj = new Baz();
}

public List<Foo> list = ...

public Foo likeObj = ...

public Func<Foo, bool> FilterPredicate(propName)
{
    // Пробую воссоздать чтобы было так: x => ReferenceEquals(x.barObj, likeObj.barObj);
    var listItem = Expression.Parameter(typeof(Foo), "x");
    // Как в Expression записать ссылку на внешний объект likeObj? Я сделал через Expression.Constant, но не уверен, что это правильно, хотя работает.
    var likeItem = Expression.Constant(likeObj);
    
    var equals = Expression.Call(typeof(object), "ReferenceEquals", null, new Exception[] {
        Expression.Field(listItem, propName),
        Expression.Field(likeItem, propName)
    });
    var lambda = Exception.Lambda<Func<Foo, bool>>(equals, listItem);
    return lambda.Compile();
}

...

// Потом использовать так
list.Where(FilterPredicate("barObj"));
// или
list.Where(FilterPredicate("bazObj"));
Отредактировано 02.03.2023 12:29 rFLY . Предыдущая версия .
Re: Построение предиката через Expression tree
От: JohnnyJ Германия  
Дата: 01.03.23 20:18
Оценка:
Здравствуйте, rFLY, Вы писали:

FLY>Хочу создать аналог предиката как в list.Where(x => ReferenceEquals(x.barObj, likeObj.barObj));


FLY> // Как в Expression записать ссылку на внешний объект likeObj? Я сделал через Expression.Constant, но не уверен, что это правильно, хотя работает.


так делать не стоит, лучше сделать делегат с двумя параметрами (см. ниже):

public class Bar {}
public class Baz {}

public class Foo
{
    public Foo(string name, Bar barObj, Baz bazObj)
    {
        Name = name;
        BarObj = barObj;
        BazObj = bazObj;
    }

    public string Name { get; }
    public Bar BarObj { get; }
    public Baz BazObj { get; }

    public override string ToString()
    {
        return $"{nameof(Name)}: {Name}"; // просто для удобства тестирования
    }
}

public static class FilterBuilder
{
    // ссылка на метод ReferenceEquals резолвится один раз и кэшируется
    private static readonly MethodInfo ReferenceEqualsMethod =
        typeof(object).GetMethod(nameof(ReferenceEquals), BindingFlags.Static | BindingFlags.Public)
        ?? throw new InvalidOperationException();

    // создает делегат с двумя параметрами - объектами для сравнения
    public static Func<Foo, Foo, bool> CreateFilter(string name)
    {
        var leftPar = Expression.Parameter(typeof(Foo), "left");
        var rightPar = Expression.Parameter(typeof(Foo), "right");

        var body = Expression.Call(ReferenceEqualsMethod,
                                   Expression.PropertyOrField(leftPar, name),
                                   Expression.PropertyOrField(rightPar, name));

        var lambda = Expression.Lambda<Func<Foo, Foo, bool>>(body, leftPar, rightPar);
        var predicate = lambda.Compile();
        return predicate;
    }
}


и потом соответственно использование:

var bar1 = new Bar();
var bar2 = new Bar();

var baz1 = new Baz();
var baz2 = new Baz();

List<Foo> fooList = new()
                        {
                            new Foo("obj1", bar1, baz1),
                            new Foo("obj2", bar1, baz2),
                            new Foo("obj3", bar2, baz1),
                            new Foo("obj4", bar2, baz2),
                        };

var likeObj = new Foo("like", bar1, baz1);

// фильтры лучше положить в переменные, иначе они будут компилироваться для каждого элемента коллекции, что просадит быстродействие
var filter1 = FilterBuilder.CreateFilter(nameof(Foo.BarObj));
var filter2 = FilterBuilder.CreateFilter(nameof(Foo.BazObj));

Console.WriteLine(String.Join(", ", fooList.Where(x => filter1(likeObj, x))));
Console.WriteLine(String.Join(", ", fooList.Where(x => filter2(likeObj, x))));


Вывод программы:

Name: obj1, Name: obj2
Name: obj1, Name: obj3
Зри в корень!
Re[2]: Построение предиката через Expression tree
От: rFLY  
Дата: 02.03.23 08:23
Оценка:
Здравствуйте, JohnnyJ, Вы писали:

JJ>так делать не стоит, лучше сделать делегат с двумя параметрами (см. ниже):

А поподробнее можно почему так делать не стоит?

У Foo свойств, по которым должен быть фильтр, 6 штук и заранее не известно по какому нужно будет его применить (надо было сразу наверное сказать, но не хотел перегружать лишней информацией). Определять по какому применять фильтр планировал так:
enum FilterProp { barObj, bazObj, ..., booObj}

...

public void DoSomething(FilterProp filterProp)
{
    var query = list.Where(FilterPredicate(filterProp.ToString()));
    ...
}

Если же создавать делегаты, как ты предлагаешь, то и Expression уже лишний (я не планирую вместо likeObj использовать другой объект для сравнения) и можно просто написать:
Func<Foo, bool> filter1 = x => ReferenceEquals(likeObj.barObj, x.barObj);

Так ведь или я что-то упускаю? А потом в свитче определять какой делегат использовать или сохранить делегаты в справочнике и доставать их по ключу.
Но хотелось избежать всего этого и решить одним Expression. Или построение и компиляция Expression при каждом фильтре слишком затратно будет?
Re: Построение предиката через Expression tree
От: Ночной Смотрящий Россия  
Дата: 02.03.23 08:39
Оценка:
Здравствуйте, rFLY, Вы писали:

FLY> // Как в Expression записать ссылку на внешний объект likeObj?


https://learn.microsoft.com/en-us/dotnet/csharp/expression-trees-execution#caveats
"Внешний объект" по правильному называется closure.
... << RSDN@Home 1.3.17 alpha 5 rev. 62>>
Re[2]: Построение предиката через Expression tree
От: rFLY  
Дата: 02.03.23 12:27
Оценка:
Здравствуйте, Ночной Смотрящий, Вы писали:

НС>"Внешний объект" по правильному называется closure.

Так то оно так, но как все же правильно привести объект к Expression чтобы его использовать в Expression.Call и в результате получить:
Func<Foo, bool> filter1 = x => ReferenceEquals(likeObj.barObj, x.barObj);
Отредактировано 02.03.2023 12:29 rFLY . Предыдущая версия .
Re[3]: Построение предиката через Expression tree
От: Ночной Смотрящий Россия  
Дата: 02.03.23 12:54
Оценка: +1
Здравствуйте, rFLY, Вы писали:

FLY>Так то оно так, но как все же правильно привести объект к Expression чтобы его использовать в Expression.Call и в результате получить:


Expression.Constant нормально
... << RSDN@Home 1.3.17 alpha 5 rev. 62>>
Re[3]: Построение предиката через Expression tree
От: JohnnyJ Германия  
Дата: 02.03.23 18:25
Оценка: 1 (1)
Здравствуйте, rFLY, Вы писали:

FLY>А поподробнее можно почему так делать не стоит?


замыкание — это милая зверушка, но которая может потенциально вырасти в злобного монстра — неприятные побочные эффекты типа утечки памяти.

FLY>У Foo свойств, по которым должен быть фильтр, 6 штук и заранее не известно по какому нужно будет его применить (надо было сразу наверное сказать, но не хотел перегружать лишней информацией). Определять по какому применять фильтр планировал так:

FLY>
FLY>enum FilterProp { barObj, bazObj, ..., booObj}

FLY>...

FLY>public void DoSomething(FilterProp filterProp)
FLY>{
FLY>    var query = list.Where(FilterPredicate(filterProp.ToString()));
FLY>    ...
FLY>}
FLY>

FLY>Если же создавать делегаты, как ты предлагаешь, то и Expression уже лишний (я не планирую вместо likeObj использовать другой объект для сравнения) и можно просто написать:
FLY>
FLY>Func<Foo, bool> filter1 = x => ReferenceEquals(likeObj.barObj, x.barObj);
FLY>

FLY>Так ведь или я что-то упускаю? А потом в свитче определять какой делегат использовать или сохранить делегаты в справочнике и доставать их по ключу.
FLY>Но хотелось избежать всего этого и решить одним Expression. Или построение и компиляция Expression при каждом фильтре слишком затратно будет?

не то чтобы это совсем уж лишняя информация
при одном типе и 6 свойствах я бы не стал расчехлять мега-гаубицу, а просто сделал бы метод и switch без всяких делегатов:

public static class FooMatchExtensions
{
    public static bool Matches(this Foo left, Foo right, string propertyName)
    {
        return propertyName switch
            {
                nameof(Foo.BarObj) => ReferenceEquals(left.BarObj, right.BarObj),
                nameof(Foo.BazObj) => ReferenceEquals(left.BazObj, right.BazObj),
                _ => false
            };
    }
}


и дальше совсем просто:

Console.WriteLine(String.Join(", ", fooList.Where(x => likeObj.Matches(x, nameof(Foo.BarObj)))));
Console.WriteLine(String.Join(", ", fooList.Where(x => likeObj.Matches(x, nameof(Foo.BazObj)))));


выводит аналогично предыдущему:

Name: obj1, Name: obj2
Name: obj1, Name: obj3
Зри в корень!
Re: Построение предиката через Expression tree
От: VladD2 Российская Империя www.nemerle.org
Дата: 03.03.23 16:57
Оценка: 3 (1)
Здравствуйте, rFLY, Вы писали:

FLY>Хочу создать аналог предиката как в list.Where(x => ReferenceEquals(x.barObj, likeObj.barObj));


Дам тебе универсальный совет. Напиши этот код на Шарпе, а потом декомпельни его ILSpy-ем. В нем можно установить версию языка. Если установить версию до той, что поддерживала Expression tree, ты получишь декомпилят, который после мелких доработок сможешь использовать в своём коде.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Отредактировано 13.03.2023 13:02 VladD2 . Предыдущая версия .
Re[4]: Построение предиката через Expression tree
От: rFLY  
Дата: 12.03.23 19:14
Оценка:
Здравствуйте, JohnnyJ, Вы писали:

JJ>замыкание — это милая зверушка, но которая может потенциально вырасти в злобного монстра — неприятные побочные эффекты типа утечки памяти.

Об я по js знаю. Но в данном случае я не вижу проблем.

JJ>не то чтобы это совсем уж лишняя информация

В рамках моего вопроса она все же лишняя. Да и задачка посложнее чем where с одним условием. Если ее выложить полностью, боюсь мало кто бы стал вчитываться, скорее пройдут мимо.

JJ>при одном типе и 6 свойствах я бы не стал расчехлять мега-гаубицу

Гаубица — это написание Expression, его компиляция или еще что?

JJ>, а просто сделал бы метод и switch без всяких делегатов:

Как я уже сказал все несколько сложнее, по этому я и решил попробовать через Expression. Но если интересно вот листинг с 3-мя вариантами решения (он опять же не полный, но примерно обрисовывает задачу)
Re[4]: Построение предиката через Expression tree
От: rFLY  
Дата: 12.03.23 20:38
Оценка:
Здравствуйте, Ночной Смотрящий, Вы писали:

НС>Expression.Constant нормально

Я почему спросил. Когда при помощи Expression.DebugView вывел законченное выражение и сравнил его с выражением предиката, то получил:
var debugView = typeof(Expression).GetProperty("DebugView", BindingFlags.Instance | BindingFlags.NonPublic);

// Expression
var recordExpression = Expression.Parameter(typeof(Record), "record");
var expr2 = Expression.Lambda<Func<Record, bool>>(Expression.Call(typeof(object), "ReferenceEquals", null, new Expression[] {
    Expression.Field(recordExpression, "f2"),
    Expression.Field(Expression.Constant(recordList.currentRecord), "f2")
}), recordExpression);
// Вывод в консоль
.Lambda #Lambda1<System.Func`2[ConsoleApp.Record,System.Boolean]>(ConsoleApp.Record $record) {
    .Call System.Object.ReferenceEquals(
        $record.f2,
        .Constant<ConsoleApp.Record>(#4: 1, 2, 4).f2)
}

// Предикат
Expression<Func<Record, bool>> expr1 = record => ReferenceEquals(record.f2, recordList.currentRecord.f2);
Console.WriteLine(debugView.GetValue(expr1));
// Вывод в консоль
.Lambda #Lambda1<System.Func`2[ConsoleApp.Record,System.Boolean]>(ConsoleApp.Record $record) {
    .Call System.Object.ReferenceEquals(
        $record.f2,
        ((.Constant<ConsoleApp.Program+<>c__DisplayClass1_0>(ConsoleApp.Program+<>c__DisplayClass1_0).recordList).currentRecord).f2)
}

Последние строки отличаются, причем в случае с Expression так же показывается результат переопределенной ToString()

Вот тут можно запустить и посмотреть
Re[5]: Построение предиката через Expression tree
От: JohnnyJ Германия  
Дата: 12.03.23 22:47
Оценка:
Здравствуйте, rFLY, Вы писали:

FLY>Здравствуйте, JohnnyJ, Вы писали:


JJ>>замыкание — это милая зверушка, но которая может потенциально вырасти в злобного монстра — неприятные побочные эффекты типа утечки памяти.

FLY>Об я по js знаю. Но в данном случае я не вижу проблем.
Да, пока проблем нет, я больше имел в виду общий случай — замыкания потенциально могут привести к проблемам.
Ну и как подсказывает опыт, лучше вообще с ними не связываться в динамических лямбдах, это дает 100% гарантию отсутствия проблем в будущем

JJ>>не то чтобы это совсем уж лишняя информация

FLY>В рамках моего вопроса она все же лишняя. Да и задачка посложнее чем where с одним условием. Если ее выложить полностью, боюсь мало кто бы стал вчитываться, скорее пройдут мимо.

JJ>>при одном типе и 6 свойствах я бы не стал расчехлять мега-гаубицу

FLY>Гаубица — это написание Expression, его компиляция или еще что?

да, гаубица — это Expression и компиляция.
Динамическая компиляция лямбд не самая очевидная штука — в простых случаях не нужна, в сложных — мощно, но требует тщательности в написании и очень плотного покрытия тестами.
Т.е. например в наличии гибкая структура данных, описываемая пользователем самостоятельно, и необходимость конструирования запросов пользователем же в рантайме.
Или свой DSL с движком по обработке данных по сильно кастомной бизнес-логике.

Второй вариант — именно то, с чем мне приходится иметь дело на основной работе. От лямбд стараемся сейчас отказаться в пользу генератора кода — есть набор правил на собственном языке, из него генерится пачка классов с известным интерфейсом, а в рантайме просто через интерфейс запихивается входной объект и получается результат.

JJ>>, а просто сделал бы метод и switch без всяких делегатов:

FLY>Как я уже сказал все несколько сложнее, по этому я и решил попробовать через Expression. Но если интересно вот листинг с 3-мя вариантами решения (он опять же не полный, но примерно обрисовывает задачу)

глянул код — не самая очевидная структура данных, не совсем понял зачем шарить инстансы объектов Field. Если Record — просто динамическая структура, то словарика вроде должно хватать, или?

В целом вариант с лямдами хорош всем, кроме одного — перфоманс компиляции. Если поиск операция не частая, то и проблемы нет как таковой.
Также важны подробности применения и перспективы роста количества юзкейсов — завтра понадобится поиск по двум колонкам, сравнение без учета регистра для строковых полей, сравнение DateTime без учета времени — реальная жизнь и хотелки пользователей безграничны

В целом тема интересная, если не хочется разворачивать подробности здесь — стучись в личку, обсудим.
Зри в корень!
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.