Статьи

Многократные Заявления о возврате

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

Поэтому я впервые сел и сравнил два подхода.

обзор

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

Чтобы не всегда писать о «методах с множественными операторами возврата», я буду называть подход к структуре методов таким образом, паттерном. Хотя это может быть немного за бортом, это, конечно, более кратко.

Дискуссия

Я обсуждаю, должен ли метод всегда выполняться до последней строки, откуда он возвращает свой результат, или может иметь несколько операторов return и «возвращаться раньше».

Это не новое обсуждение, конечно. См., Например, Wikipedia , Hacker Chick или StackOverflow .

Структурированное программирование

Идея о том, что единственное возвращаемое утверждение является желательным, вытекает из парадигмы структурного программирования , разработанной в 1960-х годах. Что касается подпрограмм, это способствует тому, что они имеют единую точку входа и одну точку выхода. Хотя современные языки программирования гарантируют первое, последнее несколько устарело по нескольким причинам.

Основной проблемой, решаемой единственной точкой выхода, были утечки памяти или ресурсов. Это происходило, когда оператор возврата где-то внутри метода препятствовал выполнению некоторого кода очистки, который был расположен в его конце. Сегодня большая часть этого обрабатывается языковой средой исполнения (например, сборкой мусора), и явные блоки очистки могут быть записаны с помощью try-catch-finally. Так что теперь обсуждение в основном вращается вокруг читабельности.

читабельность

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

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

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

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

Другие факторы

Однако читабельность может быть не единственным фактором.

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

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

Ситуации для множественных возвратов

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

Каждая ситуация будет сопровождаться примером кода. Обратите внимание, что они сокращены, чтобы донести эту мысль, и могут быть улучшены несколькими способами.

Пункты охраны

Пункты охраны стоят в начале метода. Они проверяют его аргументы и для определенных особых случаев немедленно возвращают результат.

Охранная оговорка против нулевых или пустых коллекций

1
2
3
4
5
6
7
8
9
private Set<T> intersection(Collection<T> first, Collection<T> second) {
    // intersection with an empty collection is empty
    if (isNullOrEmpty(first) || isNullOrEmpty(second))
        return new HashSet<>();
 
    return first.stream()
            .filter(second::contains)
            .collect(Collectors.toSet());
}

Исключение крайних случаев в начале имеет несколько преимуществ:

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

В основном все методы, для которых этот шаблон применим, выиграют от его использования.

Примечательным сторонником оговорок о защите является Мартин Фаулер, хотя я бы рассмотрел его пример на грани ветвления (см. Ниже).

разветвление

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

Делегирование по специализированным методам

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public Offer makeOffer(Customer customer) {
    boolean isSucker = isSucker(customer);
    boolean canAffordLawSuit = customer.canAfford(
            legalDepartment.estimateLawSuitCost());
 
    if (isSucker) {
        if (canAffordLawSuit)
            return getBigBucksButStayLegal(customer);
        else
            return takeToTheCleaners(customer);
    } else {
        if (canAffordLawSuit)
            return getRid(customer);
        else
            return getSomeMoney(customer);
    }
}

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

Использование нескольких операторов возврата имеет несколько преимуществ по сравнению с переменной результата и одним возвратом:

  • метод более четко выражает свое намерение перейти к подпрограмме и просто вернуть свой результат
  • на любом нормальном языке метод не компилируется, если ветви не охватывают все возможности (в Java это также может быть достигнуто с помощью одного возврата, если переменная не инициализирована значением по умолчанию)
  • нет дополнительной переменной для результата, которая охватывала бы почти весь метод
  • Результатом вызванного метода нельзя манипулировать до его возвращения (в Java это также может быть достигнуто с помощью одного возврата, если переменная является final а ее класс неизменным; хотя последний не очевиден для читателя)
  • если оператор switch используется в языке с ошибкой (например, в Java), операторы немедленного возврата сохраняют строку для каждого случая, потому что не требуется break , что уменьшает шаблон и улучшает читаемость

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

Каскадные проверки

Иногда поведение метода в основном состоит из нескольких проверок, где результат каждой проверки может сделать ненужными дальнейшие проверки. В этом случае лучше вернуться как можно скорее (возможно, после каждой проверки).

Каскадные проверки при поиске родительского якоря

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
private Element getAnchorAncestor(Node node) {
    // if there is no node, there can be no anchor,
    // so return null
    if (node == null)
        return null;
 
    // only elements can be anchors,
    // so if the node is no element, recurse to its parent
    boolean nodeIsNoElement = !(node instanceof Element);
    if (nodeIsNoElement)
        return getAnchorAncestor(node.getParentNode());
 
    // since the node is an element, it might be an anchor
    Element element = (Element) node;
    boolean isAnchor = element.getTagName().equalsIgnoreCase("a");
    if (isAnchor)
        return element;
 
    // if the element is no anchor, recurse to its parent
    return getAnchorAncestor(element.getParentNode());
}

Другими примерами этого являются обычные реализации equals или compareTo в Java. Они также обычно состоят из каскада проверок, где каждая проверка может определять результат метода. Если это так, значение немедленно возвращается, в противном случае метод продолжается со следующей проверкой.

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

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

Поиск

Там, где есть структуры данных, есть элементы с особыми условиями, которые можно найти в них. Методы, которые ищут их, часто выглядят похожими. Если такой метод встречает элемент, который он искал, часто легче сразу его вернуть.

Немедленно вернуть найденный элемент

01
02
03
04
05
06
07
08
09
10
11
12
13
private <T> T findFirstIncreaseElement(Iterable<T> items, Comparator<? super T> comparator) {
    T lastItem = null;
    for (T currentItem : items) {
        boolean increase = increase(lastItem, currentItem, comparator);
        lastItem = currentItem;
 
        if (increase) {
            return currentItem;
        }
    }
 
    return null;
}

По сравнению с одним оператором return это избавляет нас от необходимости искать выход из цикла. Это имеет следующие преимущества:

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

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

отражение

Мы видели аргументы за и против множественных операторов возврата и критическую роль, которую играет чистый код. Категоризация должна помочь выявить повторяющиеся ситуации, в которых метод выиграет от раннего возвращения.

Ссылка: Несколько заявлений о возврате от нашего партнера JCG Николая Парлога в блоге CodeFx .