Comments 175
«поощряется»? На мой взгляд это единственно верное выполнение функции. Все перечисленные проблемы, это же смех, притянуты за уши. Хочется добраться до реальных проблем, если таковые есть.
Тезис: функция должна соблюдать инварианты.
Сначала надо выполнить проверки входных данных и не приступать к работе, если они не соблюдены.
Дальше выполняется код, который должен иметь одну точку выхода.
Пример:
double div(a,b)
{
if(b==0.0) {
if(a==0.0) return NaN;
return sign(a)*Inf;
}
return a/b;
}
int main()
{
const double denom=3.14;
for (double num: {3.14, 2.71, 0.0}) {
cout<<div(num,denom)<<"\n";
}
}
Получается, что будет 2 лишних проверки, которые в реальном коде компилятор не сможет соптимизировать, если проверка не тривиальна.
Можно делать булевый параметр с значением по умолчанию — нужно ли делать дорогие проверки.
Если упороться в край, можно сделать его шаблонным. Тогда проверки будут вырезаны еще на этапе компиляции))
А почему, кстати?
Assert — это только для дебага, когда скорость не важна.
Тут возникает идея Design by contract.
#include <iostream>
enum class Infinity
{
Positive,
Negative
};
std::ostream& operator << (std::ostream& stream, Infinity value)
{
switch (value)
{
case Infinity::Positive:
stream << "+Inf";
break;
case Infinity::Negative:
stream << "-Inf";
}
return stream;
}
struct NaN {};
std::ostream& operator<< (std::ostream& stream, const NaN&)
{
return stream << "NaN";
}
template<size_t A, size_t B>
struct Div
{
static constexpr size_t Value = A / B;
};
template<size_t X>
struct Div<X, 0>
{
static constexpr Infinity Value = (X > 0) ? Infinity::Positive : Infinity::Negative;
};
template<>
struct Div<0, 0>
{
static constexpr NaN Value = NaN{};
};
#define CHECK(A, B) << "Div<" << #A << "," << #B << "> : " << Div<A,B>::Value << std::endl
int main()
{
std::cout
CHECK(5, 2)
CHECK(5, -2)
CHECK(-5, 2)
CHECK(-5, -2)
CHECK(5, 0)
CHECK(-5, 0)
CHECK(0, 1)
CHECK(0, -1)
CHECK(0, 0)
;
return 0;
}
Правда компилятор позволяет в шаблонах использовать только исчислимые типы.
int main()
{
auto denom=rand()+3; // всегда >0
for (int i=0; i<10; i++) {
cout<<Div<rand(),denom><<" ";
}
}
Хотелось бы что-то такое, что позволяет с одной стороны указать диапазон допустимых значений входящих параметров, а с другой — выкидывать эти проверки когда они и так выполнены уровнем выше по стеку вызова.
Если хочется рантайм с исключениями, можно использовать boost::variant / std::variant.
(несмотря на то, что это обсуждение уже далеко за пределами тематики исходной статьи)
if (isInvalid(param)) return Failed;
и
assert(isValid(param));
Первый способ связан с потерей производительности, которую вполне можно было бы избежать. Во втором случае проверки просто нет в Release, а если есть, то неудобно контролировать поток выполнения — это не return. Хотелось бы что-то вроде варианта с `if`, но так, что бы компилятор выкидывал проверку, когда это возможно. Также хотелось бы, что бы компилятор автоматически переносил эту проверку максимально высоко по стеку вызова. Лучшим решение этой проблемы, из тех что я знаю, является Design by Contract, которого в мейнстримных языках вроде нет, даже в Rust, если я правильно понимаю.
Сценарий 2. Данные получены из внешнего источника во время выполнения, их надо передать дальше. Проверить можно только во время выполнения. Выполнять следует на том слое, который уверенно может обработать данные на своём уровне.
Теперь контрольные вопросы:
0. Когда известны входные данные?
1. Применим ли наиболее быстрый вариант в вашем конкретном случае?
Если да, то выбор принят.
2. Если ответ на вопрос отрицательный, то можем ли мы локализовать место, где входные данные могут быть провалидированы?
Если да, то выбор принят. На остальных уровнях делаем код прозрачным к для исключений и сохраняющим инварианты.
3. Если ответ отрицательный, то можем ли мы спроектировать систему так, чтобы ответ на вопрос 2 стал утвердительным?
Если да, то переходим к пункту 2 и повторяем.
Если нет, то у вас проблемы с пониманием задачи.
Если откуда-то сверху этот аргумент приходит в виде, исключающем необходимость проверки, например, когда аргумент — промежуточный результат, полученный как 1+x^2?
А может быть есть другие проверки, которые гарантированно не дадут ноль в знаменателе, например, модуль аргумента строго больше единицы? Поймёт ли ваш чекер, что его проверка лишняя?
Далее, ну, допустим, проверили мы, можем сделать корректную обработку этой ситуации или для этого нужен контекст более высокий по стеку вызовов?
Могу ли я сделать вывод, что приведённый выше фрагмент не претендует на универсальность, а полезен лишь как пример, что так в некоторых ситуациях можно сделать, и закончим нашу дискуссию?
if Ошибка1 {
// Обработка ошибки 1
result = 1;
} elsif Ошибка2 {
// Обработка ошибки 2
result = 2;
} elsif Ошибка3 {
// Обработка ошибки 3
result = 3;
} else {
// Все ок, работаем дальше.
result = 4;
}
return result;
лишен недостатков 1, 3 и соответствует идее, переданной в статье.
Личное мнение по недостатку 4 — ВСЕГДА лучше использовать промежуточную переменную вместо множества RETURN операторов.
В некоторых ЯП для этих целей даже неявно переменная объявляется.
как в случае с контроллером из статьи.
Мне кажется пример с контроллером вообще не очень удачен в современном мире. Все, что в нем происходит можно "разбить" на более мелкие части и вынести в дргуие метса. Мне, например, очень нравиться как это реализовано в Laravel — валидация происходит вне контроллера, равно как и обработка ее ошибок и ACL.
Чтобы все-таки понять почему — ему придется заходить в отладчике во все if'ы и проверять «а не в этом ли условии программа выходит на уровень вверх?».
В случае с if-elsif-elsif-else достаточно поставить точку останова на 1м if-e и увидеть куда зайдет программа в процессе выполнения.
А глядя на конструкции
if Условие1 {
return;
}
if Условие2 {
return;
}
if Условие3 {
return;
}
// все ок, работаем.
создается ложное впечатление, что строка «все ок, работаем» выполняется всегда.
Выше писали, что внутри этого условия могут быть еще условия.
Во вложенных условиях еще условия.
Тогда чтобы понять тут ли выходит программа мне нужно по всей этой лесенке пройтись. Потом по следующей лесенке. Потом по третьей по счету.
Да, можно вынести лесенку в отдельный метод.
И вариант оформления if-elseif с единственным return в конце принуждает это делать.
Иначе не написать так, чтобы вложенных условий не было.
bool ok = true;
а после всех проверок
if (ok)
{
}
Иногда так даже удобней, но обычно можно и проще
// If vse Ok
{
}
Очевидное преимущество второго способа — не надо поднимать глаза и искать что за там $nextState объявлен, код четко разбит на 3 случая, читать проще будет.
Неужели для кого-то это что-то новое? Удивляет, что такую мелочь можно раздуть до размеров статьи, которую начинают переводить и репостить.
Кстати, ни слова про декларативный, императивный, контрактный и т.д. подходы, когда часть таких проверок вообще выносится из тела метода.
если честно подобный подход считаю очевидным, пришел к следующему правилам для себя:
- Только один return (не считая исключений), можно использовать переменную.
- return это последняя строка
- Исключения только для исключительных ситуаций.
- Не более одного уровня отступов, не считая отступов функции => рефакторить в приватный метод.
все остальное это следствие из этих правил.
Не более одного уровня отступов, не считая отступов функции => рефакторить в приватный метод.
И как алгоритм Флойда должен выглядеть?))
так как php используется в вебе то такое решение вполне сойдет
<?php
class Floid
{
private $w;
public function __construct(array $w, int $n)
{
// если нужно проверяем данные
$this->w = $w;
$this->n = $n;
}
public function getResult()
{
for ($k = 1; $k <= $this->n; $k++) {
$this->forByI($k);
}
return $this->w;
}
private function forByI($k)
{
for ($i = 1; $i <= $this->n; $i++) {
$this->calc($k, $i);
}
}
private function calc($k, $i)
{
for ($j = 1; $j <= $this->n; $j++) {
$this->w[$i][$j] = min(
$this->w[$i][$j],
$this->w[$i][$k] + $this->w[$k][$j]
);
}
}
}
если мы будем говорить об оптимизации производительности, потребление памяти, скорости и тд первое что стоит начать делать это взять компилируемый язык чтобы не терять время интерпретации, причем я ничего против пхп не имею в свое нише он дает достаточную производительность.
ах да по поводу отсутуов внутри min забыл упомянуть что такое в функциях можно делать но в меру, могу убрать но не вижу смысла зачем от этого избавляться
Вам не кажется, что исходный вариант (4 строки) выглядит проще?
Ну т.е. понятно, что можно разбивать код до уровня "любая инструкция в отдельной ф-ции", а в С++ это еще и работать быстро будет. Но по-моему не имеет смысл резать на отдельные функции что-то цельное, например алгоритмы.
исходный вариант 4 строки которые в примере это на самом деле, + 1 строка на функцию + 2 строки на скобки этой функции(в psr открывающая с новой строки) + 1 строка закрывающая скобка для каждого цикла (итого 3) + 1 строка на return
итого получаем 4 полезных строки размазанные по еще 7 строкам получаем 11 входных строк.
но даже учитывая 11 строк можно считать что он выглядит не сложно, но это не значит что так как я написал делать нельзя.
Но работа программиста это не писать максимально короткие программы, а писать софт который потом можно поддерживать потому что имея 11 строк я могу довольно много чего придумать так чтобы потом это было читать и понимать тяжело, поэтому мы не строками меримся, а пишем софт который потом поддерживать и большая часть времени читать.
Для примера цитата про "алгоритм установки gentoo" http://bash.im/quote/394695 если вам нравиться подобное в коде, то у всех свои вкусы и каждый вправе иметь свои взгляды.
по поводу разбивать на части то что цельное, вы когда запрос к бд посылаете вы ручками создаете сокет, подключаетесь к бд и посылаете запрос или все таки разделяете на уровень по работе с бд и потому что генерирует запросы?
ото алгоритм работы с бд можно назвать и это:
- Создать подключение
- Аутедентификация + авторизация
- выбрать бд
- цикл по запросам для бд
как бы тоже 4 строки, не надо все под одну гребенку нести.
а то что я разбил исходный текст на 3 метода это потому что у алгоритма 3 уровня отступов
При использовании готовых алгоритмов при таком подходе есть шанс усложнить поддержку. В одной функции алгоритм может быть узнаваем, а при разбитии на части — нет.
давайте я возьму очень популярный алгоритм которым большинство присутствующих пользуется и не задумывается (реализацию специально на пыхе найду) https://github.com/phpseclib/phpseclib/blob/master/phpseclib/Crypt/RSA.php
вот сразу как открываешь подобный файл и видишь родные $i, $j, $n прям сразу ясно становиться что за алгоритм
прям каждый день сниться когда все это в одной функции будет не разделено на вспомогательные вещи, прям сразу ясно что это RSA, а не DSA
теперь запишите его как вы говорили все в одну функцию, да да да decrypt, encrypt, sign, verify, createkey пусть там на входе массив будет и сказан тип операции что надо сделать и разные ключи с параметрами
я вам даже начну
<?php function rsa(array $params) { return ;/* тут надо его просто расчитать в зависимости от параметров и да я специально все в одну строку пишу, чтобы строки сэкономить это же так важно */}
и теперь ответьте честно будет выглядеть проще?
есть GodObject, но похоже можно еще создавать GodFunction или GodMethod, не знал о последних двух, спасибо что помогли их понять еще раз.
Ну, если я буду писать алгоритм RSA по описанию, то, скорее всего, выделю так как выделено в оригинальном описании, чтобы тот, кто знает это описание сразу его определил.
P.S. А экономия строк тут непричём, дело в разрыве знакомого многим процесса на выбранные из моей головы куски с придуманными мною названиями.
только что вот считать оригинальным описанием?
то что написано в rfc? RFC 2313, RFC 2437, RFC 3447 в каком именно?
или первая его оригинальная публикация в научных журналах ?
только вот в этих документах нет ничего конкретного по реализации (кроме математических формул) потому что парадигм программирования очень много:
- Императивное программирование
1.1. ООП
1.2. Процедурное и тд - Декларативное программироваине
2.1. Функциональное и тд
Вы будете искать rfc описывающее rsa в рамках ООП или ФП? таких нет, потому что rfc это описание и оно не привязано к реализации (парадигме или языку программирования), а как вы будете делать реализацию и в рамках какой парадигмы, зависит от вас и возможно от языка программирования что будете использовать (глупо например используя java писать в фп парадигме или использовать haskell и писать в ооп)
Даже если вы выберете ООП то тут есть еще куча вопрос:
- mutable или immutable ?
- stateless или statefull ?
То о чем вы говорите (о разрыве знакомого многим и лишними абстракциями) называется "связанность", что код должен отражать предметную область с которой он работает.
Только вот проблема в том что на вики выложен пример реализации в виде псевдо кода или какой нибудь наиболее наглядный пример на популярном языке, например в ФП не принято использовать циклы и вместо циклов бы тут использовали array map и реализация была бы совсем другой (в вики нет примера алгоритма флойда в рамках ФП).
Поэтому я согласен с тем что код должен быть сильно связан с предметной областью с которой он работает, но я не согласен с тем что есть одна и только одна каноническая реализация алгоритма, как минимум один и тот же алгоритм может выглядеть по разному в разных парадигмах программирования, также могут применяться разные оптимизации для повышения производительности и тд,
Я не силён в данной предметной области, чтобы аргументированно ответить на большинство ваших вопросов. Но в целом и не утверждаю, что есть одна и только одна каноническая реализация алгоритма. С другой стороны, чем меньше разбираешься в предметной области, тем меньше надо вносить отсебятины, особенно в такой области как криптография, где даже лишние вызовы функций могут быть использованы в качестве информации для взлома.
простите не вам про одну строку.
а как по вашему должен выглядеть алгоритм rsa со всеми основными вещами (decrypt, encrypt, sign, verify, createkey причем не разбивая на отдельные функции)
а также работу с BigInt тоже не выносить отдельно это же цельное, там ниже задал вопрос, но не вам к сожалению, поэтому и тут продублировал, а и еще постарайтесь сделать поменьше строк, "это очень важно".
так вопрос разделения действий очень относительный.
вот например взять тот же алгоритм флойда и пхп, там есть операция +, если я выйду за пределы int во время этой операции?
Для работы с большими числами есть bc math или gmp, но это же алгоритм флойда надо ли их использовать, мне кажется да (если можно за пределы int уйти) но вы говорите что алгоритм флойда это то неделимое и надо смотреть на всю картину в целом, давайте тогда и работу с большими числами реализуем внутри циклов, как тогда будет выглядеть алгоритм без вспомогательных функций (методов) для работы с большими числами ?
по поводу изображений пример не корректен, если было так как вы говорите то в редакторах не было бы zoom (ведь удобней смотреть на все изображение без приближений вы так написали), поэтому пример с изображениями как раз и говорит что иногда один элемент (изображение) состоит из множества разных элементов (пикселей и векторов, ведь внезапно бывает еще и векторная графика), но мы не задумываемся о этих деталях когда смотрим на изображение пока нет в этом необходимости.
к сожалению я так и не вижу реализацию rsa от вас
Сложение двух больших чисел — это одно отдельное действие. Если два вложенных цикла по одному массиву нужны для получения результата и без любого из них промежуточный результат не существует или не имеет смысла — это тоже одно отдельное действие.
Отдельные действия можно вынести в функции и использовать во многих других действиях, причем основываясь на интерфейсе и описании смысла результата. Части действий, вынесенные в функции, без знакомства с реализацией использовать нельзя.
не было бы zoom (ведь удобней смотреть на все изображение без приближений вы так написали)
Нет, я не писал, что приближения не нужны. Я писал, что в режиме большого приближения изображение целиком сложно воспринять. Так же как и воспринять реализацию алгоритма, наблюдая зависящие друг от друга куски реализации по отдельности на одном уровне.
к сожалению я так и не вижу реализацию rsa от вас
Реализация, разбитая на независимые отдельные действия меня устраивает. И писали про нее вы не мне.
И да, не вижу ни одной причины из обсуждения в этой ветке, почему кто-то должен всё объединять в одну функцию, так же как и почему кто-то должен разбивать логику на миллион функций, причем руководствуясь числом отступов, а не результатом анализа этой логики.
Я писал, что в режиме большого приближения изображение целиком сложно воспринять.
Понятие приближения зависит от изображения и от того что вы хотите увидеть, но обратите внимание вы сами начали говорить о
не имеет смысл резать на отдельные функции что-то цельное, например алгоритмы
Вы сами начали говорить о приватных методах, вместо того чтобы смотреть на картину, вы начали упоминать о пикселях из которых она состоит (приватных методов).
И я вам показал пример алгоритма где имеет смысл делить на отдельные методы, это rsa.
И да, не вижу ни одной причины из обсуждения в этой ветке, почему кто-то должен всё объединять в одну функцию
Вот я это уже цитировал выше повторю "не имеет смысл резать на отдельные функции что-то цельное, например алгоритмы"
теперь по поводу работы с большими числами это тоже алгоритмы, а значит исходя из ваших же слов не имеет смысла резать на отдельные функции, да алгоритм сложения больших чисел один, но есть куча алгоритмов умножения например.
и от того что вы хотите увидеть
Правильно. В данном случае речь о том, чтобы увидеть алгоритм целиком.
Вы сами начали говорить о приватных методах, вместо того чтобы смотреть на картину, вы начали упоминать о пикселях из которых она состоит (приватных методов).
Во-первых, это не я говорил.
Во-вторых, о том и речь, что нет места, где есть картина, есть только куча отдельных пикселей. В вашем примере нет места с двумя вложенными циклами, зато есть 3 места с одним циклом. Причем у одного из них название завязано на реализацию. А если мы for на foreach поменяем, или на map?
И я вам показал пример алгоритма где имеет смысл делить на отдельные методы, это rsa.
В этой ветке никто не говорил что делить на методы вообще не надо.
Вот я это уже цитировал выше повторю
В таком случае и я повторю: вы общались с другим человеком, обратите внимание на ники.
а значит исходя из ваших же слов не имеет смысла резать на отдельные функции
Исходя из моих слов (а именно "Отдельные действия можно вынести в функции и использовать во многих других действиях") это никак не следует. Потому что написано прямо противоположное. Сложение можно вынести в функцию и использовать в умножении. Отдельные независимые части, из которых состоит сложение тоже можно вынести в функции и использовать в сложении.
Вы подменяете понятия. Речь идет не о том, разбивать на функции или нет. Разбивать надо, это и так все знают. Речь о том, насколько подробно разбивать, и по каким границам. Если кто-то говорит, что не надо разбивать по отступам, это никак не означает, что разбивать не надо вообще. Потому что есть больше двух возможных вариантов.
Мой тезис — сложность восприятия кода нелинейно зависит от кол-ва функций. Например, в случае с алгоритмом Флойда, знающему его человеку ориентироваться будет проще, чем в длинном. Естественно это не значит, что все алгоритмы должны быть реализованы в одной функции.
Сложность зависит только от кол-ва функций? complexity(functionCount) или есть еще другие параметры ?
Например, в случае с алгоритмом Флойда, знающему его человеку ориентироваться будет проще, чем в длинном.
каким образом знание или незнание алгоритма зависит от его длинны? если я перепишу алгоритм флойда в одну строку вам будет проще в нем ориентироваться и он станет вам более знакомым?
кто вам сказал что псевдо код выложенный на сайте вики это единственная реализация?
вы слышали о различных парадигмах программирования и что в рамках этих парадигм также может применяться алгоритмы и иметь разные реализации которые визуально будут не очень похожи друг на друга, но при этом возвращать одинаковые результаты.
попробуйте реализовать алгоритм флойда в рамках фп и посмотрите понадобится ли вам вспомогательные функции (в том числе и анонимные).
Для меня главное, что значительно повышается скорость сопровождения кода, и сразу видны проблемы в чужой логике, которые люди не учли, когда вкладывали ифы.
При правильной органиизации кода return не теряется, наоборот, обработчики ошибок сразу видны в том месте где она происходит, а не после основного рабочего кода.
Не верить же на слово тому, кто вызывает функцию.
Необходимо выделить интерфейс из вашего класса (который в текущей версии содержит и валидацию, и бизнес-логику).
У интерфейса следует сделать две реализации: первая реализует бизнес-логику без какой-либо валидации входных параметров, вторая — содержит всю необходимую валидацию входных параметров. При этом вторая реализация инкапсулирует экземпляр исходного интерфейса.
После всех манипуляций вы можете использовать либо экземпляр первой реализации — при этом у вас не будет валидации (иногда бывает необходимо для тестов) — или же можете завернуть этот экземпляр в инстанс второй реализации — в этом случае добавляется валидация.
В общем, используйте декораторы :)
Проверить условие → прыгнуть дальше → проверить условие → прыгнуть дальше → проверить условие → прыгнуть на выполнение полезного действия.
Вложенные условия избавляют от прыжков, теперь цепочка инструкций будет: проверить условие → проверить условие → проверить условие → выполнить полезное действие.
Что лучше: одноуровневые условия и лишние прыжки или вложенные условия и отсутствие прыжков? Умеет ли компилятор оптимизировать лишние прыжки?
Даже если не умеет (особенно если речь об интерпретаторе, а не компиляторе), то по умолчанию приоритет у читаемости кода, а не у скорости его работы или потребляемой памяти.
Сейчас не найду точных цифр, но, помнится, что ошибочная ветка в if вместо else добавляет пару десятков тактов из-за предсказания ветвлений. Процессор идет по ветке обработки ошибки и потом, когда оказывается, что данные верны, вынужден откатиться назад. Хотя, когда все поставят патчи для Meltdown, эта проблема перестанет быть актуальной :)

А сейчас больше претензий к читаемости, конечно, для ранних выходов чётко видно, когда и что произойдёт этапами. Хотя где-то встречалось мнение, что для некоторых программ или приложений должен быть строго 1 оператор return, поэтому сомнения иногда закрадываются.
Стандарты пишут люди. Люди могут ошибаться.
А ещё стандарты развиваются, исправляются и дополняютсяИменно это я и хотел сказать. То, что это стандарт, и «так принято», еще не означает, что больше не существует других более выгодных вариантов написания кода.
И те же АЭС взрываются и самолеты падают — хотя у меня нет статистики по тому, какая часть из проблем связана с плохим кодом.
Мне ваша аргументация кажется очень слабой.
> обнаруженных дефектов в уже существующих и существовавших изделиях
Вы уверенны, что это имеет отношение к статье?
> Это к тому, что если что-то написано, то оно написано не «с потолка».
Мой пример с паролями — опровергает ваше утверждение.
> стандарты оперативно обновляются и перевыпускаются.
Вы в общем может быть правы, если исключить «оперативно».
> значит за десятки лет не нашлось ни одного события или существенного
> аргумента, обоснованно говорящих о его некорректности.
Утверждение никак не связано логически с тем, что вы написали до него. И в целом ложно.
> выверенной практике в отраслях, критичных к безопасности и
> корректности ПО.
Это факт или мнение?
> Я специально выше отметил, что это совершенно разные сферы с
> совершенно разными методами.
Безопасность и безопасность — это разные сферы. А безопасность и кодирование — это одно и то же. Я правильно вас не понимаю?
> В авиации и энергетике такое в принципе невозможно.
Это факт или мнение?
> И самое главное: когда появились обоснованные доводы
> ошибочности его пунктов, стандарт все-таки пересмотрели и
> перевыпустили.
И более ошибочные рекоммендации не используются, я так понимаю?
> Вывод очевиден: «за все это время не нашлось ни одного события
> или аргумента, весомо и объективно говорящего о его
> неправильности».
Вывод очевиден. Формальную логику в школах отменили зря.
Безопасность и безопасность — это разные сферы.
Да )) Безопасность в области "постить котиков", личных и корпоративных финансов, медицины и АЭС — это разные безопасности, разная цена ошибки, следовательно разные ресурсы выделяются на их предотвращение.
Безопасность (в контексте безошибочности) и удобство обычно противоречат друг другу, и в отраслях, где цена ошибки относительно низка удобство побеждает. Вон, посмотрите в топике про malloc какие дебаты пошли из-за одного if, причём большинство (субъективно) склоняется к мнению, что можно доверять исторически сложившемуся поведению некоторых компиляторов и некоторых реализаций ОС с некоторыми настройками. Что какие-то частные случаи не стоят вставки if после каждого malloc для проверки на NULL, поскольку на подавляющем большинстве систем реальных пользователей проверка не гарантирует реального выделения памяти, а фактически сложившиеся традиции в реализации дадут аналогичный самой простой проверке результат. Что это неопределенное поведение, в любой момент могущее измениться, никого не парит.
Я надеюсь, что статью вы читали и заметили, что автор не предлагал не проверять какие-то условия, что там кто ленится делать — это их ответственность, главное чтоб не вредили намеренно.
В статье речь шла о выделении логических блоков при написании функции. F0iL написал, что это противоречит безопасности и выверенной практике в отраслях. Что это вообще такое — выверенная практика в разработке ПО? Ну хорошо бывают стандарты — которые устаревают и пересматриваются, бывают рекоммендации, бывает то, что для кого-то работает в определенных условиях и они эти подходы масштабируют. Я привел пример того, что рекоммендации в индустрии бывают ошибочными, обновляются не достаточно быстро, мало того реализации обновленных рекоммендаций затягиваются на годы.
F0iL продолжил настаивать, воспользовавшись аргументом что это другая сфера. Что вы хотели сказать/доказать я вообще не понимаю.
Вообще статья мало нового постулирует. Схожие идеи развиваются гораздо более формально в рамках фреймворков типа .NET, где часть проверок уходит в аттрибуты, что это как не частный случай раннего возврата? В более широком смысле aspect oriented prorgramming. Точнее будет сказать, то что автор описывает в статье — это poor man's aspect oriented programming — переизобретение велосипеда.
В связи с этим дважды не понятно чего такое F0iL выдумывает, про стандарты, устоявшиеся практики и подобное.
ФАКТ, факта, муж. (лат. factum).
1. Действительное событие, явление, то, что произошло в действительности. Исторический факт. Смело смотреть фактам в лицо. Это — факт, а не выдумка.…
2. Данное, являющееся материалом для какого-нибудь заключения, вывода или служащее проверкой предположения, теории. Изложив факты, автор переходит к их толкованию. Исследование опирается на факты. Проверить факты. Опровергнуть что-нибудь с фактами в руках. Обильный фактами рассказ. Искажать факты.…
МНЕ́НИЕ, мнения, ср.
1. Взгляд на что-нибудь, суждение о чем-нибудь, выраженное в словах. Интересное мнение. Придерживаться какого-нибудь мнения по вопросу о чем-нибудь. По моему мнению. Разделять чье-нибудь мнение. Обменяться мнениями. Усвоить чужое мнение.
|| Официальное заключение по какому-нибудь вопросу, требующему решения (офиц.). Мнение экспертизы. Запросить мнение комиссии. Особое мнение (см. особый).
2. только ед. Оценка, то или иное суждение о ценности чего-нибудь. Быть низкого мнения о чем-нибудь. Быть о себе высокого мнения. Я хорошего мнения о вашем друге.
❖ Общественное мнение — суждение общества о чем-нибудь, отношение общества к чему-нибудь; само общество, как источник этих суждений. Считаться с общественным мнением. Благоприятное общественное мнение. Успокоить общественное мнение. Подготавливать общественное мнение.
----
> В статье предлагается идея, противоречащая утвержденной и
> выверенной практике в отраслях, критичных к безопасности и
> корректности ПО.
> В авиации и энергетике такое в принципе невозможно.
Вы либо заблуждаетесь, либо нас обманываете.
> То есть обосновать вы не можете. Понятно.
Вам обосновать формальную логику? Вы образцово продемонстрировали логическую ошибку.
А производительность??? Правильные данные случаются намного чаще, чем неправильные. А при этом подходе, код будет выполнятся медленней всего именно когда данные правильные.
То есть, вред конкретный и вещественный, а польза несколько виртуальная…
Нет, не думаю. Это преждевременная, сознательная пессимизация всего кода. А когда придет время оптимизировать (ведь, "преждевременно" не значит "никогда") такой код придется переписывать начисто.
Оптимизировать раньше срока, конечно плохо. Но надо писать optimization-friendly код.
В 95% прикладных задач основной затык на I/O (ожидание выполнения запроса на СУБД, ответа какого нибудь http api, чтение/запись файлов и т.д.), а уж никак не там где вы пишете. Это важно на вычислительных задачах, сложных алгоритмах, в системном ПО.
Так это же религиозная мантра. Мой опыт говорит наоборот – если пишешь оптимальный код, то программы получаются быстрые. Если всегда думаешь "преждевременная оптимизация зло!", то получаются медленные монстры, которые только убить можно, но никак не исправить. :)
Вот статья, человек просто сделал код на 20000% (sic!) быстрее, просто думая о оптимальности кода, а не написанием оптимального код:
От задач зависит все же.
А кто спорит? Но если человек сознательно начинает писать суб-оптимальный код, то он его будет писать всегда. Те же запросы к БД можно и нужно оптимизировать. И скорость реально повышается в десятки раз. Я писал форум на ассемблере и SQLite. Все говорят что SQLite медленная. Но нет, оказывается что SQLite очень даже быстрая, просто готовит запросы правильно надо. И теперь у меня есть самый быстрый форум в мире, на "самой медленной" БД.
А поскольку ожидание ввода-вывода от языка не зависит, то на PHP тот же функционал будет работать ну может на несколько процентов медленнее.
Автора знаете?
А не наоборот?
В целом именно правильные случаются чаще хоть в публичных, хоть в закрытых контурах, если ваша система не является целью постоянных атак хакеров, причём брутфорсных. Ну или ваша система сугубо одноразовая, пользователи пользуются ею один раз в жизни. При многократном использовании пользователи, пускай и с разной скоростью, но учатся вводить корректные данные для достижения своих целей. Причём корректные именно с точки зрения системы, а не документации на неё и(или) здравого смысла. Пускай на уровне "это невозможно понять, это нужно запомнить", но учатся.
Он нужен, прежде всего, во время чтения кода.
На самом деле, в языках с контрактами, а также хороших оптимизирующих компиляторах не надо делать 999999 проверок; они способны проанализировать вызывающую функцию и избыточные проверки отбросить.
Однако, это не спасает от необходимости проверять пользовательский ввод, в том числе с диска или по сети.
Получается я потратил ресурсы процессора просто так 999,999 раз и только чтоб отловить и залогировать всего одну ошибку.
А лог был "hack attempt!!!" в системе, которая ворочает деньгами на порядки бОльшими цена миллиардов тактов процессора.
// setup
int status = 0;
do {
// preconditions
status = doSomething();
if (status) break;
status = doSomethingElse();
if (status) break;
// computation
status = doWhatYouWantedToInTheFirstPlace();
} while (false);
// cleanup
return status;
Кто-то может играть на гитаре только по написанным аккордам и написанному бою (типа вверх-вниз-глушим).
А кто-то может сам понять на слух, какой аккорд взять и как бить по струнам в конкретной песне.
Только такой подход надо сочетать с принципом RAII.
А то со всеми этими return'ами можно ресурсы растерять.
Но принцип «раннего возврата» — более-менее универсальный.
Потому там где есть RAII — нелишним будет ранний возврат сочетать с RAII.
А то будут всякие утечки памяти, незакрытые файлы и навечно заблокированные мьютексы.
Да и код самой функции с «ранним возвратом» будет более стройным.
А то по мере углубления в функцию — возвраты становятся всё более хлопотными:
на первом if — просто выход (ничего ещё не делали);
на втором if — перед выходом надо файл закрыть, который открыли после первого if;
на третьем if — перед выходом надо и файл закрыть и память освободить;
на четвертом if — всё то же самое, плюс не забыть освободить мьютекс
И т.д.
Можно конечно возразить, мол надо сначала проверить все условия, а потом делать работу, в т.ч. выделять и захватывать ресурсы.
Но, во-первых, не всегда это возможно. От алгоритма зависит.
А во-вторых, принцип «раннего возврата» — более универсален, чем просто «проверка условий перед тем как засучить рукава». Необходимость возврата может возникнуть уже «в глубине» функции, а не в «проверочном заголовке». И легко можно что-то забыть освободить/закрыть.
Но надо только понимать, когда сначала стоит проверить проверить предусловия, а потом уже выделять ресурсы и отдавать их под контроль Раи. Перемешивать проверку и выделение ресурсов может быть весьма неэффективным.
Вот, наверное, да, хороший принцип — ранний возврат в блоке проверки предусловий, а дальше уже вложенные проверки или try/catch/finally, когд аначинается реальная работа.
Т.е. мы «делаем красиво», только в небольшой части функции — в начале.
А дальше — всё та же «лестница» из ифов-элсов.
Полумеры.
Как по мне, то разумный баланс между двумя крайностями. Ну и можно обойтись без лесенок в языках, где есть исключения или переходы.
А почему применение "раннего возврата" в любом месте функции (не только в "заголовке") — крайность?
При условии автоуправления ресурсами, конечно.
Фаулер описал этот прием 20 лет назад в книге "Рефакторинг".
Почему ранний возврат из функций так важен?