Если вы помните, в одном из своих постов я говорил о нулевых проверках с точки зрения контракта между производителем данных и потребителем данных. Здесь я отметил, что гораздо лучше проверить данные сразу, при извлечении, чтобы принять решение, подходят ли предоставленные данные для обработки.
Если это не так - нет смысла перетаскивать его, так как он будет выпадать, когда дело доходит до обработки в какой-то момент рабочего процесса. Или даже лучший подход - если есть какой-либо контроль над поставщиком данных, его следует запрашивать только для данных, которые могут быть обработаны.
Это так просто.
Это дизайнерское решение может избавить вас от множества нулевых проверок и избавить вас от головной боли с NullPointerException
s, если вы будете следовать ему с самого начала. Но если вы имеете дело с каким-то существующим кодом, вам понадобятся некоторые инструменты, чтобы отследить происхождение каждого рассматриваемого значения и проверить, действительно ли нулевая проверка необходима в каждом конкретном случае.
К счастью, мы больше не кодируем в блокнотах, как это делали наши дедушки и бабушки. Нам посчастливилось использовать некоторые из последних инструментов с бесчисленным количеством вспомогательных функций. Будем признательны за это.
Как вы уже догадались, я говорю о IntelliJ IDEA. А именно о функции Анализировать поток данных сюда:
Этот анализ выполняется на аргументе метода и позволяет отследить конкретный параметр до места его определения. Довольно круто, а?
А это то, что нам действительно нужно. В конце концов - для каждого объекта в вашей программе есть новый оператор. Найдем все те места, где создается рассматриваемый объект, и выясним, действительно ли нам нужна нулевая проверка.
Чтобы было немного интереснее, возьмем в качестве примера исходный код Kotlin (я работал с номером фиксации 6a808ce842d157a60f8a1ebf5864a731c37076c7 в главной ветке).
Также я почти уверен, что разработчики Kotlin что-то слышали о IntelliJ IDEA.
Первое, что я сделал после клонирования проекта, я посчитал использования org.jetbrains.annotations.Nullable
:
2270 использования в рабочих файлах и 2382 использования в целом! Это много. Проверим, все ли они нужны.
Возможно, некоторые из них можно было бы заменить, заключив более выгодные контракты с производителем данных. Или некоторые из них избыточны, потому что где-то вместе с потоком уже была нулевая проверка. Но поскольку нет правила для проверки данных сразу после их получения, мы не можем быть уверены, сможем ли мы работать с предоставленным значением.
Первый пример.
В классе org.jetbrains.kotlin.codegen.CallReceiver
есть статический метод, который определяется следующим образом:
Как мы видим, есть пара аргументов метода, допускающих значение NULL, а именно extensionReceiver
и callableMethod
.
Эти переменные, допускающие значение NULL, беспокоят меня по ряду причин:
- в случае, если
extensionReceiver
имеет значение null, вызов не имеет смысла, метод немедленно возвращается; Так зачем нам перетаскивать это нулевое значение, неизвестно сколько вызовов методов, чтобы понять, что мы не можем обрабатывать данные на самом верхнем уровне стека? - этот метод не только возвращает значение
extensionReceive == null
, он сам возвращает значение null; чтобы вернуть null в проекте, который имеет примерно 9000 значений NULL и проверок NULL, вы действительно должны быть своего рода бунтарем; кроме того, кто-то на принимающей стороне также должен будет проверить это значение null. callableMethod
влияет только на тип возвращаемого значения, поэтому может быть всего два метода: один принимаетcallableMethod
, а другой просто не имеет этого параметра; зачем нам тащить этот пустой стакан без причины?
Но сначала давайте проследим источник этого extensionReceiver
параметра:
Поток данных для значения показывает нам, что этот параметр изначально получен общедоступным статическим методом, для которого он также отмечен как необязательный. К сожалению, с этим инструментом анализа данных я не смог справиться. Мне не удалось увидеть всех потребителей этого общедоступного статического метода внутри проекта.
Но в любом случае возникает вопрос: не лучше ли ввести другой общедоступный метод без параметра, который может иметь значение null?
Но что еще интереснее - это сам метод:
Для extensionReceiverParameter
уже есть нулевая проверка! Таким образом, метод регистрации calcExtensionReceiverType
является избыточным. И это возвращаемое значение null, которое мы заметили с самого начала (а это полная чушь), никогда не используется, и это хорошо. Плохо то, что он, вероятно, прошел какой-то тест на предмет хорошего покрытия состояния.
Что насчет параметра callableMethod
- он также является входом в этот общедоступный метод. Если мы проверим все случаи использования этого параметра в методе generateCallReceiver
, мы увидим следующее:
Итак, как мы видим, этот параметр используется в следующих методах: calcExtensionReceiverType
, calcDispatchReceiverType
и isLocalFunCall
. Ну, мы начали с calcExtensionReceiverType
, но давайте еще раз проверим, как он там используется:
Вот отрывок из calcDispatchReceiverType
:
И напоследок от isLocalFunCall
:
Хорошо, я вижу здесь закономерность.
Прежде всего, каждый метод начинается с нулевой проверки.
Во-вторых, каждому методу нужна определенная информация из этого параметра callableMethod
:
ExtensionReceiverType
иExtensionReceiverKotlinType
в первом случае;DispatchReceiverType
иDispatchReceiverKotlinType
во втором;GenerateCalleeType
в третьем.
Таким образом, помимо возможности быть нулевым, ожидается, что callableMethod
объект будет предоставлять различную информацию в зависимости от варианта использования. И я очень сомневаюсь, что он может содержать, например, и ExtensionReceiverType
, и DispatchReceiverType
одновременно. Мне это кажется плохим дизайном API.
Вызываемый этого общедоступного метода generateCallReceiver
, вероятно, знает, имеет ли он дело с ExtensionReceiver
, DispatchReceiver
или GenerateCalle
; или что у него вообще нет вызываемого метода. Таким образом, все эти четыре случая кажутся четырьмя разными случаями, которые следует рассматривать отдельно для большей ясности кода.
Здесь мы только что сохранили парни из Котлина в виде семи аннотаций, допускающих значение NULL, и нулевых проверок. 👏
Второй пример, более простой.
В основном мы будем использовать выходные данные инструмента анализа потока данных и не будем так сильно углубляться в сам код в этом примере.
Давайте теперь посмотрим на частный конструктор org.jetbrains.kotlin.resolve.calls.util.ExpressionValueArgument
, который используется тремя общедоступными статическими методами для создания этого объекта:
Единственным обнуляемым здесь является параметр expression
. Отследим для него все возможные потоки данных:
Ого, это похоже на много. Но не волнуйтесь, сначала проверьте все корни. Последние три можно легко пропустить, так как параметр выражения помечен там @NotNull
. Значит, раньше уже применялась нулевая проверка. Таким образом, нам нужно проверить только эти два верхних значения на возможное значение NULL.
Оба эти источника имеют дело со списком KtExpression
. И это вызов makeValueArgument
, который имеет дело с конкретными значениями из списка.
Уже прямо из этого окна мы видим, что результат makeValueArgument
добавлен в другой список. Итак, я предполагаю, что вся эта цепочка методов могла быть одной операцией в потоке KtExpression
.
Но даже если это не так, элемент списка в этом потоке считается допускающим значение NULL. Мне это уже кажется запахом кода. Честно говоря, я не помню ни одного поставщика данных, с которым мне приходилось иметь дело, который предоставлял бы список элементов, смешанных с нулями, поэтому во время обработки мне нужно было бы проверять каждый из них, чтобы он не был нулевым.
Но даже если мы предположим, что такой поставщик данных существует, не лучше ли отфильтровать все значения NULL, как только мы получили этот набор данных, а затем просто продолжить поток, не требующий проверки NULL?
Это эротез.
Честно говоря, меня очень удивил подход Kotlin разработчиков. Знаменитая нулевая безопасность Kotlin направлена на устранение опасности нулевых ссылок из кода.
Но код, который я до сих пор видел в источниках Kotlin, больше похож на параноидальные повсюду проверки на null. Даже если один из них был выполнен раньше, даже если на этом этапе невозможно получить нулевое значение.
Нет никакого подхода и идеи, как сделать код, который они пишут, нулевым, только постулат, чтобы все проверять везде. Пару раз. Думаю, нельзя относиться с осторожностью к нулевым проверкам, верно?
Еще более удивительным было увидеть такие конструкции, как:
Это делает код чем-то вроде смеси параноидальных нулевых проверок и небрежных операторов return null
. Это просто не кажется правильным.
Как программист, вы контролируете поток своего приложения. И даже если вы не сделали этого с самого начала, есть несколько отличных инструментов для отслеживания потока данных.
И все, что вам нужно от этого, - это просто систематический подход, которого нужно придерживаться.
Потому что я считаю, что в Java есть место для нулевой проверки. И здесь потребитель встречает производителя.