Null на языке C# Печать
Добавил(а) microsin   

Ключевое слово null на C# означает "no object" (нет никакого объекта). Однако не все так просто, как кажется на первый взгляд. Здесь мы рассмотрим некоторую информацию и факты, касающиеся литерала null (перевод статьи [1]). Для null есть несколько специальных правил языка C#.

Null представлен числовым значением 0, но Вы не можете в своих программах использовать 0 вместо null. Значение null к некоторым выбрасываемым исключениям (exceptions [2]). Чтобы предотвратить такие проблемы, мы используем операторы if, чтобы проверить существование объекта (если объект существует, то он != null).

Ниже показан пример программы, назначающей литерал null ссылке на объект (также показано, как эта программа представлена на промежуточном языке IL (IL расшифровывается как Intermediate language) - здесь литерал null используется с инструкцией ldnull). Вы можете использовать null с любым ссылочным типом. Это включает стоки, массивы, типы наподобие StringBuilder, или пользовательские типы.

using System;
 
class Program
{
   static void Main()
   {
      object value = new object();
      value = null;
      Console.WriteLine(value);
   }
}

Промежуточный язык этой программы:

.method private hidebysig static void Main() cil managed
{
   .entrypoint
   .maxstack 1
   .locals init (       [0] object 'value')
   L_0000: newobj instance void [mscorlib]System.Object::.ctor()
   L_0005: stloc.0
   L_0006: ldnull
   L_0007: stloc.0
   L_0008: ldloc.0
   L_0009: call void [mscorlib]System.Console::WriteLine(object)
   L_000e: ret
}

Литерал null на языке C# не то же самое, что константа 0. Компилятор гарантирует, что эти два значения не будут использоваться в неправильном контексте. На IL идентификатор null также обрабатывается отдельно, особым образом. Однако null равен нулю на машинном уровне команд, в соответствии с документом "Expert .NET 2.0 IL Assembler".

Что происходит, когда Вы присваиваете литерал null ссылке на объект? Здесь нет никакого выделения null-объекта, вместо этого значение 0 копируется в ссылку на объект.

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

Вы можете присвоить литерал null ссылке на массив. Когда Вы затем попытаетесь получить доступ к элементу этого массива, то получите исключение NullReferenceException. То же самое произойдет и с объектом - когда ссылке на него присвоено null, то попытка обратиться к свойству или методу по этой ссылке на объект также приведет к NullReferenceException.

Массив может быть null. Массив по умолчанию инициализирован null. Это должно быть явно указано в локальных переменных. Язык C# инициализирует элементы массива ссылок в null, когда такой массив создается с помощью ключевого слова new.

Пример ниже показывает статический массив (такой как int[], который является полем у типа), который по умолчанию инициализируется null - даже если Вы явно этого не указываете. Тот же принцип сохраняется, если Вы используете экземпляры объекта какого-либо типа. Пример демонстрирует разницу между пустым массивом, в котором 0 элементов, и ссылкой на массив, которая равна null. Это также показывает результат выражения по умолчанию (default expression) для типа массива.

// Программа C#, которая использует null-ссылки на массив.
using System;
 
class Program
{
   static int[] _arrayField1 = new int[0];      // Пустой массив
   static int[] _arrayField2;                   // Null
 
   static void Main()
   {
      // Показывает, что пустой массив и null-массив это две большие разницы:
      int[] array1 = new int[0];
      Console.WriteLine(array1 == null);
 
      // Показывает, как инициализировать массив значением null.
      int[] array2 = null;
      Console.WriteLine(array2 == null);
 
      // Статические поля массивов и поля массивов экземпляра класса
      // автоматически инициализируются в null.
      Console.WriteLine(_arrayField1 == null);  // Пустой массив
      Console.WriteLine(_arrayField2 == null);  // Null
 
      // Значение по умолчанию выражения (default expression) для массивов
      // вычисляется как null.
      Console.WriteLine(default(int[]) == null);
   }
}

Вывод:

False
True
False
True
True

Локальные переменные в среде .NET сохраняются как отдельная часть метаданных. Они не будут неявно инициализированы в null. Поля, имеющие ссылочный тип, такой как тип массива наподобие int[], неявно присваивается в null. Следующий пример покажет, что выражение default(int[]) равно null.

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

Совет: никогда не стоит делать цикл по массиву при его выделении с целью обнулить все его ссылочные элементы в null. Это действие не имеет смысла, потому что автоматически, неявно делается системой CLR (common language runtime).

// Программа C#, которая проверяет значения по умолчанию.
using System;
 
class Program
{
   static void Main()
   {
      // Все элементы ссылочного типа в новом массиве уже равны null.
      string[] array = new string[3];
      Console.WriteLine(array[0] == null);
      Console.WriteLine(array[1] == null);
      Console.WriteLine(array[2] == null);
   }
}

Вывод:

True
True
True

Эта программа выделяет массив строк из трех элементов. Эти элементы автоматически инициализируются в null выполняющимся кодом (runtime), и мы можем проверить эти элементы на равенство null. Попытка использование одного из этих элементов массива приведет к ошибке/исключению NullReferenceException.

В отличие от ссылочного массива, в новом массиве значений каждый элемент имеет свою закрепленную ячейку памяти, где все биты ячейки изначально установлены в 0. 64-битное целое число имеет 64 таких обнуленных бит. 4-битная структура (такая как тип байта) имеет 4 нулевых бита. Эти значения представляются десятичной цифрой 0.

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

Общие выводы: мы протестировали ссылки на массив на равенство значению null. Ссылочные элементы массива инициализируются в null при создании массива. Поля в виде статического массива и поля массива экземпляра класса неявно инициализируются в null. Выражение default() определяет значение по умолчанию для типа.

Ошибка с выбрасыванием исключения NullReferenceException встречается очень часто. Она показывает, что Вы попытались получить доступ к полям экземпляра, или функциям типа, или к ссылке на объект, которые указывают на null. Это указывает на дефект в коде.

Рассмотрим пример короткой программы, которая вызывает генерацию этого исключения. Эта программа явно назначает литерал null переменной ссылки на строку. Такое назначение означает, что переменная строки не указывает ни на какой объект, выделенный в обслуживаемой куче (managed heap). Это эквивалентно null-указателю.

// Программа C#, которая вызывает NullReferenceException.
using System;
 
class Program
{
   static void Main()
   {
      string value = null;
      if (value.Length == 0)        // Эта строка приведет к runtime-ошибке
      {
         Console.WriteLine(value);  // Сюда из-за ошибки выполнение не дойдет
      }
   }
}

Вывод:

Unhandled Exception:
System.NullReferenceException: Object reference not set to an instance of an object.
at Program.Main() in C:\Users\...

Эта программа определяет главную точку входа в процедуре Main. Переменной ссылки на строк назначается значение литерала null. Далее происходит попытка обращения к свойству Length этой строковой ссылки. Программа скомпилируется корректно, однако при выполнении будет выброшено исключение (throw Exception). Вы не можете обратиться к свойству экземпляра наподобие свойства Length, основываясь на null-ссылке. Это всегда будет приводить к ошибке.

В следующем примере показан способ предотвращения исключения из-за null-ссылки. Часто в программах методы принимают в качестве параметра значение ссылки. Эти параметры содержат указатель на хранилище объекта в управляемой куче. Однако если такой параметр указывает на null, то компилятор не узнает об этом в момент компиляции. Вы должны сами проверить параметр на null в начале метода.

// Программа C#, которая делает проверку параметра в методе на null.
using System;
 
class Program
{
   static void Main()
   {
      // Создание массива и использование его в методе.
      int[] array = new int[2];
      array[0] = 1;
      array[1] = 2;
      Test(array);
 
      // Использование null-ссылки для параметра метода.
      array = null;
      Test(array);      // Это не приведет к сбою программы
   }
 
   static void Test(int[] array)
   {
      if (array == null)
      {
         // Здесь Вы можете выбросить исключение, или предпринять
         // какие-то особые действия.
         return;
      }
      int rank = array.Rank;
      Console.WriteLine(rank);
   }
}

Обработка исключений это сложная тема. У каждого разработчика тут своя стратегия. Однако в большинстве случаев лучшим решением будет записывать в лог любые произошедшие исключения, которые могут возникнуть во время нормальной работы программы [2]. Во многих программах Вы можете использовать ArgumentNullException для оповещения вызывающего кода об ошибках.

Обработка null-ссылок. Если у Вас есть полный контроль над кодом, вызывающим методы, и он никогда не вызывает методы с null-параметром, то можно не проверять в методах параметр на равенство null. Но для библиотек и кода API, который будут возможно использовать другие пользователи, более тщательная проверка параметров будет лучшим выбором. 

Коды операций. Компания Microsoft перечислила коды операций (opcodes), которые могут привести к NullReferenceException при определенных условиях. Обычно проще всего отлаживать код по шагам в среде Visual Studio, однако просмотр кода на уровне кодов операций IL дает большее понимание функционирования кода. Подробнее см. NullReferenceException: MSDN (NullReferenceException Class site:msdn.microsoft.com).

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

// Программа C#, которая присвоила массиву null, и выполнила
// операцию, которая привела к исключению.
using System;
 
class Program
{
   static void Main()
   {
      string[] array = { "a", "b", "c" };
      array = null;
      int value = array.Length;
      Console.WriteLine(value);
   }
}

Результат:

Unhandled Exception: System.NullReferenceException:
   Object reference not set to an instance of an object.
   at Program.Main() in C:\Users\...\Program.cs:line 9

Другой пример программы, которая присвоила массиву null, но с помощью проверки избежала появления исключения NullReferenceException:

using System;
 
class Program
{
   static void Main()
   {
      string[] array = { "a", "b", "c" };
      array = null;
      if (array != null)
      {
         int value = array.Length;
         Console.WriteLine(value);
      }
   }
}

Строки. Из-за того, что строки это ссылочный тип, их идентификаторы тоже могут быть равны null. Мы часто хотим защитить свою программу от ошибок, связанных с null-строками. Это можно сделать с помощью методов string.IsNullOrEmpty или string.IsNullOrWhiteSpace. Также можно просто проверить строку на равенство литералу null. Достоинство этого метода в том, что она работает быстрее, если не требуется проверка на длину строки.

Null-строки добавляют нам сложностей. Мы часто встречается с идентификаторами строк (ссылками на строки), которые равны, или не инициализированы. Строки автоматически инициализируются в null на уровне класса (статического или экземпляра класса), однако для использования этих строк мы должны в методах назначить им значения.

Совет: один из популярных методов проверять строки на null это метод string.IsNullOrEmpty.

Рассмотрим пример. Как мы уже говорили, на уровне класса ссылочные переменные инициализируются в null. Строки как раз и являются ссылочными типами - они не содержат значения символов текста в переменной. Вместо этого переменная строки указывает на область памяти в куче, которая содержит данные строки.

// Программа C#, использующая null-строку.
using System;
 
class Program
{
   static string _test1;
 
   static void Main()
   {
      // Экземпляры строк при создании автоматически инициализируются в null.
      if (_test1 == null)
      {
         Console.WriteLine("String is null");
      }
 
      string test2;
 
      // // Использование не назначенной локальной переменной test2:
      // if (test2 == null)
      // {
      // }
   }
}

Вывод:

String is null

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

Далее в программе есть закомментированная часть, которая покажет ошибку при компиляции (compile-time error), если Вы если Вы не назначите строке литерал null, когда будете использовать эту строку в методе. Этот процесс называется "хорошо определенные переменные" (definite assignment analysis), и он предотвращает от появления многих ошибок. Этот пример показывает, что локальные переменные, в отличие от статических переменных и переменных экземпляра класса (сейчас нас интересуют переменные строк), обрабатываются в языке C# по-разному. Здесь видно, что на C# можно использовать не назначенную переменную класса (компилятор этот код пропустит), а не назначенную локальную переменную использовать физически нельзя (компилятор выдаст ошибку).

Length (длина строки). Одна из общих проблем с null-строками состоит в попытке получения их длины через свойство Length. Это приведет к ошибки времени выполнения NullReferenceException [2].

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

Некоторые шаблоны (patterns) призывают устранять использование такого понятия, как null. Например, мы можем использовать Null Object, который в действительности не null, но рассматривается похожим способом. Это может улучшить некоторый код.

Действительно ли ничего (null) представляет из себя что-то? На этот вопрос трудно ответить. Понятие null - важная концепция императивных языков программирования. Мы рассмотрели здесь некоторые низкоуровневые детали, касающиеся работы null на C#.

[Ссылки]

1. Null. This keyword means "no object" site:dotnetperls.com.
2. Как лучше всего обрабатывать исключения на C#.