Как лучше всего заменить или заменить if..else if..else деревья в программах? - PullRequest
33 голосов
/ 06 февраля 2009

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

Чтобы быть как можно более конкретным, вот что я имею в виду:

if (i == 1) {
    doOne();
} else if (i == 2) {
    doTwo();
} else if (i == 3) {
    doThree();
} else {
    doNone();
}

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

(i == 1) ? doOne() : 
(i == 2) ? doTwo() :
(i == 3) ? doThree() : doNone();

или используя Map (на Java и я тоже думаю на C #), Dictionary или любую другую структуру K / V, например:

public interface IFunctor() {
    void call();
}

public class OneFunctor implemets IFunctor() {
    void call() {
        ref.doOne();
    }
}

/* etc. */    

Map<Integer, IFunctor> methods = new HashMap<Integer, IFunctor>();
methods.put(1, new OneFunctor());
methods.put(2, new TwoFunctor());
methods.put(3, new ThreeFunctor());
/* .. */
(methods.get(i) != null) ? methods.get(i).call() : doNone();

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

Итак, какие другие - и, скорее всего, лучшие - способы заменить if..else if .. еще там, и какой из них ваш любимый?

Ваши мысли под этой строкой!


Хорошо, вот ваши мысли:

Во-первых, самым популярным ответом был оператор switch, например:

switch (i) {
    case 1:  doOne(); break;
    case 2:  doTwo(); break;
    case 3:  doThree(); break;
    default: doNone(); break;
}

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

Другой и, возможно, более причудливый способ, которым вы, похоже, предлагаете, - это сделать это с помощью полиморфизма. Лекция на Youtube, связанная с CMS, - это отличные часы, посмотрите ее здесь: «Чистые разговоры по коду - наследование, полиморфизм и тестирование» Насколько я понял, это будет примерно так:

public interface Doer {
    void do();
}

public class OneDoer implements Doer {
    public void do() {
        doOne();
    }
}
/* etc. */

/* some method of dependency injection like Factory: */
public class DoerFactory() {
    public static Doer getDoer(int i) {
        switch (i) {
            case 1: return new OneDoer();
            case 2: return new TwoDoer();
            case 3: return new ThreeDoer();
            default: return new NoneDoer();
        }
    }
}

/* in actual code */

Doer operation = DoerFactory.getDoer(i);
operation.do();

Два интересных момента из Google Talk:

  • Использовать нулевые объекты вместо возврата нулей (и, пожалуйста, выбрасывайте только исключения времени выполнения)
  • Попробуйте написать небольшой проект без if: s.

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

Спасибо всем за ответы (пока), думаю, я мог бы кое-что узнал сегодня!

Ответы [ 21 ]

3 голосов
/ 06 февраля 2009

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

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

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

3 голосов
/ 06 февраля 2009

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

Для случаев, когда doX () всегда возвращает true.

i==1 && doOne() || i==2 && doTwo() || i==3 && doThree()

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

Вы также можете предоставить назначения.

i==1 && (ret=1) || i==2 && (ret=2) || i==3 && (ret=3)

Как написано

if(a==2 && b==3 && c==4){
    doSomething();
else{
    doOtherThings();
}

запись

a==2 && b==3 && c==4 && doSomething() || doOtherThings();

И в случаях, когда не уверены, что функция возвратит, добавьте || 1: -)

a==2 && b==3 && c==4 && (doSomething()||1) || doOtherThings();

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

"if" встречается редко в некоторых моих кодах, и я дал ему название "if-less Программирование": -)

2 голосов
/ 06 февраля 2009

Я бы сказал, что ни одна программа не должна использоваться больше. Если вы делаете, вы просите неприятностей. Вы никогда не должны предполагать, что если это не X, то должно быть Y. Ваши тесты должны проверяться для каждого отдельно и проваливаться после таких испытаний.

2 голосов
/ 06 февраля 2009

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

1 голос
/ 04 января 2018

Используйте троичного оператора!

Тернарный оператор (53 символа):

i===1?doOne():i===2?doTwo():i===3?doThree():doNone();

Если (108Characters):

if (i === 1) {
doOne();
} else if (i === 2) {
doTwo();
} else if (i === 3) {
doThree();
} else {
doNone();
}

Переключатель ((ДАЖЕ БОЛЬШЕ, ЕСЛИ!?!?) 114 символов):

switch (i) {
case 1: doOne(); break;
case 2: doTwo(); break;
case 3: doThree(); break;
default: doNone(); break;
}

это все, что вам нужно! это только одна строка, и она довольно аккуратна, намного короче, чем переключатель, и если!

1 голос
/ 06 февраля 2009

Я рассматриваю эти конструкции if-elseif -... как "шум ключевых слов". Хотя может быть ясно, что он делает, ему не хватает краткости; Я считаю лаконичность важной частью читабельности. Большинство языков предоставляют что-то вроде оператора switch. Построение карты - это способ получить что-то похожее в языках, в которых его нет, но это, безусловно, похоже на обходной путь, и есть некоторые накладные расходы (оператор switch переводит в некоторые простые операции сравнения и условные переходы, но карту сначала встраивается в память, затем запрашивается, и только после этого происходит сравнение и переход).

В Common Lisp встроены две конструкции-переключателя, cond и case. cond допускает произвольные условия, тогда как case только проверяет на равенство, но является более кратким.

(cond ((= i 1)
       (do-one))
      ((= i 2)
       (do-two))
      ((= i 3)
       (do-three))
      (t
       (do-none)))</p>

<p>(case i
      (1 (do-one))
      (2 (do-two))
      (3 (do-three))
      (otherwise (do-none)))

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

В Perl вы можете использовать оператор for, необязательно с произвольной меткой (здесь: SWITCH):

SWITCH: for ($i) {
    /1/ && do { do_one; last SWITCH; };
    /2/ && do { do_two; last SWITCH; };
    /3/ && do { do_three; last SWITCH; };
    do_none; };
1 голос
/ 06 февраля 2009

В Python я бы написал ваш код как:

actions = {
           1: doOne,
           2: doTwo,
           3: doThree,
          }
actions[i]()
1 голос
/ 06 февраля 2009

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

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

Это, как говорится: небольшое дерево if..else хорошо (на самом деле, я высказывался в пользу дней около 3 if..elses вместо использования Map недавно). Когда вы начинаете вводить в них более сложную логику, это становится проблемой из-за удобства обслуживания и читаемости.

1 голос
/ 06 февраля 2009

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

0 голосов
/ 06 февраля 2009

Если вы действительно должны иметь кучу тестов if и хотите делать разные вещи, когда тест верен, я бы порекомендовал цикл while только с ifs-no else. Каждый тест if вызывает метод, а затем выходит из цикла. Нет, нет ничего хуже, чем куча стеков, если / else / if / else и т. Д.

...