assert() vs forcece (): что выбрать?
Я с трудом выбираю, следует ли мне "принудительно" применять условие или "утверждать" условие в 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
}
...
Должно ли это быть утверждение или принуждение? Путь может исходить от пользователя - поэтому он не может быть ошибкой - но все же предварительным условием этого метода является то, что путь должен быть действительным. Я утверждаю или применяю?
Спасибо!
3 ответа
Я не уверен, что это полностью не зависит от языка. Нет языка, который я использую enforce()
и если бы я столкнулся с тем, что сделал, то я хотел бы использовать assert
а также enforce
так, как они были задуманы, что может быть идиоматичным для этого языка.
Например assert
в C или C++ останавливает программу при сбое, она не выдает исключение, поэтому ее использование может не совпадать с тем, о чем вы говорите. Вы не используете assert
в C++, если только вы не думаете, что либо вызывающий объект уже сделал ошибку настолько серьезную, что на нее нельзя полагаться для очистки (например, передав отрицательный счет), либо другой код в другом месте сделал ошибку настолько серьезную, что Программа должна находиться в неопределенном состоянии (например, ваша структура данных выглядит поврежденной). C++ действительно различает ошибки времени выполнения и логические ошибки, которые могут примерно соответствовать, но я думаю, что в основном это ошибки, которых можно избежать, а не ошибки, которых можно избежать.
В случае add
вы бы использовали логическую ошибку, если автор задумал, что программа, предоставляющая несоответствующие списки, содержит ошибки и нуждается в исправлении, или исключение времени выполнения, если это только одна из тех вещей, которые могут произойти. Например, если ваша функция должна была обрабатывать произвольные генераторы, которые не обязательно должны сообщать об их длине, не деструктивно оценивая всю последовательность, вы, скорее всего, сочтете это неизбежным условием ошибки.
Если назвать это логической ошибкой, то вызывающий абонент обязан проверить длину перед вызовом. add
Если они не могут обеспечить это с помощью чистого разума. Таким образом, они не будут передавать список от пользователя без предварительной явной проверки длины, и, честно говоря, должны считать себя счастливчиками, что даже получили исключение, а не неопределенное поведение.
Называя ее ошибкой во время выполнения, мы выражаем, что "разумно" (если оно ненормально) передавать списки разной длины, за исключением того, что это произошло в этом случае. Поэтому я думаю, что это скорее принуждение, чем утверждение.
В случае filesize
: для существования файла вы должны, если возможно, рассматривать это как потенциально исправимый сбой (принудительное применение), а не как ошибку (утверждение). Причина в том, что вызывающий не может быть уверен, что файл существует - всегда найдется кто-то с большими привилегиями, который может прийти и удалить его, или размонтировать всю файловую систему, между проверкой существования и вызовом filesize
, Следовательно, это необязательно логический недостаток в вызывающем коде, когда он не существует (хотя конечный пользователь мог выстрелить себе в ногу). Из-за этого факта, вероятно, будут абоненты, которые могут рассматривать это как одну из тех вещей, которые происходят, неизбежное состояние ошибки. Создание дескриптора файла также может привести к сбою для нехватки памяти, что является еще одной неизбежной ошибкой в большинстве систем, хотя не обязательно исправимой, если, например, включена избыточная фиксация.
Другой пример для рассмотрения operator[]
против at()
для вектора C++. at()
бросает out_of_range
логическая ошибка не потому, что вызывающий может захотеть восстановиться, или потому, что вы должны быть каким-то тупиком, чтобы совершить ошибку доступа к массиву вне диапазона, используя at()
, но поскольку ошибки можно избежать, если вызывающая сторона этого хочет, вы всегда можете проверить size()
перед доступом, если у вас нет другого способа узнать, хорош ли ваш индекс или нет. Так что operator[]
не гарантирует никаких проверок вообще, и во имя эффективности доступ вне зоны доступа имеет неопределенное поведение.
assert
должен рассматриваться как "проверенный комментарий во время выполнения", указывающий на предположение, которое программист делает в этот момент. assert
является частью реализации функции. Не удалось assert
всегда следует считать ошибкой в том месте, где делается неверное предположение, поэтому в месте расположения кода assert. Чтобы исправить ошибку, используйте надлежащие средства, чтобы избежать ситуации.
Надлежащим средством избежать неправильных входных данных функции являются контракты, поэтому примерная функция должна иметь входной контракт, который проверяет, что диапазон2 по крайней мере равен диапазону1. Утверждение внутри реализации могло бы тогда оставаться на месте. Особенно в более длинных и сложных реализациях, такое утверждение может доказать понятность.
enforce
это ленивый подход к выбрасыванию исключений во время выполнения. Это хорошо для быстрого и грязного кода, потому что лучше проверить там, а не молча игнорировать возможность плохого состояния. Для производственного кода его следует заменить соответствующим механизмом, который выдает более значимое исключение.
Я считаю, что вы частично ответили на свой вопрос самостоятельно. Утверждения обязаны нарушать поток. Если ваше утверждение неверно, вы не согласитесь продолжать что-либо. Если вы что-то принуждаете, вы принимаете решение, чтобы что-то произошло в зависимости от ситуации. Если вы обнаружите, что условия не выполнены, вы можете запретить вход в определенный раздел.