Ответ 1
Не "полностью" linq, потому что ему нужна одна дополнительная переменная, но это самый легкий, о котором я мог подумать:
int current=0;
var selection = products.TakeWhile(p => (current = current + p.Cost) <= credit);
Учитывая настройку, подобную этой.
class Product {
int Cost;
// other properties unimportant
}
var products = new List<Product> {
new Product { Cost = 5 },
new Product { Cost = 10 },
new Product { Cost = 15 },
new Product { Cost = 20 }
};
var credit = 15;
Предположим, что список будет отсортирован в данном порядке. Я хочу в основном перебрать каждый элемент в списке, сохранить суммарную стоимость и продолжать получать продукты, если общая стоимость не превышает credit
.
Я могу сделать это с помощью некоторых циклов и т.д., но мне было интересно, есть ли способ свести его к более простому запросу LINQ.
Не "полностью" linq, потому что ему нужна одна дополнительная переменная, но это самый легкий, о котором я мог подумать:
int current=0;
var selection = products.TakeWhile(p => (current = current + p.Cost) <= credit);
Другие указали на захваченный переменный подход, и есть, возможно, правильные точки зрения, что этот подход плох, потому что он мутирует состояние. Кроме того, захваченные подходы с переменными могут быть повторены только один раз и опасны, потому что a. вы можете забыть этот факт и попытаться повторить итерацию дважды; б. захваченная переменная не отражает сумму взятых элементов.
Чтобы избежать этих проблем, просто создайте метод расширения:
public static IEnumerable<TSource> TakeWhileAggregate<TSource, TAccumulate>(
this IEnumerable<TSource> source,
TAccumulate seed,
Func<TAccumulate, TSource, TAccumulate> func,
Func<TAccumulate, bool> predicate
) {
TAccumulate accumulator = seed;
foreach (TSource item in source) {
accumulator = func(accumulator, item);
if (predicate(accumulator)) {
yield return item;
}
else {
yield break;
}
}
}
Использование:
var taken = products.TakeWhileAggregate(
0,
(cost, product) => cost + product.Cost,
cost => cost <= credit
);
Обратите внимание, что теперь вы можете повторять итерацию дважды (хотя будьте осторожны, если ваш TAccumulate
изменяет тип ссылки).
Вы можете сделать это, если хотите решение без внешней переменной
var indexQuery = products.Select((x,index) => new { Obj = x, Index = index });
var query = from p in indexQuery
let RunningTotal = indexQuery.Where(x => x.Index <= p.Index)
.Sum(x => x.Obj.Cost)
where credit >= RunningTotal
select p.Obj;
ok, повторите мой комментарий выше в ответ @Aducci, здесь версия, использующая Scan
var result=products.Scan(new {Product=(Product)null, RunningSum=0},
(self, next) => new {Product=next, RunningSum=self.RunningSum+next.Cost})
.Where(x=>x.RunningSum<=credit)
.Select(x => x.Product);
И это моя реализация Scan (которая, как я предполагаю, похожа на то, что в Rx Framework, но я не проверял)
public static IEnumerable<TAccumulate> Scan<TSource, TAccumulate>(this IEnumerable<TSource> source,
TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> accumulator) {
foreach(var item in source) {
seed=accumulator(seed, item);
yield return seed;
}
}
Используйте захваченную переменную для отслеживания суммы, принятой до сих пор.
int sum = 0;
IEnumerable<Product> query = products.TakeWhile(p =>
{
bool canAfford = (sum + p.Cost) <= credit;
sum = canAfford ? sum + p.Cost : sum;
return canAfford;
});