Альтернатива нулевому/неопределенному

Наиболее распространенная ошибка/исключение, которое я встречал независимо от того, какой язык или фреймворк я использовал, это Ошибка NullPointer или Неопределенная ошибка. Разные имена, но это просто означает, что вы пытаетесь получить доступ к чему-то, чего не существует.

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

Давайте посмотрим на пример

Например, давайте предположим следующий класс.

class Dog {}

Теперь к примеру. Перед нулевой безопасностью

Dog dog = new Dog();

Теперь это определение переменной собаки может быть экземпляром Dog или null. Но заметьте, нигде не сказано, что это может быть также null. Это означает, что потребитель переменных должен убедиться, что dog не является null перед его использованием.

А теперь представьте, что в большом проекте много переменных. И каждая переменная может быть null.

Думая, что мы так долго пишем код. Без указания того, что чего-то может не быть. Это на самом деле удивительно.

Приветствуем нулевую безопасность

Вот вам и нулевая безопасность. Это означает, что если переменная будет нулевой, вы можете указать, что с помощью ?.

Пример

Dog? dog = new Dog();

Переменная dog просто не может быть Dog. Есть вероятность, что это тоже null. Именно на это указывает ?. Это, так сказать, обнуляемый Dog.

Преимущество этого в том, что вы больше не можете напрямую использовать переменную dog. Вы должны проверить допустимость значений NULL или использовать нулевой оператор.

Пример

Давайте добавим метод eat в класс Dog.

class Dog {
  void eat() {}
}

Допустим, вы хотите вызвать метод eat для dog. Раньше нужно было делать dog.eat();. Теперь dog можно обнулить. Вы должны dog?.eat();. Разница в том, что если собаке null. Эта программа не даст сбой и продолжит работу. По сравнению с предыдущим случаем dog.eat();. Если dog равно null. Это не удастся.

Компиляторы также достаточно умны, чтобы помочь вам избежать двойной проверки на допустимость значений NULL.

Пример

if (dog != null) {
  dog.eat(); // no ? operator
} 

Несмотря на то, что dog здесь можно обнулить. Компилятор не будет жаловаться, потому что он увидел, что мы выполняем проверку на нулевое значение перед использованием. Обратите внимание, что это будет работать только в том случае, если переменная dog определена в локальной области видимости.

Давайте добавим food аргументов в метод eat dog.

class DogFood {}
class Dog {
  void eat(DogFood food) {}
}

Собака не может есть null. Ей нужна еда. Значит, корм, который мы передаем собаке, не может быть null.

Dog? dog;
DogFood? food;
dog?.eat(food); // fails

Это не удастся, потому что food может быть нулевым. Нам нужно сделать проверку на null, чтобы это заработало.

if (food != null) {
  dog?.eat(food);
}

Or

if (dog != null && food != null) {
  dog.eat(food);
}

Вы можете заметить, что код быстро начинает выглядеть некрасиво. Представьте, что у вас есть еще несколько переменных, допускающих значение NULL. Вы должны посыпать свой код условиями if везде.

Нулевая безопасность — это хорошо. Мы можем сделать лучше.

Приветственный вариант

Опция имеет разные названия в разных языках программирования. Например, Необязательно в Java, Возможно, в Haskell, Опция в библиотеке dart fpdart и Опция в библиотеке fp-ts машинописного текста. Я собираюсь использовать Option библиотеки fpdart.

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

Прежде чем мы углубимся в то, что мы можем сделать с Option. Попробуем разобраться в Option.

Опция происходит из мира FP. Но я не собираюсь использовать термин ФП монада или монадный, чтобы отпугнуть вас. Вместо этого мы рассмотрим его с практической точки зрения.

Думайте о Option как о списке. Список представляет собой контейнер из нескольких значений. Точно так же Option является контейнером только для 1 значения или ничего.

Если есть значение. Option будет экземпляром Some. В противном случае это будет экземпляр None. Мы можем углубиться в то, как реализован Option. Каких законов он должен придерживаться. Но я буду проще.

Пример

Как мы создаем список номеров

List<int> numbers;

Мы можем создать вариант номера

Option<int> number;

Мы называем это number. На самом деле это вариант номера. Может быть, мы можем назвать его optNumber или maybeNumber. Но опять же, все просто. Пойдем с number.

Вы можете инициализировать number, как показано ниже.

number = None(); // or Option.none();

Это указывает на то, что параметр пуст и не содержит значения.

number = Some(10); // or Option.of(10);

Это указывает на то, что в Option есть какое-то значение. В данном случае это 10.

Теперь мы знаем основы того, что такое Option. Давайте попробуем использовать его для нашего примера с собакой и едой.

Option<Dog> dog = Option.of(new Dog());
Option<DogFood> food = Option.of(new DogFood());

Для подачи food на dog. Мы можем сделать ниже, не используя условия if.

dog.flatMap((d) {
  return food.map((f) => d.eat(f));
});

Если вы впервые используете flatMap и map или базовое функциональное программирование. Вы можете вообще не понимать, что здесь происходит.

Но позвольте мне объяснить.

Во-первых, давайте посмотрим на map. Это очень похоже на выполнение array.map. Где каждый элемент в списке сопоставляется с чем-то другим. Но в случае Option. Только 1 элемент сопоставляется с чем-то, если он существует.

Подхожу к flapMap. Один из лучших способов понять, что такое flapMap, — посмотреть на различия в сигнатурах методов map и flapMap.

Подпись FlapMap

Option<B> flatMap<B>(Option<B> Function(T t) f)

Подпись карты

Option<B> map<B>(B Function(T t) f);

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

В случае flatMap. Он возвращает Option<B>. А в случае map. Он возвращает только B.

Проще говоря. flatMap позволяет вам использовать значение и создавать новое значение, обернутое в Option.

Но в случае map. Это позволяет вам потреблять и производить новую ценность.

В нашем коде выше. Мы разворачиваем значения из Option и потребляем. Если какое-либо из значений (dog или food) отсутствует, функция flatMap или map вообще не будет вызываться.

Это очень похоже на то, как работает array.map. Если вы вызываете array.map для пустого массива. В массиве нет элементов для сопоставления. Таким образом, функция карты никогда не будет вызываться.

Другой пример

Давайте посмотрим на другой пример. Чтобы понять, насколько мощным является Option.

Для нашего примера. Предположим, нам нужно получить адрес пользователя. Есть 3 функции, с помощью которых можно получить адрес пользователя.

Option<String> getAddressFromDevice() {
  ...
}

Option<String> getAddressFromRemote() {
  ...
}

Option<String> getAddressFromThirdParty() {
  ...
}

Для простоты. Я не буду реализовывать функции. Но, пожалуйста, обратите внимание. Как 3 функции возвращают Option<String>. Указание функции может вообще не вернуть никакого адреса.

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

  • getAddressFromDevice
  • getAddressFromRemote
  • getAddressFromThirdParty

Если ни одна из этих функций не возвращает адрес. Мы можем вернуться к Hyderabad, India в качестве адреса по умолчанию.

Собираем вещи. Вот так выглядит код.

final address = getAddressFromDevice()
    .alt(() => getAddressFromRemote())
    .alt(() => getAddressFromThirdParty())
    .getOrElse(() => "Hyderabad, India");

Позвольте мне объяснить функцию alt. alt означает alternative. Он используется для предоставления альтернативного Option в случае, если текущий None.

Итак, что мы здесь делаем. Сначала пробуем getAddressFromDevice, потом getAddressFromRemote, потом getAddressFromThirdParty. Если ни один из них не вернет какой-либо адрес, мы вернемся к значению, возвращаемому из getOrElse.

Также обратите внимание, если какой-либо из функций удается получить адрес. Мы не будем вызывать следующие функции. Например, предположим, что getAddressFromDevice удалось. Он вообще не будет вызывать getAddressFromRemote и getAddressFromThirdParty.

Чтобы понять, насколько это сложно. Попробуйте написать приведенный выше код без использования Option. Вы отпустите, насколько мощным является Вариант.

Это просто царапины на поверхности. Чертовски многому еще предстоит научиться.

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

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

Спасибо за чтение.