Объясните механизм: почему функцию, принимающую String, нельзя безопасно передать туда, где ожидается функция, принимающая String??
Функция с параметром String не заменяет функцию с параметром String?, потому что вызывающая сторона вправе передать последней nil. Функция, умеющая обрабатывать только непустую строку, не удовлетворяет такому контракту.
Это пример контравариантности параметров функций: для безопасной подстановки передаваемая функция должна принимать не более узкий, а такой же или более широкий набор допустимых значений.
Статическая система типов Swift должна проверять совместимость функций до выполнения программы. Отдельная модель Optional появилась для явного представления отсутствующего значения вместо неявного использования нулевых ссылок.
При обычном вызове Swift может автоматически выполнить optional injection: значение String превращается в String? со значением .some. Но это локальное преобразование аргумента не меняет контракт функции и не делает функцию, принимающую только String, способной обработать nil.
Предположим, API принимает обработчик типа «функция от String?». Такой API может вызвать обработчик как с текстом, так и с отсутствующим значением.
Если подставить обработчик, принимающий String, возникает риск: при передаче nil внутри обработчика не существует корректного значения String, которое можно было бы использовать. Компилятор запрещает такую подстановку, предотвращая ошибку на границе вызова.
Тип String? шире по множеству значений: он содержит и строки, и nil. Поэтому функция типа «принимает String?» может выступать там, где требуется функция «принимает String»: любой переданный String является допустимым String?.
Обратное направление небезопасно. Функция типа «принимает String» гарантирует обработку только строк, а контракт String? требует обработки двух состояний — .some(String) и .none.
В строке с compatible функция, принимающая более широкий тип String?, используется в контексте, где будут передаваться только String. В обратном присваивании компилятор учитывает возможность вызова с nil и отклоняет его.
Важно не путать это с возвращаемыми типами: возвращаемое значение обычно должно быть не более узким, чем ожидаемый тип, а параметры — не более узкими. Для функций это разные направления совместимости.
Сервис аналитики принимает обработчик, который может получать отсутствующее имя пользователя: отсутствие имени означает анонимное событие. Разработчик пытается передать обработчик, работающий только со строкой, рассчитывая, что Swift автоматически обернёт аргумент в Optional.
Можно изменить обработчик так, чтобы он принимал String? и явно обрабатывал nil. Это наиболее безопасный вариант: контракт отражает реальные входные данные, хотя обработчику придётся добавить ветку для отсутствующего имени.
Другой вариант — заранее отфильтровать отсутствующие значения и передавать обработчик только в API, который гарантирует непустую строку. Это упрощает обработчик, но переносит ответственность за фильтрацию на вызывающий код и может привести к потере анонимных событий.
Выбор зависит от семантики API. Если nil имеет смысл, его следует оставить в типе обработчика; если отсутствие значения недопустимо по бизнес-правилам, лучше устранить его до вызова и использовать функцию с параметром String.
Можно, но только при передаче конкретного аргумента обычной функции. Например, вызов функции с параметром String? и аргументом String допускает оборачивание в .some. При присваивании одной функции другой проверяется совместимость их полных типов, а не возможность преобразовать один конкретный аргумент.
Да, в таком направлении это безопасно: функция, принимающая String?, умеет принять любое значение, которое может передать вызывающая сторона функции с параметром String. Вызов через более узкий контракт не обязан использовать nil, поэтому функция не нарушает ожидания вызывающего кода.
Совместимость параметра и совместимость результата проверяются независимо. Для параметра безопаснее использовать функцию, принимающую более широкий тип; для результата безопаснее возвращать более узкий тип, который гарантированно можно использовать там, где ожидается более широкий. Поэтому добавление Optional к возвращаемому типу может отдельно сделать присваивание несовместимым: вызывающая сторона может ожидать гарантированную String, а получить значение, способное быть nil.