assert () против принудительного (): что выбрать?

Мне трудно выбрать, следует ли мне «принудительно применять» условие или «утверждать» условие в D. (Однако это не зависит от языка.)

Теоретически я знаю, что вы используете утверждения для поиска ошибок и применяете другие условия для проверки нетипичных условий. Например. вы могли бы сказать assert(count >= 0) в качестве аргумента вашего метода, потому что это указывает на то, что в вызывающей программе есть ошибка, и что вы бы сказали enforce(isNetworkConnected), потому что это не ошибка, это просто то, что вы предполагаете, что вполне может быть неправдой в законной ситуации вне вашего контроля.

Кроме того, утверждения могут быть удалены из кода в качестве оптимизации без побочных эффектов, но ограничения нельзя удалить, поскольку они должны всегда выполнять свой код условия. Следовательно, если я реализую контейнер с ленивым заполнением, который заполняется при первом обращении к любому из его методов, я говорю enforce(!empty()) вместо assert(!empty()), потому что проверка на empty() должна происходить всегда, так как он лениво выполняет код внутри.

Так что я думаю, что знаю, что они должны означать. Но теория легче практики, и мне трудно применять концепции.

Рассмотрим следующее:

Я создаю диапазон (похожий на итератор), который перебирает два других диапазона и добавляет результаты. (Для функциональных программистов: я знаю, что вместо этого я могу использовать map!("a + b"), но пока игнорирую это, так как это не иллюстрирует вопрос.) Итак, у меня есть код, который выглядит так в псевдокоде:

void add(Range range1, Range range2)
{
    Range result;
    while (!range1.empty)
    {
        assert(!range2.empty);   //Should this be an assertion or enforcement?
        result += range1.front + range2.front;
        range1.popFront();
        range2.popFront();
    }
}

Это должно быть утверждением или принуждением? (Является ли вина вызывающей стороны в том, что диапазоны не опустошаются одновременно? Возможно, он не имеет контроля над тем, откуда был взят диапазон — он мог быть получен от пользователя — но опять же, он по-прежнему выглядит как ошибка, не так ли?)

Или вот еще пример псевдокода:

uint getFileSize(string path)
{
    HANDLE hFile = CreateFile(path, ...);
    assert(hFile != INVALID_HANDLE_VALUE); //Assertion or enforcement?
    return GetFileSize(hFile); //and close the handle, obviously
}
...

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

Спасибо!


person user541686    schedule 25.02.2011    source источник


Ответы (3)


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

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

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

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

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

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

Другой пример для рассмотрения — operator[] против at() для вектора C++. at() выдает out_of_range, логическую ошибку, не потому, что немыслимо, что вызывающая сторона может захотеть восстановиться, или потому, что вы должны быть каким-то тупицей, чтобы совершить ошибку доступа к массиву за пределами диапазона, используя at(), а потому, что ошибка полностью можно избежать, если вызывающая сторона этого хочет - вы всегда можете проверить size() перед доступом, если у вас нет другого способа узнать, хорош ли ваш индекс или нет. Итак, operator[] вообще не гарантирует никаких проверок, и во имя эффективности доступ вне диапазона имеет неопределенное поведение.

person Steve Jessop    schedule 25.02.2011
comment
Я имел в виду нейтральность к языку для концепции и не обязательно для конкретных функций, но да, я думаю, это может немного отличаться в зависимости от языка. Однако я действительно не понимаю вот что: все диапазоны, передаваемые в add, должны иметь одинаковый размер, чтобы вызов был правильным. Когда я создаю библиотеку (в отличие от приложения), я не знаю, указывает ли это на ошибку или нет для вызывающей стороны — все, что я знаю, это как выполнить задачу, и что я не смогу сделать это правильно, если получу неверный ввод. Так должен ли я assert или выдать ошибку времени выполнения? Может виноват звонивший, а может и нет, я не знаю. - person user541686; 27.02.2011
comment
@Mehrdad: вы должны выбрать один, и ваш выбор должен быть вдохновлен предполагаемой целью функции, если у вас есть намеченная цель. Если у вас нет намеченной цели, вы, вероятно, выберете ее произвольно, и это фактически создаст намеченную цель или, по крайней мере, направит пользователей к определенному виду использования. Если единственная практическая разница заключается в том, какой именно класс исключения генерируется, то, конечно, это всего лишь очень слабое руководство, которое легко обойти. Если это разница между перехватываемым исключением и неперехватываемым отключением, то выбор важнее. - person Steve Jessop; 27.02.2011
comment
Кстати, если вы занимаетесь дизайном API и не думаете, как и почему пользователи будут вызывать ваши функции и как они должны вызывать ваши функции, то вы, вероятно, упускаете возможность улучшить свои API. во всех смыслах, не только в этом. Вероятно, вы рассматриваете такие вещи вообще, но не в данном случае, поэтому я не думаю, что тот факт, что вы пишете библиотеку, является здесь непреодолимым препятствием. - person Steve Jessop; 27.02.2011
comment
Что ж, предполагаемая цель такая же, как и у map в функциональных языках: рефакторинг общего кода, чтобы вы могли меньше фокусироваться на механике и больше на значениях. Кроме этого, семантика зависит от вызывающего абонента, поэтому я в недоумении. Но, тем не менее, я думаю, ваше объяснение хорошее; Спасибо. :) - person user541686; 27.02.2011
comment
@Mehrdad: но реорганизовать какой общий код? Если вы намерены заменить общий код, который проверяет длину списков, а затем заархивирует с добавлением, тогда применяйте. Если вы намереваетесь заменить код, который не проверяет, потому что он знает, утвердите (или, возможно, даже не потерпите неудачу - молча отбросьте лишнюю длину с одной стороны или дополните короткий размер нулями). Если у вас буквально нет никакого мнения, не делайте ни того, ни другого и принимайте любое поведение, которое язык естественным образом генерирует из вашего кода (по крайней мере, до тех пор, пока вы не задокументируете указанное поведение и не поймете, что оно сложное и имеет неинтуитивные крайние случаи...) - person Steve Jessop; 27.02.2011
comment
@Steve: я имел в виду рефакторинг кода для перебора диапазонов и вызова другой функции для каждого элемента ... Мне нравится ваша последняя идея о принятии решения во время документации. :) - person user541686; 27.02.2011
comment
@Mehrdad: конечно, как грубое практическое правило, если что-то трудно задокументировать, существует реальная опасность, что документацию также трудно понять и / или трудно использовать правильно. Это не просто предлог для того, чтобы отвлечься от скучных частей :-) - person Steve Jessop; 27.02.2011
comment
@Steve: Ха-ха, хорошо, спасибо. :) До сих пор я имел тенденцию писать код, который вместо того, чтобы генерировать исключения, просто убеждался, что он каким-то образом падает при плохом вводе (например, я удостоверяюсь, что нулевой параметр segfaults вместо того, чтобы явно генерировать исключение для нулевой аргумент), так что до сих пор мне удавалось расслабиться на скучных частях. Но спасибо за предупреждение. :] - person user541686; 27.02.2011
comment
@Mehrdad: я был бы доволен нулевыми ошибками параметра, если вы задокументировали, что нулевой параметр не является допустимым входом для вашей функции, явно или неявно, потому что вы сказали, что параметр является указателем на объект с такими-то свойствами. Тем не менее, некоторые люди выступают за более защищенный дизайн API, и что, поскольку null — это простой указатель для передачи в функцию, вы должны определить для него поведение, и желательно разумное поведение. Стандартная библиотека C не согласна, но тогда это довольно низкоуровневый API и определенно старомодный. - person Steve Jessop; 27.02.2011
comment
@Steve: Да, я определенно пытаюсь придать значение null, если это вообще возможно - я обычно позволяю параметру, допускающему значение null, означать, что он необязателен, если только это не имеет смысла, и в этом случае я просто делаю свой код segfault. В любом случае, я не допускаю, чтобы нулевой параметр приводил к неопределенному поведению, поэтому он либо дает результат по умолчанию, либо дает сбой. :) - person user541686; 27.02.2011
comment
@Mehrdad: ах да, разыменование нулевого указателя гарантировано приводит к segfault в D, не так ли? В C и C++ это неопределенное поведение. - person Steve Jessop; 27.02.2011
comment
@Steve: Если вы имеете в виду указатель типа int*, я не знаю и предполагаю, что он не определен. Но это не то, что я имел в виду под указателем — я действительно имел в виду ссылочные типы, такие как Object, которые по умолчанию могут принимать значение NULL. Я думаю, что они гарантированно выдают ошибку сегментации, но, честно говоря, меня это не волнует — она возникает в любой типичной системе, и учитывая, что поведение сегментации предназначено только для отладки (аргумент должен не t null в правильном коде в любом случае), я использую его, не заботясь о том, определено ли его разыменование технически или нет, поскольку в системах, в которых я отлаживаю свою программу, результат предсказуем. - person user541686; 27.02.2011
comment
@Steve: или, другими словами, меня волнует переносимость, только если она имеет практические последствия. В случае segfault для нулевого указателя я обнаружил, что можно с уверенностью предположить, что это всегда происходит в коде пользовательского режима, и что не нужно беспокоиться об этом, когда вы используете его только как средство отладки. - person user541686; 27.02.2011
comment
@Mehrdad: ах, хорошо, есть некоторые действительно тонкие вещи, которые могут пойти не так с разыменованием нулевых указателей в C. Например, есть оптимизация GCC, где, если компилятор обнаруживает, что вы разыменовываете указатель, он предполагает, что он не нулевой. Например, можно заменить код if (p != 0) return true; *p; return false; на return true;. Тогда он не будет аварийно завершать работу, даже если p равно нулю. Это привело к ошибке Linux, когда некоторый код ядра играл с картами памяти, которые полагались на получение (и обработку) segfault, но gcc удалил код, который мог вызвать этот segfault... - person Steve Jessop; 27.02.2011
comment
@Steve: Но никто не оптимизирует код режима отладки, верно? ;) (И, кстати, история с кодом режима ядра отличается — я в основном программировал только в пользовательском режиме, и то, что я сказал, на самом деле не относится к тому, что я буду делать в режиме ядра. В худшем случае приложение выйдет из строя, и опять же, в качестве средства отладки, этого достаточно.Кстати, это следует той же философии, что и принцип наилучших усилий, упомянутый в Java ConcurrentModificationException.) - person user541686; 27.02.2011
comment
@Mehrdad: GCC также выдает отладочную информацию даже при включенной оптимизации, так что на самом деле это возможно и полезно отлаживать с теми же оптимизациями, которые вы планируете выпустить. Затем сдайтесь и уменьшите оптимизацию, когда так называемый одношаговый режим выглядит как продолжающаяся неисправность транспортера. Проблема в этом коде ядра заключалась в том, что, судя по их POV, разыменование нулевого указателя имело определенное поведение, которое они ожидали спасти. Просто код, который они написали, на самом деле не выполнял разыменование после того, как с ним покончил GCC. То же самое может произойти и в пользовательском режиме. - person Steve Jessop; 27.02.2011
comment
Но я, безусловно, признаю, что это был редкий и интригующий случай, вряд ли когда-либо испортивший мне или вашему день. В конечном счете, вы все равно надеетесь найти ошибку (использование нулевого указателя), потому что какой-то тест где-то завершится ошибкой. Немедленная ошибка сегментации в большинстве случаев приятна, а не является ключевой частью безопасности вашего кода... - person Steve Jessop; 27.02.2011
comment
@Steve: Просто любопытно, зачем кому-то говорить *p;? (Одна из причин, по которой я ненавижу C/C++, заключается в том, что такие операторы вообще разрешены... они поощряют загадочный/опасный код без веской на то причины. В D вы должны сказать cast(void)*p;, если вы действительно хочу такое заявление) - person user541686; 27.02.2011
comment
@Mehrdad: просто пример из моей головы. Реальный код в случае этой ошибки ядра был длиннее и сложнее. IIRC фактически использовал реферанд указателя, а затем позже проверил его на значение null. Если бы он был нулевым при более раннем использовании, тогда ядро ​​​​обработало бы segfault и возобновило работу с кодом, я точно не помню, что происходило. Таким образом, более поздний тест был оптимизирован, предполагая, что он никогда не равен нулю, поскольку с точки зрения GCC каждый путь кода, достигший теста, сначала проходил через использование указателя. Но на самом деле нужно было провести более поздний тест. - person Steve Jessop; 27.02.2011
comment
О, или вдруг мне кажется, что я помню, может быть, в более раннем использовании нулевой указатель не вызвал бы ошибку сегментации, потому что в этот момент ядро ​​​​сопоставило что-то доступное с адреса 0 вверх, поэтому без оптимизации кода просто использовал бы эту память. Но проверка того, что значение указателя не равно нулю, потребовалась позже в подпрограмме из соображений безопасности. Извините, не могу вспомнить подробности. Однако принцип заключался в том, что программист думал, что знает, что такое UB, но на самом деле GCC сделал нечто более безумное, чем они ожидали. Режим ядра усугубил последствия ошибки. - person Steve Jessop; 27.02.2011
comment
@Steve: Хм, хорошо ... Я действительно надеюсь, что код, вызвавший это, не был неинтуитивным, как *p;. В любом случае, лично мне этой стратегии всегда было достаточно, и я никогда не сталкивался с такими странными ошибками. Но если я перейду к разработке режима ядра, я попытаюсь изменить свою практику. :) - person user541686; 27.02.2011
comment
@Steve: o___o ядро ​​сопоставило что-то доступное с адреса 0 вверх? Когда я написал свой собственный загрузчик (и [нано]ядро) для практики на D, первое действие, которое я сделал после включения подкачки, сделал нулевой адрес недоступным... и, фактически, следующее то, что я сделал, заключалось в том, чтобы также сделать адреса за пределами памяти недоступными... - person user541686; 27.02.2011

assert следует рассматривать как «комментарий, проверенный во время выполнения», указывающий на предположение, которое делает программист в данный момент. assert является частью реализации функции. Ошибка assert всегда должна считаться ошибкой в ​​том месте, где делается неправильное предположение, то есть в месте кода утверждения. Чтобы исправить ошибку, используйте соответствующие средства, чтобы избежать ситуации.

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

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

person Norbert Nemec    schedule 18.07.2018

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

person Vinod R    schedule 25.02.2011
comment
Вы прерываете поток в любом случае (исключение в любом случае). Вопрос в том, как вы решаете, использовать ли assert() или принудительно()? Иногда то, что является предварительным условием, может быть вне контроля вызывающего абонента, но опять же, это предварительное условие, поэтому я не знаю, что делать. - person user541686; 25.02.2011