Проверка работоспособности: приравниваются ли эти вложенные вызовы .All к соответствующим вызовам .Where и .SelectMany в LINQ?

У меня есть следующий код:

bool b = myList
    .All(x => x.MyList
        .Where(y => y.MyBool)
        .All(y => y.MyList
            .All(z => z.MyBool)))

Является ли это функционально эквивалентным:

bool b = myList
    .SelectMany(x => x.MyList)
    .Where(x => x.MyBool)
    .SelectMany(x => x.MyList)
    .All(x => x.MyBool)

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

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

ОБНОВЛЕНИЕ:

Итак, я профилировал код, используя следующее:

static void Main(string[] args)
{
    var myList = new List<A>();

    for (var j = 0; j < 1000; j++)
    {
        var a = new A();

        for (var k = 0; k < 1000; k++)
        {
            var b = new B {MyBool = true};

            for (var l = 0; l < 1000; l++)
            {
                var c = new C {MyBool = true};
                b.MyList.Add(c);
            }

            a.MyList.Add(b);
        }

        myList.Add(a);
    }

    for (var x = 0; x < 10000; x++)
    {
        bool b1 = Foo(myList);
    }

    for (var x = 0; x < 10000; x++)
    {
        bool b2 = Bar(myList);
    }
}

private static bool Foo(List<A> myList)
{
    return myList
        .All(x => x.MyList
            .Where(y => y.MyBool)
            .All(y => y.MyList
                .All(z => z.MyBool)));
}

private static bool Bar(List<A> myList)
{
    return myList
        .SelectMany(x => x.MyList)
        .Where(x => x.MyBool)
        .SelectMany(x => x.MyList)
        .All(x => x.MyBool);
}

private class A
{
    public List<B> MyList => new List<B>();
}

private class B
{
    public bool MyBool { get; set; }

    public List<C> MyList => new List<C>();
}

private class C
{
    public bool MyBool { get; set; }
}

Я обнаружил, что второй метод (Bar) с использованием .SelectMany и .Where был почти на 80% быстрее, чем первый метод (Foo) с использованием вложенных вызовов .All. Но это можно было доказать только на очень большом наборе данных, а реально затраченное время было очень небольшим. Это может иметь большее значение для небольших наборов данных, если каждый элемент вызывает запрос (например, к базе данных), который занимает больше времени, если действительно разница в производительности связана с количеством считываний элементов. Но если разница связана с накладными расходами при чтении элементов, а элементы считываются одинаковое количество раз для любого метода, то я предполагаю, что разница в производительности всегда будет незначительной, независимо от размера набора данных или времени чтения элемента.

Результаты ниже (из профилировщика производительности Visual Studio): введите здесь описание изображения


person Neo    schedule 30.11.2016    source источник
comment
Поскольку All возвращает true для пустых наборов, они эквивалентны. Как насчет того, что лучше, это либо основано на мнении, либо зависит от реализации.   -  person Ivan Stoev    schedule 30.11.2016


Ответы (1)


myList.All // is it true for all elements in myList that…
(x => x.MyList //in their MyList property
.Where(y => y.MyBool) // those elements that have MyBool returning true
.All( // have it true for all elements in that list that…
y => y.MyList //in their MyList property
.All(z => z.MyBool) // all elements have MyBool returning true


myList.SelectMany( // for all the elements in myList
x => x.MyList)  // for all elements in their MyList property…
.Where(x => x.MyBool) // that have MyBool returning true
.SelectMany( // for all those elements
x => x.MyList) // for all elements in their MyList property
.All(x => x.MyBool) // is it true that all elements have MyBool returning true

Так что да, они имеют одно и то же значение. В частности, в любом случае пустой список на любом этапе означает true из метода All(), независимо от того, передается ли true из пустоты вызывающему All() или пустота передается в конечный All().

Читаемость - более субъективный вопрос, поскольку сам заданный вопрос включает в себя несколько шагов, если это, то то, что просто по своей сути громоздко и поэтому приведет к громоздкому выражению. Я предпочитаю первое, но не сильно и не догматично.

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

person Jon Hanna    schedule 30.11.2016
comment
Спасибо. Что касается удобочитаемости, я только что внес небольшое изменение в код, добавив возврат каретки и отступы, чтобы сделать его более реалистичным. Теперь, что вы думаете? - person Neo; 30.11.2016
comment
Это, безусловно, помогает разбить шаги в каждом, но делает это примерно равномерно в каждом, поэтому, поскольку они оба улучшаются одинаково, я все еще очень немного одобряю первый, но только немного и не ожидая, что все согласятся с этим. - person Jon Hanna; 30.11.2016
comment
Я только что профилировал два метода и вставил результаты в вопрос в качестве обновления. Кажется, второй метод работает лучше. Но я думаю, что это настолько незначительно, независимо от типа или размера набора данных, что в реальности его не стоит рассматривать. Но я могу ошибаться. - person Neo; 30.11.2016
comment
Вы, вероятно, правы, это немного быстрее, но не настолько, чтобы это имело большое значение. Вы профилировали среду рабочего стола или dotnet-core? Поскольку последнему уделялось больше внимания оптимизации, мне было бы интересно узнать, отличаются ли они, тем более, что я проделал часть этой работы сам, хотя я не думаю, что делал что-либо ни с All, ни с SelectMany. - person Jon Hanna; 30.11.2016
comment
Только что попробовал это, но, к сожалению, все инструменты профилировщика производительности, кроме «Использование графического процессора», отключены в приложении .NET Core. Та же проблема, что и здесь. - person Neo; 01.12.2016