Введение в программирование на C# 2.0

Шаблоны

Показывать лекцию целиком

Ранее рассмотренные классы ( ArrayList, Queue, Stack ) могут поддерживать коллекции произвольных типов — благодаря тому, что любой класс является производным (либо наследует его) класса object. Именно поэтому и можно организовать коллекцию объектов производного типа. Для стека и очереди это всего лишь экземпляры класса object.

Например, Stack сохраняет свои данные в массиве object, и два его метода, Push и Pop, используют object для приема и возвращения данных:

public class Stack{
 object[] items;
 int count;
 public void Push(object item) {...}
 public object Pop() {...}
 }

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

В коллекции можно разместить объекты произвольного типа. После извлечения объекта из коллекции (например, методом Pop ) полученное значение (ссылка) должна быть явно приведена к соответствующему типу:

Class XXX
 {
 :::::
 }

Stack stack = new Stack();
 stack.Push(new XXX());
 XXX c = (XXX)stack.Pop();

Если значение типа-значения (например, int ) передается в метод Push, оно автоматически упаковывается. При извлечении значения оно должно быть распаковано с явным приведением типа:

Stack stack = new Stack();
 stack.Push(3);
 int i = (int)stack.Pop();

Такие операции упаковки/распаковки увеличивают непроизводительные издержки, поскольку приводят к динамическим перераспределениям памяти и динамическим проверкам типов.

Следующая проблема заключается в невозможности задания типа данных, помещаемых в коллекцию. Объект – представитель класса XXX может быть помещен в стек и после извлечения преобразован в другой тип:

Stack stack = new Stack();
 stack.Push(new XXX());
 string s = (string)stack.Pop();

Приведенный здесь код не вызывает возражений транслятора, хотя и является примером некорректного использования класса Stack.

Проблема проявится лишь во время выполнения кода, о чем станет известно после генерации исключения InvalidCastException.

Таким образом шаблоны позволяют избежать вышеперечисленных трудностей.

Общее представление

В C# можно объявлять шаблоны классов и шаблоны функций. Шаблоны классов и шаблоны функций – две разные вещи, которые в общем случае могут сосуществовать в рамках одного и того же объявления.

Объявление любого шаблона предполагает использование специальных обозначений, которые называются параметрами шаблона. Традиционно для этого применяются однобуквенные обозначения: A, ..., K, L, M, N, ..., T, ..., Z.

При объявлении шаблонного класса или функции параметры шаблона заменяются на конкретные имена классов.

Шаблонный класс – это класс, который строится на основе ранее объявленного шаблона для определенного класса (этот класс мы будем называть подстановочным классом). Шаблонный класс (и функция) обеспечивают стандартную реализацию какой-либо функциональности для подстановочного класса — например, построения специализированной коллекции для множества объектов данного типа.

Параметры шаблонов позволяют параметризовать шаблоны классов, структур, интерфейсов, делегатов и функций классами, для которых эти шаблоны создавались с целью расширения их функциональности.

Ниже приводится пример объявления шаблона класса Stack с параметром типа <T>.

Параметр T в объявлении рассматриваемого шаблонного класса обозначает место подстановки реального типа. Он используется для обозначения типа элемента для внутреннего массива элементов, типа параметра метода Push и типа результата для метода Pop:

public class Stack<T>
 {
 T[] items;
 int count;
 public void Push(T item) {...}
 public T Pop() {...}
 }

Вместо преобразований в и из класса object, параметризованные коллекции ( Stack<T> в том числе) принимают на сохранение значения типа, для которого они созданы, и сохраняют данные этого типа без дополнительных преобразований. При использовании шаблонного класса Stack<T>, вместо T подставляется реальный тип. В следующем примере int задается как аргумент типа (type argument) для T:

Stack<int> stack = new Stack<int>();
 stack.Push(3);
 int x = stack.Pop();

Тип Stack<int> называют составным или шаблонным типом (constructed type). При объявлении этого типа каждое вхождение T заменяется аргументом типа int.

Когда объект – представитель класса Stack<int> создается, массив items оказывается объектом типа int[], а не object[].

Методы Push и Pop класса Stack<int> также оперируют значениями int, при этом помещение в стек значений другого типа приводит к ошибкам компиляции (а не ошибкам времени выполнения).

Также не требуется явного приведения извлеченных из коллекции значений к их исходному типу.

Шаблоны обеспечивают строгий контроль типов. Это означает, например, что помещение int в стек объектов XXX является ошибкой.

Точно так же, как Stack<int> ограничен возможностью работы только со значениями типа int, так и Stack<XXX> ограничен объектами типа XXX.

А компиляция последних двух строк следующего примера выдаст ошибку:

Stack<XXX> stack = new Stack<XXX>();
 stack.Push(new XXX());
 XXX xxx = stack.Pop();
 stack.Push(3);		// Ошибка несоответствия типов
 int x = stack.Pop();	// Ошибка несоответствия типов

Объявление шаблона может содержать любое количество параметров типа. В приведенном выше примере в Stack<T> есть только один параметр типа, но шаблонный класс Dictionary должен иметь два параметра типа: один для подстановочного типа ключей, другой для подстановочного типа значений:

public class Dictionary<K,V>
 {
 public void Add(K key, V value) {...}
 public V this[K key] {...}
 }

При объявлении шаблонного класса на основе шаблона Dictionary<K,V> должны быть заданы два аргумента типа:

Dictionary<string,XXX> dict = new Dictionary<string,XXX>();

 // В словарь добавляется объект типа XXX,
 // проиндексированный строкой x1
dict.Add("x1", new XXX()); 

 // Извлечение из словаря элемента, проиндексированного строкой "x1"
 XXX xxx = dict["x1"];

Пример использования шаблонов: сортировка.

Старая задача, новые решения с использованием предопределенных шаблонов классов и интерфейсов...

using System;
 using System.Collections;
 using System.Collections.Generic;

namespace PatternArrays
 {

 // Данные для массива элементов.
 // Подлежат сортировке в составе шаблонного массива методом Sort.
class Points
 {
 public int x;
 public int y;

public Points(int key1, int key2)
 {
     x = key1;
     y = key2;
 }

 // Вычисляется расстояние от начала координат.
 public int R
 {
     get
    {
         return (int)(Math.Sqrt(x * x + y * y));
     }
 }
 }


 // ...ШАБЛОННЫЙ КОМПАРЕР на основе шаблона интерфейса...   


class myComparer : IComparer<Points>
 {

 // Предлагаемый ШАБЛОННЫЙ метод сравнения возвращает разность расстояний
 // двух точек (вычисляется по теореме Пифагора) от начала координат
 // –  точки с координатами (0,0). Чем ближе точки к началу координат
 // –  тем они меньше. Не требуется никаких явных приведений 
типа.
 // Шаблон настроен на работу с классом Points.


int IComparer<Points>.Compare(Points p1, Points p2)
 {
     return (p1.R - p2.R);
 }
 }
 // После реализации соответствующего ШАБЛОННОГО интерфейса объект-
 // КОМПАРЕР обеспечивает реализацию стандартного алгоритма сортировки.

class Class1
 {
 static void Main(string[] args)
 {
     // Объект - генератор "случайных" чисел. 
    Random rnd = new Random();

    int i;
     // Очередь Points.
     Queue<Points> qP = new Queue<Points>();
     // Шаблонный перечислитель. Предназначен для обслуживания
     // шаблонной очереди элементов класса Points.
     IEnumerator<Points> enP;
     // Сортировка поддерживается классом Array.
     Points[] pp;

    // Создали Компарер, способный сравнивать пары
     // объектов - представителей класса Points.  
    myComparer c = new myComparer();

    Console.WriteLine("========================================");

    // Проинициализировали массив объектов - представителей класса Points. 
    for (i = 0; i < 10; i++)
     {
         qP.Enqueue(new Points(rnd.Next(0, 10), rnd.Next(0, 10)));
     }

    enP = ((IEnumerable<Points>)(qP)).GetEnumerator();
     for (i = 0; enP.MoveNext(); i++)
     {
         Console.WriteLine("{0}: {1},{2}", i, enP.Current.x, enP.Current.y);
     }

    // Сортируются элементы массива типа Points, который формируется на 
   // основе шаблонной очереди.
   // Условием успешной сортировки элементов массива является реализация
   // интерфейса IComparer. Если Компарер не сумеет справиться с
   // поставленной задачей – будет возбуждено исключение.
   // На основе очереди построили массив. 
    pp = qP.ToArray();

    // А саму очередь можно почистить!
     qP.Clear();

    try
    {
         Array.Sort<Points>(pp, c);
     }
     catch (Exception ex)
     {
         Console.WriteLine(ex);
     }

    // Сортировка произведена, очередь восстановлена.
     for (i = 0; i < 10; i++) qP.Enqueue(pp[i]);

    Console.WriteLine("========================================");

    enP = ((IEnumerable<Points>)(qP)).GetEnumerator();
     for (i = 0; enP.MoveNext(); i++)
     {
         Console.WriteLine("{0}: {1},{2}", i, enP.Current.x, enP.Current.y);
     }

    Console.WriteLine("========================================");
 }
 }
 }

Шаблоны классов и функций. Ограничения параметра типа

До настоящего момента обсуждались вопросы построения шаблонных классов на основе предопределенных (включенных в состав библиотеки классов) шаблонов классов. В этом разделе на простом примере обсуждается техника объявления собственных шаблонов классов и шаблонов функций.

Шаблонный класс обеспечивает стандартную реализацию дополнительной функциональности на основе ранее объявленных подстановочных классов.

Эта дополнительная функциональность может накладывать дополнительные ограничения на подстановочный класс. Например, для успешной работы объекта шаблонного класса подстановочный класс должен наследовать определенному интерфейсу. Иначе функциональность шаблонного класса просто невозможно будет реализовать.

Для формирования ограничений на подстановочные классы в C# используется механизм ограничителей параметров шаблона — он вводится при объявлении шаблона с помощью ключевого слова where, за которым могут располагаться имя параметра типа и список типов класса или интерфейса либо конструктор – ограничение new():

using System;
 using System.Collections;
 using System.Collections.Generic;

namespace PatternArrays
 {

 //========== Это заголовок шаблона класса W ==========
 // Шаблон класса своими руками. T – параметр шаблона.
 // Шаблонный класс –  это класс-шаблон, который детализируется
 // подстановочным классом.
 // При создании шаблонного класса вхождения параметра шаблона
 // (в данном случае это T) замещаются именем подстановочного
 // класса. Разработчик шаблона класса может выдвигать требования
 // относительно характеристик подстановочного класса.
 // Для этого используются специальные языковые конструкции,
 // называемые ОГРАНИЧИТЕЛЯМИ ПАРАМЕТРА ШАБЛОНА.
 // ОГРАНИЧИТЕЛЬ ПАРАМЕТРА ШАБЛОНА формулирует требования для
 // подстановочного класса.
 class W<T> where T: IComparable, new()
 //         	 	Ограничитель параметра шаблона new() – особый
 //        		 	ограничитель.
 //        	  		Во-первых, в списке ограничителей шаблона
 //          		он всегда последний.
 //          		Во-вторых, этот ограничитель НЕ ограничивает.
 //             	Он ОБЕСПЕЧИВАЕТ обязательное выполнение явно
 //             	заданного конструктора умолчания для 
 //             	подстановочного класса в шаблонном
 //             	классе. Это единственный способ заставить
 //             	выполниться конструктор умолчания
 //             	подстановочного класса при создании
 //             	объекта шаблонного класса. 
 //             	Ограничитель подстановочного класса.
 //          	Шаблонный класс строится на основе шаблона и множества
 //           	подстановочных классов, которыми замещаются параметры
 //           	шаблона. Таким образом ограничители подстановочного 
 //          	класса формулируют требования по поводу "родословной"
 //           	подстановочного класса.
 //           	В данном случае претендент на замещение параметра T
 //          	в шаблоне W обязательно должен наследовать интерфейс
 //          	IComparable. 
 {

 // Вот место, которое предназначено объекту подстановочного класса.
 // Объект - представитель шаблонного класса включает объект, 
 // представляющий подстановочный класс. 
public T t;

 // Конструктор шаблона.
 // Вот по какой схеме производится встраивание объекта-представителя
 // подстановочного класса. Всего лишь для того, чтобы эта схема
 // построения объекта - представителя шаблонного класса работала, 
 // в объявлении шаблона должен присутствовать ограничитель параметра
 // шаблона new(). Его отсутствие приводит к возникновению ошибки 
 // компиляции. С каких это пор необходимое требование стали 
 // называть ограничением?
  public W()
  {
      t = new T();
  }
  
 // Сравнение объектов в шаблоне. Обращение к функциям сравнения
 // регламентировано стандартными интерфейсами.
 // Полиморфизм через интерфейсы в действии.
  public int wCompare(T t) 
 {
   return ((IComparable)this.t).CompareTo(t);
  }

 // А вот замечательный шаблон функции.
 // Он реализован в рамках класса-шаблона W.
 // Эта функция предназначена для формирования шаблонных очередей
 // из входных массивов объектов - представителей подстановочного
 // класса, представленного параметром шаблона Z.
 // Между прочим, такое обозначение параметра ничуть не хуже любого 
 // другого. Более того, если бы здесь было использовано старое 
 // обозначение параметра, транслятор выступил бы с предупреждением 
 // по поводу того, что две разных сущности (параметр шаблона для 
 // шаблона класса и параметр шаблона для параметра функции) 
 // в рамках одного и того же объявления имеют одинаковые обозначения.
  public void QueueFormer<Z>(Queue<Z> queue, params Z[] values)
  {
      foreach (Z z in values)
      {
          queue.Enqueue(z); 
     }
  }
 }

 //===============================================================
 // Вот классы-кандидаты на подстановку в шаблон.
 // Первый класс подходит, а второй –  не подходит!
 // Все решается на этапе трансляции.
 //===============================================================
  class xPoints : IComparable
 {
     // Объект-генератор "случайных" чисел. 
    static Random rnd = new Random();

    public int x;
     public int y;

    public xPoints()
     {
         x = rnd.Next(0, 100);
         y = rnd.Next(0, 100);
     }

     // Ничто не может помешать классу иметь
      // различные версии конструкторов!
      public xPoints(int x, int y)
      {
          this.x = x;
          this.y = y;
      }
      
    // Вычисляется расстояние от начала координат.
     public int R
    {
         get
        {
             return (int)(Math.Sqrt(x * x + y * y));
         }
     }

    // После реализации соответствующего интерфейса объект-КОМПАРЕР
    // обеспечивает реализацию алгоритма сравнения.

    public int CompareTo(object p)
     {
         return (this.R - ((xPoints)p).R);
     }
 }

class yPoints
 {
     // Объект-генератор "случайных" чисел. 
    static Random rnd = new Random();

    public int x;
     public int y;

    public yPoints()
     {
         x = rnd.Next(0, 100);
         y = rnd.Next(0, 100);
     }

    // Шаблон функции в рамках объявления "обычного" класса.
    // Функция предназначена для формирования шаблонных магазинов
    // из входных массивов объектов - представителей подстановочного
    // класса, представленного параметром шаблона T. 
    public void  StackFormer<T>(Stack<T> stack, params T[] values)
     {
         foreach (T t in values)
         {
             stack.Push(t);
         }
     }
 }

 //==============================================================
 class Class1
 {
     static void Main(string[] args)
     {

        W<xPoints> xw0 = new W<xPoints>();
         W<xPoints> xw1 = new W<xPoints>();

 // Объекты - представители шаблонного класса можно сравнивать 
 // в результате
  // реализации интерфейса IComparable. 
        if (xw0.wCompare(xw1.t) == 0) Console.WriteLine("Yes");
         else                       Console.WriteLine("No");

 // В силу ограничений параметра шаблона T, следующий код 
 // для подстановочного
 // класса в принципе нереализуем.=============================

    //W<yPoints> yw0 = new W<yPoints>();
    //W<yPoints> yw1 = new W<yPoints>();
         
    //if (yw0.ww(yw1.t) == 0) Console.WriteLine("Yes");
    //else                    Console.WriteLine("No");
         //=====================================================

    // Демонстрация использования шаблона функции.
    // На основе подстановочного класса сформировали
    // шаблонную функцию для подстановочного класса,
    // которая обслуживает шаблонные очереди, формируемые на основе
    // предопределенного шаблона класса Queue<...>.
     Queue<xPoints> xpQueue = new Queue<xPoints>();
     xw1.QueueFormer<xPoints>(xpQueue,
                                      new xPoints(0, 9),
                                      new xPoints(1, 8),
                                      new xPoints(2, 7),
                                      new xPoints(3, 6),
                                      new xPoints(4, 5),
                                      new xPoints(5, 4),
                                      new xPoints(6, 3),
                                      new xPoints(7, 2),
                                      new xPoints(8, 1),
                                      new xPoints(9, 0)
                              );

    // Шаблоны классов и шаблоны функций концептуально не связаны.
    // В C# это самостоятельные и независимые конструкции.
    // Шаблон функции может быть объявлен где угодно -
    // в шаблоне класса и в рамках объявления "обычного" класса.
    // При объявлении шаблона функции ограничения на свойства 
    // подстановочного класса, представленного параметром шаблона, 
    // не упоминаются. 
    // В силу ограничений на параметр шаблона класс yPoints в принципе 
    // не может быть использован для построения шаблонного класса
    // на основе шаблона class W<T>.
    // Однако этот же самый класс может быть использован для построения
    // шаблонной функции в шаблонном классе, созданном на основе
    // подстановочного класса xPoints!  
    Queue<yPoints> ypQueue = new Queue<yPoints>();
     xw1.QueueFormer<yPoints>(ypQueue,
                                      new yPoints(),
                                      new yPoints(),
                                      new yPoints(),
                                      new yPoints(),
                                      new yPoints(),
                                      new yPoints(),
                                      new yPoints(),
                                      new yPoints(),
                                      new yPoints(),
                                      new yPoints()
                              );

 // А вот применение шаблона функции, объявленного в "обычном" классе.
 // Создали объект класса, содержащего шаблон функции
 // по обслуживанию очередей. 
yPoints yp = new yPoints();

 // Ссоздали шаблонный стек и воспользовались шаблоном функции, 
 // объявленной yPoints в классе.
 Stack<xPoints> xpStack = new Stack<xPoints>();
 yp.StackFormer<xPoints>(xpStack,
                                   new xPoints(),
                                   new xPoints(),
                                   new xPoints(),
                                   new xPoints(),
                                   new xPoints(),
                                   new xPoints(),
                                   new xPoints(),
                                   new xPoints(),
                                   new xPoints(),
                                   new xPoints()            
                                 );


    }
 }
 }

Пример использования шаблонов: сортировка

Старая задача, новые решения с использованием предопределенных шаблонов классов и интерфейсов...

using System;
using System.Collections;
using System.Collections.Generic;

namespace PatternArrays
{

// Данные для массива элементов.
// Подлежат сортировке в составе шаблонного массива методом Sort.

class Points
{
public int x;
public int y;

public Points(int key1, int key2)
{
     x = key1;
     y = key2;
 }

 // Вычисляется расстояние от начала координат.
 public int R
 {
     get
    {
         return (int)(Math.Sqrt(x * x + y * y));
     }
 }
 }


 // ...ШАБЛОННЫЙ КОМПАРЕР на основе шаблона интерфейса...   


class myComparer : IComparer<Points>
 {
 // Предлагаемый ШАБЛОННЫЙ метод сравнения возвращает разность расстояний
 // двух точек (вычисляется по теореме Пифагора) от начала координат
 // – точки с координатами (0,0). Чем ближе точки к началу координат
 // – тем они меньше. Не требуется никаких явных приведений типа.
 // Шаблон настроен на работу с классом Points.


int IComparer<Points>.Compare(Points p1, Points p2)
 {
     return (p1.R - p2.R);
 }
 }


 // После реализации соответствующего ШАБЛОННОГО интерфейса 
 // объект-КОМПАРЕР обеспечивает реализацию стандартного алгоритма 
 // сортировки.


class Class1
 {
 static void Main(string[] args)
 {
     // Объект-генератор "случайных" чисел. 
    Random rnd = new Random();

    int i;
     // Очередь Points.
     Queue<Points> qP = new Queue<Points>();
     // Шаблонный перечислитель. Предназначен для обслуживания
     // шаблонной очереди элементов класса Points.
     IEnumerator<Points> enP;
     // Сортировка поддерживается классом Array.
     Points[] pp;

    // Создали Компарер, способный сравнивать пары
     // объектов - представителей класса Points.  
    myComparer c = new myComparer();

    Console.WriteLine("========================================");

    // Проинициализировали массив объектов - представителей класса Points. 
    for (i = 0; i < 10; i++)
     {
         qP.Enqueue(new Points(rnd.Next(0, 10), rnd.Next(0, 10)));
     }

    enP = ((IEnumerable<Points>)(qP)).GetEnumerator();
     for (i = 0; enP.MoveNext(); i++)
     {
         Console.WriteLine("{0}: {1},{2}", i, enP.Current.x, enP.Current.y);
     }

    // Сортируются элементы массива типа Points, который формируется на 
    // основе шаблонной очереди.
    // Условием успешной сортировки элементов массива является 
    // реализация интерфейса IComparer. Если Компарер не сумеет 
    // справиться с поставленной задачей – будет возбуждено исключение.
    // На основе очереди построили массив. 

    pp = qP.ToArray();

    // А саму очередь можно почистить!
     qP.Clear();

    try
    {
         Array.Sort<Points>(pp, c);
     }
     catch (Exception ex)
     {
         Console.WriteLine(ex);
     }
    // Сортировка произведена, очередь восстановлена.
     for (i = 0; i < 10; i++) qP.Enqueue(pp[i]);

    Console.WriteLine("========================================");

    enP = ((IEnumerable<Points>)(qP)).GetEnumerator();
     for (i = 0; enP.MoveNext(); i++)
     {
         Console.WriteLine("{0}: {1},{2}", i, enP.Current.x, enP.Current.y);
     }

    Console.WriteLine("========================================");
 }
 }
 }

Nullable-типы

Nullable-типы (простые Nullable-типы) представляют собой расширения простых типов. Их объявления принадлежат пространству имен System.Nullable.

Это шаблонные типы, то есть типы, построенные в результате детализации шаблонов. Шаблон Nullable<> используется для расширения простых типов, которые по своей сути являются структурами. Для обозначения Nullable шаблонных (построенных на основе шаблона) типов используются две нотации:

Nullable<Int32> val; 	// Полная нотация.
 Nullable<int> val; 	// Полная нотация. Еще один вариант.

Int32? val;        	// Сокращенная нотация.
 int?   val;       	// Сокращенная нотация. Еще один вариант.

Шаблон расширяет диапазон возможностей шаблонных типов.

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

Например, переменной типа Nullable<Int32> (следует говорить "Nullable of Int32") может быть присвоено значение в диапазоне от –2147483648 до 2147483647. Этой переменной также может быть присвоено значение null.

У Nullable-типов к тому же шире диапазон функциональных возможностей.

Переменной типа Nullable<bool> могут быть присвоены значения true, false, null.

Возможность присвоения такого значения переменной арифметического или Boolean типа может оказаться полезным при работе с базами данных и разработке типов (классов или структур), содержащих поля, которым может быть НЕ присвоено НИКАКОГО значения.

Пример объявления и применения nullable-значений приводится ниже:

class NullableExample
 {
     static void Main()
     {
 // Вопросительный знак в объявлении - признак nullab'ости переменной.
         int? num = null;
 // Здесь мы обращаемся к свойству, которое проверяет наличие "непустого"
 // значения у переменной num.
         if (num.HasValue == true)
         {
             System.Console.WriteLine("num = " + num.Value);
         }
         else
        {
             System.Console.WriteLine("num = Null");
         }

 // Простой двойник (типа int) - переменная y устанавливается в 0.
 // И это безопасный способ инициализации "простого" двойника. 
       
 int y = num.GetValueOrDefault();

 // Можно, конечно, попытаться присвоить значение "напрямую",
 // однако при этом сохраняется опасность того, что nullable в
 // данный момент (num.Value - свойство, позволяющее получить 
 // значение num) проинициализировано пустым значением. Тип int этого 
 // не вынесет - будет возбуждено исключение. Поэтому присвоение 
 // значения и сопровождается такими предосторожностями. 
        try
        {
             y = num.Value;
         }
         catch (System.InvalidOperationException e)
         {
             System.Console.WriteLine(e.Message);
         }
     }
 }

Результат выполнения программки:

num = Null
Nullable object must have a value.

Обзор Nullable-типов

Nullable обладают следующими характеристиками:

  • Переменной Nullable типа-значения может быть присвоено значение из "базового" диапазона значений или null.
  • шаблон System.Nullable<T> не может быть применен для расширения ссылочных типов. (Ссылочные типы и без того поддерживают null значение).
  • Синтаксис допускает сокращенную форму для объявления Nullable типа: T? ( T — обозначение типа-значения) и System.Nullable<T> —две эквивалентные формы для обозначения типа.
  • Операция присвоения выглядит аналогично операции присвоения типов-значений, например int? x = 10; или double? d = 4.108.
  • Свойство System.Nullable.GetValueOrDefault обеспечивает возвращение присвоенного переменной значения, либо значения, которое соответствует значению, присваиваемому по умолчанию переменной типа-значения. Это свойство позволяет организовать безопасное взаимодействие между объектами – представителями типов System.Nullable<T> и T. Пример использования свойства:
    int j = x.GetValueOrDefault();
  • применение read-only свойств HasValue и Value также позволяет взаимодействовать объектам типа System.Nullable<T> и T в рамках одного предложения. Свойство используется для проверки непустого значения перед присвоением. HasValue возвращает true, если переменная содержит значение, либо false, это null. Свойство Value возвращает значение, если таковое было присвоено. В противном случае возбуждается исключение System.InvalidOperationException:
    if(x.HasValue) j = x.Value;
  • предопределенное значение для объектов Nullable типа может быть определено с помощью ранее описанных свойств. Применительно к значению по умолчанию свойство HasValue возвращает false. Значение свойства Value НЕ ОПРЕДЕЛЕНО.
  • объявленная в шаблоне операторная функция ?? работает следующим образом: если Nullable объекту присвоено непустое значение, операторная функция возвращает это значение. В противном случае возвращается значение второго операнда операторной функции. Например:
    int? x = null; int y = x ?? -1;
  • вложенные конструкции при объявлении Nullable типов не допускаются:
    Nullable<Nullable<int>> n;
  • диапазон значений Nullable<T> типа соответствует диапазону значений простого типа T, и включает еще одно дополнительное (пустое) значение null. Синтаксис C# допускает две эквивалентные формы объявления Nullable -типа:
    System.Nullable<T> variable
    T? variable
  • здесь T обозначает "основной" тип шаблонного типа. Тип T должен быть типом-значением, возможно, что структурой. Тип T не может быть типом-ссылкой.
  • Любой тип-значение может быть основой для шаблонного nullable типа. Например:
    int? i = 10;
     double? d1 = 3.14;
     bool? flag = null;
     char? letter = 'a';
     int?[] arr = new int?[10];
  • Nullable Types. Члены класса

    Прежде всего, пара свойств:

    HasValue

    Свойство HasValue является свойством типа bool. Оно принимает значение true, если переменная имеет непустое значение (значение, отличное от null ):

    Value

    Тип свойства Value соответствует "основному" типу. Если значение свойства HasValue является true, свойство Value принимает соответствующее значение. Если свойство HasValue принимает значение false, попытка определения значения свойства Value завершается генерацией исключения InvalidOperationException.

    В следующем примере свойство HasValue позволяет без генерации исключения (если это возможно) определить (и распечатать) значение nullable переменной типа int?:

    int? x = 10;
     if (x.HasValue)
     {
         System.Console.WriteLine(x.Value);
     }
     else
     {
         System.Console.WriteLine("Undefined");
     }

    Проверку значения можно также произвести следующим образом:

    int? y = 10;
     if (y != null)
     {
         System.Console.WriteLine(y.Value);
     }
     else
     {
         System.Console.WriteLine("Undefined");
     }

    Явное преобразование

    Особенности явного приведения типов. Значение Nullable может быть явным образом приведено к regular type. При этом можно использовать как выражение явного приведения к типу, так и свойство Value. Например:

    int? n = null;
    
    //int m1 = n;    // Будет зафиксирована ошибка на этапе компиляции.
    int m2 = (int)n;  // Compiles, but will create an exception if x is null.
    int m3 = n.Value; // Compiles, but will create an exception if x is null.

    Возможно также объявление user-defined операций приведения между двумя типами. Это же преобразование будет применяться и для Nullable-вариантов этих типов.

    Неявное преобразование

    Переменной nullable -типа может быть присвоено пустое ( null ) значение:

    int? n1 = null;

    Значение "основного" типа допускает неявное преобразование к соответствующему nullable -типу.

    int? n2;
     n2 = 10;  // Implicit conversion.

    Операции

    Предопределенные одноместные и двуместные операции, а также user-defined операции (операторные функции), которые существуют для value types-могут применяться для Nullable типов. Эти операторные функции produces null value, если значение операндов null. В противном случае значения операндов используются для определения значения. Например:

    int? a = 10;
     int? b = null;
    
     a++;         // Increment by 1, now a is 11.
     a = a * 10;  // Multiply by 10, now a is 110.
     a = a + b;   // Add b, now a is null.

    Основное правило, по которому реализуются операции сравнения значений Nullable типов, состоит в следующем. Результат сравнения двух Nullable -операндов всегда будет false, если хотя бы один из операндов имеет значение null. Например:

    int? num1 = 10;
     int? num2 = null;
     if (num1 >= num2)
     {
         System.Console.WriteLine("num1 is greater than or equal to num1");
     }
     else
     {
         // num1 НЕ МЕНЬШЕ num2
     }

    Заключение в части else условного оператора НЕКОРРЕКТНО, поскольку num2 является равным null, то есть пустым и поэтому не содержит никакого значения.

    Операция ??

    Операция ?? определяет то значение, которое должно присваиваться переменной "основного" типа в случае, когда значением переменной nullable типа является null:

    int? c = null;
     // d = c, если только c не равно null. В этом случае d = -1.
     int d = c ?? -1;

    Эта операция позволяет строить сложные выражения. Например:

    int? e = null;
     int? f = null;
     // g = e или f, если только e и f одновременно не равны  null. 
     // В этом случае g = -1.
     int g =  e ?? f ?? -1;

    Тип bool?

    Область значений bool? Nullable -типа состоит из трех значений: true, false, null.

    Поэтому значения данного типа неприменимы в операторах управления выполнением кода — условных операторах цикла — if, for, while. Попытка применения этого типа в вышеперечисленных операторах пресекается уже на стадии компиляции с кодом Compiler Error CS0266:

    bool? b = null;
     if (b) // Error CS0266.
     {
     }

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

    Nullable Booleans могут быть явно преобразованы к типу bool, однако, если операнд все же окажется пустым ( null ), возбуждение InvalidOperationException неизбежно. Поэтому непосредственно перед применением преобразования следует прибегать к помощи HasValue -свойства.

    Для Nullable -типа, основанного на типе bool, определены операторные функции

    bool? operator (bool? x, bool? y)
     bool? operator |(bool? x, bool? y)

    Таблица истинности трехзначной логики прилагается:

    x y xy x|y
    true true true true
    true false false true
    true null null true
    false true false true
    false false false false
    false null false null
    null true null true
    null false false null
    null null null null
    Вернуться к учебному плану