Чи поганий стиль зайве перевірити стан?


10

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

Я хочу навести вам невеликий приклад: припустимо, що існує текстовий файл, який містить рядки, що починаються з «а», рядки, що починаються з «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()
# ...

Ви можете собі уявити, що я не буду перевіряти цей стан лише тут, але, можливо, також і в інших функціях тощо.

Ви вважаєте це шумом чи це додає моєму коду якусь цінність?


5
Це в основному про те, чи ти кодуєш оборонно. Ви бачите, що цей код багато редагується? Чи ймовірно, що це буде частиною системи, яка повинна бути надзвичайно надійною? Я не бачу великої шкоди в засуванні assert()туди, щоб допомогти з тестуванням, але поза цим, мабуть, надмірно. Однак, це залежно від ситуації.
Latty

ваш інший випадок - це фактично мертвий / недоступний код. Переконайтеся, що немає системних вимог, які забороняють це.
NWS

@NWS: ти кажеш, що я повинен тримати інший випадок? Вибачте, я вас не розумію повністю.
marktani

2
не особливо пов'язане з питанням - але я зробив би це "твердження" інваріантом - що вимагатиме нового класу "Рядок" (можливо, з похідними класами для A&B), а не трактувати рядки як рядки і розповідати їм, що вони представляють ззовні. Я був би радий детальніше зупинитися на цьому на CodeReview
MattDavey

ти мав на увазі elif (line.startsWith("b"))? до речі, ви можете безпечно видалити ці сурові дужки за умов, вони не є ідіоматичними в Python.
tokland

Відповіді:


14

Це надзвичайно поширена практика, і спосіб поводження з нею здійснюється через фільтри вищого порядку .

По суті, ви передаєте функцію методу фільтра разом зі списком / послідовністю, за якою ви хочете фільтрувати, і отриманий список / послідовність містить лише ті елементи, які ви хочете.

Мені незнайомий синтаксис 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 # (передбачається безліч, інакше буде використано 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 # урожайність та / або вихід! заяви. Це трохи очевидніше в моєму прикладі f #, тому що Seq.filter можна застосовувати лише до колекцій IEnumerable <T>, але обидва приклади коду працюватимуть, якщо linesце створена колекція.
Стівен Еверс

@mcwise: Коли ви починаєте дивитись на всі інші доступні функції, що діють таким чином, він починає отримувати справді сексуальну та неймовірно виразну, тому що їх можна зв'язати і скласти разом. Подивіться skip, take, reduce( aggregateв .NET), map( selectв .NET), і є більше , але це дійсно тверде початок.
Стівен Еверс

14

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

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

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

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


"Це дуже рідко, коли кілька циклів процесора варті того, щоб витратити день на усунення неполадок." Дякую за відповідь, у вас є хороший момент.
Мартані

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", це може бути менш зрозумілим.
Латті

Перша пропозиція має заслугу ... друга (припустимо, "б") - погана ідея
Андрій

@Lattyware Я покращив відповідь. Дякуємо за ваші коментарі.
Tulains Córdova

1
@Andrew я покращив відповідь. Дякуємо за ваші коментарі.
Tulains Córdova

3

При проектуванні за контрактом , можна здогадатися, що кожна функція повинна виконувати свою роботу, як описано в її документації. Отже, кожна функція має перелік попередніх умов, тобто умов на входах функції, а також пост-умов, тобто умов виведення функції.

Ця функція повинна гарантувати своїм клієнтам, що, якщо входи відповідають умовам, тоді вихід буде таким, як описано пост-умовами. Якщо хоча б одна з попередніх умов не виконується, функція може робити все, що завгодно (збій, повернення будь-якого результату, ...). Тому до та після умови - це семантичний опис функції.

Завдяки контракту функція впевнена, що її клієнти правильно її використовують, а клієнт впевнений, що функція виконує свою роботу правильно.

Деякі мови обробляють контракти на самому або через спеціальні рамки. Для інших найкраще перевірити попередні та пост-умови завдяки твердженням, як сказав @Lattyware. Але я б не називав це оборонним програмуванням, оскільки, на мій погляд, ця концепція більше орієнтована на захист від (людського) входу користувача.

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

Складніше - це визначити, яка функція відповідає за що, і чітко документувати ці ролі.


1

Насправді вам не потрібні clear_lines () на початку. Якщо рядок не є ні "a" або "b", умовні умови просто не запускаються. Якщо ви хочете позбутися цих рядків, тоді перетворіть інші в clear_line (). Як відомо, ви робите два проходи через ваш документ. Якщо ви пропустили clear_lines () на початку і зробите це як частина циклу foreach, ви скоротите час обробки в два рази.

Це не тільки поганий стиль, це погано обчислювально.


2
Можливо, ці рядки використовуються для чогось іншого, і з ними потрібно розібратися, перш ніж мати справу з "a"/ "b"рядками. Не кажучи, що це ймовірно ( чітка назва означає, що їх відкидають), тільки що є можливість, що це потрібно. Якщо цей набір рядків повторно повторюється в майбутньому, можна також заздалегідь видалити їх, щоб уникнути безлічі безглуздих ітерацій.
Латті

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 ... } )
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.