2015-10-28 1 views
3

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

x => x.A.HasValue || (x.B.HasValue && x.C == q) || (!x.C.HasValue && !x.A.HasValue) || //...expression goes on 

Я хочу использовать это в качестве Expression<Func<T,bool> в (например, Linq-To-Entities) Queryable.Where метода. Я также хочу использовать его в методе Enumerable.Where, но метод Where принимает только Func<T,bool>, а не Expression<Func<T,bool>.

Сам лямбда синтаксис может быть использован для создания либоExpression<Func<T,bool>> или Func<T,bool> (или любой другой тип делегата по этому вопросу), но в этом контексте он не может генерировать более чем один сразу.

Например, можно написать:

public Expression<Func<Pair,bool>> PairMatchesExpression() 
{ 
    return x => x.A == x.B; 
} 

так же легко, как я могу написать:

public Func<Pair,bool> PairMatchesDelegate() 
{ 
    return x => x.A == x.B; 
} 

Проблема заключается в том, что я не могу использовать такое же выражение точной лямбды (то есть х = > xA == xB) в обоих направлениях, без физического дублирования его на два отдельных метода с двумя разными типами возврата, несмотря на способность компилятора скомпилировать его в один из них.

Другими словами, если бы я хотел использовать выражение лямбда в методах Queryable, тогда мне нужно использовать подпись метода Expression. Как только я это сделаю, Я не могу использовать его как Func так же легко, как я мог бы, я только что объявил метод возвращаемого типа как Func. Вместо этого, я теперь называть Compile на Expression, а затем беспокоиться о кэширования результатов вручную следующим образом:

static Func<Pair,bool> _cachedFunc; 
public Func<Pair,bool> PairMatchesFunc() 
{ 
    if (_cachedFunc == null) 
     _cachedFunc = PairMatchesExpression().Compile(); 
    return _cachedFunc; 
} 

Есть ли решение этой проблемы, так что я могу использовать лямбда-выражение в более общем виде без он блокируется до определенного типа во время компиляции?

ответ

1

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

Пожалуйста, обратите внимание, что это работает отлично без с помощью атрибута [DelegateConstraint] (от Fody.ExtraConstraints), но с ним, вы получите время компиляции проверки параметра конструктора. Атрибуты заставляют классы действовать так, как будто они имеют ограничение where T : Delegate, которое в настоящее время не поддерживается на C#, хотя оно -, поддерживаемое в ILE (не уверен, что я говорю это правильно, но вы получите эту идею).

public class VersatileLambda<[DelegateConstraint] T> where T : class { 
    private readonly Expression<T> _expression; 
    private readonly Lazy<T> _funcLazy; 

    public VersatileLambda(Expression<T> expression) { 
     if (expression == null) { 
      throw new ArgumentNullException(nameof(expression)); 
     } 
     _expression = expression; 
     _funcLazy = new Lazy<T>(expression.Compile); 
    } 

    public static implicit operator Expression<T>(VersatileLambda<T> lambda) { 
     return lambda?._expression; 
    } 

    public static implicit operator T(VersatileLambda<T> lambda) { 
     return lambda?._funcLazy.Value; 
    } 

    public Expression<T> AsExpression() { return this; } 
    public T AsLambda() { return this; } 
} 

public class WhereConstraint<[DelegateConstraint] T> : VersatileLambda<Func<T, bool>> { 
    public WhereConstraint(Expression<Func<T, bool>> lambda) 
     : base(lambda) { } 
} 

красота неявном является то, что в условиях, когда конкретный Expression<Func<>> или Func<> ожидается, вы не должны делать что-либо вообще, просто, использования его.

Теперь, с учетом объекта:

public partial class MyObject { 
    public int Value { get; set; } 
} 

Это представлено в базе данных следующим образом:

CREATE TABLE dbo.MyObjects (
    Value int NOT NULL CONSTRAINT PK_MyObjects PRIMARY KEY CLUSTERED 
); 

Тогда это работает так:

var greaterThan5 = new WhereConstraint<MyObject>(o => o.Value > 5); 

// Linq to Objects 
List<MyObject> list = GetObjectsList(); 
var filteredList = list.Where(greaterThan5).ToList(); // no special handling 

// Linq to Entities 
IQueryable<MyObject> myObjects = new MyObjectsContext().MyObjects; 
var filteredList2 = myObjects.Where(greaterThan5).ToList(); // no special handling 

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

var expression = (Expression<Func<MyObject, bool>>) greaterThan5; 

Обратите внимание, что вы не действительно нужен WhereConstraint класс, или вы могли бы избавиться от VersatileLambda, перемещая его содержимое в WhereConstraint, но мне нравилось делать два отдельных (как теперь вы можете использовать VersatileLambda что-то который возвращается, кроме bool). (. И эта разница в основном то, что отличает мой ответ от Диего) Используя VersatileLambda, как теперь выглядит следующим образом (вы можете увидеть, почему я завернул):

var vl = new VersatileLambda<Func<MyObject, bool>>(o => o.Value > 5); 

Я подтвердил, что это прекрасно работает для IEnumerable а также IQueryable, правильно проецируя лямбда-выражение в SQL, что подтверждается запуском SQL Profiler.

Кроме того, вы можете делать действительно классные вещи с помощью выражений, которые нельзя сделать с помощью лямбда. Проверьте это:

public static class ExpressionHelper { 
    public static Expression<Func<TFrom, TTo>> Chain<TFrom, TMiddle, TTo>(
     this Expression<Func<TFrom, TMiddle>> first, 
     Expression<Func<TMiddle, TTo>> second 
    ) { 
     return Expression.Lambda<Func<TFrom, TTo>>(
      new SwapVisitor(second.Parameters[0], first.Body).Visit(second.Body), 
      first.Parameters 
     ); 
    } 

    // this method thanks to Marc Gravell 
    private class SwapVisitor : ExpressionVisitor { 
     private readonly Expression _from; 
     private readonly Expression _to; 

     public SwapVisitor(Expression from, Expression to) { 
      _from = from; 
      _to = to; 
     } 

     public override Expression Visit(Expression node) { 
      return node == _from ? _to : base.Visit(node); 
     } 
    } 
} 

var valueSelector = new Expression<Func<MyTable, int>>(o => o.Value); 
var intSelector = new Expression<Func<int, bool>>(x => x > 5); 
var selector = valueSelector.Chain<MyTable, int, bool>(intSelector); 

Вы можете создать перегрузку Chain, которая принимает VersatileLambda в качестве первого параметра, и возвращает VersatileLambda. Теперь ты действительно испепеляешься.

+0

Ваша «VersatileLambda» - это именно то, что я изначально предполагал как решение, прежде чем реализовать C# не поддерживает «делегат» как ограничение типа. Я столкнулся с обходным решением Fody, и я прочитал то же самое, что вы сказали, - что это ограничение компилятора, а не ограничение .NET. Я бы выбрал решение Диего по этому поводу, только потому, что это вводит зависимость от Fody, но если делегат был изначально поддерживается как ограничение типа, то это решение лучше всего потому, что для него требуется только один класс, который будет поддерживать любую лямбду. Я хочу, чтобы ограничения типа «Делегат» и «Числовые» поддерживались. – Triynko

+0

На второй, хотя, я собираюсь отметить это как ответ, потому что он будет работать даже без Fody. Сам класс 'Expression' требует типа делегата, но для определения имени типа типа' TDelegate'; поэтому, если он достаточно хорош для рамки, для меня это достаточно хорошо. В худшем случае, если используется тип, не являющийся делегатом, он будет выходить из строя в 'Expression ' 'окончательный листинг в вызове' TDelegate Compile': '(TDelegate) ((object) LambdaCompiler.Compile (this, null)) ; ' – Triynko

+0

Для чего это стоит, Диего и мой код очень похожи, и он сначала публиковал, но это было почти то, о чем я думал. Я также пошел на некоторые существенные проблемы, чтобы проверить, что неявное преобразование работает очень хорошо в выражении linq для сущностей. Я думаю, что метод AsFunc или ToFunc стоит того. (И выражение.) Я также думаю, что цепочка, которую я представил, добавлена. И мне нравится имя, которое я выбрал лучше. :) – ErikE

-2

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

Учитывая тип для использования в выражении:

public class SomeClass 
{ 
    public int A { get; set; } 
    public int? B { get; set; } 
} 

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

static class SomeClassMeetsConditionName 
{ 
    private static Expression<Func<SomeClass,bool>> _expression; 
    private static Func<SomeClass,bool> _delegate; 
    static SomeClassMeetsConditionName() 
    { 
     _expression = x => (x.A > 3 && !x.B.HasValue) || (x.B.HasValue && x.B.Value > 5); 
     _delegate = _expression.Compile(); 
    } 
    public static Expression<Func<SomeClass, bool>> Expression { get { return _expression; } } 
    public static Func<SomeClass, bool> Delegate { get { return _delegate; } } 
} 

Тогда вместо используя Where(SomeClassMeetsConditionName()), вы просто проходите SomeClassMeetsConditionName, за которым следует либо .Delegate, либо .Expression, в зависимости от контекста:

public void Test() 
{ 
    IEnumerable<SomeClass> list = GetList(); 
    IQueryable<SomeClass> repo = GetQuery(); 

    var r0 = list.Where(SomeClassMeetsConditionName.Delegate); 
    var r1 = repo.Where(SomeClassMeetsConditionName.Expression); 
} 

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

+0

Он попросил способ скомпилировать лямбду в выражение и делегат без компиляции делегата во время выполнения; вы компилируете его во время выполнения. – Servy

+0

Я никогда не говорил «без компиляции делегата во время выполнения». Я спросил, есть ли способ предотвратить/остановить его от «блокировки до определенного типа во время компиляции». Вы уделяете слишком много внимания «времени компиляции» и не замечаете, что я пытаюсь не дублировать сложное выражение, имея возможность использовать его как выражение и делегат для использования с Queryable и Enumerable методы. Метод «Компиляция» отменяет блокировку времени компиляции как выражение, поэтому ваш комментарий недействителен. – Triynko

+0

Учитывая, что в вопросе конкретно упоминается способность компилировать выражения, и вы продолжаете в течение некоторого времени в комментариях о том, как это не решало вашу проблему, по-видимому, это не решило вашу проблему, поэтому публикация ее как ответа довольно бессмысленный. По-видимому, у вас никогда не было вопроса в первую очередь, если ответ функционально идентичен коду в вопросе. – Servy

1

Вы можете создать класс оболочки. Что-то вроде этого:

public class FuncExtensionWrap<T> 
{ 
    private readonly Expression<Func<T, bool>> exp; 
    private readonly Func<T, bool> func; 

    public FuncExtensionWrap(Expression<Func<T, bool>> exp) 
    { 
     this.exp = exp; 
     this.func = exp.Compile(); 
    } 

    public Expression<Func<T, bool>> AsExp() 
    { 
     return this; 
    } 

    public Func<T, bool> AsFunc() 
    { 
     return this; 
    } 

    public static implicit operator Expression<Func<T, bool>>(FuncExtensionWrap<T> w) 
    { 
     if (w == null) 
      return null; 
     return w.exp; 
    } 

    public static implicit operator Func<T, bool>(FuncExtensionWrap<T> w) 
    { 
     if (w == null) 
      return null; 
     return w.func; 
    } 
} 

И тогда он будет использоваться, как это:

static readonly FuncExtensionWrap<int> expWrap = new FuncExtensionWrap<int>(i => i == 2); 

// As expression 
Expression<Func<int, bool>> exp = expWrap; 
Console.WriteLine(exp.Compile()(2)); 

// As expression (another way) 
Console.WriteLine(expWrap.AsExp().Compile()(2)); 

// As function 
Func<int, bool> func = expWrap; 
Console.WriteLine(func(1)); 

// As function(another way) 
Console.WriteLine(expWrap.AsFunc()(2)); 
+0

Мне нравится это решение, использующее один класс, который предоставляет членам доступ к делегату Expression и Func вместе с неявными операторами трансляции, поэтому вам даже не нужно использовать эти члены. Также приятно, что для каждого метода лямбда-метода требуется только один класс, а не один класс на каждую уникальную лямбду, как у меня. Он не вводит новые проблемы, такие как условия гонки, и не требует сохранения лишних имен переменных - просто статическая переменная, которой вы назначаете экземпляр, что отлично. Он не вводит дубликаты копий лямбды или каких-либо дополнительных накладных расходов за звонок. – Triynko

+0

* Никаких дополнительных накладных расходов на вызов (потенциально) до тех пор, пока вы напрямую обращаетесь к переменным exp и func, в отличие от вызова неявных операторов, выполняющих нуль-чек; не то, чтобы это было важно, и что это ненужная проверка для такого статического оператора. – Triynko

+0

Я рад, что вам понравилось мое решение. Однако @ ErikE, хотя и очень похоже, лучше. Я не знал, что мне не хватает ограничения делегирования. Это была моя первая попытка, но я не мог этого сделать. Поэтому я рад, что что-то новое увидел :) – Diego

Смежные вопросы

 Смежные вопросы