Почему некоторые функции очень длинные? (идеи, необходимые для академического исследования!)
Я пишу небольшой академический исследовательский проект о чрезвычайно длинных функциях. Очевидно, я не ищу примеры плохого программирования, но я привожу примеры функций длиной 100, 200 и 600 строк, что имеет смысл.
Я буду исследовать исходный код ядра Linux с помощью сценария, написанного для степени магистра, написанного в Еврейском университете, который измеряет различные параметры, такие как количество строк кода, сложность функций (измеряемая MCC) и другие полезности. Кстати, это аккуратное исследование об анализе кода и рекомендуемый материал для чтения.
Мне интересно, можете ли вы придумать какую-либо вескую причину, почему любая функция должна быть исключительно длинной? Я буду смотреть на C, но примеры и аргументы из любого языка будут полезны.
15 ответов
Я могу поймать злобу за это, но читабельность. Сильно последовательное, но независимое выполнение, которое может быть разбито на N вызовов функций (функций, которые больше нигде не используются), на самом деле не выигрывает от декомпозиции. Если вы не учитываете достижение произвольного максимума по длине функции в качестве выгоды.
Я бы предпочел прокрутить блоки кода N функционального размера по порядку, чем перемещаться по всему файлу, нажимая N функций.
Все, что сгенерировано из других источников, т.е. конечный автомат из генератора синтаксического анализатора или тому подобное. Если это не предназначено для потребления человеком, эстетические или ремонтопригодные проблемы не имеют значения.
Функции могут становиться длиннее с течением времени, особенно если они модифицируются многими разработчиками.
Показательный пример: я недавно (~1 год или 2 назад) произвел рефакторинг некоего устаревшего кода для обработки изображений с 2001 года или около того, который содержал несколько функций в несколько тысяч строк. Не несколько файлов с несколькими тысячами строк - несколько функций с несколькими тысячами строк.
За эти годы к ним добавилось так много функциональных возможностей, что не потребовало усилий по их правильному рефакторингу.
Прочтите главу в McConnell's Code Complete о подпрограммах, в ней есть рекомендации и указания, когда нужно разбивать вещи на функции. Если у вас есть алгоритм, в котором эти правила не применяются, это может быть хорошей причиной для использования длинной функции.
Единственное, что я недавно написал, - это то, что он не достигает больших результатов, чтобы уменьшить их или сделать код менее читабельным. Представление о том, что функция, имеющая определенную длину, каким-то образом изначально является плохим, является просто слепой догмой. Как любая слепо применяемая догма, избавляет последователя от необходимости на самом деле думать о том, что применимо в любом конкретном случае...
Последние примеры...
Разбор и проверка файла конфигурации с простой структурой name = value в массив, преобразование каждого значения, как я нахожу, - это один массивный оператор switch, один случай на параметр конфигурации. Зачем? Я мог бы разделить на множество вызовов 5/6 строк тривиальных функций. Это добавило бы около 20 частных членов в мой класс. Ни один из них не используется повторно где-либо еще. Разложение на более мелкие куски просто не добавило достаточной ценности, чтобы стоить того, так что это было то же самое с момента создания прототипа. Если я хочу другой вариант, добавьте еще один случай.
Другой случай - код связи клиента и сервера в одном приложении и его клиент. Много вызовов для чтения / записи, любой из которых может потерпеть неудачу, в этом случае я освобождаю залог и возвращаю false Так что эта функция в основном линейная и имеет точки возврата (если не удалось, возврат) после почти каждого вызова. Опять же, ничего не выиграть, сделав его меньше, и нет никакого способа сделать его меньше.
Я также должен добавить, что большинство моих функций - это пара "экранов", и я стремлюсь в более сложных областях, чтобы сохранить один "экран", просто потому, что я могу одновременно рассмотреть всю функцию. Это нормально для функций, которые в основном линейны по своей природе и не имеют большого количества сложных циклов или условий, поэтому процесс прост. В качестве последнего замечания я предпочитаю применять обоснование затрат и выгод при принятии решения о том, какой код реорганизовать, и расставлять приоритеты соответственно. Помогает избежать вечно недоделанного проекта.
Сгенерированный код может генерировать очень и очень длинные функции.
Скорость:
- Вызов функции означает добавление в стек, затем прыжок, затем сохранение в стеке, а затем снова прыжок. если вы используете параметры для функции, у вас обычно есть еще несколько нажатий.
Рассмотрим цикл:
for...
func1
внутри цикла все эти толчки и скачки могут быть фактором.
Это было в значительной степени решено с представлением встроенных функций на C99 и неофициально до этого, но некоторый код, написанный ранее или созданный с учетом совместимости, возможно, был длинным по этой причине.
Также Inline имеет свои потоки, некоторые описаны в ссылке Inline Functions.
Редактировать:
В качестве примера того, как вызов функции может замедлить выполнение программы:
4 static void
5 do_printf()
6 {
7 printf("hi");
8 }
9 int
10 main()
11 {
12 int i=0;
13 for(i=0;i<1000;++i)
14 do_printf();
15 }
Это производит (GCC 4.2.4):
.
.
jmp .L4
.L5:
call do_printf
addl $1, -8(%ebp)
.L4:
cmpl $999, -8(%ebp)
jle .L5
.
.
do_printf:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
movl $.LC0, (%esp)
call printf
leave
ret
против:
int
main()
{
int i=0;
for(i=0;i<1000;++i)
printf("hi");
}
или против:
4 static inline void __attribute__((always_inline)) //This is GCC specific!
5 do_printf()
6 {
7 printf("hi");
8 }
Оба продукта (GCC 4.2.4):
jmp .L2
.L3:
movl $.LC0, (%esp)
call printf
addl $1, -8(%ebp)
.L2:
cmpl $999, -8(%ebp)
jle .L3
Который быстрее.
Иногда я пишу простой файл (для использования третьими лицами), который включает заголовки, трейлеры и подробные записи, которые все связаны между собой. Для вычисления итогов проще иметь длинную функцию, чем придумать какую-то схему для передачи значений вперед и назад через множество небольших функций.
Чаще всего я вижу / пишу это длинные операторы переключения или операторы полу-переключения if / else для типов, которые нельзя использовать в операторах переключения этого языка (уже упоминалось несколько раз). Сгенерированный код - интересный случай, но я сосредоточусь на написанном человеком коде здесь. Глядя на мой текущий проект, единственная действительно длинная функция, не включенная выше (296 LOC/650 LOT), это некоторый Ковбойский код, который я использую в качестве ранней оценки для вывода генератора кода, который я планирую использовать в будущем. Я определенно буду рефакторинг, который удалит его из этого списка.
Много лет назад я работал над некоторыми научными компьютерными программами, которые выполняли долгую функцию. Метод использовал большое количество локальных переменных, и рефакторинг метода продолжал приводить к измеримой разнице на профилирование. Даже улучшение этого раздела кода на 1% сэкономило часы вычислений, поэтому функция работала долго. С тех пор я многому научился, поэтому не могу говорить о том, как бы я справился с ситуацией сегодня.
Очень длинные функции, с которыми я сталкиваюсь, не написаны на C, поэтому вам придется решить, относится ли это к вашему исследованию или нет. Я имею в виду некоторые функции PowerBuilder длиной в несколько сотен строк по следующим причинам:
- Они были написаны более 10 лет назад людьми, которые в то время не имели в виду стандарты кодирования.
- Среда разработки усложняет создание функций. Едва ли это хорошее оправдание, но это одна из тех мелочей, которая иногда отговаривает вас работать должным образом, и я думаю, что кто-то просто ленился.
- Функции развивались с течением времени, добавляя как код, так и сложность.
- Функции содержат огромные циклы, каждая итерация, возможно, обрабатывает разные типы данных по-своему. Используя десятки (!) Локальных переменных, некоторые переменные-члены и некоторые глобальные переменные, они стали чрезвычайно сложными.
- Будучи таким старым и уродливым, никто не осмеливается разложить их на более мелкие части. Имея в своем распоряжении так много особых случаев, разбивая их на части, вы сталкиваетесь с проблемами.
Это еще одно место, где очевидные плохие методы программирования встречаются с реальностью. В то время как любой студент первого курса CS мог бы сказать, что эти звери плохие, никто бы не потратил денег на то, чтобы заставить их выглядеть красивее (учитывая, что, по крайней мере, на данный момент, они все еще делают).
Я думаю, что есть один момент, который имеет отношение к разным языкам и инструментам, связанным с функциями.
Например, Java позволяет вам подавлять предупреждения с помощью аннотации. Может быть желательно ограничить область действия аннотации, и поэтому вы сохраняете функцию короткой для этой цели. На другом языке разбиение этого раздела на его собственную функцию может быть совершенно произвольным.
Спорные: В JavaScript, я склонен создавать только функции с целью повторного использования кода. Если фрагмент выполняется только в одном месте, я нахожу обременительным переходить по файлу (файлам) после спагетти ссылок на функции. Я думаю, что замыкания облегчают и, следовательно, усиливают более длинные [родительские] функции. Поскольку JS является интерпретируемым языком, а реальный код отправляется по проводам, хорошо, чтобы длина кода была небольшой - создание соответствующих объявлений и ссылок не помогает (это можно считать преждевременной оптимизацией). Функция должна быть довольно длинной в JS, прежде чем я решу ее разделить для явной цели "держать функции короткими".
Опять же, в JS иногда весь "класс" технически является функцией со многими вложенными подфункциями, но есть инструменты, помогающие справиться с этим.
С другой стороны, в JS переменные имеют область действия для длины функции, так что это фактор, который может ограничивать длину данной функции.
Функции, с которыми я имею дело (не пишу), становятся длинными, потому что они расширяются и расширяются, и никто не тратит время на то, чтобы перефакторировать функции. Они просто продолжают добавлять логику в функции, не задумываясь над общей картиной.
Я имею дело с множеством разработок "Cut-N-Paste"...
Таким образом, для бумаги, один аспект, на который нужно обратить внимание, это плохой план обслуживания / цикл и т. Д.
Код синтаксического анализа XML часто содержит множество обработчиков escape-символов в одной функции настройки.
Несколько идей, не упомянутых явно:
- повторяющиеся задачи, например, функция считывает таблицу базы данных со 190 столбцами и должна выводить их как плоский файл (при условии, что столбцы должны обрабатываться индивидуально, поэтому простой цикл по всем столбцам не годится). Конечно, вы можете создать 19 функций, каждая из которых будет содержать 10 столбцов, но это не улучшит программу.
- сложные, подробные API, такие как Oracle OCI. Когда кажущиеся простыми действия требуют большого количества кода, трудно разбить его на маленькие функции, которые имеют какой-либо смысл.