Создание пользовательских типов данных

Причины использования типов в языках программирования

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

В предыдущих лабораторных работах мы познакомились со множеством типов данных, доступных в стандартных библиотеках C#. Тем не менее, необходимость в создании собственных типов данных возникает при разработке любого нетривиального проекта. Например, вместо того, чтобы передавать несколько взаимосвязанных переменных в качестве параметров функции (скажем, ФИО студента, номер зачётной книжки, номер курса и т.д.), гораздо удобнее “упаковать” их в новый пользовательский тип Student и далее работать с ним.

Объявление пользовательских типов

Чтобы получить возможность использовать собственный тип при создании переменных, необходимо сперва объявить этот тип. Объявление осуществляется с помощью ключевых слов class или struct, однако отличие между ними будет раскрыто позднее. Код объявления располагается внутри блока namespace. Рассмотрим пример, в котором создаётся тип Точка, представляющий собой координаты на двумерной плоскости:

using System;

namespace ConsoleApp1
{
    class Program
    {
        public static void Main(string[] args)
        {
        }
    }

    class Точка
    {
        public int x;
        public int y;
    }
}

Имя создаваемого типа может состоять из русских и латинских букв, а так же цифр, но не может начинаться с цифры. Внутри фигурных скобок, следующих за именем типа записываются компоненты, из которых составляется новый тип, называющеся полями. Синтаксис объявления отдельного поля похож на синтаксис объявления переменной с добавленным ключевым словом public, смысл которого будет так же раскрыт позднее. Порядок объявлений типов в C# значения не имеет - внутри class Program можно использовать Точка, несмотря на то, что объявление последнего следует после первого.

Имея код объявления пользовательского типа, приведём пример его непосредственного использования:

public static void Main(string[] args)
{
    Точка p = new Точка();
    p.x = 123;
    p.y = 456;
}

Доступ к полям осуществляется с помощью символа '.', следующего за именем переменной.

В качестве инициализатора для пользовательского типа могут фигурировать следующие выражения:

Проиллюстрируем применение описанных выше инициализаторов следующим кодом:

Точка k1 = null;
k1.x = 123; // вызовет ошибку, т.к. k1 содержит null

Точка k2 = new Точка();
Точка k3 = k2;
k3.x = 123;
Console.WriteLine(k2.x); // напечатает "123", т.к. k2 и k3 ссылаются на одно
                         // и то же значение

k3 = new Точка();   // теперь в k2 и k3 находятся разные значения, и
                         // изменение одного не будет влиять на другое

Для того чтобы лучше понять причины, приводящие к такому поведению, необходимо познакомиться с двумя категориями типов данных в языке C#.

Ссылочные и значимые типы данных

Все типы в языке C#, как встроенные, так и пользовательские, можно разделить на две большие категории: значимые типы и ссылочные типы. К первой группе относятся целые числовые типы (int, short и т.д.), числовые типы с плавающей точкой (float, double), типы bool и char, а так же все пользовательские типы, созданные с помощью ключевых слов enum и struct. Ко второй группе относятся все типы, не попавшие в первую категорию: строки и массивы, тип object, а так же пользовательские типы, созданные с помощью ключевого слова class.

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

  1. Выделяется участок для хранения значения;
  2. Выделяется еще один участок, в который записывается адрес первого участка.

Имя создаваемой переменной ассоциируется только со вторым участком, называемым ссылкой. Доступ к содержимому переменной, хранимому в первом участке, осуществляется только посредством ссылки.

Описанное отличие между ссылочными и значимыми типами данных оказывает большое влияние на семантику оператора присваивания. Присваивание одной переменной другой создает копию этой переменной в случае значимых типов. Если же переменные имеют ссылочный тип, то присваивание приводит к появлению двух ссылок на одну и ту же область памяти. Как следствие, изменение содержимого этой области через одну переменную будет приводить к тому, что эти изменения будут видны при доступе через вторую переменную. Рассмотрим код, иллюстрирующий это отличие.

/* =========================
 * Пример для значимых типов
 * =========================
 */
int x = 1;
int y = x; // Создается копия x, которая заносится в y
x = 2; // Изменение x не приводит к изменению y

/* ==========================
 * Пример для ссылочных типов
 * ==========================
 */
int[] x = {1, 2, 3}; // Выделяется два участка памяти:
                     // под сам массив и под ссылку на него
                     // В x заносится ссылка

int[] y = x;   // Создается копия ссылки x на тот же самый массив и заносится в y
x[0] = 123;    // Изменяется первый элемент массива через ссылку x

bool yes = x[0] == y[0]; // Даёт true, т.к. в y[0] теперь тоже 123

Другим оператором, изменяющим своё поведение, в зависимости от типа операндов является оператор сравнения. При сравнении значимых типов с помощью == происходит сравнение непосредственных значений. При сравнении же ссылочных типов, в действительности сравниваются между собой ссылки, а не данные, на которые эти ссылки указывают. Как следствие, сравнение двух разных объектов с одинаковым содержимым даёт false в качестве результата:

int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
bool no = a == b; // Даёт false

На практике, при создании пользовательских типов, в большинстве случаев, используется ключевое слово class.

Передача значимых типов по ссылке

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

В языке C# с помощью ключевого слова ref возможна передача любых типов по ссылке. Рассмотрим пример:

static void Main(string[] args)
{
    int x = 1;
    Foo(ref x);
    Console.WriteLine(x); // Выведет "2"
}

static void Foo(ref int x)
{
    x = x + 1;
}

Ключевое слово ref должно фигурировать как в списке фактических аргументов функции, так и в списке ее формальных параметров. Добавление этого ключевого слова заставляет переменную x передаваться по ссылке, несмотря на то, что тип int - значимый тип.

Может показаться бессмысленным применять ref к ссылочным типам. На самом деле, это может быть полезно, когда вызываемая функция должна изменять не значения по ссылке, а саму ссылку.

Контрольные вопросы

  1. Для чего нужно создавать собственные типы данных?
  2. Может ли создаваемый тип данных иметь такой же тип в качестве своего поля?
  3. Зачем может понадобиться ключевое слово ref при передаче массива в функцию?
  4. Если две переменные равны по ссылке, есть ли смысл дополнительно сравнивать их по содержимому?
  5. Может ли ссылочный тип иметь значимый тип в качестве своего поля?
  6. Имея пользовательский тип данных с большим числом полей, и передавая его в качестве аргумента в некоторую функцию, что будет эффективнее - объявить этот тип с помощью class или с помощью struct?

Задание на лабораторную работу

Написать программу, организующую ввод с клавиатуры и вывод на экран данных, соответствующих своему варианту. Вводимая информация должна вноситься в один или несколько пользовательских типов данных. Поля в создаваемых типах необходимо придумать самостоятельно. Для упрощения задания, создавать поля-массивы не нужно.

Вариант 1

Типы Книга и Автор.

Вариант 2

Типы УчебнаяГруппа и Студент.

Вариант 3

Типы Монстр, Игрок и НИП (неигровой персонаж, NPC).

Вариант 4

Типы ПрограммныйПродукт и КомпанияРазработчик.

Вариант 5

Типы Сотрудник и Подразделение.

Вариант 6

Типы Военнослужащий и ВоеннаяЧасть.

Вариант 7

Типы Звезда, Планета и ЗвёзднаяСистема.

Вариант 8

Типы Художник и Картина.

Вариант 9

Типы НаучныйЖурнал и Статья.

Вариант 10

Типы Преподаватель и УчебнаяДисциплина.

Вариант 11

Типы ЯзыкПрограммирования и Компилятор.

Вариант 12

Типы Клиент и Заказ.

Вариант 13

Типы Заболевание, Врач и МедицинскоеУчреждение.

Вариант 14

Типы Смартфон и Производитель.

Вариант 15

Типы ОбъектНедвижимости и Собственник.