![]() |
Создание пользовательских типов данных |
![]() |
Причины использования типов в языках программирования
Типизацией в языках программирования называется возможность дополнять различные языковые конструкции информацией о типах выражений, входящих в них. Для объявлений переменных эта информация содержит возможные значения, которые могут быть размещены в них; для объявлений функций - то, какие значения функция может принять и возвратить. Использование типов при написании программ позволяет, с одной стороны, повысить читаемость кода за счет группировки переменных в единую сущность, а с другой - автоматически обнаруживать некоторые виды ошибок.
В предыдущих лабораторных работах мы познакомились со множеством типов данных,
доступных в стандартных библиотеках 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;
}Доступ к полям осуществляется с помощью символа '.', следующего за именем
переменной.
В качестве инициализатора для пользовательского типа могут фигурировать следующие выражения:
null. Подобно инициализаторуnullдля строк, с пользовательским типом, инициализированным этим значением, нельзя производить никаких операций, кроме сравнения и перезаписывания другим значением;x, гдеx- другая переменная такого же типа. В отличие отint, инициализация другой переменной не создаёт копии этой переменной;new ИмяТипа(). Этот инициализатор применяется для того, чтобы создать новое значение указанного типа. В предыдущих работах этот синтаксис применялся для создания значений типаStringBuilder. Как можно догадаться, использование этого инициализатора приводит к выделению нового участка памяти.
Проиллюстрируем применение описанных выше инициализаторов следующим кодом:
Точка 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 байта), которое напрямую ассоциируется с
именем создаваемой переменной. При объявлении переменной ссылочного типа
происходит следующее:
- Выделяется участок для хранения значения;
- Выделяется еще один участок, в который записывается адрес первого участка.
Имя создаваемой переменной ассоциируется только со вторым участком, называемым ссылкой. Доступ к содержимому переменной, хранимому в первом участке, осуществляется только посредством ссылки.
Описанное отличие между ссылочными и значимыми типами данных оказывает большое влияние на семантику оператора присваивания. Присваивание одной переменной другой создает копию этой переменной в случае значимых типов. Если же переменные имеют ссылочный тип, то присваивание приводит к появлению двух ссылок на одну и ту же область памяти. Как следствие, изменение содержимого этой области через одну переменную будет приводить к тому, что эти изменения будут видны при доступе через вторую переменную. Рассмотрим код, иллюстрирующий это отличие.
/* =========================
* Пример для значимых типов
* =========================
*/
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 к ссылочным типам. На самом деле,
это может быть полезно, когда вызываемая функция должна изменять не значения по
ссылке, а саму ссылку.
Контрольные вопросы
- Для чего нужно создавать собственные типы данных?
- Может ли создаваемый тип данных иметь такой же тип в качестве своего поля?
- Зачем может понадобиться ключевое слово
refпри передаче массива в функцию? - Если две переменные равны по ссылке, есть ли смысл дополнительно сравнивать их по содержимому?
- Может ли ссылочный тип иметь значимый тип в качестве своего поля?
- Имея пользовательский тип данных с большим числом полей, и передавая его в
качестве аргумента в некоторую функцию, что будет эффективнее - объявить этот
тип с помощью
classили с помощьюstruct?
Задание на лабораторную работу
Написать программу, организующую ввод с клавиатуры и вывод на экран данных, соответствующих своему варианту. Вводимая информация должна вноситься в один или несколько пользовательских типов данных. Поля в создаваемых типах необходимо придумать самостоятельно. Для упрощения задания, создавать поля-массивы не нужно.
Вариант 1
Типы Книга и Автор.
Вариант 2
Типы УчебнаяГруппа и Студент.
Вариант 3
Типы Монстр, Игрок и НИП (неигровой персонаж, NPC).
Вариант 4
Типы ПрограммныйПродукт и КомпанияРазработчик.
Вариант 5
Типы Сотрудник и Подразделение.
Вариант 6
Типы Военнослужащий и ВоеннаяЧасть.
Вариант 7
Типы Звезда, Планета и ЗвёзднаяСистема.
Вариант 8
Типы Художник и Картина.
Вариант 9
Типы НаучныйЖурнал и Статья.
Вариант 10
Типы Преподаватель и УчебнаяДисциплина.
Вариант 11
Типы ЯзыкПрограммирования и Компилятор.
Вариант 12
Типы Клиент и Заказ.
Вариант 13
Типы Заболевание, Врач и МедицинскоеУчреждение.
Вариант 14
Типы Смартфон и Производитель.
Вариант 15
Типы ОбъектНедвижимости и Собственник.



