Pull to refresh

Comments 290

UFO just landed and posted this here
Что, на сколько я понимаю, не избавляет от теоретической возможности появления null где-нибудь и от необходимости проверки значений на null. При программировании на Scala тоже раздражает существование null-ей при том, что есть Nothing.
UFO just landed and posted this here
UFO just landed and posted this here
В смысле «чистый F#»? Без внешних, написанных на C# .Net библиотек? А какой от него практический толк тогда? Спортивная информатика? Или там весь основной функционал (включая функции работы с внешними web-сервисам, файловой системой, графикой и прочим) продублирован в null-safe манере?
UFO just landed and posted this here
Бедный этот null. Добавьте в статью опрос, как часто вы имеете NullReferenceException? Лично вот я — не чаще раза в неделю, а то и реже, использую только ?:,?.. и ??.. Я конечно, стараюсь null не возвращать и не передавать, но это на уровне смысла методов скорее, чем претензия к языку.

Проблема не в том что разыменование бросает исключение. Есть и другие исключения, тоже летят. Выход за границы массива например. А представьте, нету null. Тогда все как в анекдоте про буратино и яблоки. Добавлять метод IsValid? Те же проверки же. По-моему суть вопроса не в наличии маркера (отсутствия чего-либо) null, а в том что языки программирования и компиляторы различают объекты от ссылок. Переменная на то и переменная, что она может быть изменена.
UFO just landed and posted this here
Ну и получаем (с Maybe) тот же if null, только обернутый.

Моя мысль скорее в том, что языки из 90-х (и их наследники) оперируют в императивном стиле, и, следовательно, есть переменные. C# не исключение, и ждать от него обязательств что у переменной всегда должен быть объект… зачем? Там выше про F# сказали. Давно уже хотел присмотреться к нему как следует, но всё времени нет. Теперь его приоритет увеличил. Может следующий проект на нём буду :) Если Xamarin F#-ready (а это вроде так).

С Maybe у вас есть возможность требовать обязательной обработки как наличия, так и отсуствия значения, не оставляя места для ошибок в коде, ведущих к NRE.

UFO just landed and posted this here
UFO just landed and posted this here
языки из 90-х (и их наследники) оперируют в императивном стиле, и, следовательно, есть переменные
Скорее, тут корни идут от указателей и ручного управления памятью.
Не идут. Присвоение к null не обязывает объект быть удаленным.
Но после удаления хорошо бы присвоить null, если переменная ещё в области видимости.
Дело не в частностях. А в том, что мозги разработчиков были воспитаны на unmanaged языках, и объектную ссылку никто не воспринимал иначе, как указатель, со всеми вытекающими последствиями.

В школе их учили Паскалю, или Аде кодом типа
var
  p: ^TMyObject;
begin
  New(p);
  p.DoSomething;
  Dispose(p);
end.
в воздухе ещё не витали концепции монад, optional-ов и т.п.
1. Задним числом все умные.
2. Откуда инфа, кто что как воспринимал? Я вот таких обобщений не делаю.

В этом ответвлении я уже всё сказал, и совсем в другую сторону. Язык и то, во что он компилируется далеко не только предысторией привыкших к чему-то программистов связаны. И те или иные решения в нём приняты не потому что так у всех, или все привыкли, но еще и потому что бывают и коммерческие ограничения (время, бюджет), и технические (что мог тогда .NET и что сейчас), и много всего разного. А по-вашему весь контекст ситуации — это программисты на Паскале.

NRE раз в неделю — это ровно на один раз в неделю больше чем нужно. NRE в продакшене вообще недопустимо.
Проблема именно в том, что разыменование любого ссылочного типа может бросить исключение.


  1. Если null в этом месте не нужен, то гораздо лучше поддерживать non-nullable типы с помощью компилятора и не иметь NRE.
  2. Если null в этом месте возможен по бизнес-логике, то гораздо лучше требовать обработку обоих случаев (наличие и отсутсвие значения) на этапе компиляции и не иметь NRE.
NRE в продакшене… Думаете я посреди ночи проснусь в холодному поту, если приснится? В разработке крупной платежной системы лет 10 назад работал, на С++ писал — и нормально с этим жили, читали дампы, а специальные демоны рестартовали сервисы. У конечного пользователя все это неприятно, но и в Java, и в Swift есть тот самый null.

Да и вообще причем тут NRE. Объясните, почему другие исключения не удостаиваются чести иметь столько внимания и языки, в которых они не случаются? К чему такое избирательство? Все это в случае конкретного языка и конкретно одного исключения больше напоминает упражнения для ума.

По поводу compile-time проверок. Еще раз. Переменная (ссылка) может иметь любое значение, это значит что выполняющее (программу) устройство зарезервировало под него кусок места в памяти. Вот этот кусок в памяти может указывать на что угодно, на умерший, невалидный объект, или валидный. Отказ от null в языке означал бы по факту отказ от ссылок, и превратил бы язык в фактически интерпретируемый, потому что в компайл-тайме объекты не создаются.
Раскрою немного мысль. Все ухищрения по поводу оборачивания языковых конструкций — начиная со smart_ptr из С++, это создание собственного интепретатора в языке и написание кода на нём. Дело не в оверхеде, дело в том, что если не нравится язык — лучше его сменить, чем и без того обрастаемые костылями старые языки латать. Благо сейчас много чего быстро работает.

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

Not null reference type планировалось добавить в C#7, но потом отложили эту фичу до лучших времен (лучшие времена ожидаются с С#8, но там опять же… кто знает что будет). Вот народ и негодует, что средство для практически автоматического улучшения качества программ и упрощения разработки еще прийдется черт знает сколько ждать.
> Объясните, почему другие исключения не удостаиваются чести иметь столько внимания и языки, в которых они не случаютс

Потому, что NRE — это всегда ошибка разработчика, а какое-нибудь UnauthorizedAccessException — нет.
А какое-нибудь index out of range, или division by zero, или stack overflow? Тред не читай@на коммент отвечай?
Любой язык будет допускать ошибки разработчика. Даже русский.

Мне лично самому не нравится, когда в рантайме летят исключения, касающиеся языка. Очень раздражает. Это с того момента, как я с плюсов пересел на Java/C#. Хотя это намного, намного лучше, чем core dump, gdb и stack frames.

Какой смысл предъявлять к дизайну языка претензии через 10 лет после его создания, он для своего времени и так больше чем мог сделал. Я больше по этому поводу озадачен.
> index out of range

Не знаю о чём вы, я использую foreach

> division by zero

Нет

> stack overflow

Придумаете как победить на уровне языка — дайте знать.

> Какой смысл предъявлять к дизайну языка претензии через 10 лет после его создания

Почему 10 лет? Язык называется «C# 6.0», в названии, кстати, и ответ на «какой смысл» заложен.
Научи́те избегать division by zero без дополнительной проверки операнда на нуль, пожалуйста.

А 2 инта складывать без OverflowException научить?

оборачиваете в try и выдаёте пользователю, что некорректные данные.
Я же написал,NRE это ошибка разработчика. Пользователю бесполезно о ней ошибку выбрасывать. А данные в знаменателе поправить обычно может.
Вообще, NRE оборачивать не то, что запрещено, но довольно бессмысленно. Т.е. обернув вызов чистой функции какого-то расчёта и получив division by zero я уверен, что ошибка изолирована и приложение дальше работает корректно, а получив NRE — нет, и дальше продолжать выполнение некорректно.
Не вижу никакой разницы между NRE и DBZ в случае чистой функции.
Какая разница, какой-то метод ошибочно вернул 0 в переменную «кол-во процессоров в системе» и дальше упало при делении, или вернул null в переменную CPUManager.
Разница в том, откуда приходят данные. Циферки, обычно, приходят извне (их там миллионы, косяки всегда есть). А null — это где-то присвоить забыли в своём коде.

Одна ошибка на миллион пользовательских данных — это нормально, нужно быть готовым обрабатывать.
Вернуть 0 в переменную «кол-во процессоров в системе» — это экзотика.
Миллион CPUManager, один из которых null — это надо талант иметь, скорее у нас память битая.
Все(один) CPUManager null(WTF, а это вообще кто-то тестил?) — зачем так жить?

Можно придумать какие-то исключения, но, в целом, DBZ — не кодерская ошибка, но «бытовая». А NRE — точно какая-то хрень, которая сама не починиться.
если бы получили DBZ из функции, значит функция написана неверено, какой смысл что-то изолировать, если у вас программа написана неверно? хоть какой-то смысл это имеет на границе плагина или чего-то такого что можно безболезненно прибить и оно при этом нам не подконтрольно
0) возврат DBZ — часть естественного контракта функции. Функции деления, например.
1) в программе кнопок больше одной, одна может не работать. Главное — побочных эффектов не настругать.
2) Функция может работать в рамках контракта, который мы где-то нарушили. Самое банальное — плохо проверили пользовательский ввод. Нехорошо, но не смертельно — введут ещё раз.
возврат DBZ — часть естественного контракта функции. Функции деления, например.


нет

в программе кнопок больше одной, одна может не работать. Главное — побочных эффектов не настругать.


и что? не работает — перепиши

Функция может работать в рамках контракта, который мы где-то нарушили. Самое банальное — плохо проверили пользовательский ввод. Нехорошо, но не смертельно — введут ещё раз.


контракт нарушен -> написано неверено -> смысла работать дальше нет
> нет

Это факт, а не вопрос

> и что? не работает — перепиши

В следующую итерацию
если у вас ожидается что инты будут переполняться, то, очевидно, использовать механизм исключений для этого глубоко неправильно. Впрочем как и ловля любого SystemException без rethrow
Как бы вы написали минимально простой калькулятор, в котором пользователь формулу вводит? Ну, без ловли ArithmeticException.
валидировал бы ввод и отселаживал overflow/underflow без эксепций, удивительно просто, да?
А как? Получить ArithmeticException способов больше одного: сложение, умножение, деление, логарифм — всё проверками обложить?

SystemException нельзя ловить как таковой, а OverflowException — можно
А как? Получить ArithmeticException способов больше одного: сложение, умножение, деление, логарифм — всё проверками обложить?


ну если делаем лабораторку в универе, то можно и не обкладывать )) а какие еще есть варианты? input всегда нужно валидировать

SystemException нельзя ловить как таковой, а OverflowException — можно


я попробую раскрыть свою мысль: если мы говорим, например, про деление то контракт у деления должен выгялдить вот так:

Contract.Require(divisor != 0);

если мы говорим про overflow то у функции сложения контракт будет вглядеть вот так:

Contract.Require(int.MaxValue — arg1 > arg2 && int.MaxValue — arg2 > arg1);

этот контракт всем доступен, значит клиент может выполнить эти условия, а значит нарушение контракта сигнализирует о неверном коде. т.о ничего ловить не надо, надо закрашиться

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

а вообще за всю мою практику из наследников SystemException мне приходилось ловить и проглатывать только TaskCanceledException, все что было связано с арифметикой — это всегда была моя ошибка
> input всегда нужно валидировать

Если формула отпарсилась в дерево — инпут годный. Но от переполнения при умножении это не спасёт.

> Contract.Require(int.MaxValue — arg1 > arg2 && int.MaxValue — arg2 > arg1);

А для возведения в степень? А во сколько раз дольше ваша проверка будет считаться (да и писаться, с тестами), чем само вычисление?

Если нам не надо, например, подсвечивать место в формуле, где произошла ошибка, то это чистый оверхед. Программирование ради программирования, выбрасывание которого — необходимость, а не компромисс.
>Если формула отпарсилась в дерево — инпут годный. Но от переполнения при умножении это не спасёт.

а инпут к формуле?

>А во сколько раз дольше ваша проверка будет считаться (да и писаться, с тестами), чем само вычисление?

один раз — при комипяции. вы вообще слышали про контракты?
Как вы пользовательский ввод при компиляции проверите?
воувоу, я все понял. вам нужны лекции «бертран мейер» «основы объектно-ориентированного программирования». там вы узнаете почему контракты и ввод не соотносятся чуть менее чем никак, и почему контракты должны проверяться во время компиляции
Я спрашивал про пользователя вводящего формулу, это вы контракты притащили.

Ввод хоть тушкой, хоть чучелком в рантайме проверять придёться на соответствие «int.MaxValue — arg1 > arg2 && int.MaxValue — arg2 > arg1»
конечно, и это правильно
Конечно. Но долго, дорого, и часто не понятно зачем именно нужно.
>Программирование ради программирования, выбрасывание которого — необходимость, а не компромисс.

я про это и говорю, но я все же надеялся что мы разбираем абстрактного коня, а не кокретный пример. Возвращаясь к теме: можно найти кучу контрпримеров когда станет плохо от выкидывания null, но это не значит, что не нужно ничего менять
Я против абстрактных коней. В физике вакуум убирает некоторую погрешность, а в другом это приводит к рассуждениям «лучше быть здоровым и богатым, чем бедным и больным».

Инструмент должен соответствовать задаче, хоть null, хоть goto.
ну нет, всегда должен быть недостижимый идеал
https://www.ted.com/talks/malcolm_gladwell_on_spaghetti_sauce
>А для возведения в степень?

ЗЫ. Math.Pow не бросает эксепций, также как и деление у double ))
Значит у нас будет не double. Не спрашивайте, заказчик попросил.
тогда не получится в отрицательную степень возвести, тоже выглядит как контракт функции ))
По логике противников NRE stack overflow легко победить запретив рекурсию и так далее.
Нельзя запретить рекурсию, не запертив функции.
Вы уже утомили хуже горькой редьки отказом понимать весь смысл написанного (открою тайну: бывают языки и без функций). Своими доморощенными классификациями и наколенными соображениями опустили уровень дискуссии на дно.
Вы уже утомили хуже горькой редьки отказом понимать весь смысл написанного. Бывает и brainfck, только на нём никто не пишет.
Не думаю что это хорошая идея, но все же, что мешает запретить в языке рекурсию не запрещая функции?
Можно не запрещать. Достаточно отслеживать циклические вызовы компилятором (чтобы неявную рекурсию убрать) и запретить вызов по указателю (чтобы не проверять в рантайме). Если с первым еще можно смириться (любой рекурсивный алгоритм достаточно тривиально превращается в нерекурсивный), то второе уже серьезно ограничивает область применения такого языка.
> Достаточно отслеживать циклические вызовы компилятором

На какую глубину? На сколько такой компилятор будет тормозить?

Можно прикинуть (очень грубо). Пускай N — количество функций. Наибольшее количество уникальных вызовов (в смысле откуда и что) будет достигаться, если каждая функция будет вызывать каждую. Таким образом для проверки нам потребуется проверить не более N^2 вариантов. Соответственно сложность проверки ограничена O(N^2), что не так уж и много.


К тому же, фехтование верхом на стульях требует времени: https://xkcd.com/303/

Соответственно сложность проверки ограничена O(N^2), что не так уж и много.

Для типичного проекта в 100.000 функций — много.
Тем более, надо отсекать варианты непрямых вызовов (A → B → C → D → A), а это построение матрицы достижимости на ориентированном графе, что с использованием лучших известных алгоритмов имеет сложность O(N3)
Нам достаточно проверить ориентированный граф вызовов на ацикличность. Граф без петель (вызов функцией самой себя тривиально проверяется). Сложность этого алгоритма — O(M), где M — количеством ребер, которое ограничено количеством ребер в полном графе, т.е. N*(N-1). Т.е. общая сложность — O(N^2). Вот задача поиска ВСЕХ таких циклов уже другое дело.
> Вот задача поиска ВСЕХ таких циклов уже другое дело.

Хотелось бы в ошибках компиляции весь список, конечно.

И, кстати, не забудьте про неявные вызовы через .net fw, я затейник.

А вообще, вы почему-то думаете, что граф вызовов статический и известен при компиляции. Это немножко не так. По крайней мере я не представляю как иначе.
Я в изначальном комментарии сказал, что вызовов по указателю нет. Любой непрямой вызов попадает под это условие.
Это в рамках языка. Из внешнего мира то нас могут вызывать?

А это все имеет смысл только в рамках языка.


Компилятор не может отвечать за внешний мир. Любое взаимодействие с внешним миром не является ответственностью компилятора. Компилятор может проверить правильность только того кода, который обрабатывается этим компилятором. Мы можем любой код подменить во время выполнения, и компилятор не может гараниторовать соблюдение контрактов в этом случае.


Максимум что может сделать копилятор — запретить использовать неправильный код в случае если компилятору известно о его неправильности.


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


Условие на невозможность вызовов по указателю нам дает известность графа при компиляции. Внешний мире же тут либо соглашается с контрактами языка, либо...


… Либо не соглашается и делает что хочет, но ответственность лежит полностью на внешнем мир

> ответственность лежит полностью на внешнем мир

Если я использую внешний мир так, что возникает рекурсия — это не вина внешнего мира. И не вина компилятора.

Пока есть стек — его можно переполнить.
> Для типичного проекта в 100.000 функций — много.
В типичном проекте не будет глубины в 100.000 вложенностей.

> надо отсекать варианты непрямых вызовов
Для этого и достаточно O(N^2).
Не обязательно даже отслеживать. Если это язык исполняемый строго сверху вниз (как большинство интерпретируемых), просто не добавляем функцию в контекст видимости пока не завершится ее тело. В этом случае любой рекурсивный вызов все равно что вызов необъявленной функции.
UFO just landed and posted this here
Вы свой язык когда-нибудь пробовали писать? Я писал. Он был в 2004 году, без null, и был фактически SQL с классами. На нём было написано штук пять среднего размера программ для работы с данными (всякого рода поиски по различным сущностям). Он был интерпретируемым.

Картинку из Ералаша про «батарейки забыл» лень искать. Если бы я захотел его транслировать в машинный код, в нём был бы null. В противном случае бинарник содержал бы в себе исходник и интерпретатор.
UFO just landed and posted this here
Программистам на диване читают лекции в другом месте.
Вот не нужно драматизировать, пожалуйста. Нулевая ссылка — это банальный бутафорский пульт, при нажатии кнопки на котором ничего не происходит. Правда, если запустить им в окно, окно разобьется, но бытует мнение, что пульт в этом не виноват.

Не правда, как минимум если на него нажать, то он сломается и черт его знает чего еще поломает.
SEGFAULT — это «ничего не происходит» ))
Похоже у товарища iqiaqqivik свои особенные null'ы после которых ничего не происходит ))
Ох. А есть ли какая-нибудь принципиальная разница в случае гипотетической транзакции (whatever it means), упали мы в корку, или просто элегантно упали в мессаджбокс «ошибка 512».

Я вообще адепт идеологии «падать лучше как можно раньше»: эрланг, ОТП, супервизоры, вот это все.
UFO just landed and posted this here
Можно разрулить. Не будет NRE. Я это все понимаю. Я не понимаю, почему это вдруг всегда лучше.

Ну, кроме аргумента «это же очевидно».

Аргумент "вместо ошибки периода выполнения непонятно где и когда получаем ошибку периода компиляции по месту дефекта" вы тоже не увидели?

Это не аргумент. Во-первых, «где» в рантайме ничуть не хуже, даже наведенный, если есть стектрейс. «Когда» — да, это весомо. Но цену за строгую типизацию приходится платить тоже немаленькую.

Вместо того, чтобы, грубо говоря, вызвать цепочку `a ⇒ b ⇒ c ⇒ d ⇒ e`, словить NRE, по стеку определить, кто виноват и выплюнуть: «не могу этого сделать, „c“ не определен», мне придется заморачиваться наличием у всех акторов сигнатуры, отличающей Empty, или заворачивать это все в Either. Иногда это оправдано. Иногда — нет.

И когда это оправдано, мне никто не помешает быть готовым ко всему. А когда нет — меня очень выручает null. Еще раз: ваш аргумент блестяще доказывает несомненную пользу подхода в первом случае. Зачем при этом вымарывать очень удобный иногда null из языка — убейте, не пойму.

Желания убрать null нет, есть желание, чтобы язык поддерживал not nullable ссылки, а для nullable ссылок выдавал ошибку на возможные места возникновения NRE.

UFO just landed and posted this here
Я именно так и делаю, спасибо.

UFO just landed and posted this here
Я конечно, стараюсь null не возвращать и не передавать, но это на уровне смысла методов скорее, чем претензия к языку.

Почему бы не починить проблему на уровне языка, которую можно починить на уровне языка?

Лично я был бы рад не писать повсеместно [NotNull], а использовать корректный тип. null не является валидным значением типа. Это отсутствие значения. И то, что объекты по-умолчанию нулы это явная недоработка.

Почему просто так скастовать default(int?) в int нельзя, а (null) Person в Person можно? Почему в первом случае мы явно выделяем отдельный тип, а во втором это одно и то же? Ведь как все помним, вся разница между структурами и классами — в семантике копирования ссылки против копирования значения.
Интересно, а можно ли было ссылочные типы объединить с optional? То есть, иными словами можно ли на уровне языка любой ссылочный тип рассматривать как optional у которого null — значение none?
Или может быть можно сформулировать иначе: возможно ли (опять на уровне языка) сделать optional таким, чтобы любой ссылочный тип рассматривался как optional, но при этом была возможность явно указать что данный ссылочный тип не может иметь значения null (атрибут NotNull?)

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

Хайп вокруг заменителей null напоминает страшный социальный эксперимент, в котором 9 подставных человек называют белое черным, чтобы смутить десятого. Потому что, я бы ещё понял null-aware работу с обычным указателем, без которой компилятор вас изобьет. Но попытка убедить людей в том, что какой-то особый монадический контейнер с точно такой же повсеместной проверкой на IfPresent (или как-то иначе) засоряющий клиентский код это намного лучше — это просто какой-то артхаусный фильм про 'мир сошёл с ума'. Что там, кстати, с NULL в sql, не надумали отказаться? Пусть реальный мир подвинется, в ИТ не может не быть значения, пусть даже это значение НетЗначения.
Да тут все проще. Сам null — вполне нормальное явление. Но некоторые типы нуллабельны по умолчанию, некоторые — ненуллабельны. И получается так, что это свойство типа неявное, оно как-бы спрятано внутри самого типа. То есть когда вы имеете дело с типом T, вы заранее не знаете, может ли он быть null, или нет.
А если бы например при разработке языка ввели правило, что T — это всегда ненуллабельный тип, а скажем T? — всегда нуллабельный, вероятно ясности было бы больше.

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

Это не так.Вот вам контрпример:
1) Вы пишите библиотеку
2) Я спустя год пишу код, который ее использует

Как вы на этапе компиляции библиотеки отследите все случаи, когда использующий ее код передает в нее null?

Это "не так" в C# и многих других языках как раз из-за ошибки дизайна.
В Haskell NRE невозможен по построению.
Тип A требует значение типа A. Тип Maybe A позволяет не иметь значения, но требует обрабатывать и его наличие (Just a), и отсутствие (Nothing).

Переменная типа Object требует значение типа Object или null, и мог бы требовать обработку null, но мы не будем об этом говорить, лучше поговорим про обфункционаливание всего подряд.

Конечно, ошибка дизайна тоже имеется, но она не в null заключается, а в отсутствии понятия «предусловия метода» в подавляющем числе систем модульности в любой экосистеме, хоть в джаве, хоть в дотнете (про .so вообще молчу, позорище из 70-х).
Переменная типа Object требует значение типа Object или null
Из-за ошибки дизайна.

Без нее переменная типа Object требовала бы значения типа Object, а null допускала бы переменная типа Object? (точно также как и для значимых типов)

UFO just landed and posted this here
В хаскелле — очень даже возможен, только компилятор будет ругаться (или не будет, если его немножко запутать) на неисчерпывающий перебор.
foo (Just x) = "foo"
-- foo Nothing не добавили

bar x = case x of (Just y) -> "bar"
--              | otherwise не добавили


f = foo Nothing
b = bar Nothing


вылетит исключение Non-exhaustive patterns in case

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

Не обломает. http://ideone.com/jN7nAB

foo (Just t) = "just"
--foo Nothing    = "nothing"

main = do
  putStrLn "begin..."
  putStrLn (foo Nothing)
  putStrLn "end."

А это что? prog: prog.hs:1:1-21: Non-exhaustive patterns in function foo
А это — stderr исполнения программы, куда был направлен вывод обработчика исключений!
Сперва в stdout попало «begin...», а затем, не доходя до «end.», прилетела ошибка, которая вывалилась в stderr (в соседний блок на странице).

Была бы ошибка компиляции, там ideone совсем по-другому написало бы, — в ещё одном блоке.
UFO just landed and posted this here
>Как вы на этапе компиляции библиотеки отследите все случаи, когда использующий ее код передает в нее null?
Проверкой всех входных параметров.
Проблема в том, что сейчас всё это приходится делать руками, если бы была возможность использовать для этого стандартную языковую конструкцию — было бы намного менее обременительно.
Как вы на этапе компиляции библиотеки отследите все случаи, когда использующий ее код передает в нее null?


code contracts
Как вы на этапе компиляции библиотеки отследите все случаи, когда использующий ее код передает в нее null?

В том-то и прелесть not-null типов, что это делать будите вы, когда захотите передать в библиотеку null тип, а не автор библиотеки. А коли передается без проверок на наличие объекта — то ССЗБ.

Если воспринимать `null` так, как его воспринимают в реальном мире, а именно — как отсутствие чего либо, пустой слот — то все типы нуллабельны, вообще-то.

Как null не воспринимай, в int по умолчанию его присвоить нельзя.
нельзя, потому что язык не позволяет.
а если смотреть шире (что и предлагает делать iqiaqqivik), то вполне себе можно
а если смотреть шире (что и предлагает делать iqiaqqivik), то вполне себе можно

И создать себе геморрой еще и со значимыми типами.

про какой геморрой вы говорите? NULL — это значение. У него логическая нагрузка другая, это да, но это значение. Третье состояние, если использовать терминологию схемотехники. Ведь никто не возражает против третьего состояния в отношении микросхем, никто не возражает, что у типа данных Variant есть состояние unassigned. Потому что это нормально — отсутствие известного значения. Это не бесплатно, да. Но это нормально
про какой геморрой вы говорите?
Например про необходимость каждому полю и переменной значимых типов где-то хранить флаг отвечающий за наличие значения. Итого выравнивание в структурах/классах слетает, памяти жрется больше, кеш-миссы всегда чаще.
Примерно в этом месте обычно уже начинается проповедь про то, что строгая типизация — это хорошо.

В некоторых (далеко не во всех, впрочем) случаях — это даже правда.

Реальный же мир устроен по другому. Я только на это указал.

Реальный мир устроен так, что функция работает только с определённым набором значений (не пересекающийся с типами в общем случае). В них null может входить, или не входить. Предикаты вы можете проверять на компиляции или вручную. Но вы не должны выполнять функцию при невыполнении её предикатов.

А входит ли «null» или «68543» в конкретный тип (подмножество значений) — это на совести конкретного типа. Тип — это не элемент «реального мира».
Реальный мир устроен так, что значение всегда есть (опуская квантовые эффекты, но и там вместо null должна быть другая конструкция, более определённая, чем универсальный null).

Если под null подразумевать «мы не знаем», то это уже не конструкция реального мира, а наша абстрация поверх неё.
> всегда есть (опуская

Это замечательно.

null часто подразумевает не «не знаю», а «не применим». Какого цвета электрон? Какой запах у песни?
В реальном мире нет этого значения, значит, под него вообще не требуется переменная.
Если у вас есть переменная, хранящая «запах песни» = null, это плохой дизайн )))
В реальном мире нет переменных, и типов(каждый объект уникален). Есть характеристики.
В реальном мире нет способа (функции), который померяет у музыки запах и вернет null. Несовместимость интерфейсов.
Вы вообще что называете функцией в реальном мире?
Ничего не называю. Функцию я написал как ближайший аналог в ЯП.
Под «всегда есть» я имел ввиду, что null-значений не бывает.
Если у объекта есть температура, она не-null
Это как то противоречит тому, что я сказал?
Да. В хорошем дизайне не должно быть ситуации, в которой в переменную «цвет электрона» кладём значение «не применим». эта информация должна быть на мета-уровне, на уровне схемы данных, а не как значение «null»
Что значит «в хорошем дизайне»? Мы о реальном мире? Не нравится цвет электрона — скажите размер ноги безногого.
О программе, моделирующей реальный мир. О плохом дизайне модели, если модель легко может отобразить невозможную ситуацию.
Так и что там с моделью безногого? Это невозможная ситуация?
В хорошей модели безногого нет метода, возвращающего отсутствующую ногу, чтобы что-то там у нее измерять.
В реальном мире объекты уникальны, невозможно составить модель каждого без-чего-то-там.
Делают одну модель человека.
> В реальном мире объекты уникальны, невозможно составить модель каждого без-чего-то-там
Но вы ведь называете это моделью безногого. Зачем модели безногого возможности иметь ноги? Может заодно и возможность наличия колес ему дать, на случай кибернетизации?
> Но вы ведь называете это моделью безногого.

*модель, способную описать безногого
> *модель, способную описать безногого
А зачем навязывать модель, которая возможно хранит ноги, если нам нужен именно безногий?

Так можно уйти в философию и дойти до концептов уровня any UniversalType.getProperty(string propertyName).

Но вообще, можно и с опциональной ногой:
bool human.applyIfLegPresent(toolCallback);
> А зачем навязывать модель, которая возможно хранит ноги, если нам нужен именно безногий?

Не нужен, у нас равноправие и толерантность. Безногих в обувной магазин пускают (так и вижу, тыкаешь в раздел «обувь», а всплывает плашка «а у вас ноги есть?).

> Так можно уйти в философию и дойти до концептов уровня any UniversalType.getProperty(string propertyName).

dynamic — это практика. А в JS — суровая реальность.

> bool human.applyIfLegPresent(toolCallback);

Это про интерфейс, а не модель. Ну а внутри то переменная есть?

И дальше что? IfKneePresent, ifMiddleFingerPresent? Сразу же написал — »невозможно составить модель каждого без-чего-то-там"
> Безногих в обувной магазин пускают (так и вижу, тыкаешь в раздел «обувь», а всплывает плашка «а у вас ноги есть?)
Пускают, но никакого null при доступе к размеру отсутствующих ног они не получают, потому что и доступа этого нет.

> Ну а внутри то переменная есть?
Нет. У безногого этот метод вернет false и ничего не сделает.

> И дальше что? IfKneePresent
А чем это отличается от «knee = getKnee(); if (knee != null) {… };», кроме отсутствия null?
Да, у человека очень много свойств, и никакая модель не поможет их убрать, если не абстрагироваться.

> невозможно составить модель каждого
Можно составить модель каждого, кто предусмотрен к обработке в системе. В любой момент времени мы либо знаем тип объекта (и знаем наверняка, что у него можно мерять), либо не знаем (а там у нас возможно щупальценогий крылоклюв). Если интерфейс совпадает — меряем. Если нет — мы бессильны.
скажите размер ноги безногого
Какое приложение делаем? По подбору обуви? Не должно оно запускаться у безногого. Если это часть более общего приложения по подбору гардероба, модуль обуви должен быть недоступен для безногого.
Здесь одного null-а недостаточно.
Сегодня требуют вариант «я безногий инвалид», а завтра захотят «я параноик, поэтому не скажу».
Тут в любом случае Option, с вариантами, и если вариант — числовой, доступен метод получения значения.
Не надо придумывать сложностей на ровном месте. В социльной сети фетишистов параноики не регистрируются.

Если вы под «Option» и «доступен метод» подразумеваете, что при обращении к недоступному методу я получу исключение — так работает null. Хотя, конечно, и у него есть фатальный недостаток.
> вместо null должна быть другая конструкция, более определённая, чем универсальный null

Да почему? Почему вы считаете, что ваша менее универсальная конструкция лучше? В некоторых случаях — лучше, а в некоторых — нет. В конце концов, не нравится — не пользуйся. Но объявлять это ошибкой дизайна как минимум смешно. Мода на монады пройдет так же, как прошла в свое время мода на ООП. Empty в подавляющем большинстве случаев — овердизайн, который только заслоняет суть.

Потому что null-object «без запаха» имеет тип, и вместо него нельзя подставить «без цвета».
Почему это нельзя? При наличии в системе null очень даже можно. И сразу же из коробки можно получить список всех элементов «без одного признака». Что, мягко говоря, поторебует километров ненужного кода при строгой типизации.

Шах и мат.
Я, вообще-то, про паттерн «null object».
А может все-таки все наоборот и not-null reference types + optional более явно бы показывало суть, чем текущий дизайн reference types? Если опустить варианты оптимизации производительности через null, как особое значение, то я не вижу не одного случая где бы null'ы делали бы код читабельнее или безопаснее. Можете такой случай привести?
Легко.

(Сразу оговорюсь, что такое «делать код безопаснее» я не понимаю, в моем мире безопасным код делает разработчик, поэтому далее — про читабельность.)

Анкета. 500 вопросов. Ответы бывают самые разные: галочка, радиокнопочка, текст, картинка. Нужна (помимо прочего) функция, получающая среднее количество ответов в анкете. Пойдет? Или желаете чего-нибудь поизощреннее, с промежуточной генерацией кода?
У вас у анкеты один инстанс подразумевается? Разряженный массив объектов ответов (разных типов и с null вперемешку) и доступ по индексу с последующим кастингом по метаданным анкеты? Хочу читабельностью вдохновиться.

А в псевдокоде можно? А то не понятно что вам мешает null object пересчитать, вместо null'ов.
Наследовать надо от BaseAnswer типы CheckBoxAnswer, TextAnswer, ImageAnswer. И заодно туда же — NoAnswer.
А что именно вы хотите от BaseAnswer в NoAnswer отнаследовать?
Да, верно. По-хорошему, NoAnswer не нужен. Должна быть коллекция заполненных ответов, map: QuestionId -> BaseAnswer.
«безопаснее» и удобнее в моем понимании — это перекладывать на компилятор всю рутинную работу, которую компилятор может со 100% точностью сделать гораздо эффективнее человека. Например: проверка ошибок (не давать изменять readonly-поле), представление компилятором более безопасного и читаемого синтаксиса (ссылки, с их как бы автоматическим разыменованием по сравнению с указатеями), генерация кода по определенному паттерну(типа генерация конечного автомата по async..await-синтаксису) и т. п. Это все из той же оперы. Компьютер практически не делает ошибок(если у него есть строгий корректный алгоритм) по сравнению с человеком в такой рутинной работе. Следовательно, преимущества на лицо: гораздо быстрее получаем корректный результат. Просто нужно иметь возможность это удобно выразить в языке.

П. С. iqiaqqivik, мне как и areht'у хотелось бы пример с пояснением, что вы считаете удобством. Только желательно с упором в «безопасность» т. е. простоту поддержки этого кода другими членами команды.
UFO just landed and posted this here
var r;
var foo = getFoo();
var bar = getBar();
if(foo != null && bar != null){
  r = calc(foo, bar);
}

r = do
  foo <- getFoo
  bar <- getBar
  return $ calc foo bar

Где тут проверка на IfPresent?

Вместе с null выплеснули всё остальное императивное программирование? Лихо.
UFO just landed and posted this here
UFO just landed and posted this here

Основная тема статьи: не как отказаться от null, а как избежать ошибок разыменования и сделать это как можно раньше.


Что там, кстати, с NULL в sql, не надумали отказаться?

В sql с null-значения проблемы другого рода: можно получить проблемы с производительностью (например, oracle не индексируют null-значения), можно легко сделать логическую ошибку в предикате запроса (троичная логика не всегда очевидна). И отказ от null — одно из возможных решений.

В SQL с null куча своего геммороя.

Так, например, Oracle считает, что пустая строка и null — это одно и то же.

Да, писать банальное условие str = '' в oracle противопоказано, можно долго отлаживаться. Кстати, из-за этой "фичи" строковый тип называется varchar2, а varchar не рекомендован к использованию.

даже так скажу. не надо брать varchar2, надо использовать nvarchar2, потому что иначе серверные сортировки могут очень странно работать для неанглоязычных строк.
Мне одному кажется что проблема излишне раздута в статье?

Все (по крайней мере должны) проверяют входные параметры в функцию и выходные из функции. Работа кода, который делает что-то с пустым объектом обычно бесполезна. Что толку, если вы получили пустой объект? Логика работы программы всё равно нарушена.
Да и возврат не null, а пустого объекта тоже имеет место быть.
Все (по крайней мере должны) проверяют входные параметры в функцию и выходные из функции.

Да, восемь строк проверки на три строки полезного кода смотрятся просто, чисто и элегантно.
Проверки выходных параметров и возвращаемых значений в коде не припоминаю за все время работы.
Посмотрите решение 11 — оно справится с проверками лучше программиста-человека.

UFO just landed and posted this here
Статья не понравилась. Интрига есть (я «почти» согласился, что null — это «однозначно ошибочное решение, бездумно скопированное из более ранних языков»), но в «исторической альтернативе» все как-то, извините, «сдулось» в один маленький абзац.

Нет примеров, видно, что автор знает больше, но рассказывать явно не собирается (ссылки — хорошо, но примеры никто не отменял, кстате, некоторые из ссылк на ru-ru msdn, некоторые на en-us). Сухие перечисления без примеров — бесполезны, никто не будет «заучивать» их наизусть, без понимания почему так.

Почему ?. называют Элвисом (кстате, правильно использовать в качестве термина словосочетание «null-coalescing», а не глалог «coalesce)?

Почему if(something != null) вдруг „антипаттерн“? „Единственное назначение которого — выбросить исключение поближе к месту предательства“ — никто не выбрасывает NullReferenceException и никто не отменял валидацию параметров метода (InvalidArgumentException) или ошибочных сценариев (InvalidOperationException). Или имелось в виду что-то другое?
UFO just landed and posted this here
По ссылке — только Peter, и ему там в комментариях указали на его заблуждение.

Комментарии от Nick Orlando и Randall Deetz тоже были от меня?
И Bill Wagner по ссылке ниже тоже я?
http://www.informit.com/articles/article.aspx?p=2421572
И гугл при поиске "elvis operator c#" почему-то находит null-conditional operator раньше тернарного.
И это все я один?

Элвис — это не тернарный ?:, а как раз-таки бинарный.

Похоже, что только tyomitch знает про бинарный ?: в C#

По ссылке ниже оператором Элвиса в шарпе называют '??'. При этом указывают, что официально он называется «null-conditional operator», ибо на смайл Элвиса он уже не похож.

?? официально называется null-coalescing operator. null-conditional operator — это как раз официальное название для ?.

Вы это не мне, а процитированному мной человеку напишите. Суть в том, что функциональную роль оператора Элвиса в шарпе выполняет как раз '??'.

> null-conditional operator — это как раз официальное название для?..
Быть точнее, null-conditional member access operator.
Где же элвис одноглазый? Привыкли все, что смайлики повёрнуты, как совёнок-брат-дебил. А тут чуб наверху, два глаза внизу:
?..
Ну значит, два разных оператора — это «горизонтальный Элвис» и «вертикальный Элвис».
Если верить вики, в Groovy есть они оба.
Элвисом называют ?: — тут хорошо виден чуп и глаза, если смотреть на ?. — то и правда неясно — здесь, видимо, «король» нам подмигивает.
Я посмотрел повнимательнее и теперь и во втором варианте тоже вижу Элвиса! Кажется, я прозрел!1 Привычность восприятия играет с нами шутку. (хинт: точка от вопросительного знака — первый глаз). Только насколько я знаю такой элвис обычно называют safe call и у него немного другой смысл, чем у того элвиса, что повернут как классические смайлы.
Почему if(something != null) вдруг „антипаттерн“? Единственное назначение которого — выбросить исключение поближе к месту предательства“ — никто не выбрасывает NullReferenceException и никто не отменял валидацию параметров метода (InvalidArgumentException) или ошибочных сценариев (InvalidOperationException).


потому что в свете последних за 20 лет веяний в ООП эта задача должна быть переложена на контракты
в «исторической альтернативе» все как-то, извините, «сдулось» в один маленький абзац.

А там и не требуется сильно больше: источником серьезной проблемы стала экономия буквально на спичках. Добавил примеры кода к альтернативе.


Сухие перечисления без примеров — бесполезны, никто не будет «заучивать» их наизусть, без понимания почему так.

В этом случае каждое решение грозит превратиться в отдельную статью, что повысит трудоемкость для меня и ухудшит усвояемость для читателей.
Я не стремился подробно осветить каждый нюанс каждого частного случая. Моей задачей было дать обзор типовых проверенных решений и высокоуровневые критерии для их выбора. Для тех, кто хочет узнать больше о конкретном варианте — в тексте есть ссылки.

Null и вправду великий и предателский, вопрос в другом, что делать если есть таблица 59 миллионов записей,

А вот вы говорите что поле не должно быть null. Получается так что для того чтобы добавить поле субд должно будет ставить этому поле значение по умолчанию? В таком случае справится ли СУБД?

Больше все го в действительно напрягает что нужно все время делать COALESCE на проверках в СУБД.
Но с другой стороны для поля бит мы должны будем инициализировать либо 0 или 1, для поля интегер 0, для пустой строки мы должны будем проинициализировать \0 (нулевым символом). То есть null обусловлен тем что многие субд немогут просто так взять и выкинуть NULL, Потому что при ALTER новый столбец при не NULL порождает дорогостоящую операцию для инициализации 59 миллионов UPDATE.

А самая тормозну тая операция в СУБД это как раз таки UPDATE!
NULL необходим в СУБД. По крайне мере в RDBMS. И условность это больше техническая нежели концептуальная.
В БД нужно ставить два поля — bit/bool есть значение (результат измерения) и само измерение(int, например), ибо не всегда понятно — 0 это отсутствие измерения или значение измерения 0.
вообще бит булл, мы понимаем что в теории это один бит, но для того чтобы прочитать бит нужно сначала прочитать байт потом извлечь бит, то как хранится в rdbms разнится для каждой rdbms.

И по большей части зависит от того как это реализуется в конкретной СУБД.
Может, не очень удачно выразился — я как раз против излишнего добавления поля (ставить два поля вместо одного — плохо). NULL, конечно, тоже хранится где-то (в виде маски свободных/занятых полей в записи, например), но по крайней мере код объявления, вставки, обновления не растёт от использования NULL, а при втором поле растёт.

Проблема не в том, есть null или нет, а в том что


  1. null допускают все типы полей в БД
  2. Операции с null ведут к ошибкам при исполнении а не при компиляции запроса.

Если бы значения integer null и integer not null были бы


  1. разных типов,
  2. не совместимых между собой без явного coalesce
  3. с доступом к большинству операций только для integer not null,

то с null и в SQL было бы намного меньше проблем.

В C++ есть довольно простой способ не писать часть лишних проверок — передача интерфейсов в функцию по ссылке. Тогда внутри самой функции и вызываемых из нее проверки на NULL не нужны (синтаксически), а вызывающему коду приходится явно разыменовать указатель, что для любого программиста красная тряпка и повод железно убедиться, что разыменовываешь не null:


void StartVehicle( Vehicle& object )
{
  object.CloseDoors();
  RunEngine(object); // void RunEngine( Vehicle& object );
}
void CallerCode()
{
  Vehicle* object = FindObjectSomewhere();
  if(object) // Проверка только в момент фактического появления неопределенности
  {
    StartVehicle(*object);
  }
}

Недостатки:


  • Если нужно хранить объект, то это не всегда возможно сделать в ссылке (например, при отложенной инициализации)
  • Не для всех привычно использовать перегруженные интерфейсы через ссылку.
  • Это C++.

Пожалуй, в описанном сценарии (передача параметров в функцию) это аналог вашего варианта 2 (NotNull). По-моему, вы его несколько недооценили. Конечно, для полностью нового кода писать везде NotNull (или &) — некоторая морока, но зато на рабочей кодобазе можно потихоньку очищать код, добавляя NotNull снизу вверх, пока указатели с возможно нулевым значением не останутся только на инфраструктурном уровне в местах, где это объективно необходимо.

Есть еще один недостаток — в ваш метод rvalue ссылку вы не передадите, нужно или использовать универсальные ссылки (если это шаблонный код), либо делать ссылку константной, либо дублировать код для rvalue ссылок при необходимости.

Rvalue ссылки имеют смысл, когда объекты передаются по значению (в C# это была бы структура). Это иная парадигма, и там нет проблемы нулевых указателей. Как и при передаче интерфейсов по указателю нет проблемы rvalue ссылок, т.к. указатели тривиально копируются.
Кстати, парадигма передачи объектов по значению, если она реализуется последовательно, — тоже, пожалуй, один из ответов на проблему null.


Вопрос константности в данном случае продиктован требованиями функции. Для не изменяющей объект функции ссылка, конечно, будет константной.

грамотный разбор, добавить нечего

На предвосхищение ООП через 20 лет не претендую


это пять ))
UFO just landed and posted this here
В императивном подходе избавляться от null нет никакого смысла. Статья написана так густо, что «мессаджа» за ней и не видно. У null-а проблема скорее не в его наличии, а в том, что знание о том может быть значение null-ом или нет содержится в головах, которые пишут код, в хорошем случае в документации и в идеальном в виде аннотации к параметру. Но хотелось бы, чтобы компилятор сам предостерегал нас от проблем с такими значениями. Потому и появляются всякие Optional в таких языках как java, чтобы обозначить, что возвращаемого значения может и не быть и потом что-то с этим удобно сделать.
UFO just landed and posted this here
Что делать — это философский вопрос. Это решают создатели языка. В котлине, например, получая из метода значение, тип которого помечен, что там не может быть null, просто нет никакого смысла его проверять на null. А если всё-таки тип возвращенного значения nullable, то ты просто не сможешь вызвать у него метод, пока не проверишь, что это не null. Поначалу кажется сложным, по факту привыкаешь и потом очень без такого страдаешь в java.
UFO just landed and posted this here
UFO just landed and posted this here

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

UFO just landed and posted this here

В котлине есть ссылочные типы, не допускающие null. Им проверки на null не нужны по построению.

UFO just landed and posted this here

Вы спрашивали, избавит ли в C# от проверок добавка фичи из котлина и каким образом.
Да, от части проверок избавит — ссылочные типы без null соответсвуют решению 12 из статьи и позволяют не проверять их значения на null по построению.
Смысл обсуждать котлин в теме про C# простой:


  1. Ссылочные типы без null всегда обсуждаются при разработке новых версий C#.
  2. Котлин свободен от NRE, кроме случаев взаимодействия с внешним не-котлин кодом.
  3. У JVM и Java проблема с null в точности та же что и в .NET и C#
  4. У C# в сравнении с котлином есть дополнительное обременение в виде совместимости с предыдущими версиями языка.
UFO just landed and posted this here

Смысл в том, что по ссылке всегда объект, и с ней можно работать не опасаясь NRE. Если ссылка позволяет null, то компилятор должен выдать ошибку при обращении к объекту, если нет явной проверки на null. Пустой объект не создается, так как корректность использования ссылок проверяется на этапе компиляции.

UFO just landed and posted this here

Вы можете попробовать читать именно то что написано.


"Смысл в том, что по ссылке всегда объект" — исходя из Ваших же слов, именно тут и будет создан пустой объект.

Это не чьи-то слова — это ваши собственные идеи.


  1. Совсем не факт что будет использован паттерн null object — у него есть существенные ограничения.
  2. Когда null object все-таки применяется — нейтральный объект обычно создается статически и переиспользуется многократно.

Например, метод FindPerson вместо null (всеми почему-то ненавистного) вернет «пустой» объект, потому что null вернуть он не может.

  1. Вы статью вообще прочитали? Недостатки реализации null в C# расписаны еще до ката.
  2. С чего вы взяли что FindPerson должен использовать именно null object? Исключение при неудачном поиске бросить не судьба?
  3. Позволяющих null ссылок заведомо меньше, чем их общее число, а на практике от силы процентов 5. Кроме того, при наличии ссылок без поддержки null проверку для каждого случая появления null достаточно сделать лишь однажды.

Лично для меня в такой логике ничего полезного нет, проверки на null остались, плюс добавились абсолютно бесполезные «пустые» объекты.

Вы сами предложили такую логику, не надо приписывать ее кому-то еще.

  1. Чем пустой объект отличается от null? Если фунция должна вернуть объект, она должна вернуть реальный объект или, если не может этого сделать, бросить исключение:


    Person! p =  FindPerson(); // Получаем либо объект, либо исключение PersonNotFound
    Log(p.ToString()); // Работаем с объектом, не опасаясь NRE

    Если функция может вернуть null, то перед использование результата необходима проверка:


    Person p =  FindPersonOrDefault();
    Log(p.ToString()); // должна быть ошибка компиляции, т.к. нет гарантии, что p != null
    if (p != null) {
        Log(p.ToString()); // Работаем с объектом, не опасаясь NRE
    }

  2. Не избавляет, а наоборот, вынуждает писать проверки по месту использования ссылки.


  3. Зачем нужен "пустой" объект и что с ним дальше делать? Проще не создавать.

P.S. Мы же 12-й пункт обсуждаем?

UFO just landed and posted this here
Прям как на MSDN про винапи: вторым параметров всегда должно идти TRUE, а десятый должен всегда быть NULL. Мы это задокументировали, отлично, раз в доке написано, значит поведение корректное и система хороша.
по поводу Вашего пункта 2 — выше Bonart написал, что код необходимо избавлять от «кучи шаблонного кода», но с таким подходом шаблонный код никуда не девается.
Много проверок пишете для проверки int на NULL? Ведь для типов, не содержащих NULL «шаблонный код никуда деться не может».

Если метод описан, и указано, что он может возвращать null, то в этом нет ничего плохого.

Не знаю как у вас в 90х, а в 2016 принято кидать эксепшн, а не возвращать код ошибки (в данном случае null это флаг, что нинашла).
UFO just landed and posted this here
Потому что есть любители возвращать null ссылочного типа вместо того, чтобы бросить нормальное исключение. Ну вот если бы мы с вами вместе например работали, вы бы наверняка просто вернули бы null (вы же сами это выше и написали), и я бы конечно же сделал проверку на null (ибо фиг знает, что там ваш метод возвращает, лучше подстраховаться).

И вот эти лишние проверки, которых вполне не было бы если б нулл вообще не мог прийти занимают по статистике 20% кода (источник не приведу — уже не помню где видел).
UFO just landed and posted this here
Нормально. Только null не должен быть валидным значением типа. Например int не должен быть null, для возможности вернуть такое значение его явно нужно обернуть в int?.. Что и нужно было изначально сделать для референсных типов.

Когда я создаю новый метод, то всегда пишу к нему xml комментарии, где описываю сам метод, его аргументы и возвращаемое значение. И если бы мы с вами работали вместе, то у вас не возникло бы ни единого вопроса (это конечно только если вы читаете описание методов, которые вызываете).

Ну то есть так и есть, то что должен делать компилятор вы перекладываете на человека. «Не прочитал XML комментарий — получи эксепшн, так тебе и надо». Почему бы проверкой не заняться компилятору? Ведь если я не прочитал комментарий и не сделал проверку — то я не прав? Почему нет ОБЯЗАТЕЛЬСТВА сделать проверку? Это ведь легко сделать. Например с тем же int? у меня просто нет другого способа передать его в другой метод, который требует число, кроме как вначале проверить на HasValue (ну или свалиться при попытке получить значение).

Но конечно же лучше отказаться от всего этого, ведь можно написать xml-комментарий. А еще можно всегда везде возвращать dynamic, а в XML комментарии писать, что за тип на самом деле пришел. Вуух, какая гибкость! А кто не прочитал, тот сам себе злобный буратино.
UFO just landed and posted this here
«Референсные типы» для того и reference types, что бы не содержать значение, а только на него указывать.
Верно, это означает исключительно семантику оператора «присвоить». Соответственно
А пока объект ссылочного типа не указывает на значение в куче, то он равен null, по моему это более чем логично.
нифига не так. Ссылка всегда должна указывать на существующий объект. Собственно в том же котлине это и сделано, афайк.
Это как бы вы выстрелили себе в ногу, и жаловались на то, что револьвер сделан неправильно, ведь это мелочи, что инструкцию вы не прочитали та и вообще использовали его неправильно.

Нет, это у вас чуть что не так — пользователь виноват.

Вы можете 100 раз ему сказать, что он должен был прочитать документацию, посидеть с бубном и т.п., но если программа сделала не то, что ожидал пользователь — то виноват разработчик. Можете потом сидеть доказывать ему в спину, что он должен был прочитать 200-страничный мануал, а человек просто уйдет к конкурентам, у которых перед тем как дропнуть все данные выводится окошко с подтверждением, а не подход «сам виноват».
Кстати, если вам так действительно не хватает данной «фичи», то предложите ее реализовать (или проголосуйте за реализацию, если такая уже есть) для разработчиков самого языка. А то пока вы будете придумывать очередной пример с dynamic, то «зарелизят» следующую версию.
Вы за меня не переживайте, такая фича уже заявлена, и я заявку давно заапрувил.

Ну и вы не ответили на вопрос, чем плохо переложить на компилятор проверку этих XML-комментариев?
Например int не должен быть null, для возможности вернуть такое значение его явно нужно обернуть в int?.. Что и нужно было изначально сделать для референсных типов
Я бы хотел использовать такой язык. Но пока не понимаю, как будет выглядеть default-значение. Например, если Document — не-null тип, то чему равен default(Document)?

Например, default(bool)==false, а default(int)==0.
Этим значением заполняются поля классов и структур, сразу после их создания.

Или not-null ссылки допустимы только в локальных переменных и в возвращаемых из функций значениях?
UFO just landed and posted this here
Думаю, если попытаться надёжно покрыть покрыть все сценарии, получим сложную спецификацию, в стиле c++ с его конструкторами копирования и прочими плюшками.

Самый простой случай — как должен работать Array.Resize?
UFO just landed and posted this here
amironov, по поводу Вашего пункта 2 — выше Bonart написал, что код необходимо избавлять от «кучи шаблонного кода», но с таким подходом шаблонный код никуда не девается.

Еще раз: когда гарантия not null есть — избавляет от проверок, когда нет — обязывет сделать проверку.

UFO just landed and posted this here
В таком случае компилятор заставит писать «шаблонный код» if == null, от которого в начале это дискуссии пытались уйти.

Цель — избавиться от NRE: чтобы были подсказки от компилятора, когда эти проверки обязательны, а когда нет.


Каким образом вы собираетесь избавлять от «шаблонного» кода if == null?

void ProcessPerson(Person! person)
{
        // Не надо писать
        // if (person == null)
        //     throw new ArgumentNullException("person");
        Log(p.ToString());
}

ProcessPerson(new Person()); // ok
ProcessPerson(null); // ошибка компиляции
UFO just landed and posted this here
От if == null вы уходите, только если ссылочный тип не может быть null

Совершенно верно. Заодно компилятор даст по рукам, тому кто попытается null туда запихнуть.


а если может — вы снова напишите шаблонную проверку.

Совершенно верно. Только после первой же успешной проверки у меня будет значение ссылочного типа, не допускающего null и его, в отличие от нынешних, не надо будет проверять по всей глубине стека вызовов. Заодно компилятор даст по рукам тому, кто рискнет пропустить обязательную проверку.


Итоги:


  1. Полностью исключены проверки с выбросом InvalidArgumentException
  2. А там где null — не ошибка, наличие проверки проконтролирует компилятор.
  3. NRE исключено по построению
  4. PROFIT
Никто не говорил, что компилятор решит все проблемы. Смысл в том, чтобы на представленном ниже рисунке оставить только элементы в зеленых областях, потому что красные это либо мусорные проверки (которые не нужны), либо как раз место для NullReference
image
Я не понял каким образом разработчики будут вынуждены использовать NOT NULL. Например, найдется человек, который не читает ни xml описания, ни документации (по словам PsyHaSTe это нормально, так как все само должно работать и исправлять ошибки), и вернет по старинке NULL тип вместо NOT NULL. В таком случае компилятор заставит писать «шаблонный код» if == null, от которого в начале это дискуссии пытались уйти.

stalsoft не передергивайте. Я говорил про то, что не надо заставлять читать документацию, когда это не является обязательным. А то так действительно дойдем до венгерской нотации, когда все туда-сюда кидается одним и тем же образом, а программист должен сопоставив кучу фактов правильно угадать, что же это такое.
UFO just landed and posted this here
Ясно, вы видимо либо один всегда на проекте работаете, либо у вас идеальные разработчики, которые не могут устать/забыть/просмотреть, которые никогда не допускают багов (ведь если прочитал документацию и XML-комментарий, то правильно использовал, а система состоящая только из рабочих компонент, которые правильно связаны друг с другом не может вываливаться с ошбками), и всегда с первого раза пишут корректный код, который идет сразу в прод.
UFO just landed and posted this here
А вы проверяете возвращаемое значение этого метода на null?
UFO just landed and posted this here
в 99% случаев — не читаю, потому что как раз сигнатура метода + грамотное использование атрибутов (вроде [Obsolete(true)]) делают неправильное применение метода невозможный.

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

И я не меняю тему. Просто я знаю, сколько в проектах используются обычные структуры, а сколько — Nullable. Во-первых не очень много. Во-вторых при попытке засунуть null туда, куда нельзя, получите ошибку компиляции. А для референсов это нормально. Все же любят NullReference.

Раз уж я ответил, теперь ответье и вы: действительно вы считаете, что то что компилятор МОЖЕТ проверить ошибки использования нулевой ссылке он НЕ ДОЛЖЕН делать, обойдясь XML-комментариями?

Примечание: как выше я уже сказал, у нас на проекте комментариев практически нет, и не потому, что мы говнокодеры, а потому что мест где комментарии нужны исчезающе мало. С другой стороны верно и обратное, когда проект весь закомментирован хлам, зачастую это означает именно «хмм, потратить время и переделать нормально или написать комментарий и пойти гулять… Да нахрен надо, я и так тут 2 часа над этим методом сижу, лучше напишу коммент».

Кстати, для сравнения, на моей старой работе на проекте было в трекере примерно 10% тасков и 83% багов (ну и другие категории в оставшихся 7%). Там на уровне студии было установлено правило обязательно комментировать публичные метода, классы, и т.п.

На новой работе нет обязательства писать комментарии, в результате примерно 67% тасков и 33% багов.

Проекты были начаты примерно в одно время, +- пара месяцев.

Делайте выводы сами. Например о моей нерепрезентативной выборке :D Но кроме статистики есть еще и голова, и если подумать, можно прийти к таким же выводам.
UFO just landed and posted this here
Засим думаю спор и закончим, думаю к общей точке все же пришли. Первый абзац как раз об этом говорит.

Что касается второго, XML комментарии действительно очень помогают, но они нужны как раз для тех вещей, которые автоматически не проверяются. Но когда такая вещь появляется, документация с поддержкой со стороны IDE (при автокомплите параметры подписываются и т.п.) весьма удобно, никто с этим не спорит.
UFO just landed and posted this here
А почему нельзя просто не использовать null там где не надо? как вариант защиты — использовать контракт. А там где может быть null обрабатывать его корректно.
В котлине достаточно неплохо решены некоторые проблемы null-ов. Тип переменной или поля задается сразу с указанием может ли в нем null храниться или нет. Это позволяет компилятору избавить нас от лишних проверок в тех местах, где null-а быть не может и требуя их там, где он может появиться. Думаю, аннотируя хотя бы параметры методов аннотациями, которые добавляют эту информацию, можно частично решить на этапе компиляции эту проблему и в C#.

А что будет при тесном взаимодействии с любым Java-фреймворком?

UFO just landed and posted this here
после долгих мытарств, они пришли к такому подходу — если есть информация о нулабельности (вроде аннотаций), то она используется, а если нет, то отдается на откуп разработчика определить тип и считается, что в этом месте безопасность не гарантируется.
Ну вот мне, кажется, что гораздо хуже чем null, в дизайне C#, это void. Из-за этого сейчас активно дублируется всё API по работе с функциями (Action и Func, Task и Task<>). Вот это, раздражает гораздо больше.
Учитывая, что для обработки ошибок используются исключения и что в императивной парадигме не всякий код возвращает значение, то альтернатив особо нет. Можно было бы сделать специальный класс, который бы обозначал отсутствие возвращаемого значения (назовем его Void), который бы использовался во всех API в качестве возвращаемого значения. И получилось бы примерно так: Action превращается в Func, Action<...> в Func<..., Void>, Task в Task и так далее. Что-то особого улучшения не ощущается.
Ну, вообще void — это тип. System.Void, и вы можете сделать typeof(void), больше, правда ничего не можете сделать с ним. Т.е. в кишках тип номинально уже есть. Какой-нить Invoke, имеющий возвращаемый тип object, на void возвращает null (с горя). Т.е. уже встроенная библиотека .NET борется с этим типом.
На мой взгляд, Func<void> всё-таки лучше чем грустный Action. Хотелось бы, чтобы его можно было использовать более гибко.
Эта ошибка дизайна языка приводит всего лишь к неудобству и возможно некоторому уменьшению скорость разработки. А проблема с null'ом приводети к ошибкам в программе и страдают не только программисты, но и пользователи.
«Хитроумный идальго Дон Кихот Ламанчский» на современный лад
Чем отличаются 8 и 9?
Option и Maybe — просто разные названия одного и того же (первое во всех языках, кроме Haskell, второе — в остальных).
Option (или optional) — это тип контейнера, а Maybe — это монада.
Так повелось, что в хаскелле монада уникально идентифицируется типом контейнера. То есть Maybe t — это монада Maybe, а Either () t — это уже нет, хотя логика там будет точно такая же.

Maybe — это вовсе не обязательно конкретный тип, это всего лишь монада оперирующая возможно отсутствующим значением. Монаду Maybe в C# можно определить как над обычным ссылочным типом так и над специальным (Optional).
Optional — тип, который допускает хранение значения или явное указание на его отсутствие, при этом не приводится к типу значения и требует явно обработать оба варианта использования, что позволяет обойтись без NRE by design.

> Обрабатывать это исключение бесполезно: оно означает безусловную ошибку в коде.

Автор считает, что ошибки в коде не надо обрабатывать? Видимо автор очень далеко от тех областей, где из-за ошибок в коде могут погибнуть люди. Как минимум в надежном коде надо:

1) Изолировать отказ. То есть отказ в одной функциональности программы не должен прекращать ее исполнение, а лишь уменьшать её функциональность.

2) Вывести диагностику в лог отказов.

3) Пересоздать объекты, задействованные в отказавшей функциональности.

4) Перезапустить подсистемы, задействованные в отказавшей функциональности.

5) Использовать резервный сервер.

Пункты 1 и 2 — выгодны, они упрощают сопровождение. Пункты 3, 5, 5 — достаточно затратны и нужны лишь для программ высокой надежности.

Вы, возможно, спутали разные виды отказов. От NRE ваши способы 1-3 не помогают никак, 4-5 чуть лучше, но тоже ничего не гарантируют. Этим ошибки в коде кардинально отличается от сбоев в окружении, для обхода которых и придуманы ваши пять пунктов.

Опыт 25 лет библиотеки VCL и 10 лет работы 365*24 моего приложения показывает, что помогает. Не во всех типах программ, но во многих — помогает.

С диагностикой понятно — она всегда полезна. Анализ логов позволяет выделить часто встречающиеся проблемы и понять приоритет при исправлении. Да и сам поиск ошибок становится проще.

Изоляция полезна всегда, когда у приложения много функций. Как пример — интерактивные приложения вроде Word. Получаем NRE при тройном клике мышкой (выделение фразы). Если NRE не изолирован — приложение падает. Если изолирован — пользователь выделит нужный кусок стрелками или движением мыши с нажатой клавишей.

Я уточню, что речь идет об отлаженных приложениях. То есть NRE при выделении фразы — будет не на любой фразе, а на фразе со сложными знаками препинания, например с непарными кавычками. Тем самым мы сводим отказ от «программа вылетает» до «иногда не работает, но легко обходится». То есть понижаем важность исправления на 2 ступени.

Именно так работает любое GUI приложение, основанное на VCL. Базовая изоляция внесена в основную структуру VCL.

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

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

Насчет окружения — вы правы. NRE в отлаженной программе — следствие редких событий в окружении. В действиях пользователя, в последовательности исполнения, в значениях текущих данных…

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

Ещё раз повторюсь, что я веду речь об отлаженных приложениях с редкими отказами.
С диагностикой понятно — она всегда полезна. Анализ логов позволяет выделить часто встречающиеся проблемы и понять приоритет при исправлении. Да и сам поиск ошибок становится проще.

Логирование исключений в диагностических целях не требует перехвата и обработки именно NRE. Обычно делается хук на выброс исключения и логируется все подряд, за редким исключением. Для ошибок в коде разве что приоритет ставится FATAL.


Изоляция полезна всегда, когда у приложения много функций.

"Изоляция" при NRE — это попытка делать вид, что "все хорошо, прекрасная маркиза", при том, что о реальном состоянии системы вам известно только то, что воспроизвелась ошибка в коде.
Типовая реакция от VCL на этот случай годится только для тех приложений, ответственность за исправное функционирование которых невелика.
Опять-таки, это делается хуком c отловом всех необработанных исключений без специальной реакции конкретно на NRE.
Во всем остальном коде пытаться перехватить и обработать NRE значит стрелять себе же в ногу.


Я уточню, что речь идет об отлаженных приложениях. То есть NRE при выделении фразы — будет не на любой фразе, а на фразе со сложными знаками препинания, например с непарными кавычками. Тем самым мы сводим отказ от «программа вылетает» до «иногда не работает, но легко обходится». То есть понижаем важность исправления на 2 ступени.
Такая реакция — сладкая ложь для пользователя. Если ошибка и впрямь привязана к окружению и пользовательскому вводу, то перезапуск даст шанс их обойти, сохранив корректность работы. А вот продолжение работы с гарантированно поломанными инвариантами в надежде на "изоляцию" означает ненулевую вероятность действительно крупных неприятностей: от порчи важных данных и заведомо неудачных автоматических сделок на бирже до инъекции смертельной дозы обезболивающего.

Если вы не в курсе, самые опасные сбои в промышленной эксплуатации из-за ошибок в коде как раз редкие и плавающие. Частые и легко воспроизводимые чаще всего не доходят до боевой площадки, а если доходят, то легко исправляются.


У программ нет такого показателя как надежность. Есть корректность — отсутствие ошибок кодирования, есть отказоустойчивость — способность восстанавливаться после сбоев. Описанная вами методика при NRE дает иллюзию корректности с помощью иллюзии отказоустойчивости, по факту не обеспечивая ни того, ни другого.


В программе с высокой ценой сбоев, исключений, означающих ошибку в коде, не должно быть вообще. Для NRE этого добиться просто, для дедлоков — сложнее, но цель должна быть только такой. У меня был опыт доведения проекта, выдававшего по десятку страниц разных исключений вроде AV и т.п., до вылизанного состояния.

Тем не менее let it crash подход с локализацией любого падения и перезапуском упавшего куска супервизором с дальнейшим пересозданием состояния начисто из доверенного персистентного источника вполне прижился в эрланге.
Впрочем в эрланге это прокатывает лишь благодаря куче других хитрых особенностей рантайма — повальной иммутабельности, взаимодействию через обмен сообщениями итд.
Так что при определённой гигиене кодирования данная техника действительно позволяет писать отказоустойчивый код. Корректный — нет.

Так в эрланге и изоляция настоящая, и источник реально доверенный, и отказоустойчивость неиллюзорная.

всё так. нужно использовать сразу весь комплекс техник оттуда, чтоб получить реальный рост отказоустойчивости. иначе первый же залетевший race condition разрушит цивилизацию.

я вас удивлю — у нас гонки были, ничего не рушилось. Программа сама убивала одну (или обе) нити, участвующие в гонке и запускала их заново. Для этого нужен всего лишь механизм транзакций. Как пример — SQL живет себе на транзакциях и ему этого хватает.

"Всего лишь механизм транзакций" — это уже из серии "90% любой крупной программы на C++ представляет собой медленную и глючную реализацию подмножества спецификации Common Lisp" (по памяти).
Если нужны качества, которые может дать только безопасный рантайм — логично использовать готовый и проверенный безопасный рантайм вместо велосипедов. А для некоторых тем вроде криптографии это вообще категорический императив.

Ну мы по нужным нам характеристикам обогнали Industrial SQL Server.
1) Количество записей — до миллиона переключений в секунду. И это не только в пике, мы на этой скорости и работать могли.
2) Формат хранения — упакованный (сжатие в 6 раз).
3) Поиск редких событий по всей базе за секунды.

Все это было на 300 Мгц компе (пень II)

Но, самой собой, это все очень не универсально.

Ну а не нравятся ошибки рантайма — пишите свой собственный. В Delphi ошибок в рантайме хватает, в других языках — в общем-то тоже. Исходить нужно из того, что ошибки были, есть и будут и в компиляторе, и в библиотеках и в вашем собственном коде. А не надеяться, что за 10 лет вы найдете ВСЕ ошибки.

Транзакционный доступ — это как раз про отсутствие гонок =)
Транзакции — это абстракция немножко выше уровнем, чем параллельный доступ к кускам памяти. Нужна ещё реализация, которая их обеспечит.
Дедлоки при транзакциях возможны, и за этим проследят супервизоры и отстрелят зависшие.


Гонка — это когда происходят чтения или записи кусков, которые в этот же момент пишут другие, т.е. в реализации транзакций будет баг (блокировку не захватили или барьеры не выставили, или версию забыли поднять в mvcc, а уже пишем в shared-кусок), к примеру.
Они чреваты тем, что вместо целостной структуры будет полная галиматья, где полструктуры обновлены одним потоком, а вторые полструктуры — другим. Это, конечно, можно вычислять какой-то функцией, которая в рантайме будет проверять инварианты, но откуда она возьмёт данные, на которые надо откатиться, и как она найдет виноватых?


У вас была какая-то готовая и надежная реализация транзакций, или пришлось писать самим и вылавливать затейливые баги?

У нас все намного проще было и колхозней. Гонки — отдельно, транзакции — отдельно.

Схема такая. 12 контролеров, в каждом кольцевой буфер событий минут на 20. По читающему треду на контроллер. Потом из читателей данные идут на первый синхронизатор. Он выдает общую очередь событий + срез на последнйи момент времени. это все идет на диагностику и виртуальный контроллер. Потом — второй синхронизатор вместе с результатами диагностики и виртконтроллера. И уже сформированный потом — на тред, ведающий записью.

А уже запись — базировалась на атомарных операциях Windows, включая запись на диск в обход дискового кэша. И транзакционно — работала только запись. То есть прочли с диска одной операцией чтения, добавили данные, записали данные одной операцией в.tmp, потом переименовали старое в .bak, а .tmp в основной файл. Операции переименования в NTFS -атомарны, даже если будет перезагрузка компа во время операции — все равно будет или старое или новое состояние, но не промежуточный вариант.

Так что фактически — мы паразитировали на транзакционности NTFS. Зато — мы были на 100% уверены, что на диск пишутся корректные данные. И могли перезапускать любые нити без потерь данных. Максимум убытка — перезапрос данных заново с контроллера.

А гонки… гонки могли быть в синхронизаторах, в диагностике, в вирт.контролере, но в треде записи на диск — их не было.

Само собой, что очереди передачи данных были хорошо отлажены. Основа — TThreadQueue в нашей собственной реализации.

т.е. в треде записи на диск стоял верификатор данных, который анализировал данные на бред и если что-то не так, то сбрасывал рекурсивно все поломавшиеся кеши между слоями и перезапрашивал данные?

Ну не совсем так… Треды друг друга контролировали. Основной контроль — на синхронизаторах, которые сливали несколько потоков в один. Им как раз видно было, откуда данные идут, откуда не идут, какие длины очередей и так далее. Но если синхронизаторы сбоили — то второй синхронизатор перезапускал первый, а тред записи — второй синхронизатор. То есть второй синзронизатор в качестве последнего бастиона. И операция переменования изTMP как окончание транзакции.

А при перезапуске — любой тред перезапрашивал предыдущие дать данные, начиная грубо говоря с контрольной точки.

Ну и очень много контролей для проверки инвариантов. один ассерт — на 10-20 строк кода (если вне много раз исполняемого цикла).

А между слоями — не кэши, там очереди. Причем очереди — рассчитанные на доступ из разных тредов.

Там всего 130 тысяч строк кода, это и GUI-клиент, и OPC-сервер, и собственно основной сервер. Так что наворочено защит было много. :-)
Не в качестве рекламы (фирмы уже нет), но вот описание http://www.sysauto.ru/index.php?pageid=508
В 2001 году, оказывается внедрение было. Окончание опытной эксплуатации — 2002ой год. И второе внедрение — в 2005ом.

Ну и в 2017ом хотят модернизацию на другой тип контроллера.

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

В 2005ом я смотрел, что на АНГА. За 3 года — ни минуты простоя по вине службы автоматики. То есть и у нас все хорошо, и заводчане с помощью нашей системы отладили программы управления станом в контролерах. А там — 8 тысяч входов и 2 тысячи выходов.
Корректность тоже сильно увеличивается. Дело в том, что такая техника позволяет раньше перейти к опытной эксплуатации. Стадии, когда имитаторы не выявляют ошибок, мы достигли рано. Но кривая обнаружения ошибок показывала, что на самом деле их ещё порядочно. А комплексная отладка на оборудовании — 2 часа в месяц, только на выходе и планово-предупредительного ремонта. Так что поставили на опытную эксплуатацию и уже во время неё, по логам обнаруживали и исправляли ошибки.

Таким образом, эта техника позволяет за то же календарное время обнаружить больше ошибок. Ну и исправить их.

Ну в опытной эксплуатации на фоне промышленной все наоборот — надо "ломать и крушить", давая нагрузки и условия заведомо жестче боевых.

А это уже коммерция не позволяет. Стоимость продукции за сутки-была порядка миллиона долларов. Стоимость всей разработки — НАМНОГО меньше. С одной стороны, 10 остановок предотвратили — мы себя окупили. С другой стороны, одну остановку на нас повесили — фирма разорилась. Это ещё было после кризиса 98ого года когда доллар в 6 раз скакнул.

Так что на отладку — 2 часа в месяц. На выходе из планово-предупредительного ремонта были 2 часа планового брака. Но там и без нас хватало желающих потестироваться.

Зато с помощью нашего софта умудрились уменьшить время выхода рулона стали с часа до 40 минут. То есть вместо миллиона долларов — полтора. Выигрыш — продукции на полмиллиона в сутки.

А нагрузки мы давали на имитаторах. Работали на скоростях в 100 раз больше реальной пиковой нагрузки.

На самом деле, заказчики просто прикололись. Пик — 10 тысяч записей одновременно. Мы достигли 100 тысяч. А заказчики в шутку попросили миллион. Ну напряглись и сделали им миллион. Потом оказалось, что они просто пошутили. Но — запас карман не тянет.

98 год?
Да, все то, про что я думал как о лучших альтернативах, еще не существовало, даже файберы надо руками писать.
Дикий капитализм — цена разработки системы меньше чем недельная прибыль от ее эксплуатации, офигеть.

Начало разработки — весна 1999ого. Планировалась окупаемость за год, то есть предотвращение олной остановки стана в месяц. Реально — вышло за сутки, время оцинковки рулона стали с помощью нашей системы уменьшили с часа до 40 минут. Мы писали события с привязкой к сканам и это позволило заводским инженерам убрать все узкие места, мешающие прохождению стальной ленты на высокой скорости.

Капитализм там до сих пор такой же. За переделку системы под новый контролер мы с трудом доторговались до цены полутора рулонов стали. :-)

Поразило другое. Ежемесячный трехдневный планово-предупредительный ремонт. Вся инженерная команда трое суток не спит. И вот после 3х суток напряженной работы инженер-автоматчик подходит к начальнику:
— Стан поехал, все в норме, МОЖНО я пойду спать?

И отсутствие рабочих. Ну есть уборщицы, есть крановщики, но у всех остальных — минимум незаконченное высшее. И пустота в цехах. На 3 стана ночью — человек 15 персонала на 2 километра длины цеха.

И весь город, в 2001 году переведенный на банковские карточки. Даже в ночных ларьках спрашивали «у вас денюшка али карточка». Питер и Москва до этого уровня до сих пор не дошли.

И дыра в потолке, дождь падает почти на контроллеры, сам контролеры прикрыты наклонным листом стали.
— Что вы делаете, когда ломаются контроллеры OMRON?
— Они не ломаются.

Вода льется ручьем в 40 сантиметрах от контролеров, а они — не ломаются.
В бытовках за месяц выпадает миллиметр металлической окалины, а они — не ломаются.

И Ethernet II — мегабит на толстом коаксиале для связи с контроллером. Надежная техника — техника на очень изученной элементной базе.

Файберы — это легковесные thread? Они там не нужны, обычных нитей хватало. Это же не веб-сервер, где операция создания-удаления треда частая.Клиенты подключаются-отключается редко, обычных нитей хватало. Из экзотики — я широко использовал виндовые PIPE, сейчас все бы сделал на TCP/IP. Но тогда PIPE казался более универсальным вариантом.
Согласен, что обработка — более-менее одинакова для большинства типов исключений, ну кроме исключений по вине неверного ввода пользователя.

Вы знаете, можно при каждом укусе осы и каждом порезе пальца в реанимацию ложиться. От укуса осы умерла жена знакомого (анафилактический шок), про смерти от столбняка немало написано в литературе. Только в большинстве случаев (99.99%) все ограничивается ерундой.

Инварианты проверяются НАПРЯМУЮ и довольно регулярно. Для каждого объекта, функциональности, подсистемы, всего приложения. Шансов пропустить разрушения инвариантов — меньше, чем шансов умереть от укуса осы. А если нашли разрушение — все по той же схеме — пересоздать (перезапустить) разрушенный уровень. То есть функциональность, подсистему, приложение. Если 3 раза не помогло — перезапускаем уровень выше. Аналогично — при зависании, оно тоже контролируется. Причем зависание одной подсистемы (нити) контролирует несколько других нитей.

Само собой, что приложение должно позволять безопасно пересоздавать его части. То есть иметь контрольные точки, с которых можно перезапуститься. Ну как пример: при записи на диск проверяем соблюдение всех инвариантов. А при перезапуске подсистемы архивирования — заново считываем и исходные данные с датчиков и состояние архива после последней записи.

На самом деле реальная шкала опасности исключений совсем другая.

1) Опаснее всего NOT EHOUGTH MEMORY, то есть нехватка памяти. В большинстве случаев она означает, что структура кучи разрушена безвозвратно.То есть по хорошему при любой нехватке памяти надо проверять кучу. Причем не системными процедурами (они могут зависнуть), а своими собственными с контролем времени исполнения. Вторая причина — фатальная фрагментация кучи. Третья — ошибка в алгоритме, например разрастание очередей из-за неверных приоритетов. И самое редкое — ну действительно не хватило виртуальной памяти. То есть может так и бывает, но в реальности — не видел.

2) Следующее по опасности — FPE. То есть корень из отрицательного числа, деление на ноль и так далее. Говорит о реальном разрушении инвариантов. Грубо говоря, вся накопленная матрица — летит в мусорную корзину. Причем если матрица копилась часы — без возможности восстановления, если секунды — есть шансы хранить исходные данные и перезапустить вычисление, но это маловероятно, что поможет.

3) Далее у нас Access Violation. Тот, что не через нулевой указатель. Это часто означает, что или нам потерли память или мы пытаемся затереть чужую память (а скорее всего уже что-то потерли). Часто бывает следствием оставшегося указателя на уже удаленный объект. Ну или двойного выполнения деструктора. Пережить можно, но только с восстановлением с диска. Или из областей памяти, защищенных контрольной суммой.

4) Stack Overflow. Обычно возникает при исключениях на обработке исключений. Иногда означает разрушение структуры кучи. Если алгоритм не рекурсивный по своей природе — скорее всего восстановление не поможет. Рекомендация — если Stack Overflow удалось поймать — проверить структуру кучи. Но чаще это гибель приложения и диагностика уровня ОС.

Это вот действительно опасная четверка. А NRE — это ошибка легкая. Дело в том, что память при NRE не портится. Даже, если NRE на запись — все равно память не испорчена. В большинстве случаев NRE — это пропущенная проверка на NULL в деструкторе или в записи в лог о редкой
ошибке. Вы правильно писали в https://habrahabr.ru/post/72959/ что в деструкторе надо использовать FreeAndNil. Только забыли упомянуть, что он появился лишь в Delphi 5, и в VCL далеко не всегда используется.Так что получить NRE в деструкторе формы после исключения во время создания формы — не так сложно.

Теоретически бывают ситуации, когда NRE — следствие порчи памяти, но я такого не припомню. Ошибки типа +-1 (неверный подсчет числа оборотов цикла) бывало, что приводили к NRE. А порча памяти — сама по себе зверь редкий, да и проявляется не так. Скорее уж указатель на уже удаленный объект так проявится.

А вообще все зависит от того КТО и в каком стиле пишет. Ели вам не лень — приведите ситуации, когда NRE означает что-то серьезное и необнаруживаемое проверкой инвариантов.

Теперь о мелочах.

> Во всем остальном коде пытаться перехватить и обработать NRE значит стрелять себе же в ногу.
НЕВЕРНО. Изоляция должна быть многоуровневой. Try Except Raise EAbort. При этом роль ближайшего except — прежде всего диагностика как можно ближе к точке ошибки. Ресурсы, как правило, отдаются в finally. Перехвата только на одном уровне — просто мало для нахождения ошибок по логу.

> продолжение работы с гарантированно поломанными инвариантами в надежде на «изоляцию» означает ненулевую вероятность действительно крупных неприятностей:
Сломанные инварианты далеко не всегда ведут к NRE. Так что, если у вас что-то серьезно — проверяйте инварианты напрямую. И тогда NRE — означает именно NRE и ничего более. Ну а если инварианты регулярно не проверяются — значит у вас та самая «сладкая ложь».

> Описанная вами методика при NRE дает иллюзию корректности с помощью иллюзии отказоустойчивости, по факту не обеспечивая ни того, ни другого.
По ФАКТУ — 10 лет 365*24 без сбоев (на обоих станах). 130 тысяч строк кода, примерно 2-4 ошибки на 1000 строк кода осталось (это по динамике обнаружения ошибок). Комплексная отладка на стане не проводилась вообще — там непрерывное производство, время на отладку — 2 часа в месяц. Отладка на имитаторах закончилась после года без багрепортов от персонала и месяца отсутствия сбоев в логах. Ориентировочное ожидаемое количество сбоев в логах — несколько в год.

> В программе с высокой ценой сбоев, исключений, означающих ошибку в коде, не должно быть вообще.
Это маркетинговые сказки. Или некомпетентность. Невозможно отловить все ошибки — последние из них будут проявляться с частотой 1 раз в тысячу лет и не отлавливаться на тестах. Это лишь САМООБМАН, что можно найти все ошибки в коде. Написать надежное приложение — можно. Написать безошибочное приложение большого размера — просто нельзя.

> Для NRE этого добиться просто, для дедлоков — сложнее, но цель должна быть только такой.
Дедлоск — следствие ошибок в архитектуре. У нас их не было, от слова совсем. А вот NRE вполне могли остаться в частях кода, используемых реже, чем раз в год. Повторюсь, типичный NRE — это пропуск If NOT Assigned при обработке редкой исключительной ситуации. Он вторичен и не важен.

А цель — надежность, а не корректность. То есть ошибок должно быть мало, а восстановление — работать уверенно. Перекос в любую сторону — ухудшает характеристики программы.

> У меня был опыт доведения проекта, выдававшего по десятку страниц разных исключений вроде AV и т.п., до вылизанного состояния.
И что такое для вас «вылизанное состояние»? Сколько сбоев за 10 лет на миллион строк кода? Можете дать свои цифры? Я вот не постеснялся дать свои.

Сколько сбоев за 10 лет на миллион строк кода?

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


Это маркетинговые сказки. Или некомпетентность. Невозможно отловить все ошибки

Речь ни в коем случае не идет об ликвидации всех ошибок! Я имею в виду только предотвращение тех из них, которые вызывают исключения, чей тип гарантированно означает ошибку в коде (не в окружении и не во входных данных).


Следующее по опасности — FPE.

Это исключение само по себе ошибку в коде не означает. Но в ваших условиях его цена понятна. Остальное из вашей четверки для меня всегда было уже некрологом.


Только забыли упомянуть, что он появился лишь в Delphi 5, и в VCL далеко не всегда используется.Так что получить NRE в деструкторе формы после исключения во время создания формы — не так сложно.

Коммерческий опыт у меня начался с Delphi 7. Для младших версий FreeAndNil несложно написать самому. От формы (а в идеале от GUI вообще) исправность системы ИМХО зависеть не должна.


PS: Большая статья о вашей системе станет украшением любого тематического сайта.

> Тем не менее от исключений, которые сами по себе означали ошибку в коде, мы избавились.
Мы тоже избавились, но не могу гарантировать, что стопроцентно. Срабатывает редкий Assert, ну скажем раз в 5 лет. И при выводе в лог — забыли проставить проверку, что все объекты существуют. И получаем вторичный NRE. Вот такое — могло и остаться.

Мы старались избавится от всех ошибок, но прикидывали количество неизвестных ошибок по падению частоты их обнаружения и количеству уже исправленных ошибок. Формула, кажется, была у Майерса — http://publ.lib.ru/ARCHIVES/M/MAYERS_Glenford_Dj/_Mayers_G.Dj..html в книге «Надежность программного обеспечения»

FPE — это из несколько другого проекта, связанного с GPS. Моя текущая работа. Там формируется матрица на основе многих измерений. И FPE — означает, что матрицу можно выкидывать, а накопление вести заново. Ну и из математики получается, что FPE — следствие ошибок в коде, а не шума в измерениях.

Мы начали с Delphi 4, потом перешли на Delphi 5, потом адаптировали под Delphi 7, но он оказался хуже — больше ошибок в VCL. Нарвавших на ошибку в функции ожидания окончания завершения треда, зависящую от скорости процессора — я решил, что на Delphi 5 надежней оставаться.

А лучшая книга по написанию компонентов для VCL — это Рей Конопка, описывающий вообще Delphi 1. Кстати, визуальные компоненты AVT (хитрые гриды с фильтрацией и сортировкой по колонкам) у нас писал 17 летний Дмитрий Жемеров (@yole), ныне большая шишка в JetBrains, и один из авторов языка Котлин. :-)

> От формы (а в идеале от GUI вообще) исправность системы ИМХО зависеть не должна.
От формы не зависит сохранность данных. Но смсыл системы был именно в GUI.

Ночь. Два дежурных автоматчика дрыхнут. Стан встал, сигнал по громкой связи. И далее — счет на минуты и секунды. НАДО:
1) Найти причину аварии. Например — отказавшая лампочка на линейке фотодатчиков контроля положения полосы.
2) Модернизировать программу в контроллере для обхода аварии.

Не нашел, не успел — простой запишут на службу автоматики. 3 часа простоя в месяц — 100% премии снимается со ВСЕЙ службы. Не с работника, а со всех.

Потом, днем — разбор, анализ, подготовка планов на ППР (ежемесячный планово-предупредительный ремонт). Но пока стан стоит — главное время. Быстро найти причину и ликвидировать её. Быстро — пока ещё весь рулон стали не улетел в брак.

Так что работоспособность GUI — очень важный фактор. Поэтому в GUI любая операция могла делаться многими способами. Отказал один — не смертельно, просто лишние 5 секунд потрачено.

После внедрения системы — за 3 года ни минуты простоя по вине службы автоматики. Да, датчики иногда отказывают. Но анализ любой аварии и её обход — идет быстро. Собственно в этом и состоит смысл системы — при аварии стана быстро и в 99% случаев автоматически понять, что её вызвало. Это вот та часть, которой я горжусь — автоматический анализ ladder-диаграмм. Причем ещё и многотактный и по нескольким нетворкам. В качестве хвастовства — в единственном случае, когда заводчане не согласились с результатами автоматического анализа, в итоге выяснилось, что автор программы контроллера не разобрался, как работает его код в сложной многотактовой ситуации.

> PS: Большая статья о вашей системе станет украшением любого тематического сайта
СПАСИБО. Я пока минусы получаю, когда пытаюсь объяснить, что можно изолировать ошибки, а assert — не надо отключать в боевом коде. Статья о системе — http://www.sysauto.ru/index.php?pageid=508
Ещё одна хитрость. Почти каждая ошибка исправляется 4 раза
1) Проверяется, что после ошибки система восстанавливается. если нет — исправляется.
2) Обеспечивается раннее обнаружение ошибок данного класса. То есть на уровне проверок, а не реальной ошибки в коде.
3) Ошибка исправляется.
4) Ведет поиск аналогичных ошибок по всему коду.
На самом деле у вас неправильная модель NRE в голове. Вы считаете, что NRE — это неверная структура данных. На самом деле, наоборот — NRE — это корректная (но упрощенная) структура данных, а вот алгоритм не понимает некоторые редкие упрощенные случаи.

В такой трактовке понятно, почему NRE не означает нарушение инвариантов?
В Obj-C
```
(null).anyMethod == 0/null
```
и никаких nullReferenceException.

Это было чуть ли не идеальное решение.

> Обрабатывать это исключение бесполезно: оно означает безусловную ошибку в коде.

Что? Нет! Ну как, конечно если язык падает при доступе к такой переменной и заставляет ее всячески обворачивать то да, ошибка. А так — нет. И никаких лишних движений.
>Что? Нет! Ну как, конечно если язык падает при доступе к такой переменной и заставляет ее всячески обворачивать то да, ошибка. А так — нет. И никаких лишних движений.

Framework Design Guidelines

DO NOT catch System.Exception or System.SystemException in framework code, unless you intend to rethrow.


Единственное, что можно сделать: поймать, написать логи/дампы/перезапуск и упасть. NRE это однозначная ошибка в коде, работать дальше смысла не имеет
Ну да, и мне интересен ответ на вопрос «а зачем так сделали?». Вон Swift сейчас с этими оптционалами городит. И зачем оно? А если меня устраивает null повсюду? А если в выражении `var firstCarIsRed = (containers.cars.firstObject.color == red)` меня не волнует, что containers == null / cars.count = 0? А если в 90% кода ситуация аналогичная? Моя мысль в том, что писать код гораздо проще несколько с другой стороны: проще требовать не-нулл в тех местах, где это важно, чем повсеместно проверять на null везде, чтобы оно не упало.
тогда, очевидно, должен быть способ задать поведение по умолчанию. А оно может быть различным для каждого случая, и тогда начинаем использовать?.. и ??, либо различным для каждого типа, тогда нам нужен или Null Object или типизированный null(еще вопрос где описать поведение такого null). Все это очень сложно, гораздо проще не иметь дела с null вообще, и тогда, возможно, мы увидим какой-то сахар для этого в языке

Когда есть выбор между


  1. "писать код проще" и
  2. "задешево превратить целый класс ошибок периода выполнения в ошибки периода компиляции"

результат немного предсказуем.

"(null).anyMethod == 0/null"

Период компиляции говорите? На мой взгляд это полная потеря контроля над работой программы.
> Бесполезен в остальных ситуация: как только вам потребовалось отличать в коде нулевой объект от остальных — вы имеете эквивалент null вместо null object, что является уже двойным предательством: неполноценный объект, который даже NRE не выбрасывает.

Смысл null object в том, что бы не проверять, что это null. Зачем вы предлагаете его неполноценным делать, а потом проверять?

> Поддержка атрибута AllowNull — с одной стороны это очень хорошо, а с другой — аналогичный атрибут у решарпера другой.

Решарпер настраивается, на сколько я помню.

> С библиотеками, агрессивно использующими null, требуется довольно много ручной работы по добавлению атрибутов AllowNull
> Поддержка отключения проверки для отдельных классов и целых сборок

Очевидно, довольно много ручной работы не требуется, если вынести в отдельную сборку
Кстати, волею судеб вынужден работать с C#. Вопрос к тем, кому скучно и он долистал до дна коментариев:
Как в С# симулировать поведение obj-c:
```
Class1 a = obj1.method1().method2()
```
В а должен оказаться null, даже если obj1 == null или obj1.method1() == null? Это какое-то издевательство бегать и ловить эти null'ы. Эквивалентный код:

```
Class1 a = null;
if (obj1 != null && obj1.method1() != null) { a = obj1.method1() }
```
Как в С# симулировать поведение obj-c

Элементарно:


var a = obj1?.method1()?.method2()
UFO just landed and posted this here
Надо же, такая холиварная статья, а никто не вспомнил про фичу TypeScript 2. Возможно, пора набраться смелости и пойти на аналогичный шаг и в C# — добавить в компилятор режим, в котором будет использоваться более строгое подмножество языка. Не могу сказать, насколько это выполнимо в принципе, но было бы весьма заманчиво.

Как говорил Хоар про null-ы у указателей — ошибка на миллиард.
Судя по комментариям, существует отдельный класс людей, которых эта ошибка вполне устраивает и даже нравится

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

Как я люблю эти разговоры про отдельный класс людей…
Sign up to leave a comment.

Articles

Change theme settings