2012-04-20 2 views
7

У меня есть следующий код:Можно ли кэшировать частично выполненные запросы LINQ?

IEnumerable<KeyValuePair<T, double>> items = 
    sequence.Select(item => new KeyValuePair<T, double>(item, weight(item))); 
if (items.Any(pair => pair.Value<0)) 
    throw new ArgumentException("Item weights cannot be less than zero."); 

double sum = items.Sum(pair => pair.Value); 
foreach (KeyValuePair<T, double> pair in items) {...} 

Где weight является Func<T, double>.

Проблема в том, что я хочу, чтобы weight выполнялся как можно несколько раз. Это означает, что он должен выполняться не более одного раза для каждого элемента. Я мог бы добиться этого, сохранив его в массиве. Однако, если какой-либо вес возвращает отрицательное значение, я не хочу продолжать выполнение.

Есть ли способ сделать это легко в рамках LINQ?

ответ

15

Конечно, это полностью выполнимо:

public static Func<A, double> ThrowIfNegative<A, double>(this Func<A, double> f) 
{ 
    return a=> 
    { 
     double r = f(a); 
     // if r is NaN then this will throw. 
     if (!(r >= 0.0)) 
     throw new Exception(); 
     return r; 
    }; 
} 

public static Func<A, R> Memoize<A, R>(this Func<A, R> f) 
{ 
    var d = new Dictionary<A, R>(); 
    return a=> 
    { 
     R r; 
     if (!d.TryGetValue(a, out r)) 
     { 
      r = f(a); 
      d.Add(a, r); 
     } 
     return r; 
    }; 
} 

А теперь ...

Func<T, double> weight = whatever; 
weight = weight.ThrowIfNegative().Memoize(); 

и вы сделали.

+0

Почему? Memoization - правильный термин для этой концепции: http://en.wikipedia.org/wiki/Memoization – mellamokb

+0

@ L.B: Хорошо, какое имя вы предпочитаете для * memoizer * функции *? –

+0

@EricLippert Я не думаю, что любой из ответов находится в рамках LINQ (исходный вопрос). Если, с другой стороны, Memoize() уже была частью LINQ, тогда THAT (imho) будет ответом на вопрос OP. Я думаю, что ваш ответ должен начинаться с: «Нет, но вы можете расширить функциональность таким образом:« Возможно, это не буквальное намерение OP. – payo

2

Один из способов, чтобы переместить исключение в функцию weight, или по крайней мере имитировать Поступая таким образом, делая что-то вроде:

Func<T, double> weightWithCheck = i => 
    { 
     double result = weight(i); 
     if (result < 0) 
     { 
      throw new ArgumentException("Item weights cannot be less than zero."); 
     } 
     return result; 
    }; 

IEnumerable<KeyValuePair<T, double>> items = 
    sequence.Select(item => new KeyValuePair<T, double>(item, weightWithCheck(item))); 

double sum = items.Sum(pair => pair.Value); 

К этому моменту, если есть исключение, которое будет иметься, вы должны иметь это. Вам нужно перечислить items, прежде чем вы сможете быть уверены в получении исключения, однако, как только вы его получите, вы не будете снова звонить weight.

0

Оба ответа хороши (где выбрасывать исключение и запоминать функцию).

Но ваша реальная проблема заключается в том, что ваше выражение LINQ оценивается каждый раз, когда вы его используете, за исключением того, что вы вынуждаете его оценивать и хранить как список (или аналогичный). Просто измените это:

sequence.Select(item => new KeyValuePair<T, double>(item, weight(item)));

к этому:

sequence.Select(item => new KeyValuePair<T, double>(item, weight(item))).ToList();

0

Вы могли бы сделать это с помощью цикла Еогеасп. Вот способ сделать это в одном заявлении:

IEnumerable<KeyValuePair<T, double>> items = sequence 
    .Select(item => new KeyValuePair<T, double>(item, weight(item))) 
    .Select(kvp => 
    { 
     if (kvp.Value < 0) 
      throw new ArgumentException("Item weights cannot be less than zero."); 
     else 
      return kvp; 
    } 
    ); 
0

Нет, нет ничего уже В Рамочной LINQ, чтобы сделать это, но вы могли бы, конечно, написать свои собственные методы и вызывать их из запроса Linq (Как уже было показано многими).

Лично я бы либо ToList первый запрос, либо использовать предложение Эрика.