PHP: значения по-умолчанию и типизация
Рассмотрим примеры, когда значения по-умолчанию в комбинации с указанием типов входных и выходных параметров функции могут ввести в заблуждение. Для начала рассмотрим такой сценарий: требуется разработать функцию, которая будет возвращать всегда целочисленное значение, а на вход принимает один параметр - тоже целочисленный. Заготовка:
<?php
class Product
{
/**
* @param int $productId
*
* @return int
*/
public function getStrangeProduct(int $productId): int
{
return $productId;
}
}
$obProduct = new Product();
При таком раскладе совершенно очевидно, что вызов
var_dump($obProduct->getStrangeProduct(0));
вернёт целочисленное значение 0:
/mnt/projects/sites/server.local/www/test-functions.php:16:int 0
Теперь представим, что ТЗ изменилось. Теперь нужно, чтобы функция имела значение по-умолчанию для входного параметра, когда функция вызывается без указания параметра. Так функция примет вид:
/**
* @param int|null $productId
*
* @return int
*/
public function getStrangeProduct(?int $productId = 100500): int
{
return $productId;
}
Это позволит успешно выполнить функцию без указания параметра и входной параметр будет инициализирован значением по-умолчанию:
var_dump($obProduct->getStrangeProduct());
На выходе:
/mnt/projects/sites/server.local/www/test-functions.php:18:int 100500
Но также появилось и нежелательное поведение: входной параметр теперь может принимать значение NULL. Получается, то значение NULL и неуказанное значение с точки зрения входного параметра имеют одинаковую проверку типа (пустота она и есть пустота). Но вот значение по-умолчанию уже не присваивается! Значение по-умолчанию делается только для случая, когда ничего не передано. Такой вызов:
var_dump($obProduct->getStrangeProduct(null));
уже "упадёт", но не напроверке входного параметра, а на проверке выходного: мы объявили, что возвращать будем int, а возвращается NULL:
TypeError: Return value of Product::getStrangeProduct() must be of the type int, null returned in /mnt/projects/sites/server.local/www/test-functions.php on line 12
Значение по-умолчанию не подхватилось! Ведь на вход было передано значение NULL - это не тоже самое, как если бы совсем ничего не передавать!
Чтобы уложиться в ТЗ, придётся явно указать значение по-умолчанию самостоятельно:
/**
* @param int|null $productId
*
* @return int
*/
public function getStrangeProduct(?int $productId = 100500): int
{
if (null === $productId) {
$productId = 100500;
}
return $productId;
}
Вот теперь всегда будет возвращаться целочисленное значение: и при $obProduct->getStrangeProduct()
, и при $obProduct->getStrangeProduct(null)
.
Но для совсем хорошего бизнес-решения, эту функцию ещё нужно доработать. Тут название функции намекает, что идёт работа с неким товаром. В подавляющем большинстве реальных случаев, программист имеет дело с сущностями из базы данных. Идентификаторы в базе данных почти всегда автоматически инкрементируются. И часто начиная с единицы. Т.е. товара с ID = 0 обычно не бывает (но это не точно - зависит от движка сайта, настройки СУБД и пр.). Поэтому лучше добавить в функцию также проверку входящего значения для учёта бизнес-логики:
/**
* @param int|null $productId
*
* @return int|null
*/
public function getStrangeProduct(?int $productId = 100500): ?int
{
$result = null;
if (null === $productId) {
$result = 100500;
} elseif ($productId > 0) {
$result = $productId;
}
return $result;
}
Тут появляются варианты: что делать, когда входное значение не удовлетворяет требованиям бизнеса? Можно кидать исключение. А можно расширить возвращаемое значение и возвращать NULL - во всех случаях, когда невозможно вычислить верное зачение. Так и сделано в примере выше. Внимание! в этом случае во всём коде, где будет производится вызов этой функции, придётся делать проверку возвращённого значения - если вернётся NULL, значит функция не отработала по корректному сценарию.
Но даже на этом этапе функция может "упасть" в неожиданном месте. Можно сложить два целочисленных числа, передать на вход и получить исключение:
var_dump($obProduct->getStrangeProduct(PHP_INT_MAX + 1));
Это приведёт к переполнению максимально допустимого целочисленного типа. PHP увеличит размерность числа (до float) и "упадёт" на проверке входного значения (которое у нас int):
TypeError: Argument 1 passed to Product::getStrangeProduct() must be of the type int or null, float given, called in /mnt/projects/sites/server.local/www/test-functions.php on line 25 in /mnt/projects/sites/server.local/www/test-functions.php on line 10
PHP попытался превратить входное значение в 9.2233720368548E+18.
А что будет, если принудительно сделать приведение входного значения к целочисленному типу?
var_dump($obProduct->getStrangeProduct((int) (PHP_INT_MAX + 1)));
Это вернёт:
/mnt/projects/sites/server.local/www/test-functions.php:25:null
А всё потому, что после приведение типа, на вход в функцию будет подано значение -9223372036854775808 - это то, что получилось после прибавления единицы к самому большому поддерживаемому челому числу. Да, получилось отрицательное число! И наша функция отработала по проверке бизнес-логики - должно быть положительного входное значение.
В общем, нужно иметь ввиду, что на очень больших числах можно словить переполнение. А отрицательные числа лучше сразу фильтровать на входе - если они реально не нужны.
Комментарии
Отправить комментарий