В интернете часто можно встретить вопрос, нужно ли передавать NULL в параметрах методов, а также возвращать NULL из методов. Если нет, то почему и как писать код в таких случаях? Об этом и пойдёт речь в статье.

В чём вообще проблема использования null значений? Дело в том, что NULL можно трактовать по разному — это когда значение неизвестно, его используют в качестве значений по умолчанию для параметров методов, возвращают когда не смогли получить успешный результат или когда что-то произошло не так. Есть куча способов и причин использовать NULL. Однако это не всегда приемлемо.

Передача null в параметры

Передавать null в параметры метода может быть полезно, если нужно указать отсутствие значения или инициализировать параметр с нулевым значением по умолчанию.

Например, предположим, что у вас есть метод, который принимает параметр $gender:

public function getUserGender($gender) {
    return $gender;
}

Если вы хотите обрабатывать случаи, когда параметр не указан, вы можете установить для $gender значение по умолчанию null:

public function getUserGender($gender = null) {
    if ($gender === null) {
        return ‘Мужик’;
    } 

    return $gender;
}

В этом случае, если вы вызываете getUserGender() метод без передачи каких-либо аргументов, он примет для параметра $gender значение null по умолчанию.

Причина отказа от null заключается в том, что не нужно постоянно его проверять, если вы ожидаете в значении какое-то другое содержимое. Вместо этого вы можете вернуть, пустой массив, если ожидаемый результат — это массив. Если это целое число, возможно, будет достаточно 0, если это строка, то пустая строка и так далее. 

Часто встречаются и другие способы передачи и проверки аргументов метода. К примеру, передача аргументы в виде массива:

function getParams(array $params) {
    return [
        'columns' => $buttonParameters['col'] ?: 1,
        'rows' =>  $params[‘count’]
        ‘count’ => $params[‘count’]
    ];
}

В другой способе, аргументы функции не указываются явно, а используются динамические аргументы с помощью функции func_get_args():

function getParamsFunction()
{
    var_dump(func_get_args());
}
getParamsFunction(1, 2, 3, 7, 10, 12);

Дополнительно при этом можно встретить использование функций func_num_args() и func_get_arg().

Однако эти два способа сильно ухудшают понимание кода и требует множество дополнительных проверок каждого параметра. По возможности следует избегать их использование.

Вместо этого возможно использовать именованные аргументы и передать параметры следующим образом:

function params($a = 1, $b = 2) {
    print "a: $a, b: $b";
}
params(b: 5); // Выведет "a: 1, b: 5"

В этом случае аргументы функции передаются по имени параметра, а не по его позиции. Аргумент не зависит от порядка и указанного значения по умолчанию. 

Именованный аргумент передаётся с помощью добавления двоеточия перед значением имени параметра. Для имени параметра можно использовать зарезервированные слова. Однако имя не может быть создано динамически и должно быть идентификатором.
 

Возврат null из метода

Обычно из функции возвращают false, если что-то пошло не так. Аналогичное делают и стандартные функции PHP. Для этих же целей возвращают null, пустую строку, пустой массив или вообще ничего. В общем, у каждого свой поход и причины возвращать то или иное значение. Многие даже как-то не сильно об этом задумываются. Однако в некоторых случаях, возврат null как и возврат других значений, которые изначально не подразумевались для возврата, могут приводить к ошибкам и непонятным замешательствам. Здесь конечно же серьёзную роль играет сам PHP, поскольку типы данных в нём слабо типизированы и мы можем вертеть данными как угодно.

К примеру, возьмем такой метод:

function getClients()
{
  $query = 'SELECT * FROM clients';
  $result = $this—>mysqli—>query($query);

  if (!$result) {
    return false;
  }

  $data = [];

  while ($row = $result->fetch_assoc())
  {
    $data[] = $row;
  }

  return $data;
}

В нём мы собираемся получить список клиентов из базы данных. Если клиенты успешно получены, мы возвращаем массив, а если что-то пошло не так, то метод возвращает false. Затем мы используем данный метод, чтобы далее проводить операции с клиентами:

$result = $db—>getClients();

Вот здесь и начинаются первые сложности, поскольку метод возвращает несколько типов данных: array и false. Поэтому нужно дополнительно определить, что именно было возвращено. Вроде бы здесь нет ничего такого необычного и проверка данных может показаться обычным делом, но здесь  как раз возможно ошибиться на проверке типа данных. Если мы используем вместо false другое значение, пустую строку или null, то ситуация может возникнуть практически такая же. Как в таком случае лучше изменить код и что использовать?

Вместо возврата false, пустой строки или null можно использовать исключения. Это позволит сразу же определить, что именно и на каком моменте произошло.

function getClients()
{
  $query = 'SELECT * FROM clients';
  $result = $this—>mysqli—>query($query);

  $data = [];

  while ($row = $result->fetch_assoc())
  {
    $data[] = $row;
  }


  return $data;
}
try {
  $result = $db—>getClients();
  // все норм, двигаемся дальше
} catch (ErrorException $e) {
  // схватили ошибку!
}

Здесь уже не нужно дополнительно проверять на всякие false или null и можно быть точно уверенным, почему метод не отдал правильные данные. Важно лишь правильно перехватывать и обрабатывать такие исключения.

public getFileSize(File $file) {
  if (!$file—>exists()) {
    return null;
  }
  
  return $file—>getLength();
}

В этом простом примере метод возвращает размер файла. Если файла не существует, то возвращается null, 0 или пустая строка — неважно. Вот здесь и начинается проблема, что файла не существует, он был перемещён или удалён, но вместо того чтобы узнать об этом, подобная проверка маскирует проблему. В этом случае можно использовать исключение:

public getFileSize(File $file) {
  if (!$file—>exists()) {
     throw new Exception(
      "Нельзя получить размер файла. Файл не найден!"
    );
  }
  
  return $file—>getLength();
}

Для любых функций лучше всего сразу устанавливать тип возвращаемого значения. Например:

function getValue(): int {
  return 777;
}

Результат:

int(777)

Если указать цифру строкой:

function getValue(): int {
  return ‘777’;
}

То она автоматически будет преобразована в int.

Результат:

int(777)

Чтобы включить строгую типизацию, нужно установить:

declare(strict_types=1);

Тогда если попытаться задействовать в этой функции строку, будет выдана ошибка:

Uncaught TypeError: Return value of getValue() must be of the type integer, string returned

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

function getValue() : ?int {
  return null;
}

Кроме этого, если в методе вообще не использовать return или просто указать его без всякого значения, метод вернёт null. Например:

function getValue1(){
  return;
}

function getValue2(){
}

Поскольку return;и return null; эквивалентны в PHP, то если возвращаемое значение не указано, PHP выполнит работу null за вас. То же самое касается и свойств объектов. По умолчанию они имеют значение NULL до тех пор, пока не будут заданы. Поскольку это поведение по умолчанию, лучше помнить об этом и проверять свойства, которые имеет ваш класс, прежде чем совершать вызовы или что-то ещё.

В большинстве случаев рекомендуется возвращать Null-object который предоставляет поведение по умолчанию в зависимости от того как реагирует ваш код на ситуацию когда ничего не найдено. 

Так что же в итоге возвращать?

Все индивидуально, зависит от проекта. Главное следовать установленной спецификации в рамках проекта, чтобы соблюдался единый стиль. Проверка нулевых значений — обычная практика, иначе вы рискуете повсюду получить исключения. Лучше корректно обработать ошибку, чем генерировать исключения, когда в этом нет необходимости. NULL это не обязательно плохо, как и в случае со многими другими решениями, это зависит от ситуации.

Использование NULL может быть допустимо, когда это целесообразно и не противоречит остальным частям проекта. В большинстве случаев null следует либо заменить выдачей исключения, либо предоставлением нулевого объекта.

Плохое использование NULL, это когда идет попытка заменить или скрыть исключительные ситуации, такие как: ошибка добавления записи в бд, подключение к какому-то API, неверные параметры и так далее. В этих случаях создание исключения более адекватно, поскольку значение NULL не дает значимой информации об ошибке и любые проверки игнорируют проблему.