Java: Пограничные случаи
Функция mysubstr()
, которую вы реализовали в прошлом уроке, содержит множество ошибок. «Но ведь она прошла проверки!». Да, но в этих проверках не было так называемых пограничных случаев. Функция нормально работала с нормальными аргументами, но как она поведет себя, если передать ей такие варианты длины?
0
- Отрицательное число
- Число, превышающее реальный размер строки
Функция mysubstr()
не рассчитана на такие варианты. Можно подумать, что это не проблема: функция работает в нормальных условиях, и просто не нужно передавать ей «плохие» аргументы. В идеальном мире — да, но в реальном мире ваш код будет запускаться в разных ситуациях, с разными комбинациями условий и данных. Тот, кто будет его запускать, скорее всего не будет читать его внимательно, и даже документацию по нему, если вы ее напишете, он, возможно, не будет читать внимательно. Поэтому он не будет точно знать, с чем ваш код работать сможет, а с чем нет; да и просто он может ошибиться. То есть нельзя быть уверенным, что аргументы всегда будут корректными.
Ошибки в пограничных случаях — самая частая причина логических ошибок в программах. Программисты всегда забывают что-нибудь учесть. Такие ошибки часто проявляются не сразу, и могут долгое время не приводить к видимым проблемам. Программа продолжает работать, но в какой-то момент обнаруживается, что в результатах есть ошибки.
Умение справляться с такими ошибками приходит с опытом, через постоянные косяки в стиле «ой, забыл проверить на пустую строку!».
Поэтому нужно учитывать различные особые случаи, в рамках здравого смысла. Учитывать - означает например постараться избежать порчи данных, которые по логике операции изменяться не должны были; а также, вместо того чтобы «молча» выполнить что-то бессмысленное или вообще ничего - как-то сообщить о проблеме, и тем самым помочь вызывающему программисту, подсказав ему, что он передал недопустимые аргументы.
Например, если функция удаляет из какой-то строки все символы с индексами от N и далее, где N - аргумент функции, то мы могли написать функцию так, что если вдруг ей подали значение N<0, то функция уже первый (с индексом 0) символ посчитает лишним и удалит все. Но не факт, что это разумно: передача отрицательного числа в эту функцию почти наверняка говорит об ошибке программиста, и вместо того чтобы портить строку и продолжать работать как будто так и надо, лучше сгенерировать ошибку, которая укажет вызвавшему нас программисту, что он что-то сделал не так. Мы в этих уроках не говорим о том, как сигнализировать об ошибках, но как минимум, даже не зная этого, мы могли бы добавить в функцию первым действием запрос из строки символа с индексом N-1, и тогда при попытке проделать это при N<0 выполнение функции прервется с ошибкой. А некоторые другие случаи неверного указания аргументов мы можем посчитать безобидными. К примеру, мы можем решить, что указание индекса большего, чем длина строки, нужно понимать так что удалять ничего не нужно - скажем, мы хотим дать вызывающему программисту возможность обрезать несколько строк до одной и той же нужной ему максимальной длины, при этом не заставляя его каждую строку проверять: не короче ли она уже, чем ему нужно. Но даже если мы решили, что такой пограничный случай - корректен, все равно мы это сделали, сначала его обдумав - то есть всегда нужно задаваться вопросом, а какие неожиданные для нас значения аргументов нам могут передать.
Заметим, что можно было написать функцию и таким образом, что она в цикле идет от индекса N в сторону возрастания до тех пор, пока счетчик цикла не станет равен длине строки-1. И тогда, если вдруг на вход подали N большее чем длина строки, то счетчик цикла никогда не станет равным длине строки-1, и цикл станет вечным. Нет, на самом деле не станет, потому что при попытке увелить счетчик свыше максимально допустимого значения целого числа в Java произойдет ошибка; но это не наша заслуга, нас спас компилятор, добавляющий такие проверки автоматически. Но все равно нет ничего хорошего, если перед этим тело цикла успеет выполниться 2 миллиарда раз. А если бы в нем выводилось на экран текущее значение счетчика?
Давайте представим себе расширенную функцию mysubstr()
. Она принимает три аргумента: строку, индекс и длину извлекаемой подстроки. Функция возвращает подстроку указанной длины начиная с указанного индекса. Примеры вызова:
var str = "If I look back I am lost";
mysubstr(str, 0, 1); // => 'I'
mysubstr(str, 3, 6); // => 'I look'
Прикинем, что может пойти не так. Какие пограничные случаи стоит учитывать:
- Отрицательная длина извлекаемой подстроки
- Отрицательный заданный индекс
- Заданный индекс выходит за границу всей строки
- Длина подстроки в сумме с заданным индексом выходит за границу всей строки
В реализации функции каждый пограничный случай будет отдельным куском кода, скорее всего реализованным с помощью if
.
Чтобы написать функцию mysubstr()
и защититься от этих случаев, стоит написать отдельную функцию, которая будет проверять аргументы на корректность. Займемся этим в задании.
Задание
Реализуйте функцию-предикат isArgumentsForSubstrCorrect()
, которая принимает три аргумента:
- строку
- индекс, с которого начинать извлечение
- длину извлекаемой подстроки
Функция возвращает false
, если хотя бы одно из условий истинно:
- Отрицательная длина извлекаемой подстроки
- Отрицательный заданный индекс
- Заданный индекс выходит за границу всей строки
- Длина подстроки в сумме с заданным индексом выходит за границу всей строки
В ином случае функция возвращает true
.
Не забывайте, что индексы начинаются с 0
, поэтому индекс последнего элемента — это «длина строки минус 1».
Пример вызова:
var str = "Sansa Stark";
isArgumentsForSubstrCorrect(str, -1, 3); // => false
isArgumentsForSubstrCorrect(str, 4, 100); // => false
isArgumentsForSubstrCorrect(str, 10, 10); // => false
isArgumentsForSubstrCorrect(str, 11, 1); // => false
isArgumentsForSubstrCorrect(str, 3, 3); // => true
isArgumentsForSubstrCorrect(str, 0, 5); // => true