Подтвердить что ты не робот

Как заменить for-loops на функциональный оператор в С#?

Однажды коллега сказал, что Бог убивает котенка каждый раз, когда я пишу для цикла.

Когда его спросили, как избежать "петель", его ответ заключался в использовании функционального языка. Однако, если вы застряли с нефункциональным языком, скажите С#, какие методы существуют, чтобы избежать циклов или избавиться от них путем рефакторинга? Возможно, с лямбда-выражениями и LINQ? Если да, то как?

Вопросы

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

  • Почему for-loops плохо? Или, в каком контексте нужно избегать циклов и почему?
  • Можете ли вы представить примеры кода С#, как это выглядит раньше, т.е. с циклом, а затем без цикла?
4b9b3361

Ответ 1

Функциональные конструкции часто выражают ваше намерение более четко, чем for-loops, в тех случаях, когда вы работаете с некоторым набором данных и хотите преобразовать, фильтровать или агрегировать элементы.

Петли очень подходят, если вы хотите многократно выполнять некоторые действия.


Например

int x = array.Sum();

гораздо более четко выражает ваше намерение, чем

int x = 0;
for (int i = 0; i < array.Length; i++)
{
    x += array[i];
}

Ответ 2

Почему for-loops плохо? Или, в чем контекст - это те, которые следует избегать и почему?

Если ваш коллега имеет функциональное программирование, он, вероятно, уже знаком с основными причинами исключения для циклов:

Fold/Map/Filter охватывает наиболее часто используемые случаи обхода списка и хорошо подходит для работы с композицией. For-loops не являются хорошим шаблоном, потому что они не являются составными.

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

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

Чтобы дать нетривиальный пример, рассмотрите следующее на императивном языке:

let x = someList;
y = []
for x' in x
    y.Add(f x')

z = []
for y' in y
    z.Add(g y')

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

Основная проблема с императивной версией - просто, что for-loops - это детали реализации. Если вы хотите изменить функцию, вы измените ее реализацию - и вы в конечном итоге модифицируете много кода.

Случай, как бы вы написали map g (filter f x) императивно? Ну, так как вы не можете повторно использовать свой исходный код, который отображает карты и карты, вам нужно написать новую функцию, которая вместо этого фильтрует и отображает карты. И если у вас есть 50 способов сопоставления и 50 способов фильтрации, как вам нужны функции 50 ^ 50, или вам нужно смоделировать возможность передавать функции в качестве первоклассных параметров с помощью шаблона команды (если вы когда-либо пробовали функциональное программирование в Java, вы понимаете, какой кошмар это может быть).

В функциональном юниверсе вы можете обобщить map g (map f x) таким образом, чтобы вы могли поменять map на filter или fold по мере необходимости:

let apply2 a g b f x = a g (b f x)

И назовите его, используя apply2 map g filter f или apply2 map g map f или apply2 filter g filter f или все, что вам нужно. Теперь вы, вероятно, никогда не будете писать такой код в реальном мире, вы, вероятно, упростите его, используя:

let mapmap g f = apply2 map g map f
let mapfilter g f = apply2 map g filter f

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

Абстрагирование деталей реализации циклов позволяет вам без проблем переставлять один цикл для другого.

Помните, что for-loop - это детализация реализации. Если вам нужно изменить реализацию, вам нужно изменить каждый цикл for.

Карта/свернуть/отфильтровать абстрактный цикл. Поэтому, если вы хотите изменить реализацию своих циклов, вы меняете его в этих функциях.

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

К сожалению, детали реализации последовательных карт и параллельных карт не являются взаимозаменяемыми. Если у вас есть тонна последовательных карт по всему вашему коду, и вы хотите поменять их на параллельные карты, у вас есть два варианта: скопировать/вставить один и тот же код параллельного сопоставления по всей базе кода или абстрактную схему отображения в две функции map и pmap. Когда вы идете по второму маршруту, вы уже находитесь на коленях в области функционального программирования.

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

Ответ 3

  • Для петель не плохо. Существует много очень веских причин для сохранения цикла for.
  • Вы часто можете "избегать" цикла for, перерабатывая его с помощью LINQ в С#, который обеспечивает более декларативный синтаксис. Это может быть хорошим или плохим в зависимости от ситуации:

Сравните следующее:

var collection = GetMyCollection();
for(int i=0;i<collection.Count;++i)
{
     if(collection[i].MyValue == someValue)
          return collection[i];
}

vs foreach:

var collection = GetMyCollection();
foreach(var item in collection)
{
     if(item.MyValue == someValue)
          return item;
}

против. LINQ:

var collection = GetMyCollection();
return collection.FirstOrDefault(item => item.MyValue == someValue);

Лично все три варианта имеют свое место, и я использую их все. Это вопрос использования наиболее подходящего варианта для вашего сценария.

Ответ 4

Для циклов нет ничего плохого, но вот некоторые из причин, по которым люди могут предпочесть функциональные/декларативные подходы, такие как LINQ, где вы объявляете то, что хотите, а не как это получается: -

  • Функциональные подходы потенциально легче распараллеливать либо вручную, используя PLINQ, либо компилятором. Поскольку процессоры переходят на еще большее количество ядер, это может стать более важным.

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

  • Функциональные подходы часто короче и легче читать.

  • Функциональные подходы часто устраняют сложные условные тела внутри циклов (например, операторы if и выражения continue), потому что вы можете разбить цикл for на логические шаги - выбор всех элементов, которые соответствуют, выполнение операции над ними,...

Ответ 5

Для петель не убивайте людей (или котят, или щенков, или триблей). Люди убивают людей. Для петель, сами по себе, неплохо. Однако, как и все остальное, как вы их используете, это может быть плохо.

Ответ 6

Иногда вы не убиваете только одного котенка.

for (int я = 0; я < kittens.Length; я ++) {    котята [I].kill(); }

Иногда вы убиваете их всех.

Ответ 7

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

Взяв пример из AndyC:

Петля

// mystrings is a string array
List<string> myList = new List<string>();
foreach(string s in mystrings)
{
    if(s.Length > 5)
    {
        myList.add(s);
    }
}

Linq

// mystrings is a string array
List<string> myList = mystrings.Where<string>(t => t.Length > 5)
                               .ToList<string();

Вы используете первую или вторую версию внутри своей функции, легче читать

var filteredList = myList.GetStringLongerThan(5);

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

Ответ 8

Ваш коллега не прав. Для петель не так уж плохо. Они чисты, понятны и не подвержены ошибкам.

Ответ 9

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

Скажем, у вас есть метод расширения, который выглядит так:

void ForEach<T>(this IEnumerable<T> collection, Action <T> action)
{
    foreach(T item in collection)
    {
        action(item)
    }
}

Затем вы можете написать такой цикл:

mycollection.ForEach(x => x.DoStuff());

Это может быть не очень полезно сейчас. Но если вы затем замените свою реализацию метода расширения ForEach на использование многопоточного подхода, вы получите преимущества parallelism.

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

Также: всегда будьте осторожны с людьми, которые говорят, что некоторая программирующая конструкция всегда неверна.

Ответ 10

Простой (и бессмысленный действительно) пример:

Loop

// mystrings is a string array
List<string> myList = new List<string>();
foreach(string s in mystrings)
{
    if(s.Length > 5)
    {
        myList.add(s);
    }
}

Linq

// mystrings is a string array
List<string> myList = mystrings.Where<string>(t => t.Length > 5).ToList<string>();

В моей книге второй выглядит намного более аккуратным и простым, хотя в первом нет ничего плохого.

Ответ 11

Иногда for-loop является плохим, если существует более эффективная альтернатива. Например, поиск, где может быть более эффективным сортировать список, а затем использовать сортировку quicksort или двоичную. Или когда вы повторяете элементы в базе данных. Обычно гораздо эффективнее использовать операции с наборами в базе данных, а не итерации по элементам.

В противном случае, если for-loop, особенно для каждого из них, имеет наибольший смысл и читабельен, тогда я бы пошел с этим, а не с ним, в то, что не так интуитивно. Я лично не верю в это религиозное звучание "всегда делаю это так, потому что это единственный путь". Скорее, лучше иметь руководящие принципы и понимать, в каких сценариях целесообразно применять эти рекомендации. Хорошо, что вы спрашиваете Почему?

Ответ 12

Для цикла, скажем, "плохо", поскольку он подразумевает предсказание ветвления в CPU и, возможно, снижение производительности при пропуске предсказания ветвления.

Но CPU (с точностью предсказания ветвления 97%) и компилятор с tecniques, например, с разворачиванием цикла, делают снижение производительности петель незначительным.

Ответ 13

Ваш коллега может предлагать при определенных обстоятельствах, когда данные базы данных связаны с тем, что лучше использовать совокупную функцию SQL, такую ​​как Average() или Sum() во время запроса, а не обрабатывать данные со стороны С# в пределах ADO.NET.

В противном случае для циклов очень эффективны при правильном использовании, но поймите, что если вы обнаружите, что вы вложили их в три или более заказов, вам может понадобиться более эффективный алгоритм, например, который включает рекурсию, подпрограммы или и то, и другое. Например, сортировка пузырьков имеет O (n ^ 2) время исполнения в худшем случае (обратный порядок), но алгоритм рекурсивного сортирования - это только O (n log n), что намного лучше.

Надеюсь, это поможет.

  • Джим

Ответ 14

Любая конструкция на любом языке существует по какой-то причине. Это инструмент, который можно использовать для выполнения задачи. Значит до конца. В каждом случае есть манеры, в которых можно использовать его надлежащим образом, то есть в ясном и сжатом виде и в духе языка и манеры злоупотреблять им. Это относится к значительно несогласованному выражению goto, а также к вашей голосовой петле for, а также while, do-while, switch/case, if-then-else и т.д. Если цикл for правильный инструмент для того, что вы делаете, USE IT и ваш коллега должны будут прийти к соглашению с вашим дизайнерским решением.

Ответ 15

Это зависит от того, что находится в цикле, но он может ссылаться на рекурсивную функцию

    //this is the recursive function
    public static void getDirsFiles(DirectoryInfo d)
    {
        //create an array of files using FileInfo object
        FileInfo [] files;
        //get all files for the current directory
        files = d.GetFiles("*.*");

        //iterate through the directory and print the files
        foreach (FileInfo file in files)
        {
            //get details of each file using file object
            String fileName = file.FullName;
            String fileSize = file.Length.ToString();
            String fileExtension =file.Extension;
            String fileCreated = file.LastWriteTime.ToString();

            io.WriteLine(fileName + " " + fileSize + 
               " " + fileExtension + " " + fileCreated);
        }

        //get sub-folders for the current directory
        DirectoryInfo [] dirs = d.GetDirectories("*.*");

        //This is the code that calls 
        //the getDirsFiles (calls itself recursively)
        //This is also the stopping point 
        //(End Condition) for this recursion function 
        //as it loops through until 
        //reaches the child folder and then stops.
        foreach (DirectoryInfo dir in dirs)
        {
            io.WriteLine("--------->> {0} ", dir.Name);
            getDirsFiles(dir);
        }

    }

Ответ 16

Если вы абстрагируете цикл for напрямую, вы получите:

void For<T>(T initial, Func<T,bool> whilePredicate, Func<T,T> step, Action<T> action)
{
    for (T t = initial; whilePredicate(t); step(t))
    {
        action(t);
    }
}

Проблема с этим с точки зрения функционального программирования - это тип возврата void. По сути, это означает, что петли for не сочиняют красиво ни с чем. Таким образом, цель состоит не в том, чтобы преобразовать 1-1 из цикла for в какую-либо функцию, а в том, чтобы думать функционально и избегать делать вещи, которые не сочиняют. Вместо того, чтобы думать о цикличности и действии, думайте о всей проблеме и о том, с чем вы клонируете, и к.

Ответ 17

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

Смотрите "ForEach" против "ForEach" в блоге Eric Lippert.

Ответ 18

Цикл for всегда может быть заменен рекурсивной функцией, которая не связана с использованием цикла. Рекурсивная функция является более функциональной стратегией программирования.

Но если вы вслепую замените циклы рекурсивными функциями, тогда котята и щенки будут умирать миллионами, и вы будете совершены с помощью ускорителя.

ОК, вот пример. Но, пожалуйста, имейте в виду, что я не защищаю это изменение!

Цикл for

for (int index = 0; index < args.Length; ++index)
    Console.WriteLine(args[index]);

Может быть изменен на этот вызов рекурсивной функции

WriteValuesToTheConsole(args, 0);


static void WriteValuesToTheConsole<T>(T[] values, int startingIndex)
{
    if (startingIndex < values.Length)
    {
        Console.WriteLine(values[startingIndex]);
        WriteValuesToTheConsole<T>(values, startingIndex + 1);
    }
}

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