Это плохой стиль, чтобы избыточно проверить состояние?

10

Я часто попадаю на позиции в моем коде, где я снова и снова проверяю определенное условие.

Я хочу привести небольшой пример: предположим, что есть текстовый файл, который содержит строки, начинающиеся с «a», строки, начинающиеся с «b», и другие строки, и я на самом деле хочу работать только с первыми двумя видами строк. Мой код будет выглядеть примерно так (используя python, но читайте его как псевдокод):

# ...
clear_lines() # removes every other line than those starting with "a" or "b"
for line in lines:
    if (line.startsWith("a")):
        # do stuff
    elif (line.startsWith("b")):
        # magic
    else:
        # this else is redundant, I already made sure there is no else-case
        # by using clear_lines()
# ...

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

Вы думаете об этом как о шуме или это добавляет ценность моему коду?

marktani
источник
5
В основном это вопрос о том, защищаете ли вы код или нет. Вы видите, что этот код много редактируется? Возможно ли, что это будет частью системы, которая должна быть чрезвычайно надежной? Я не вижу большого вреда в том, чтобы засунуть assert()туда, чтобы помочь с тестированием, но помимо этого, вероятно, чрезмерно. Тем не менее, это будет меняться в зависимости от ситуации.
Latty
ваш случай "else" по сути мертвый / недоступный код. Убедитесь, что нет общесистемных требований, которые запрещают это.
NWS
@NWS: ты хочешь сказать, что я должен заняться другим делом? Извините, я вас полностью не понимаю.
Марктани
2
не особенно связанный с вопросом - но я бы превратил это «утверждение» в инвариант - что потребовало бы нового класса «Line» (возможно, с производными классами для A & B), вместо того, чтобы рассматривать строки как строки и сообщать им, что они представляют снаружи. Я был бы рад подробно остановиться на этом на CodeReview
MattDavey
ты имел ввиду elif (line.startsWith("b"))? кстати, вы можете смело убирать те окружающие скобки на условиях, они не идиоматичны в Python.
Tokland

Ответы:

14

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

По сути, вы передаете функцию методу filter вместе со списком / последовательностью, по которой вы хотите фильтровать, и результирующий список / последовательность содержит только те элементы, которые вы хотите.

Я не знаком с синтаксисом Python (хотя он содержит такую ​​функцию, как видно по ссылке выше), но в c # / f # это выглядит так:

C #:

var linesWithAB = lines.Where(l => l.StartsWith("a") || l.StartsWith("b"));
foreach (var line in linesWithAB)
{
    /* line is guaranteed to ONLY start with a or b */
}

f # (предполагается, что ienumerable, иначе будет использован List.filter):

let linesWithAB = lines
    |> Seq.filter (fun l -> l.StartsWith("a") || l.StartsWith("b"))

for line in linesWithAB do
    /* line is guaranteed to ONLY start with a or b */

Итак, чтобы быть ясно: если вы используете проверенный и проверенный код / ​​шаблоны, это плохой стиль. Это и изменение списка в памяти так, как вы это делаете через clear_lines (), лишает вас безопасности потоков и любых надежд на параллелизм, которые вы могли бы иметь.

Стивен Эверс
источник
3
Как примечание, синтаксис питона для этого было бы выражение генератора: (line for line in lines if line.startswith("a") or line.startswith("b")).
Latty
1
+1 за указание на то, что (ненужная) императивная реализация clear_linesдействительно плохая идея. В Python вы, вероятно, будете использовать генераторы, чтобы избежать загрузки всего файла в память.
Tokland
Что происходит, когда входной файл больше доступной памяти?
Blrfl
@Blrfl: хорошо, если термин генератор совпадает между c # / f # / python, то, что @tokland и @Lattyware переводит в c # / f # yield и / или yield! заявления. Это немного более очевидно в моем примере f #, потому что Seq.filter можно применять только к коллекциям IEnumerable <T>, но оба примера кода будут работать, если linesэто сгенерированная коллекция.
Стивен Эверс
@mcwise: Когда вы начинаете смотреть на все другие доступные функции, которые работают таким образом, он становится действительно сексуальным и невероятно выразительным, потому что все они могут быть объединены в цепочку и объединены. Посмотрите skip, take, reduce( aggregateв .NET), map( selectв .NET), и есть больше , но это действительно твердое начало.
Стивен Эверс
14

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

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

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

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

Карл Билефельдт
источник
«Очень редко, когда несколько циклов процессора стоят потраченного дня на поиск и устранение неисправностей». Спасибо за ответ, у вас есть хорошая точка зрения.
Марктани
5

Вы можете поднять исключение в elseслучае. Таким образом, это не избыточно. Исключение составляют вещи, которые не должны происходить, но в любом случае проверяются.

clear_lines() # removes every other line than those starting with "a" or "b"
for line in lines:
    if (line.startsWith("a)):
        # do stuff
    if (line.startsWith("b")):
        # magic
    else:
        throw BadLineException
# ...
Тулаинс Кордова
источник
Я бы сказал, что последнее - плохая идея, так как она менее явная - если позже вы решите добавить "c", это может быть менее понятно.
Latty
Первое предложение имеет свои достоинства ... второе (предположим, "b") - плохая идея
Андрей
@ Lattyware я улучшил ответ. Спасибо за ваши комментарии.
Тулаинс Кордова
1
@ Андрей я улучшил ответ. Спасибо за ваши комментарии.
Тулаинс Кордова
3

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

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

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

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

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

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

mgoeminne
источник
1

На самом деле вам не нужен clear_lines () в начале. Если строка не является ни «a», ни «b», условия просто не сработают. Если вы хотите избавиться от этих строк, превратите остальное в clear_line (). В его нынешнем виде вы делаете два прохода через ваш документ. Если вы пропустите clear_lines () в начале и сделаете это как часть цикла foreach, тогда вы сократите время обработки вдвое.

Это не только плохой стиль, это плохо в вычислительном отношении.

Мировой инженер
источник
2
Может случиться так, что эти строки используются для чего-то другого, и с ними нужно разобраться, прежде чем иметь дело с линиями "a"/ "b". Не сказать, что это вероятно ( ясное имя подразумевает, что их отбрасывают), просто есть вероятность, что это необходимо. Если этот набор строк будет многократно повторяться в будущем, может быть целесообразно удалить их заранее, чтобы избежать бессмысленной итерации.
Latty
0

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

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

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

Бок МакДонах
источник
0

... предположим, что есть текстовый файл, который содержит строки, начинающиеся с "a", строки, начинающиеся с "b", и другие строки, и я на самом деле хочу работать только с первыми двумя видами строк. Мой код будет выглядеть примерно так (используя python, но читайте его как псевдокод):

# ...
clear_lines() # removes every other line than those starting with "a" or "b"
for line in lines:
    if ...

Я ненавижу if...then...elseконструкции. Я бы избежал всей проблемы:

process_lines_by_first_character (lines,  
                                  'a' => { |line| ... a code ... },
                                  'b' => { |line| ... b code ... } )
Кевин Клайн
источник