Comments 290
Проблема не в том что разыменование бросает исключение. Есть и другие исключения, тоже летят. Выход за границы массива например. А представьте, нету null. Тогда все как в анекдоте про буратино и яблоки. Добавлять метод IsValid? Те же проверки же. По-моему суть вопроса не в наличии маркера (отсутствия чего-либо) null, а в том что языки программирования и компиляторы различают объекты от ссылок. Переменная на то и переменная, что она может быть изменена.
Моя мысль скорее в том, что языки из 90-х (и их наследники) оперируют в императивном стиле, и, следовательно, есть переменные. C# не исключение, и ждать от него обязательств что у переменной всегда должен быть объект… зачем? Там выше про F# сказали. Давно уже хотел присмотреться к нему как следует, но всё времени нет. Теперь его приоритет увеличил. Может следующий проект на нём буду :) Если Xamarin F#-ready (а это вроде так).
С Maybe у вас есть возможность требовать обязательной обработки как наличия, так и отсуствия значения, не оставляя места для ошибок в коде, ведущих к NRE.
языки из 90-х (и их наследники) оперируют в императивном стиле, и, следовательно, есть переменныеСкорее, тут корни идут от указателей и ручного управления памятью.
В школе их учили Паскалю, или Аде кодом типа
var
p: ^TMyObject;
begin
New(p);
p.DoSomething;
Dispose(p);
end.
в воздухе ещё не витали концепции монад, optional-ов и т.п.2. Откуда инфа, кто что как воспринимал? Я вот таких обобщений не делаю.
В этом ответвлении я уже всё сказал, и совсем в другую сторону. Язык и то, во что он компилируется далеко не только предысторией привыкших к чему-то программистов связаны. И те или иные решения в нём приняты не потому что так у всех, или все привыкли, но еще и потому что бывают и коммерческие ограничения (время, бюджет), и технические (что мог тогда .NET и что сейчас), и много всего разного. А по-вашему весь контекст ситуации — это программисты на Паскале.
NRE раз в неделю — это ровно на один раз в неделю больше чем нужно. NRE в продакшене вообще недопустимо.
Проблема именно в том, что разыменование любого ссылочного типа может бросить исключение.
- Если null в этом месте не нужен, то гораздо лучше поддерживать non-nullable типы с помощью компилятора и не иметь NRE.
- Если null в этом месте возможен по бизнес-логике, то гораздо лучше требовать обработку обоих случаев (наличие и отсутсвие значения) на этапе компиляции и не иметь NRE.
Да и вообще причем тут NRE. Объясните, почему другие исключения не удостаиваются чести иметь столько внимания и языки, в которых они не случаются? К чему такое избирательство? Все это в случае конкретного языка и конкретно одного исключения больше напоминает упражнения для ума.
По поводу compile-time проверок. Еще раз. Переменная (ссылка) может иметь любое значение, это значит что выполняющее (программу) устройство зарезервировало под него кусок места в памяти. Вот этот кусок в памяти может указывать на что угодно, на умерший, невалидный объект, или валидный. Отказ от null в языке означал бы по факту отказ от ссылок, и превратил бы язык в фактически интерпретируемый, потому что в компайл-тайме объекты не создаются.
А что касается NRE, я их тоже не люблю, просто это вообще не самая большая проблема в моей практике.
Not null reference type планировалось добавить в C#7, но потом отложили эту фичу до лучших времен (лучшие времена ожидаются с С#8, но там опять же… кто знает что будет). Вот народ и негодует, что средство для практически автоматического улучшения качества программ и упрощения разработки еще прийдется черт знает сколько ждать.
Потому, что NRE — это всегда ошибка разработчика, а какое-нибудь UnauthorizedAccessException — нет.
Любой язык будет допускать ошибки разработчика. Даже русский.
Мне лично самому не нравится, когда в рантайме летят исключения, касающиеся языка. Очень раздражает. Это с того момента, как я с плюсов пересел на Java/C#. Хотя это намного, намного лучше, чем core dump, gdb и stack frames.
Какой смысл предъявлять к дизайну языка претензии через 10 лет после его создания, он для своего времени и так больше чем мог сделал. Я больше по этому поводу озадачен.
Не знаю о чём вы, я использую foreach
> division by zero
Нет
> stack overflow
Придумаете как победить на уровне языка — дайте знать.
> Какой смысл предъявлять к дизайну языка претензии через 10 лет после его создания
Почему 10 лет? Язык называется «C# 6.0», в названии, кстати, и ответ на «какой смысл» заложен.
оборачиваете в try и выдаёте пользователю, что некорректные данные.
А NRE в `try` в шарпе запрещено оборачивать? Разница-то в чем?
Какая разница, какой-то метод ошибочно вернул 0 в переменную «кол-во процессоров в системе» и дальше упало при делении, или вернул null в переменную CPUManager.
Одна ошибка на миллион пользовательских данных — это нормально, нужно быть готовым обрабатывать.
Вернуть 0 в переменную «кол-во процессоров в системе» — это экзотика.
Миллион CPUManager, один из которых null — это надо талант иметь, скорее у нас память битая.
Все(один) CPUManager null(WTF, а это вообще кто-то тестил?) — зачем так жить?
Можно придумать какие-то исключения, но, в целом, DBZ — не кодерская ошибка, но «бытовая». А NRE — точно какая-то хрень, которая сама не починиться.
1) в программе кнопок больше одной, одна может не работать. Главное — побочных эффектов не настругать.
2) Функция может работать в рамках контракта, который мы где-то нарушили. Самое банальное — плохо проверили пользовательский ввод. Нехорошо, но не смертельно — введут ещё раз.
возврат DBZ — часть естественного контракта функции. Функции деления, например.
нет
в программе кнопок больше одной, одна может не работать. Главное — побочных эффектов не настругать.
и что? не работает — перепиши
Функция может работать в рамках контракта, который мы где-то нарушили. Самое банальное — плохо проверили пользовательский ввод. Нехорошо, но не смертельно — введут ещё раз.
контракт нарушен -> написано неверено -> смысла работать дальше нет
SystemException нельзя ловить как таковой, а OverflowException — можно
А как? Получить ArithmeticException способов больше одного: сложение, умножение, деление, логарифм — всё проверками обложить?
ну если делаем лабораторку в универе, то можно и не обкладывать )) а какие еще есть варианты? input всегда нужно валидировать
SystemException нельзя ловить как таковой, а OverflowException — можно
я попробую раскрыть свою мысль: если мы говорим, например, про деление то контракт у деления должен выгялдить вот так:
Contract.Require(divisor != 0);
если мы говорим про overflow то у функции сложения контракт будет вглядеть вот так:
Contract.Require(int.MaxValue — arg1 > arg2 && int.MaxValue — arg2 > arg1);
этот контракт всем доступен, значит клиент может выполнить эти условия, а значит нарушение контракта сигнализирует о неверном коде. т.о ничего ловить не надо, надо закрашиться
надо ли делать так всегда? конечно же нет, иначе будет еще один замечательный чистый язык, на котором никто ничего не пишет, а компромиссы в простых случаях даже не приемлемы, а желанны
а вообще за всю мою практику из наследников SystemException мне приходилось ловить и проглатывать только TaskCanceledException, все что было связано с арифметикой — это всегда была моя ошибка
Если формула отпарсилась в дерево — инпут годный. Но от переполнения при умножении это не спасёт.
> Contract.Require(int.MaxValue — arg1 > arg2 && int.MaxValue — arg2 > arg1);
А для возведения в степень? А во сколько раз дольше ваша проверка будет считаться (да и писаться, с тестами), чем само вычисление?
Если нам не надо, например, подсвечивать место в формуле, где произошла ошибка, то это чистый оверхед. Программирование ради программирования, выбрасывание которого — необходимость, а не компромисс.
а инпут к формуле?
>А во сколько раз дольше ваша проверка будет считаться (да и писаться, с тестами), чем само вычисление?
один раз — при комипяции. вы вообще слышали про контракты?
я про это и говорю, но я все же надеялся что мы разбираем абстрактного коня, а не кокретный пример. Возвращаясь к теме: можно найти кучу контрпримеров когда станет плохо от выкидывания null, но это не значит, что не нужно ничего менять
ЗЫ. Math.Pow не бросает эксепций, также как и деление у double ))
На какую глубину? На сколько такой компилятор будет тормозить?
Можно прикинуть (очень грубо). Пускай N — количество функций. Наибольшее количество уникальных вызовов (в смысле откуда и что) будет достигаться, если каждая функция будет вызывать каждую. Таким образом для проверки нам потребуется проверить не более N^2 вариантов. Соответственно сложность проверки ограничена O(N^2), что не так уж и много.
К тому же, фехтование верхом на стульях требует времени: https://xkcd.com/303/
Соответственно сложность проверки ограничена O(N^2), что не так уж и много.
Для типичного проекта в 100.000 функций — много.
Тем более, надо отсекать варианты непрямых вызовов (A → B → C → D → A), а это построение матрицы достижимости на ориентированном графе, что с использованием лучших известных алгоритмов имеет сложность O(N3)
Хотелось бы в ошибках компиляции весь список, конечно.
И, кстати, не забудьте про неявные вызовы через .net fw, я затейник.
А вообще, вы почему-то думаете, что граф вызовов статический и известен при компиляции. Это немножко не так. По крайней мере я не представляю как иначе.
А это все имеет смысл только в рамках языка.
Компилятор не может отвечать за внешний мир. Любое взаимодействие с внешним миром не является ответственностью компилятора. Компилятор может проверить правильность только того кода, который обрабатывается этим компилятором. Мы можем любой код подменить во время выполнения, и компилятор не может гараниторовать соблюдение контрактов в этом случае.
Максимум что может сделать копилятор — запретить использовать неправильный код в случае если компилятору известно о его неправильности.
Язык может предоставлять безопасные средства обращения с внешним миром, но это не является гаранитей соблюдения внешним миром контрактов.
Условие на невозможность вызовов по указателю нам дает известность графа при компиляции. Внешний мире же тут либо соглашается с контрактами языка, либо...
… Либо не соглашается и делает что хочет, но ответственность лежит полностью на внешнем мир
В типичном проекте не будет глубины в 100.000 вложенностей.
> надо отсекать варианты непрямых вызовов
Для этого и достаточно O(N^2).
Картинку из Ералаша про «батарейки забыл» лень искать. Если бы я захотел его транслировать в машинный код, в нём был бы null. В противном случае бинарник содержал бы в себе исходник и интерпретатор.
Я вообще адепт идеологии «падать лучше как можно раньше»: эрланг, ОТП, супервизоры, вот это все.
Ну, кроме аргумента «это же очевидно».
Аргумент "вместо ошибки периода выполнения непонятно где и когда получаем ошибку периода компиляции по месту дефекта" вы тоже не увидели?
Вместо того, чтобы, грубо говоря, вызвать цепочку `a ⇒ b ⇒ c ⇒ d ⇒ e`, словить NRE, по стеку определить, кто виноват и выплюнуть: «не могу этого сделать, „c“ не определен», мне придется заморачиваться наличием у всех акторов сигнатуры, отличающей Empty, или заворачивать это все в Either. Иногда это оправдано. Иногда — нет.
И когда это оправдано, мне никто не помешает быть готовым ко всему. А когда нет — меня очень выручает null. Еще раз: ваш аргумент блестяще доказывает несомненную пользу подхода в первом случае. Зачем при этом вымарывать очень удобный иногда null из языка — убейте, не пойму.
Я конечно, стараюсь null не возвращать и не передавать, но это на уровне смысла методов скорее, чем претензия к языку.
Почему бы не починить проблему на уровне языка, которую можно починить на уровне языка?
Лично я был бы рад не писать повсеместно [NotNull], а использовать корректный тип. null не является валидным значением типа. Это отсутствие значения. И то, что объекты по-умолчанию нулы это явная недоработка.
Почему просто так скастовать default(int?) в int нельзя, а (null) Person в Person можно? Почему в первом случае мы явно выделяем отдельный тип, а во втором это одно и то же? Ведь как все помним, вся разница между структурами и классами — в семантике копирования ссылки против копирования значения.
Или может быть можно сформулировать иначе: возможно ли (опять на уровне языка) сделать optional таким, чтобы любой ссылочный тип рассматривался как optional, но при этом была возможность явно указать что данный ссылочный тип не может иметь значения null (атрибут NotNull?)
А если бы например при разработке языка ввели правило, что T — это всегда ненуллабельный тип, а скажем T? — всегда нуллабельный, вероятно ясности было бы больше.
Дело в том, что все ошибки, связанные с null, можно и нужно обнаруживать на этапе компиляции.
1) Вы пишите библиотеку
2) Я спустя год пишу код, который ее использует
Как вы на этапе компиляции библиотеки отследите все случаи, когда использующий ее код передает в нее null?
Конечно, ошибка дизайна тоже имеется, но она не в null заключается, а в отсутствии понятия «предусловия метода» в подавляющем числе систем модульности в любой экосистеме, хоть в джаве, хоть в дотнете (про .so вообще молчу, позорище из 70-х).
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
Насколько я помню, компилятор обломает в обоих случаях и до ошибок периода исполнения дело не дойдет.
foo (Just t) = "just"
--foo Nothing = "nothing"
main = do
putStrLn "begin..."
putStrLn (foo Nothing)
putStrLn "end."
Сперва в stdout попало «begin...», а затем, не доходя до «end.», прилетела ошибка, которая вывалилась в stderr (в соседний блок на странице).
Была бы ошибка компиляции, там ideone совсем по-другому написало бы, — в ещё одном блоке.
Проверкой всех входных параметров.
Проблема в том, что сейчас всё это приходится делать руками, если бы была возможность использовать для этого стандартную языковую конструкцию — было бы намного менее обременительно.
Как вы на этапе компиляции библиотеки отследите все случаи, когда использующий ее код передает в нее null?
code contracts
Как вы на этапе компиляции библиотеки отследите все случаи, когда использующий ее код передает в нее null?
В том-то и прелесть not-null типов, что это делать будите вы, когда захотите передать в библиотеку null тип, а не автор библиотеки. А коли передается без проверок на наличие объекта — то ССЗБ.
а если смотреть шире (что и предлагает делать iqiaqqivik), то вполне себе можно
а если смотреть шире (что и предлагает делать iqiaqqivik), то вполне себе можно
И создать себе геморрой еще и со значимыми типами.
В некоторых (далеко не во всех, впрочем) случаях — это даже правда.
Реальный же мир устроен по другому. Я только на это указал.
А входит ли «null» или «68543» в конкретный тип (подмножество значений) — это на совести конкретного типа. Тип — это не элемент «реального мира».
Если под 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?
Да, у человека очень много свойств, и никакая модель не поможет их убрать, если не абстрагироваться.
> невозможно составить модель каждого
Можно составить модель каждого, кто предусмотрен к обработке в системе. В любой момент времени мы либо знаем тип объекта (и знаем наверняка, что у него можно мерять), либо не знаем (а там у нас возможно щупальценогий крылоклюв). Если интерфейс совпадает — меряем. Если нет — мы бессильны.
скажите размер ноги безногогоКакое приложение делаем? По подбору обуви? Не должно оно запускаться у безногого. Если это часть более общего приложения по подбору гардероба, модуль обуви должен быть недоступен для безногого.
Сегодня требуют вариант «я безногий инвалид», а завтра захотят «я параноик, поэтому не скажу».
Тут в любом случае Option, с вариантами, и если вариант — числовой, доступен метод получения значения.
Да почему? Почему вы считаете, что ваша менее универсальная конструкция лучше? В некоторых случаях — лучше, а в некоторых — нет. В конце концов, не нравится — не пользуйся. Но объявлять это ошибкой дизайна как минимум смешно. Мода на монады пройдет так же, как прошла в свое время мода на ООП. Empty в подавляющем большинстве случаев — овердизайн, который только заслоняет суть.
(Сразу оговорюсь, что такое «делать код безопаснее» я не понимаю, в моем мире безопасным код делает разработчик, поэтому далее — про читабельность.)
Анкета. 500 вопросов. Ответы бывают самые разные: галочка, радиокнопочка, текст, картинка. Нужна (помимо прочего) функция, получающая среднее количество ответов в анкете. Пойдет? Или желаете чего-нибудь поизощреннее, с промежуточной генерацией кода?
А в псевдокоде можно? А то не понятно что вам мешает null object пересчитать, вместо null'ов.
П. С. iqiaqqivik, мне как и areht'у хотелось бы пример с пояснением, что вы считаете удобством. Только желательно с упором в «безопасность» т. е. простоту поддержки этого кода другими членами команды.
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, а как избежать ошибок разыменования и сделать это как можно раньше.
Что там, кстати, с NULL в sql, не надумали отказаться?
В sql с null-значения проблемы другого рода: можно получить проблемы с производительностью (например, oracle не индексируют null-значения), можно легко сделать логическую ошибку в предикате запроса (троичная логика не всегда очевидна). И отказ от null — одно из возможных решений.
Так, например, Oracle считает, что пустая строка и null — это одно и то же.
Да, писать банальное условие str = ''
в oracle противопоказано, можно долго отлаживаться. Кстати, из-за этой "фичи" строковый тип называется varchar2, а varchar не рекомендован к использованию.
Все (по крайней мере должны) проверяют входные параметры в функцию и выходные из функции. Работа кода, который делает что-то с пустым объектом обычно бесполезна. Что толку, если вы получили пустой объект? Логика работы программы всё равно нарушена.
Да и возврат не null, а пустого объекта тоже имеет место быть.
Все (по крайней мере должны) проверяют входные параметры в функцию и выходные из функции.
Да, восемь строк проверки на три строки полезного кода смотрятся просто, чисто и элегантно.
Проверки выходных параметров и возвращаемых значений в коде не припоминаю за все время работы.
Посмотрите решение 11 — оно справится с проверками лучше программиста-человека.
Нет примеров, видно, что автор знает больше, но рассказывать явно не собирается (ссылки — хорошо, но примеры никто не отменял, кстате, некоторые из ссылк на ru-ru msdn, некоторые на en-us). Сухие перечисления без примеров — бесполезны, никто не будет «заучивать» их наизусть, без понимания почему так.
Почему ?. называют Элвисом (кстате, правильно использовать в качестве термина словосочетание «null-coalescing», а не глалог «coalesce)?
Почему if(something != null) вдруг „антипаттерн“? „Единственное назначение которого — выбросить исключение поближе к месту предательства“ — никто не выбрасывает NullReferenceException и никто не отменял валидацию параметров метода (InvalidArgumentException) или ошибочных сценариев (InvalidOperationException). Или имелось в виду что-то другое?
По ссылке ниже тоже только я? Да еще за нескольких участников дискуссии?
http://stackoverflow.com/questions/27493541/null-conditional-operator-and-string-interpolation-in-c-sharp-6
Комментарии от Nick Orlando и Randall Deetz тоже были от меня?
И Bill Wagner по ссылке ниже тоже я?
http://www.informit.com/articles/article.aspx?p=2421572
И гугл при поиске "elvis operator c#" почему-то находит null-conditional operator раньше тернарного.
И это все я один?
В некоторых других языках — есть.
?? официально называется null-coalescing operator. null-conditional operator — это как раз официальное название для ?.
?..
Почему if(something != null) вдруг „антипаттерн“? Единственное назначение которого — выбросить исключение поближе к месту предательства“ — никто не выбрасывает NullReferenceException и никто не отменял валидацию параметров метода (InvalidArgumentException) или ошибочных сценариев (InvalidOperationException).
потому что в свете последних за 20 лет веяний в ООП эта задача должна быть переложена на контракты
в «исторической альтернативе» все как-то, извините, «сдулось» в один маленький абзац.
А там и не требуется сильно больше: источником серьезной проблемы стала экономия буквально на спичках. Добавил примеры кода к альтернативе.
Сухие перечисления без примеров — бесполезны, никто не будет «заучивать» их наизусть, без понимания почему так.
В этом случае каждое решение грозит превратиться в отдельную статью, что повысит трудоемкость для меня и ухудшит усвояемость для читателей.
Я не стремился подробно осветить каждый нюанс каждого частного случая. Моей задачей было дать обзор типовых проверенных решений и высокоуровневые критерии для их выбора. Для тех, кто хочет узнать больше о конкретном варианте — в тексте есть ссылки.
А вот вы говорите что поле не должно быть null. Получается так что для того чтобы добавить поле субд должно будет ставить этому поле значение по умолчанию? В таком случае справится ли СУБД?
Больше все го в действительно напрягает что нужно все время делать COALESCE на проверках в СУБД.
Но с другой стороны для поля бит мы должны будем инициализировать либо 0 или 1, для поля интегер 0, для пустой строки мы должны будем проинициализировать \0 (нулевым символом). То есть null обусловлен тем что многие субд немогут просто так взять и выкинуть NULL, Потому что при ALTER новый столбец при не NULL порождает дорогостоящую операцию для инициализации 59 миллионов UPDATE.
А самая тормозну тая операция в СУБД это как раз таки UPDATE!
NULL необходим в СУБД. По крайне мере в RDBMS. И условность это больше техническая нежели концептуальная.
И по большей части зависит от того как это реализуется в конкретной СУБД.
Проблема не в том, есть null или нет, а в том что
- null допускают все типы полей в БД
- Операции с null ведут к ошибкам при исполнении а не при компиляции запроса.
Если бы значения integer null и integer not null были бы
- разных типов,
- не совместимых между собой без явного coalesce
- с доступом к большинству операций только для 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 ссылки имеют смысл, когда объекты передаются по значению (в C# это была бы структура). Это иная парадигма, и там нет проблемы нулевых указателей. Как и при передаче интерфейсов по указателю нет проблемы rvalue ссылок, т.к. указатели тривиально копируются.
Кстати, парадигма передачи объектов по значению, если она реализуется последовательно, — тоже, пожалуй, один из ответов на проблему null.
Вопрос константности в данном случае продиктован требованиями функции. Для не изменяющей объект функции ссылка, конечно, будет константной.
На предвосхищение ООП через 20 лет не претендую
это пять ))
Самым обыкновенным — если тип не допускает null, то его значения проверять на null не нужно. Совсем как в котлине сделать не выйдет — обратная совместимость нужна.
В котлине есть ссылочные типы, не допускающие null. Им проверки на null не нужны по построению.
Вы спрашивали, избавит ли в C# от проверок добавка фичи из котлина и каким образом.
Да, от части проверок избавит — ссылочные типы без null соответсвуют решению 12 из статьи и позволяют не проверять их значения на null по построению.
Смысл обсуждать котлин в теме про C# простой:
- Ссылочные типы без null всегда обсуждаются при разработке новых версий C#.
- Котлин свободен от NRE, кроме случаев взаимодействия с внешним не-котлин кодом.
- У JVM и Java проблема с null в точности та же что и в .NET и C#
- У C# в сравнении с котлином есть дополнительное обременение в виде совместимости с предыдущими версиями языка.
Смысл в том, что по ссылке всегда объект, и с ней можно работать не опасаясь NRE. Если ссылка позволяет null, то компилятор должен выдать ошибку при обращении к объекту, если нет явной проверки на null. Пустой объект не создается, так как корректность использования ссылок проверяется на этапе компиляции.
Вы можете попробовать читать именно то что написано.
"Смысл в том, что по ссылке всегда объект" — исходя из Ваших же слов, именно тут и будет создан пустой объект.
Это не чьи-то слова — это ваши собственные идеи.
- Совсем не факт что будет использован паттерн null object — у него есть существенные ограничения.
- Когда null object все-таки применяется — нейтральный объект обычно создается статически и переиспользуется многократно.
Например, метод FindPerson вместо null (всеми почему-то ненавистного) вернет «пустой» объект, потому что null вернуть он не может.
- Вы статью вообще прочитали? Недостатки реализации null в C# расписаны еще до ката.
- С чего вы взяли что FindPerson должен использовать именно null object? Исключение при неудачном поиске бросить не судьба?
- Позволяющих null ссылок заведомо меньше, чем их общее число, а на практике от силы процентов 5. Кроме того, при наличии ссылок без поддержки null проверку для каждого случая появления null достаточно сделать лишь однажды.
Лично для меня в такой логике ничего полезного нет, проверки на null остались, плюс добавились абсолютно бесполезные «пустые» объекты.
Вы сами предложили такую логику, не надо приписывать ее кому-то еще.
Чем пустой объект отличается от 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 }
Не избавляет, а наоборот, вынуждает писать проверки по месту использования ссылки.
- Зачем нужен "пустой" объект и что с ним дальше делать? Проще не создавать.
P.S. Мы же 12-й пункт обсуждаем?
по поводу Вашего пункта 2 — выше Bonart написал, что код необходимо избавлять от «кучи шаблонного кода», но с таким подходом шаблонный код никуда не девается.Много проверок пишете для проверки int на NULL? Ведь для типов, не содержащих NULL «шаблонный код никуда деться не может».
Если метод описан, и указано, что он может возвращать null, то в этом нет ничего плохого.
Не знаю как у вас в 90х, а в 2016 принято кидать эксепшн, а не возвращать код ошибки (в данном случае null это флаг, что нинашла).
И вот эти лишние проверки, которых вполне не было бы если б нулл вообще не мог прийти занимают по статистике 20% кода (источник не приведу — уже не помню где видел).
Когда я создаю новый метод, то всегда пишу к нему xml комментарии, где описываю сам метод, его аргументы и возвращаемое значение. И если бы мы с вами работали вместе, то у вас не возникло бы ни единого вопроса (это конечно только если вы читаете описание методов, которые вызываете).
Ну то есть так и есть, то что должен делать компилятор вы перекладываете на человека. «Не прочитал XML комментарий — получи эксепшн, так тебе и надо». Почему бы проверкой не заняться компилятору? Ведь если я не прочитал комментарий и не сделал проверку — то я не прав? Почему нет ОБЯЗАТЕЛЬСТВА сделать проверку? Это ведь легко сделать. Например с тем же int? у меня просто нет другого способа передать его в другой метод, который требует число, кроме как вначале проверить на HasValue (ну или свалиться при попытке получить значение).
Но конечно же лучше отказаться от всего этого, ведь можно написать xml-комментарий. А еще можно всегда везде возвращать dynamic, а в XML комментарии писать, что за тип на самом деле пришел. Вуух, какая гибкость! А кто не прочитал, тот сам себе злобный буратино.
«Референсные типы» для того и reference types, что бы не содержать значение, а только на него указывать.Верно, это означает исключительно семантику оператора «присвоить». Соответственно
А пока объект ссылочного типа не указывает на значение в куче, то он равен null, по моему это более чем логично.нифига не так. Ссылка всегда должна указывать на существующий объект. Собственно в том же котлине это и сделано, афайк.
Это как бы вы выстрелили себе в ногу, и жаловались на то, что револьвер сделан неправильно, ведь это мелочи, что инструкцию вы не прочитали та и вообще использовали его неправильно.
Нет, это у вас чуть что не так — пользователь виноват.
Вы можете 100 раз ему сказать, что он должен был прочитать документацию, посидеть с бубном и т.п., но если программа сделала не то, что ожидал пользователь — то виноват разработчик. Можете потом сидеть доказывать ему в спину, что он должен был прочитать 200-страничный мануал, а человек просто уйдет к конкурентам, у которых перед тем как дропнуть все данные выводится окошко с подтверждением, а не подход «сам виноват».
Кстати, если вам так действительно не хватает данной «фичи», то предложите ее реализовать (или проголосуйте за реализацию, если такая уже есть) для разработчиков самого языка. А то пока вы будете придумывать очередной пример с dynamic, то «зарелизят» следующую версию.Вы за меня не переживайте, такая фича уже заявлена, и я заявку давно заапрувил.
Ну и вы не ответили на вопрос, чем плохо переложить на компилятор проверку этих XML-комментариев?
Например int не должен быть null, для возможности вернуть такое значение его явно нужно обернуть в int?.. Что и нужно было изначально сделать для референсных типовЯ бы хотел использовать такой язык. Но пока не понимаю, как будет выглядеть default-значение. Например, если
Document
— не-null тип, то чему равен default(Document)
?Например,
default(bool)==false
, а default(int)==0
.Этим значением заполняются поля классов и структур, сразу после их создания.
Или not-null ссылки допустимы только в локальных переменных и в возвращаемых из функций значениях?
amironov, по поводу Вашего пункта 2 — выше Bonart написал, что код необходимо избавлять от «кучи шаблонного кода», но с таким подходом шаблонный код никуда не девается.
Еще раз: когда гарантия not null есть — избавляет от проверок, когда нет — обязывет сделать проверку.
В таком случае компилятор заставит писать «шаблонный код» 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); // ошибка компиляции
От if == null вы уходите, только если ссылочный тип не может быть null
Совершенно верно. Заодно компилятор даст по рукам, тому кто попытается null туда запихнуть.
а если может — вы снова напишите шаблонную проверку.
Совершенно верно. Только после первой же успешной проверки у меня будет значение ссылочного типа, не допускающего null и его, в отличие от нынешних, не надо будет проверять по всей глубине стека вызовов. Заодно компилятор даст по рукам тому, кто рискнет пропустить обязательную проверку.
Итоги:
- Полностью исключены проверки с выбросом InvalidArgumentException
- А там где null — не ошибка, наличие проверки проконтролирует компилятор.
- NRE исключено по построению
- PROFIT

Я не понял каким образом разработчики будут вынуждены использовать NOT NULL. Например, найдется человек, который не читает ни xml описания, ни документации (по словам PsyHaSTe это нормально, так как все само должно работать и исправлять ошибки), и вернет по старинке NULL тип вместо NOT NULL. В таком случае компилятор заставит писать «шаблонный код» if == null, от которого в начале это дискуссии пытались уйти.
stalsoft не передергивайте. Я говорил про то, что не надо заставлять читать документацию, когда это не является обязательным. А то так действительно дойдем до венгерской нотации, когда все туда-сюда кидается одним и тем же образом, а программист должен сопоставив кучу фактов правильно угадать, что же это такое.
Можно говорить, что я говнокодер, потому что не читаю описание, но у нас на проекте принято, чтобы человеку не надо было разбираться в том, как реализован метод, по названию и сигнатуре можно получить полную информацию о том, как его использовать.
И я не меняю тему. Просто я знаю, сколько в проектах используются обычные структуры, а сколько — Nullable. Во-первых не очень много. Во-вторых при попытке засунуть null туда, куда нельзя, получите ошибку компиляции. А для референсов это нормально. Все же любят NullReference.
Раз уж я ответил, теперь ответье и вы: действительно вы считаете, что то что компилятор МОЖЕТ проверить ошибки использования нулевой ссылке он НЕ ДОЛЖЕН делать, обойдясь XML-комментариями?
Примечание: как выше я уже сказал, у нас на проекте комментариев практически нет, и не потому, что мы говнокодеры, а потому что мест где комментарии нужны исчезающе мало. С другой стороны верно и обратное, когда проект весь закомментирован хлам, зачастую это означает именно «хмм, потратить время и переделать нормально или написать комментарий и пойти гулять… Да нахрен надо, я и так тут 2 часа над этим методом сижу, лучше напишу коммент».
Кстати, для сравнения, на моей старой работе на проекте было в трекере примерно 10% тасков и 83% багов (ну и другие категории в оставшихся 7%). Там на уровне студии было установлено правило обязательно комментировать публичные метода, классы, и т.п.
На новой работе нет обязательства писать комментарии, в результате примерно 67% тасков и 33% багов.
Проекты были начаты примерно в одно время, +- пара месяцев.
Делайте выводы сами. Например о моей нерепрезентативной выборке :D Но кроме статистики есть еще и голова, и если подумать, можно прийти к таким же выводам.
Что касается второго, XML комментарии действительно очень помогают, но они нужны как раз для тех вещей, которые автоматически не проверяются. Но когда такая вещь появляется, документация с поддержкой со стороны IDE (при автокомплите параметры подписываются и т.п.) весьма удобно, никто с этим не спорит.
А что будет при тесном взаимодействии с любым Java-фреймворком?
На мой взгляд, Func<void> всё-таки лучше чем грустный Action. Хотелось бы, чтобы его можно было использовать более гибко.
Option и Maybe — просто разные названия одного и того же (первое во всех языках, кроме Haskell, второе — в остальных).
Так повелось, что в хаскелле монада уникально идентифицируется типом контейнера. То есть 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 чуть лучше, но тоже ничего не гарантируют. Этим ошибки в коде кардинально отличается от сбоев в окружении, для обхода которых и придуманы ваши пять пунктов.
С диагностикой понятно — она всегда полезна. Анализ логов позволяет выделить часто встречающиеся проблемы и понять приоритет при исправлении. Да и сам поиск ошибок становится проще.
Изоляция полезна всегда, когда у приложения много функций. Как пример — интерактивные приложения вроде 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 разрушит цивилизацию.
"Всего лишь механизм транзакций" — это уже из серии "90% любой крупной программы на C++ представляет собой медленную и глючную реализацию подмножества спецификации Common Lisp" (по памяти).
Если нужны качества, которые может дать только безопасный рантайм — логично использовать готовый и проверенный безопасный рантайм вместо велосипедов. А для некоторых тем вроде криптографии это вообще категорический императив.
1) Количество записей — до миллиона переключений в секунду. И это не только в пике, мы на этой скорости и работать могли.
2) Формат хранения — упакованный (сжатие в 6 раз).
3) Поиск редких событий по всей базе за секунды.
Все это было на 300 Мгц компе (пень II)
Но, самой собой, это все очень не универсально.
Ну а не нравятся ошибки рантайма — пишите свой собственный. В Delphi ошибок в рантайме хватает, в других языках — в общем-то тоже. Исходить нужно из того, что ошибки были, есть и будут и в компиляторе, и в библиотеках и в вашем собственном коде. А не надеяться, что за 10 лет вы найдете ВСЕ ошибки.
Транзакционный доступ — это как раз про отсутствие гонок =)
Транзакции — это абстракция немножко выше уровнем, чем параллельный доступ к кускам памяти. Нужна ещё реализация, которая их обеспечит.
Дедлоки при транзакциях возможны, и за этим проследят супервизоры и отстрелят зависшие.
Гонка — это когда происходят чтения или записи кусков, которые в этот же момент пишут другие, т.е. в реализации транзакций будет баг (блокировку не захватили или барьеры не выставили, или версию забыли поднять в mvcc, а уже пишем в shared-кусок), к примеру.
Они чреваты тем, что вместо целостной структуры будет полная галиматья, где полструктуры обновлены одним потоком, а вторые полструктуры — другим. Это, конечно, можно вычислять какой-то функцией, которая в рантайме будет проверять инварианты, но откуда она возьмёт данные, на которые надо откатиться, и как она найдет виноватых?
У вас была какая-то готовая и надежная реализация транзакций, или пришлось писать самим и вылавливать затейливые баги?
Схема такая. 12 контролеров, в каждом кольцевой буфер событий минут на 20. По читающему треду на контроллер. Потом из читателей данные идут на первый синхронизатор. Он выдает общую очередь событий + срез на последнйи момент времени. это все идет на диагностику и виртуальный контроллер. Потом — второй синхронизатор вместе с результатами диагностики и виртконтроллера. И уже сформированный потом — на тред, ведающий записью.
А уже запись — базировалась на атомарных операциях Windows, включая запись на диск в обход дискового кэша. И транзакционно — работала только запись. То есть прочли с диска одной операцией чтения, добавили данные, записали данные одной операцией в.tmp, потом переименовали старое в .bak, а .tmp в основной файл. Операции переименования в NTFS -атомарны, даже если будет перезагрузка компа во время операции — все равно будет или старое или новое состояние, но не промежуточный вариант.
Так что фактически — мы паразитировали на транзакционности NTFS. Зато — мы были на 100% уверены, что на диск пишутся корректные данные. И могли перезапускать любые нити без потерь данных. Максимум убытка — перезапрос данных заново с контроллера.
А гонки… гонки могли быть в синхронизаторах, в диагностике, в вирт.контролере, но в треде записи на диск — их не было.
Само собой, что очереди передачи данных были хорошо отлажены. Основа — TThreadQueue в нашей собственной реализации.
т.е. в треде записи на диск стоял верификатор данных, который анализировал данные на бред и если что-то не так, то сбрасывал рекурсивно все поломавшиеся кеши между слоями и перезапрашивал данные?
А при перезапуске — любой тред перезапрашивал предыдущие дать данные, начиная грубо говоря с контрольной точки.
Ну и очень много контролей для проверки инвариантов. один ассерт — на 10-20 строк кода (если вне много раз исполняемого цикла).
А между слоями — не кэши, там очереди. Причем очереди — рассчитанные на доступ из разных тредов.
Там всего 130 тысяч строк кода, это и GUI-клиент, и OPC-сервер, и собственно основной сервер. Так что наворочено защит было много. :-)
В 2001 году, оказывается внедрение было. Окончание опытной эксплуатации — 2002ой год. И второе внедрение — в 2005ом.
Ну и в 2017ом хотят модернизацию на другой тип контроллера.
Собственно уникальность там в том, что вроде мы единственные, кто записывает данные с привязкой к номеру скана, то есть синхронно, а все остальные — асинхронно.
В 2005ом я смотрел, что на АНГА. За 3 года — ни минуты простоя по вине службы автоматики. То есть и у нас все хорошо, и заводчане с помощью нашей системы отладили программы управления станом в контролерах. А там — 8 тысяч входов и 2 тысячи выходов.
Таким образом, эта техника позволяет за то же календарное время обнаружить больше ошибок. Ну и исправить их.
Ну в опытной эксплуатации на фоне промышленной все наоборот — надо "ломать и крушить", давая нагрузки и условия заведомо жестче боевых.
Так что на отладку — 2 часа в месяц. На выходе из планово-предупредительного ремонта были 2 часа планового брака. Но там и без нас хватало желающих потестироваться.
Зато с помощью нашего софта умудрились уменьшить время выхода рулона стали с часа до 40 минут. То есть вместо миллиона долларов — полтора. Выигрыш — продукции на полмиллиона в сутки.
А нагрузки мы давали на имитаторах. Работали на скоростях в 100 раз больше реальной пиковой нагрузки.
На самом деле, заказчики просто прикололись. Пик — 10 тысяч записей одновременно. Мы достигли 100 тысяч. А заказчики в шутку попросили миллион. Ну напряглись и сделали им миллион. Потом оказалось, что они просто пошутили. Но — запас карман не тянет.
98 год?
Да, все то, про что я думал как о лучших альтернативах, еще не существовало, даже файберы надо руками писать.
Дикий капитализм — цена разработки системы меньше чем недельная прибыль от ее эксплуатации, офигеть.
Капитализм там до сих пор такой же. За переделку системы под новый контролер мы с трудом доторговались до цены полутора рулонов стали. :-)
Поразило другое. Ежемесячный трехдневный планово-предупредительный ремонт. Вся инженерная команда трое суток не спит. И вот после 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
1) Проверяется, что после ошибки система восстанавливается. если нет — исправляется.
2) Обеспечивается раннее обнаружение ошибок данного класса. То есть на уровне проверок, а не реальной ошибки в коде.
3) Ошибка исправляется.
4) Ведет поиск аналогичных ошибок по всему коду.
В такой трактовке понятно, почему NRE не означает нарушение инвариантов?
```
(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 это однозначная ошибка в коде, работать дальше смысла не имеет
Когда есть выбор между
- "писать код проще" и
- "задешево превратить целый класс ошибок периода выполнения в ошибки периода компиляции"
результат немного предсказуем.
Смысл null object в том, что бы не проверять, что это null. Зачем вы предлагаете его неполноценным делать, а потом проверять?
> Поддержка атрибута AllowNull — с одной стороны это очень хорошо, а с другой — аналогичный атрибут у решарпера другой.
Решарпер настраивается, на сколько я помню.
> С библиотеками, агрессивно использующими null, требуется довольно много ручной работы по добавлению атрибутов AllowNull
> Поддержка отключения проверки для отдельных классов и целых сборок
Очевидно, довольно много ручной работы не требуется, если вынести в отдельную сборку
Как в С# симулировать поведение 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() }
```
Как говорил Хоар про null-ы у указателей — ошибка на миллиард.
Null, великий и ужасный