Разница в выражении‹Func‹T,bool››

Только быстро и коротко, на этот раз. Func<T,TResult> является контравариантным (ИЗМЕНИТЬ : параметр типа T). Сейчас я работаю не с Func<T,TResult>, а с Expression<Func<T,TResult>> и, похоже, зашел в тупик. ОБНОВЛЕНИЕ – ПОЛНЫЙ ОБРАЗЕЦ КОДА:

public interface IColoredObject
{
    string Color { get; }
}

public class Item : IColoredObject
{
    public string Color { get; set; }

    public double Price { get; set; }
}

public partial class MainWindow : Window
{
    private IList<Item> _items;

    public IList<Item> Items
    {
        get
        {
            if (_items == null)
            {
                _items = new List<Item>();
                _items.Add(new Item() { Color = "black" });
                _items.Add(new Item() { Color = "blue" });
                _items.Add(new Item() { Color = "red" });
            }
            return _items;
        }
    }

    public MainWindow()
    {
        InitializeComponent();
        Expression<Func<IColoredObject, bool>> filter = x => x.Color == "black";
        Item i = Get(filter);
    }

    public Item Get(Expression<Func<Item, bool>> filter)
    {
        return Items.AsQueryable().Where(filter).FirstOrDefault();
    }
}

Вызов выполняется с использованием Expression<Func<IColoredObject, bool>> в качестве аргумента и должен, если я правильно понял контравариантность, работать, потому что IColoredObject является менее производным, чем Item.

Я получаю исключение преобразования, говорящее что-то вроде

не может конвертировать

System.Linq.Expressions.Expression`1[System.Func`2[MyNs.IColoredObject,System.Boolean]]

To

System.Linq.Expressions.Expression`1[System.Func`2[MyNs.Item,System.Boolean]]

Есть ли способ исправить это и заставить его работать?

ИЗМЕНИТЬ:

Поскольку в том, что я сказал, есть некоторая неточность, вот еще предыстория. Образец кода обновлен. Кроме того, я проверил, что MSDN говорит о Func<T, TRes>:

public Item GetFunc(Func<Item, bool> filter)
{
    return Items.AsQueryable().Where(filter).FirstOrDefault();
}

Как указано MS, это можно использовать с контравариантным параметром типа, как указано ниже:

 Func<IColoredObject, bool> filterFunc = x => x.Color == "black";
 GetFunc(filterFunc);

Что снова заставляет меня задаться вопросом, почему это работает для Func<T, TRes>, но не для Expression<Func<T, TRes>>...

НАКОНЕЦ...

Проверенный ответ был выбран, потому что это то, что я в конечном итоге сделал. Как я сказал где-то в комментариях ниже, метод Get использует NHibernate для получения данных. Но очевидно, что NHibernate имеет возможность принимать запросы через интерфейс и автоматически выбирать типы, реализующие интерфейс. Это не решает саму проблему, но, как вы можете прочитать ниже, реального решения нет, поскольку то, что здесь встречается, было ожидаемым поведением.


person Sebastian Edelmeier    schedule 12.07.2012    source источник
comment
Где определен ваш метод? С каким выражением вы это называете? На каком объекте вы его вызываете?   -  person Richard    schedule 12.07.2012
comment
Метод находится в классе на моем уровне доступа к данным. Я изолирую проблему и вставлю сюда исходный код.   -  person Sebastian Edelmeier    schedule 12.07.2012
comment
Сказать, что Func<T,TResult> контравариантно, немного неправильно. Можно сказать, что он контравариантен по одному параметру своего типа (и ковариантен по другому).   -  person Damien_The_Unbeliever    schedule 12.07.2012
comment
Я думаю, что ваше предположение о Func правильное, однако, как только оно будет заключено в выражение, я думаю, что оно больше не будет иметь значения. Вы пробовали тестировать это без использования выражения, а только для проверки делегатов func.   -  person stevethethread    schedule 12.07.2012
comment
@SteveSolomon: Да, проверил это в моем редактировании. Данг.   -  person Sebastian Edelmeier    schedule 12.07.2012
comment
@SebastianEdelmeier: Зачем вам нужны выражения, а не простые делегаты?   -  person Konstantin Oznobihin    schedule 12.07.2012
comment
Передайте выражение в NHibernate. Н.Х. Не могу работать с делегатами.   -  person Sebastian Edelmeier    schedule 12.07.2012
comment
@SebastianEdelmeier: похоже, вам действительно придется изменить типы ваших деревьев выражений.   -  person Konstantin Oznobihin    schedule 12.07.2012


Ответы (4)


Эта строка:

public Item Get(Expression<Func<Item, bool>> filter) { /* ... */  }

должно быть:

public Item Get(Expression<Func<IColoredObject, bool>> filter) { /* ... */  }

В этом случае вам придется работать с интерфейсом, если вы хотите вызвать метод Get, передавая Expression<Func<IColoredObject, bool>>.

person Leniel Maccaferri    schedule 12.07.2012
comment
Это был мой первоначальный ход мыслей, но он мог фильтровать по методам или свойствам объекта типа Item. - person Richard; 12.07.2012
comment
На самом деле, это невозможно в моем сценарии. Метод передает Expression<Func<Item, Bool>> в NHibernate, который требует, чтобы Item был сопоставленным классом. - person Sebastian Edelmeier; 12.07.2012
comment
Итак, вам придется передать Expression<Func<Item, bool>> вместо Expression<Func<IColoredObject, bool>>. - person Leniel Maccaferri; 12.07.2012
comment
Это возвращает меня к тому состоянию, в котором я был до того, как задал вопрос... Это невозможно. Метод определен на моем уровне доступа к данным и вызывается из бизнес-логики с выражением, загруженным интерфейсом, которое он получает из пользовательского интерфейса... Итак, если я не найду способ преобразования Expression<Func<Item, bool>> в Expression<Func<IColoredObject, bool>>, это не будет быть доступным... - person Sebastian Edelmeier; 12.07.2012

Expression<TDelegate> - это класс, поэтому он не может иметь отклонений для общих параметров. Между Delegate и Expression<Delegate> также есть большая разница. Хотя вы можете передать объект Item в Func<IColoredObject, bool> и, таким образом, преобразовать Func<IColoredObject, bool> в Func<Item, bool>, Expression<Func<Item, bool>> точно так же, как код метода, принимающего Item и возвращающего логическое значение. Вы можете анализировать и изменять этот код, добавляя Item специальные методы и свойства, очевидно, что это невозможно для кода, работающего с IColoredObject.

Возможно изменить все записи параметра IColoredObject в объекте Expression<Func<IColoredObject, bool>> на Item с помощью ExpressionVisitor. Ниже приведен посетитель, который выполняет такое преобразование в простых случаях (т.е. без явных реализаций интерфейса). Возможно, есть гораздо более простое решение вашей проблемы, но его сложно найти, не зная подробностей.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

interface IGizmo
{
    bool Frobnicate();
}

class Gizmo : IGizmo
{
    public bool Frobnicate()
    {
        Console.WriteLine("Gizmo was frobnicated!");

        return true;
    }
}

public sealed class DelegateConversionVisitor : ExpressionVisitor
{
    IDictionary<ParameterExpression, ParameterExpression> parametersMap;

    public static Expression<Func<T2, TResult>> Convert<T1, T2, TResult>(Expression<Func<T1, TResult>> expr)
    {
        var parametersMap = expr.Parameters
            .Where(pe => pe.Type == typeof(T1))
            .ToDictionary(pe => pe, pe => Expression.Parameter(typeof(T2)));

        var visitor = new DelegateConversionVisitor(parametersMap);
        var newBody = visitor.Visit(expr.Body);

        var parameters = expr.Parameters.Select(visitor.MapParameter);

        return Expression.Lambda<Func<T2, TResult>>(newBody, parameters);
    }

    public DelegateConversionVisitor(IDictionary<ParameterExpression, ParameterExpression> parametersMap)
    {
        this.parametersMap = parametersMap;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return base.VisitParameter(this.MapParameter(node));
    }

    private ParameterExpression MapParameter(ParameterExpression source)
    {
        var target = source;
        this.parametersMap.TryGetValue(source, out target);

        return target;
    }
}

class Program
{
    static void Main()
    {
        Expression<Func<IGizmo, bool>> expr = g => g.Frobnicate();

        var e2 = DelegateConversionVisitor.Convert<IGizmo, Gizmo, bool>(expr);

        var gizmo = new Gizmo();
        e2.Compile()(gizmo);
    }
}

person Konstantin Oznobihin    schedule 12.07.2012

C# имеет ковариантность и контравариантность только для типов interface и delegate. Например, это будет работать:

IEnumerable<Func<IColoredObject, bool>> ie1 = XXX;
IEnumerable<Func<Item, bool>> ie2 = ie1;                // works!

Обратите внимание, что в приведенном выше примере назначение ie2 = ie1 выполняется успешно, даже если типы не совпадают. Это связано с тем, что Func<T, TResult> является контравариантным ("in") в своем первом параметре типа T, и IEnumerable<T> является ковариантным ("out") в своем T. Здесь Func<,> — делегат, а IEnumerable<> — интерфейс.

Но Expression<TDelegate> — это класс. C# не поддерживает ковариантность/контравариантность для типов классов.

Поэтому Expression<Something> никогда не преобразуется в Expression<SomethingAlmostTheSame>.

person Jeppe Stig Nielsen    schedule 12.07.2012
comment
Спасибо, Йеппе, это действительно добавляет мне понимания! - person Sebastian Edelmeier; 13.07.2012

Я всегда путаюсь, когда пытаюсь вычислить ковариантность и контравариантность. Но ваш пример кажется мне понятным. Для вашего метода Get(...) требуется выражение с типом Item. Поскольку Item является производным от IColoredObject, вы не можете использовать выражение с типом IColoredObject, только с типом Item или типами, производными от Item. IColoredObject является «более абстрактным», и поэтому все, что ожидает тип Item (или его производное), не может с ним работать.

Спросите себя: предположим на мгновение, что Item определил свойство X, которого нет у IColoredObject, тогда как выражение с типом IColoredObject будет определять свойство X?

person Maarten    schedule 12.07.2012
comment
Я просто был сбит с толку, потому что MSDN сказал, что это контравариантно для T, но, насколько я понимаю, то, что вы говорите, кажется правдоподобным: все, что ковыляет и крякает, - это утка. Теперь у меня есть метод, который работает с мандаринскими утками и может проверять определенные черты мандаринок вместо общих черт утки ... Как вы говорите, дисперсия меня тоже смущает. - person Sebastian Edelmeier; 12.07.2012
comment
Просто чтобы вы знали: работает с Func<T, TResult>, но не с Expression<Func<T, TResult>> - person Sebastian Edelmeier; 12.07.2012