Null, великий і жахливий

Помилка дизайну
Саме так і ніяк інакше: null в C# — однозначно помилкове рішення, бездумно скопійоване з більш ранніх мов.
  1. найстрашніше: значення будь-якого посилального типу може використовуватися універсальний зрадник — null, на якого ніяк не зреагує компілятор. Зате під час виконання легко отримати ніж у спину — NullReferenceException. Обробляти це виняток марно: воно означає безумовну помилку в коді.
  2. Перець на рану: збій (NRE при спробі розіменування) може знаходиться дуже далеко від дефекту (використання null там, де чекають повноцінний об'єкт).
  3. Вгодований хутровий звір: null невиліковний — ніякі майбутні нововведення в платформі і мовою не позбавлять нас від прокаженого успадкованого коду, який фізично неможливо перестати використовувати.
Цей ящик Пандори відкрито ще при створенні мови ALGOL W великим Хоаром, який пізніше назвав власну ідею помилкою на мільярд доларів.
Краща історична альтернатива
Зрозуміло, вона була, причому очевидна за сучасними мірками
  1. Уніфікований Nullable для значущих і посилальних типів.
  2. Розіменування Nullable тільки через спеціальні оператори (тернарний — ?:, Елвіса — ?., coalesce — ??), що передбачають обов'язкову обробку обох варіантів (наявність або відсутність об'єкта) без викидання винятків.
найтрагічніше, що все це не було одкровенням і навіть новинкою вже до моменту проектування першої версії мови. На жаль, тоді досвідчених функциональщиков в команді Хейлсберга не було.
Ліки для поточної реальності
Хоча прогноз дуже серйозний, летального результату можна уникнути за рахунок застосування різних практик та інструментів. Способи і їх особливості пронумеровані для зручності посилань.
  1. Явні перевірки на значення null в операторі if. Дуже прямолінійний спосіб з масою серйозних недоліків.
    1. Гігантська маса шумового коду, єдине призначення якого — викинути виняток ближче до місця зради.
    2. Основний сценарій, захаращений перевірками, читається погано
    3. Необхідну перевірку легко пропустити або полінуватися написати
    4. Перевірки можна додавати аж ніяк не всюди (наприклад, це можна зробити для автосвойств)
    5. Перевірки не безкоштовні під час виконання.
  2. Атрибут NotNull. Трохи спрощує використання явних перевірок
    1. Дозволяє використовувати статичний аналіз
    2. Підтримується R#
    3. Вимагає додавання неабиякої кількості швидше шкідливого, ніж непотрібного коду: в левовій частці варіантів використання null неприпустимий, а значить атрибут доведеться додавати буквально скрізь.
  3. Шаблон проектування Null object. Дуже хороший спосіб, але з обмеженою сферою застосування.
    1. Дозволяє не використовувати перевірок на null там, де існує еквівалент нуля у вигляді об'єкта: порожній IEnumerable, масив пустий, порожній рядок, ордер з нульовою сумою і т. п. Саме вражаюче застосування — автоматична реалізація інтерфейсів в мок-бібліотеках.
    2. Даремний в інших ситуація: як тільки вам потрібно відрізняти в коді нульовий об'єкт від інших — ви маєте еквівалент null замість null object, що є вже подвійним зрадою: неповноцінний об'єкт, який навіть NRE не викидає.
  4. Конвенція про повернення живих об'єктів за замовчуванням. Дуже просто і ефективно.
    1. Будь-який метод або властивість, для яких явно не заявлена можливість повертати null, повинні завжди надавати повноцінний об'єкт. Для підтримки досить вироблення гарної звички, наприклад, за допомогою рев'ю коду.
    2. Розробники сторонніх бібліотек нічого про ваше угода не знають
    3. Порушення угоди виявити непросто.
  5. Конвенція про стандартні способи явно вказати властивість або метод може повернути null: наприклад, префікс Try або суфікс OrDefault в імені методу. Органічне доповнення до повернення повноцінних об'єктів за замовчуванням. Достоїнства і недоліки ті ж.
  6. Атрибут CanBeNull. Добрий антипод-близнюк атрибута NotNull.
    1. Підтримується R#
    2. Дозволяє позначати явно небезпечні місця, замість масової розмітки по площах як NotNull
    3. Незручний у разі коли повертається null часто.
  7. Оператори C# (тернарний, Елвіса, coalesce)
    1. Дозволяють елегантно і лаконічно організувати перевірку та обробку null значень без втрати прозорості основного сценарію обробки.
    2. Практично не спрощують викид ArgumentException при передачі null значення NotNull параметра.
    3. Покривають лише деяку частину варіантів використання.
    4. Інші недоліки ті ж, що і у перевірок в лоб.
  8. Тип Optional. Дозволяє явно підтримати відсутність об'єкта.
    1. Можна повністю виключити NRE
    2. Можна гарантувати наявність обробки обох основних варіантів на етапі компіляції.
    3. Проти легасі цей варіант трохи допомагає, вірніше, допомагає небагато.
    4. Під час виконання крім додаткових інструкцій додається ще й memory traffic
  9. Монада Maybe. LINQ для зручної обробки випадків як наявності, так і відсутності об'єкта.
    1. Поєднує елегантність коду з повнотою покриття варіантів використання.
    2. У поєднанні з типом Optional дає кумулятивний ефект.
    3. Налагодження утруднена, так як з точки зору відладчика весь ланцюжок викликів є одним рядком.
    4. Легасі залишається ахіллесовою п'ятою.
  10. Програмування за контрактом.
    1. В теорії майже ідеал, на практиці все набагато сумніше.
    2. Бібліотека Code Contracts швидше мертва, ніж жива.
    3. Дуже сильне уповільнення складання, аж до неможливості використовувати в циклі редагування-компіляція-налагодження.
  11. Пакет Fody/NullGuard. Автоматичні перевірки на значення null на стероїдах.
    1. Перевіряється все: передача параметрів, запис, читання і повернення значень, навіть автосвойста.
    2. Ніякого оверхеда у вихідному коді
    3. Ніяких випадкових пропусків перевірок
    4. Підтримка атрибуту AllowNull — з одного боку це дуже добре, а з іншого — аналогічний атрибут у решарпера інший.
    5. З бібліотеками, агресивно використовують null, потрібно досить багато ручної роботи за додавання атрибутів AllowNull
    6. Підтримка відключення перевірки для окремих класів цілих збірок
    7. Використовується вплетення коду після компіляції, але час складання росте помірно.
    8. Самі перевірки працюють тільки під час виконання.
    9. Гарантується викид виключення максимально близько до дефекту (поверненню null туди, де очікується реальний об'єкт).
    10. Тотальність перевірок допомагає навіть при роботі з легасі, дозволяючи як можна швидше виявити, позначити і знешкодити навіть null, отриманий з чужого коду.
    11. Якщо відсутність об'єкта допустимо — NullGuard зможе допомогти тільки при спробах передати його куди не слід.
    12. Вичистивши дефекти у тестовій версії, можна зібрати промислову з тих же джерел з відключеними перевірками, отримавши нульову вартість під час виконання при гарантії збереження всієї іншої логіки.
  12. Посилальні типи без можливості присвоєння null (якщо додадуть в одну з майбутніх версій C#)
    1. Перевірки під час компіляції.
    2. Можна повністю ліквідувати NRE в новому коді.
    3. В реальності не реалізовано, сподіваюся, що тільки поки
    4. Однаковості зі значимими типами не буде.
    5. Легасі дістане і тут.
Підсумки
Буду лаконічним — всі висновки в таблиці:




Наполеглива рекомендація Антипаттерн На ваш смак і потреби 4, 5, 7, 11, 12 (коли і якщо буде реалізовано) 1, 2 3, 6, 8, 9, 10
На передбачення ООП через 20 років не претендую, але з доповненнями і критиці буду дуже радий.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.