Опубликован: 02.12.2009 | Уровень: специалист | Доступ: платный | ВУЗ: Тверской государственный университет
Лекция 3:

Структуры

< Лекция 2 || Лекция 3: 1234 || Лекция 4 >
Аннотация: Структуры, как частный случай класса, позволяют задавать развернутый тип данных. Подробно обсуждаются отличия ссылочных и развернутых типов данных, когда и где следует применять тот или иной тип – выбирать структуру или ссылочный класс. Лекция сопровождается задачами.

Проект к данной лекции Вы можете скачать здесь.

Развернутые и ссылочные типы

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

Итак, будем рассматривать классы, задающие тип данных. Такой класс Т представляет описание множества объектов - экземпляров класса, задавая их свойства и поведение. Если класс представляет собой текст - статическое описание, то объекты класса создаются динамически в процессе работы программы. Как правило, объекты класса Т создаются в других классах, являющихся клиентами класса Т. Рассмотрим в клиентском классе объявление объекта класса T, выполненное с инициализацией:

T x = new T();

Напомню, как выполняется этот оператор, создающий объект класса Т. Объектам нужна память, чтобы с ними можно было работать. Рассмотрим две классические стратегии выделения памяти и связывания объекта, создаваемого в памяти, и сущности, объявленной в тексте. Есть два типа памяти - стек ( stack ) и куча ( heap ). Сущности x в стеке всегда отводится память, но какая память - зависит от того, к развернутому или ссылочному типу относится класс Т.

Определение 1.Если класс T относится к развернутому типу, то в стеке сущности x отводится память, необходимая объекту класса T. Говорят, что объект разворачивается на памяти, жестко связанной с сущностью x. Эту память сущность x ни с кем не разделяет.

Определение 2.Если класс T относится к ссылочному типу, то память объекту отводится в куче, а в стеке сущности x отводится дополнительная память, содержащая ссылку на объект.

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

Развернутые и ссылочные типы порождают две различные семантики. Рассмотрим присваивание:

y = x;

Когда сущность y принадлежит развернутому типу, значения полей объекта, связанного с сущностью y, при присваивании изменяются, получая значения полей объекта, связанного с x. Напомним, что поля y хранятся в стеке.

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

Рассмотрим операцию проверки объектов на эквивалентность:

if (x == y)

Для развернутых типов объекты x и y эквивалентны, если эквивалентны значения всех их полей. Для ссылочных типов объекты x и y эквивалентны, если эквивалентны значения ссылок.

Язык программирования должен позволять программисту в момент определения класса указать, к развернутому или ссылочному типу относится класс. В языке C# это делается следующим образом. Если объявление класса задается с использованием служебного слова class, то такой класс относится к ссылочным типам.

public class A { ... }

Если объявление класса задается с использованием служебного слова struct, то такой класс относится к развернутым типам, которые также называют значимыми типами или value типами.

public struct B { ... }

Напомню: к значимым типам относятся все встроенные арифметические типы, булевский тип, перечисления, структуры. К ссылочным типам относятся массивы, строки, классы. Так можно ли в C# спроектировать свой собственный класс так, чтобы он относился к значимым типам? Ответ на этот вопрос - положительный, хотя и с рядом оговорок. Для того чтобы класс отнести к значимым типам, его достаточно объявить как структуру.

Классы и структуры

Структура - это частный случай класса. Исторически структуры используются в языках программирования раньше классов. В языках PL/1, C, Pascal они представляли собой только совокупность данных (полей класса), но не включали ни методов, ни событий. В языке С++ возможности структур были существенно расширены, и они стали настоящими классами, хотя и c некоторыми ограничениями. В языке C# сохранен именно такой подход к структурам.

Чем следует руководствоваться, делая выбор между структурой и классом? Полагаю, можно пользоваться следующими правилами:

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

Поскольку на структуры накладываются дополнительные ограничения, может возникнуть необходимость в компромиссе - согласиться с ограничениями и использовать структуру либо пожертвовать развернутостью и эффективностью и работать с настоящим классом. Стоит отметить: когда говорится, что встроенные типы - int и другие - представляют собой классы, то, на самом деле, речь идет о классах, реализованных в виде структур.

Структуры

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

Синтаксис структур

Синтаксис объявления структуры аналогичен синтаксису объявления класса:

[атрибуты][модификаторы]struct имя_структуры[:список_интерфейсов]
{тело_структуры}

Какие изменения произошли в синтаксисе заголовка структуры в сравнении с синтаксисом класса? Их немного. Перечислим их:

  • ключевое слово class изменено на слово struct ;
  • список родителей, который мог включать имя родительского класса, заменен списком, допускающим только имена интерфейсов. Для структур не может быть задан родитель (класс или структура). Заметьте, структура может наследовать интерфейсы;
  • для структур не применимы модификаторы abstract и sealed. Причиной является отсутствие механизма наследования.

Все, что может быть вложено в тело класса, может быть вложено и в тело структуры: поля, методы, конструкторы и прочее, включая классы и интерфейсы.

Аналогично классу, структура может иметь статические и нестатические поля и методы, может иметь несколько конструкторов, в том числе статические и закрытые конструкторы. Для структур можно создавать собственные константы, используя поля с атрибутом readonly и статический конструктор. Структуры похожи на классы по своему описанию и ведут себя сходным образом, хотя и имеют существенные различия в семантике присваивания и проверке эквивалентности.

Рассмотрим ограничения, накладываемые на структуры.

Структуры и наследование

Самое серьезное ограничение связано с ограничением наследования. Для структуры не может быть задан родительский класс или родительская структура. У структуры не может быть наследников. Конечно, всякая структура, как и любой класс в C#, является наследником класса object, наследуя все свойства и методы этого класса. Структура может наследовать один или несколько интерфейсов, реализуя методы этих интерфейсов.

С чем связано такое серьезное ограничение. Одно из объяснений состоит в следующем. Рассмотрим присваивание

y = x;

При присваивании допустимо, чтобы y принадлежал родительскому классу, а x - его потомку. При ссылочном присваивании никаких проблем не возникает. В результате присваивания ссылка y будет указывать на объект потомка, который помимо полей родителя может иметь дополнительные поля. Главное, что все поля родителя будут определены. После такого присваивания возможно и обратное присваивание с приведением типа

x = (T)y;

Для структур, принадлежащих к значимому типу, подобное присваивание между родителем и потомком было бы невозможным. Родителю нельзя присвоить объект потомка, поскольку у родителя меньше полей, чем у потомка, поэтому можно было бы присвоить лишь часть полей. А уж в обратную сторону операция присваивания была бы принципиально невозможна. И это одна из причин, по которой решились отказаться от наследования для значимых типов в языке C#.

Структуры и инициализация полей

Второе серьезное ограничение связано с процессом создания объектов и их инициализацией.

Рассмотрим объявление:

int[] x;
int y = x[0];

Это объявление корректно. А теперь рассмотрим похожее объявление:

int u; 
int v = u;

Это объявление некорректно, возникнет ошибка периода компиляции, сообщающая, что переменная v не инициализирована и не может быть использована в вычислениях. В чем разница? Сущность x - это массив и, следовательно, относится к ссылочным типам. При создании объектов ссылочного типа все поля объектов инициализируются некоторыми значениями. В данном конкретном случае все элементы целочисленного типа получат значение по умолчанию, равное нулю. Сущность u арифметического типа относится к развернутому типу. Все поля объектов развернутого типа должны быть инициализированы явно или стандартным конструктором по умолчанию в момент создания объекта. Разная семантика инициализации приводит к следующим ограничениям на структуры.

  • Если в классе поля можно объявлять с инициализацией, то поля структуры не допускают инициализацию при их объявлении. Объявление с инициализацией полей означало бы их неявную инициализацию, а потому запрещено.
  • Стандартный конструктор по умолчанию у структур имеется, он в стеке создает объект, инициализируя поля созданного объекта значениями по умолчанию. Этот конструктор нельзя заменить собственным конструктором без аргументов.
< Лекция 2 || Лекция 3: 1234 || Лекция 4 >
Федор Антонов
Федор Антонов

Здравствуйте!

Записался на ваш курс, но не понимаю как произвести оплату.

Надо ли писать заявление и, если да, то куда отправлять?

как я получу диплом о профессиональной переподготовке?

Илья Ардов
Илья Ардов

Добрый день!

Я записан на программу. Куда высылать договор и диплом?

Дмитрий Штаф
Дмитрий Штаф
Россия
Дмитрий Слапогузов
Дмитрий Слапогузов
Россия, Бийск