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