Не смотря на кажущуюся простоту, это очень важная тема. Очень большой процент всех ошибок связан с null.
Значение null в примитивном типе данных
Пусть мы хотим, например, посчитать сумму чисел из списка. Тогда наш код может выглядеть подобным образом:
(Здесь при создании списка мы сразу указываем, какие числа будут в нём лежать.)
Но что делать, если список окажется пуст? То есть, в нём не будет ни одного числа. Тогда сумму подсчитать нельзя, а наш код выдаёт в ответ 0. Нелогично. Для подобных случаев, когда нет каких-либо данных, можно положить в переменную особое значение - null. Значение null показывает, что значения в переменной нет.
При объявлении переменной с var тип данных определится автоматически по правой части присваивания - то есть, переменная sum будет типа int, потому что справа от присваивания стоит 0. В новых версиях C# никакие типы данных не умеют хранить null. То есть, null нельзя положить в переменную типа int. Чтобы разрешить класть туда null, необходимо к типу данных добавить вопросик:
Тогда мы можем написать
или положить туда null позже.
Важно, что при объявлении переменной, допускающей null, не получится использовать var. Если мы напишем конкретное значение для этой переменной, то var автоматически определит тип по значению, и null хранить в нём будет нельзя. А если мы напишем null, то C# не сможет определить тип переменной - это int? / string? / что-то ещё? и выдаст такую ошибку:
Вернёмся к задаче. Нужно рассмотреть два случая: когда в списке есть хотя бы одно число и когда он пуст. Различить это можно по количеству элементов в списке (поле Count):
Тогда при выдаче ответа нужно проверить sum на null:
Ради эксперимента попробуйте посчитать сумму чисел, если изначально в переменной sum будет null. Посмотрите, что получится.
Задача решена.
Значение null в типе данных класса
А что, если нет самого списка?
Тогда нельзя обратиться к полю Count, потому что поля хранятся внутри объекта, а здесь сам объект отсутствует. Посмотрите в отладке, какая ошибка будет в этом случае. Жёлтая подсказка предупреждает нас об этом:
Это одна из самых часто возникающих ошибок в программах, поэтому, чтобы её избежать, нужно тщательно следить за всеми жёлтыми предупреждениями. В старых версиях C#, где null можно было положить почти куда угодно, был просто кошмар.
Чтобы починить эту ошибку, не всегда правильно следовать автоматическим исправлениям, которые предлагает лампочка:
Всегда надо думать. Например, в данном случае не подходит ни один из 4 вариантов исправления, потому что они делают, в том числе, вот это:
В таком случае, если списка нет, то мы проваливаемся в else, где пытаемся пройти по несуществующему списку с помощью foreach (и сталкиваемся с этой ошибкой ещё раз). Это неправильно. Корректным решением было бы в таком случае считать сумму равной null, то есть, объединить случай отсутствия списка со случаем пустого списка:
Nullable-тип
Примитивные (и некоторые другие) типы данных, допускающие null, на самом деле представляют собой типы-обёртки над данными. Например, int? - это на самом деле Nullable<int>. Как следствие, у него есть два поля, которые принадлежат любому объекту типа Nullable<T>:
- Value (текущее значение, имеет тип T)
- HasValue (логический флажок, тип bool, который показывает, есть ли Value, и если его нет, то доступ к Value завершится ошибкой).
Например, проверку значения sum и собственно считывание самого значения можно было вместо вот этого:
написать вот так:
Здесь "!" используется для отрицания логического условия. По сути, мы сначала проверяем, нет ли Value, и если его нет, то печатаем заготовленный текст, а иначе обращаемся к значению Value.
Если мы перепутаем части if между собой, то снова встретимся с жёлтым предупреждением на доступе к Value:
null conditional operator (?.)
Пусть мы хотим обратиться к полю/методу объекта, который может быть null. Например, мы хотим добавить элемент в наш список:
Чтобы не писать бесконечные проверки на null, есть сокращённая форма через null conditional operator (вопросик с точкой):
Такое выражение не обращается к полю/методу, если объект равен null, а просто становится равно null. То есть, если numbers = null, то numbers?.ЧтоТо = null.
Усложним задачу. Пусть теперь этот список есть внутри класса User:
Тогда в файле Program.cs мы можем написать:
Если user = null, то доступ к Numbers и к последующему Count не осуществляется, они безопасны (выделено зелёным). Таким образом, если Numbers не может быть равен null, то второй вопросик с точкой не нужен. Он нужен только на случай Numbers = null.
null-coalescing operators (?? ??=)
Пусть мы хотим проверить ту же user.Numbers.Count, но при этом заменить её на 0 в случае, если там null. Для этого нужен оператор "??". Если левая часть оператора не равна null, то результат равен ей, а если левая часть равна null, то результат равен правой части:
В общем, берётся первое, что не null.
Вопросики с точкой остаются, так как они нужны, чтобы мы не обращались к полям несуществующих объектов. В случае, если user = null, сработает первый "?." и всё жёлтое будет заменено на null. Тогда сработает "??" и результатом будет 0 (зелёное). В случае, если Numbers = null, сработает второй "?." и всё жёлтое также будет заменено на null, а потом на 0. А если оба user и Numbers есть, то мы достанем из них Count, который не null, и результат "??" будет равен ему.
Пусть теперь мы хотим положить в переменную user нового пользователя, если его там ещё нет. Можно было бы написать так:
Но есть сокращённая запись с помощью "??="
Она делает присваивание только в том случае, если в целевой переменной (user) лежит null.
Если в переменной user был объект, то он сохранится, а если было null, то оно перезапишется новым объектом. Таким образом, после этого оператора user уже заведомо не может быть null. Поэтому первый вопросик с точкой больше не нужен (он горит серым).
Вывод
Итак, нужно быть очень внимательным с null. Такие ошибки проявляются только во время работы программы, что плохо. Но нам могут помочь жёлтые предупреждения. А чтобы они всегда были полезны, нужно стараться разгребать все предупреждения, какие есть в коде, чтобы полезные предупреждения не терялись среди висящих там остальных.
Далее
Исключения. try - catch - https://dzen.ru/a/aZNB63fytCO7KPUt?share_to=link
Оглавление - https://dzen.ru/a/aXisxwt_Mnz2qTjs?share_to=link