linq 我可以在没有两个查询的情况下通过布尔条件将IEnumerable拆分为两个吗?

a8jjtwal  于 2022-12-06  发布在  其他
关注(0)|答案(6)|浏览(131)

我是否可以使用LINQ和一条查询/LINQ语句将一个IEnumerable<T>拆分为两个IEnumerable<T>
我想避免对IEnumerable<T>进行两次迭代。例如,是否可以将下面的最后两条语句组合起来,使allValues只被遍历一次?

IEnumerable<MyObj> allValues = ...
List<MyObj> trues = allValues.Where( val => val.SomeProp ).ToList();
List<MyObj> falses = allValues.Where( val => !val.SomeProp ).ToList();
4szc88ey

4szc88ey1#

有些人喜欢字典,但我更喜欢查找,因为当一个键丢失时的行为。

IEnumerable<MyObj> allValues = ...
ILookup<bool, MyObj> theLookup = allValues.ToLookup(val => val.SomeProp);

// does not throw when there are not any true elements.
List<MyObj> trues = theLookup[true].ToList();
// does not throw when there are not any false elements.
List<MyObj> falses = theLookup[false].ToList();

不幸的是,这种方法枚举了两次-一次是创建查找,另一次是创建列表。
如果你真的不需要列表,你可以把它缩减到一次迭代:

IEnumerable<MyObj> trues = theLookup[true];
IEnumerable<MyObj> falses = theLookup[false];
0h4hbjxa

0h4hbjxa2#

您可以使用此选项:

var groups = allValues.GroupBy(val => val.SomeProp);

要像示例中那样强制立即求值,请执行以下操作:

var groups = allValues.GroupBy(val => val.SomeProp)
                      .ToDictionary(g => g.Key, g => g.ToList());
List<MyObj> trues = groups[true];
List<MyObj> falses = groups[false];
gajydyqb

gajydyqb3#

复制面食扩展方法以方便您。

public static void Fork<T>(
    this IEnumerable<T> source,
    Func<T, bool> pred,
    out IEnumerable<T> matches,
    out IEnumerable<T> nonMatches)
{
    var groupedByMatching = source.ToLookup(pred);
    matches = groupedByMatching[true];
    nonMatches = groupedByMatching[false];
}

或在C# 7.0中使用元组

public static (IEnumerable<T> matches, IEnumerable<T> nonMatches) Fork<T>(
    this IEnumerable<T> source,
    Func<T, bool> pred)
{
    var groupedByMatching = source.ToLookup(pred);
    return (groupedByMatching[true], groupedByMatching[false]);
}

// Ex.
var numbers = new [] { 1, 2, 3, 4, 5, 6, 7, 8 };
var (numbersLessThanEqualFour, numbersMoreThanFour) = numbers.Fork(x => x <= 4);
z9zf31ra

z9zf31ra4#

现代C#示例只使用Linq,没有自定义扩展方法:

(IEnumerable<MyObj> trues, IEnumerable<MyObj> falses) 
    = ints.Aggregate<MyObj,(IEnumerable<MyObj> trues, IEnumerable<MyObj> falses)>(
        (new List<MyObj>(),new List<MyObj>()), 
        (a, i) => i.SomeProp ? (a.trues.Append(i), a.falses) : (a.trues, a.falses.Append(i))
    );

这是否回答了这个问题,是的;这比foreach更好或更可读吗,不。

rur96b6h

rur96b6h5#

在所有这些答案中,你失去了LINQ的第二大功能(当然是在表达能力之后);懒惰!当我们调用ToDictionary()ToLookup()时,我们是在强制枚举。
让我们看一下分区在Haskell中的实现,Haskell是一种很棒的懒惰函数式编程语言。
来自Hoogle:

'partition' p xs = ('filter' p xs, 'filter' (not . p) xs)

正如你所看到的,分区是一个表达式,它返回两个其他表达式的元组。首先, predicate 被应用于元素,其次, predicate 的逆被应用于元素。在这种情况下,Haskell被隐式地延迟计算,类似于LINQ通过使用表达式而不是委托来延迟计算。
那么,为什么我们不以同样的方式实现我们的分区扩展方法呢?在LINQ中,filter被称为where,所以让我们使用它。

public static (IEnumerable<T>, IEnumerable<T>) Partition<T>(
        this IEnumerable<T> source, Func<T, bool> predicate) 
        => (source.Where(predicate), source.Where(x => !predicate(x)));

一个警告是,如果你强制对匹配项AND进行求值,你将执行一个double枚举。但是,不要试图过早地优化。使用这种方法,你可以用LINQ来表示分区,从而保留它的有益特性。

bnl4lu3b

bnl4lu3b6#

根据其他答案中的ToLookup建议,我很高兴地想到了这个扩展方法:

public static (IEnumerable<T> XS, IEnumerable<T> YS) Bifurcate<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
    var lookup = source.ToLookup(predicate);
    return (lookup[true], lookup[false]);
}

调用站点如下所示:

var numbers = new []{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var (evens, odds) = numbers.Bifurcate(n => n % 2 == 0);

我认为这是很好的可用性,这就是为什么我张贴这个答案。
我们可以更进一步:

public static (IEnumerable<T> XS, IEnumerable<T> YS, IEnumerable<T> ZS) Trifurcate<T>(this IEnumerable<T> source, Func<T, bool> predicate1, Func<T, bool> predicate2)
{
    var lookup = source.ToLookup(x =>
    {
        if (predicate1(x))
            return 1;

        if (predicate2(x))
            return 2;

        return 3;
    });

    return (lookup[1], lookup[2], lookup[3]);
}

predicate 的顺序对这个集合很重要。例如,如果按顺序传递n => n > 5n => n > 100,则第二个集合将始终为空。
人们甚至可能渴望想出一个版本,它将与一个可变数量的 predicate (我知道我做到了),但据我所知,这是不可能的元组返回值在C#。

相关问题