Непонимание .NET на перегруженных методах с разными параметрами (Call Ambiguous)

У меня проблема с некоторыми перегруженными методами, и я постараюсь дать простую реализацию этого.

Итак, вот класс содержит два метода ниже:

public class MyRepo<TEntity>
{
    public List<TEntity> GetData(Expression<Func<TEntity, Boolean>> expression)
    {
        //Do something
    }

    public List<TEntity> GetData(Func<TEntity,Boolean> whereClause)
    {
        //Do something
    }
}

а это моя сущность

public class MyEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Вот где я использую это:

{
    ...
    MyRepo<MyEntity> myRepo = new MyRepo<MyEntity>();
    myRepo.GetData(x => x.Id == 1); // The ambiguity point
    ...
}

Проблема в том, что у меня просто есть два метода с одинаковыми именами и разными аргументами, поэтому, исходя из концепции полиморфизма ООП, я ожидаю, что .NET поймет мой нужный метод.

Но очевидно, что .NET не может этого понять, потому что форма экземпляра Expression<Func<TEntity, Boolean>> и Func<TEntity, Boolean> одинакова, и это ошибка времени компиляции, которая возникает .NET:

The call is ambiguous between the following methods or properties:
    'Program.MyRepo<TEntity>.GetData(Expression<Func<TEntity, bool>>)' and
    'Program.MyRepo<TEntity>.GetData(Func<TEntity, bool>)'

Вопрос: как я могу предотвратить эту ошибку во время компиляции?

Я предпочитаю не трогать способ, которым я GetData() в этой строке:

myRepo.GetData(x => x.Id == 1);

Ответы

Ответ 1

Я полагаю, что самый простой способ избавиться от путаницы, связанной с перегрузкой, - привести данные к входу до его отправки в функцию. Это может быть сделано неявно (inline) или в форме определения типизированного ввода (рекомендуемый способ), а не анонимного. Вот как я это проверил, и это работает без предупреждения.

MyRepo<MyEntity> myRepo = new MyRepo<MyEntity>();
Func<MyEntity, bool> predicate = x => x.Id == 1;
Expression<Func<MyEntity, bool>> expression = x => x.Id == 1;
// both below lines are fine now
myRepo.GetData(predicate);
myRepo.GetData(expression);

По-видимому, компилятор С# не достаточно точен, чтобы различать два, потому что он требует некоторого эвристического поведения, а анонимные входы по своей сути одинаковы. Во всяком случае, этот обходной путь может решить проблему.

Ответ 2

Лямбда-выражения (x=> x.Id==1) сами по себе не имеют типа - они автоматически "приводятся" к Expression или Func/делегату соответствующего типа, когда тип известен. Т.е. почему должно быть приведено лямбда-выражение, если оно представлено в виде простого параметра Delegate, имеет дело с похожей проблемой только между различными типами делегатов.

В вашем случае методы, которые являются потенциальными кандидатами, предлагают оба варианта, и компилятор не может сделать выбор.

Если вам действительно нужно сохранить одно и то же имя, вызывающим абонентам придется самим указывать тип:

 myRepo.GetData((Expression<Func<TEntity, Boolean>>)(x => x.Id == 1));
 myRepo.GetData((Func<TEntity, Boolean>)(x => x.Id == 2));

Я не думаю, что вы можете использовать метод расширения для одной из альтернатив, так как поиск остановится на уровне класса. Так что на самом деле наличие методов с разными именами - единственный реальный вариант (если вам нужны оба). Подумайте, достаточно ли только версии Expression. В качестве альтернативы вы можете разделить их между различными классами (аналогично тому, как расширения IQueryable принимают Expression когда аналогичные методы в IEnumerable принимают Func (см. QueryableExtenasions).

Ответ 3

Проблема в том, что когда вы компилируете выражение, вы создадите метод с той же сигнатурой, что и вторая.

Я рекомендую вам изменить название первого метода

Кроме того, если вы собираетесь использовать Expression, верните IQueryable, чтобы воспользоваться отложенным выполнением.

Ответ 4

Я изменил свой класс и решил проблему:

public class MyRepo<TEntity>
{
    public void GetData(Expression<Func<TEntity, bool>> expression, out List<TEntity> result)
    {
        result = null;
    }

    public List<TEntity> GetData(Func<TEntity, bool> whereClause)
    {
        return null;
    }
}

private void button1_Click(object sender, EventArgs e)
{
    var myRepo = new MyRepo<MyEntity>();
    var i = myRepo.GetData(x => x.Id == 1);
    myRepo.GetData(x => x.Id == 1, out i);
}

Ответ 5

Рассмотрите возможность использования наследования интерфейса для этих двух методов. Согласно принципам SOLID, вам следует избегать использования ссылок конкретного типа, а вместо этого вы должны использовать абстракции через интерфейсы. Что-то вроде этого:

public interface IQueryDataByPredicateExpression 
{
List<TEntity> GetData(Expression<Func<TEntity, Boolean>> whereClause);
}

public interface IQueryDataByPredicate
{
List<TEntity> GetData(Func<TEntity,Boolean> whereClause);

}

    public class MyRepo<TEntity> : IQueryDataByPredicateExpression, IQueryDataByPredicate
    {
        public List<TEntity> GetData(Expression<Func<TEntity, Boolean>> expression)
        {
            //Do something
        }

        public List<TEntity> GetData(Func<TEntity,Boolean> whereClause)
        {
            //Do something
        }
    }

Теперь, в зависимости от того, как вы хотите выполнять запросы, вы должны использовать переменную того типа интерфейса, который вам нужен. Теперь у вас должна быть одна ссылка на интерфейс, ссылающаяся на один и тот же экземпляр Repo.

IQueryDataByPredicateExpression  queryRepoWithPredicateExpression = myRepo as IQueryDataByPredicateExpression;

IQueryDataByPredicate queryRepoWithPredicate = myRepo as IQueryDataByPredicate;


 queryRepoWithPredicateExpression.GetData(x => x.Id == 1);
 queryRepoWithPredicate.GetData(x => x.Id == 2);

Но если вы не можете или хотите изменить способ вызова этих методов, ответ Алексея Левенкова выглядит великолепно