Я думаю, що короткий підсумок того, чому нульове значення є небажаним, полягає в тому, що безглузді стани не повинні бути представницькими .
Припустимо, я моделюю двері. Він може бути в одному з трьох станів: відкритий, закритий, але розблокований, і закритий і заблокований. Тепер я міг би моделювати це за принципом
class Door
private bool isShut
private bool isLocked
і зрозуміло, як відобразити три мої стани в ці дві булеві змінні. Але це залишає четверте, небажаний стан є: isShut==false && isLocked==true
. Оскільки типи, які я вибрав як своє представлення, визнають цей стан, я повинен витратити ментальні зусилля на те, щоб клас ніколи не потрапляв у цей стан (можливо, чітко кодуючи інваріант). На відміну від цього, якби я використовував мову з алгебраїчними типами даних або перевіряв перерахування, що дозволяє мені визначати
type DoorState =
| Open | ShutAndUnlocked | ShutAndLocked
тоді я міг би визначитись
class Door
private DoorState state
і турбот більше немає. Система типів забезпечить наявність лише трьох можливих станів для екземпляра, який class Door
має бути. Це те, в чому типові системи хороші - явно виключаючи цілий клас помилок під час компіляції.
Проблема null
полягає в тому, що кожен тип опор отримує цей додатковий стан у своєму просторі, який зазвичай небажаний. string
Змінна може бути будь-яка послідовність символів, або це може бути цей божевільний додаткове null
значення , яке не відображає моєї проблемної області. Triangle
Об'єкт має три Point
с, які самі по собі мають X
і Y
цінність, але , на жаль Point
s або Triangle
самі по собі може бути божевільним нульовим значення , що НЕ має сенс для побудови графіків області я працюю в. Etc.
Якщо ви маєте намір моделювати можливе неіснуюче значення, тоді слід чітко ввімкнути його. Якщо я маю намір моделювати людей, це те, що у кожного Person
є FirstName
а LastName
, а у деяких людей тільки MiddleName
s, то я хотів би сказати щось на зразок
class Person
private string FirstName
private Option<string> MiddleName
private string LastName
де string
тут передбачається ненульовий тип. Тоді немає складних інваріантів для встановлення, і немає несподіваних NullReferenceException
s, коли намагаються обчислити довжину чийого імені. Система типу гарантує, що будь-який код, який має MiddleName
облік з обліковими записами, за можливістю його існування None
, тоді як будь-який код, що має справу з FirstName
може, безпечно припустити, що там є значення.
Так, наприклад, використовуючи тип, описаний вище, ми могли б написати цю нерозумну функцію:
let TotalNumCharsInPersonsName(p:Person) =
let middleLen = match p.MiddleName with
| None -> 0
| Some(s) -> s.Length
p.FirstName.Length + middleLen + p.LastName.Length
без турбот. На відміну від цього, в мові з нульовими посиланнями на такі типи, як рядок, то припущення
class Person
private string FirstName
private string MiddleName
private string LastName
ви закінчуєте авторські речі на кшталт
let TotalNumCharsInPersonsName(p:Person) =
p.FirstName.Length + p.MiddleName.Length + p.LastName.Length
який вибухає, якщо об'єкт, що надходить, не має інваріанта всього того, що є недійсним, або
let TotalNumCharsInPersonsName(p:Person) =
(if p.FirstName=null then 0 else p.FirstName.Length)
+ (if p.MiddleName=null then 0 else p.MiddleName.Length)
+ (if p.LastName=null then 0 else p.LastName.Length)
або можливо
let TotalNumCharsInPersonsName(p:Person) =
p.FirstName.Length
+ (if p.MiddleName=null then 0 else p.MiddleName.Length)
+ p.LastName.Length
якщо припустити, що p
гарантує, що перші / останні є, але середина може бути нульовою, або, можливо, ви робите перевірки, які викидають різні типи винятків, або хто знає, що. Всі ці божевільні варіанти впровадження та речі, про які варто задуматися, тому що є ця дурна репрезентативна цінність, яка вам не потрібна чи потрібна.
Null зазвичай додає непотрібної складності. Складність - ворог усього програмного забезпечення, і вам слід прагнути до зниження складності, коли це є розумним.
(Зверніть увагу, що навіть у цих простих прикладах є складніше. Навіть якщо цього FirstName
не може бути null
, а string
може представляти ""
(порожня рядок), що, ймовірно, також не є ім'ям людини, яке ми маємо намір моделювати. Як такий, навіть із не- незмінні рядки, все ж може бути так, що ми "представляємо безглузді значення". Знову ж, ви можете вирішити боротьбу з цим або через інваріанти та умовний код під час виконання, або використовуючи систему типу (наприклад, щоб мати NonEmptyString
тип). останнє, можливо, є необдуманим ("хороші" типи часто "закриваються" над набором загальних операцій, наприклад NonEmptyString
, не закриваються над.SubString(0,0)
), але це демонструє більше балів у дизайнерському просторі. Зрештою, у будь-якій системі типів є деякі складності, від яких буде дуже добре позбутися, та інші складності, позбутися яких по суті важче. Ключовим моментом цієї теми є те, що майже в кожній системі типів перехід від "нульових посилань за замовчуванням" на "нерегульованих посилань за замовчуванням" майже завжди є простою зміною, яка робить систему типів значно кращою у складності боротьби та виключаючи певні типи помилок і безглузді стани. Тож досить божевільно, що так багато мов постійно і знову повторюють цю помилку.)