Основы программирования на C#

Структуры и перечисления

Разбить на страницы
Показывать лекцию целиком

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

Рассмотрим объявление объекта класса T с инициализацией:

T x = new T();

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

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

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

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

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

y = x;

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

Язык программирования должен позволять программисту в момент определения класса указать, к развернутому или ссылочному типу относится класс. К сожалению, язык C# не позволяет этого сделать напрямую - в нем у класса нет модификатора, позволяющего задать развернутый или ссылочный тип. Какие же средства языка позволяют частично решить эту важную задачу? В лекции 3, где рассматривалась система типов языка C#, отмечалось, что все типы языка делятся на ссылочные и значимые. Термин "значимый" является синонимом термина "развернутый". Беда только в том, что деление на значимые и ссылочные типы предопределено языком и не управляется программистом. Напомню, к значимым типам относятся все встроенные арифметические типы, булев тип, структуры ; к ссылочным типам - массивы, строки, классы. Так можно ли в C# спроектировать свой собственный класс так, чтобы он относился к значимым типам? Ответ на этот вопрос положительный, хотя и с рядом оговорок. Для того чтобы класс отнести к значимым типам, его нужно реализовать как структуру.

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

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

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

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

    Структуры

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

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

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

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

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

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

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

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

  • Самое серьезное ограничение связано с ограничением наследования. У структуры не может быть наследников. У структуры не может быть задан родительский класс или родительская структура. Конечно, всякая структура, как и любой класс в C#, является наследником класса Object, наследуя все свойства и методы этого класса. Структура может быть наследником одного или нескольких интерфейсов, реализуя методы этих интерфейсов.
  • Второе серьезное ограничение связано с процессом создания объектов. Пусть T - структура, и дано объявление без инициализации - T x. Это объявление корректно, в результате будет создан объект без явного вызова операции new. Сущности x будет отведена память, и на этой памяти будет развернут объект. Но поля объекта не будут инициализированы и, следовательно, не будут доступны для использования в вычислениях. Об этих особенностях подробно говорилось при рассмотрении значимых типов. В этом отношении все, что верно для типа int, верно и для всех структур.
  • Если при объявлении класса его поля можно инициализировать, что найдет отражение при работе конструктора класса, то поля структуры не могут быть инициализированы.
  • Конструктор по умолчанию у структур имеется, при его вызове поля инициализируются значениями по умолчанию. Этот конструктор нельзя заменить, создав собственный конструктор без аргументов.
  • В конструкторе нельзя вызывать методы класса. Поля структуры должны быть проинициализированы до вызова методов.
  • Класс Rational или структура Rational

    Вернемся к классу Rational, спроектированному в предыдущей лекции. Очевидно, что его вполне разумно представить в виде структуры. Наследование ему не нужно. Семантика присваивания развернутого типа больше подходит для рациональных чисел, чем ссылочная семантика, ведь рациональные числа - это еще один подкласс арифметического класса. В общем, класс Rational - прямой кандидат в структуры. Зададимся вопросом, насколько просто объявление класса превратить в объявление структуры? Достаточно ли заменить слово class словом struct? В данном случае одним словом не обойтись. Есть одно мешающее ограничение на структуры. В конструкторе класса Rational вызывается метод nod, а вызов методов в конструкторе запрещен. Нетрудно обойти это ограничение, изменив конструктор, то есть явно задав вычисление общего делителя в его теле. Приведу текст этого конструктора:

    public struct Rational
    {
    	public Rational(int a, int b)
    	{
    		if(b==0) {m=0; n=1;}
    		else
    		{
    			//приведение знака
    			if( b<0) {b=-b; a=-a;}
    			//приведение к несократимой дроби
    			int p = 1, m1=a, n1 =b;
    			m1=Math.Abs(m1); n1 =Math.Abs(n1);
    			if(n1>m1){p=m1; m1=n1; n1=p;}
    			do
    			{
    				p = m1%n1; m1=n1; n1=p;
    			}while (n1!=0);
    			p=m1;
    			m=a/p; n=b/p;
    		}
    	}//Конструктор
    //поля и методы класса
    }

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

    public void TwoSemantics()
    {
    	Rational r1 = new Rational(1,3), r2 = new Rational(3,5);
    	Rational r3, r4;
    	r3 = r1+r2; r4 = r3;
    	if(r3 >1) r3 = r1+r3 + Rational.One; 
    	else r3 = r2+r3 - Rational.One;
    	r3.PrintRational("r3"); r4.PrintRational("r4");
    }

    В этом примере используются константы, работает статический конструктор, закрытый конструктор, перегруженные операции сравнения, арифметические выражения над рациональными числами. В результате вычислений r3 получит значение 8/15, r4- 14/15. Заметьте, аналогичный пример для класса Rational даст те же результаты. Для класса Rational и структуры Rational нельзя обнаружить разницу между ссылочным и развернутым присваиванием. Это связано с особенностью класса Rational - он по построению относится к неизменяемым ( immutable ) классам, аналогично классу String. Операции этого класса не изменяют поля объекта, а каждый раз создают новый объект. В этом случае можно считать, что объекты класса обладают присваиванием развернутого типа.

    Встроенные структуры

    Как уже говорилось, все значимые типы языка реализованы структурами. В библиотеке FCL имеются и другие встроенные структуры. Рассмотрим в качестве примера структуры Point, PointF, Size, SizeF и Rectangle, находящиеся в пространстве имен System.Drawing и активно используемые при работе с графическими объектами. Первые четыре структуры имеют два открытых поля X и Y ( Height и Width ), задающие для точек - структур Point и PointF - координаты, целочисленные или в форме с плавающей точкой. Для размеров - структур Size и SizeF - они задают высоту и ширину, целочисленными значениями или в форме с плавающей точкой. Структуры Point и Size позволяют задать прямоугольную область - структуру Rectangle. Конструктору прямоугольника можно передать в качестве аргументов две структуры - точку, задающую координаты левого верхнего угла прямоугольника, и размер - высоту и ширину прямоугольника.

    Между четырьмя структурами определены взаимные преобразования: точки можно преобразовать в размеры и наоборот, сложение и вычитание определено над точками и размерами, но не над точками, плавающий тип которых разными способами можно привести к целому. Ряд операций над этими структурами продемонстрирован в следующем примере:

    public void TestPointAndSize()
    {
    	Point pt1 = new Point(3,5), pt2 = new Point(7,10), pt3;
    	PointF pt4 = new PointF(4.55f,6.75f);
    	Size sz1 = new Size(10,20), sz2;
    	SizeF sz3 = new SizeF(10.3f, 20.7f);
    	pt3 = Point.Round(pt4);
    	sz2 = new Size(pt1);
    	Console.WriteLine ("pt1: " + pt1);
    	Console.WriteLine ("sz2 =new Size(pt1): " + sz2);
    	Console.WriteLine ("pt4: " + pt4);
    	Console.WriteLine ("pt3 =Point.Round(pt4): " + pt3);
    	pt1.Offset(5,7);
    	Console.WriteLine ("pt1.Offset(5,7): " + pt1);
    	Console.WriteLine ("pt2: " + pt2);
    	pt2 = pt2+ sz2;
    	Console.WriteLine ("pt2= pt2+ sz2: " + pt2);
    }//TestPointAndSize

    Результаты его выполнения показаны на рис. 17.1

    (рис 17.1) Операции над точками и размерами

    Отметим, что метод ToString, определенный для этих структур, выдает строку со значениями полей в приемлемой для восприятия форме.

    Еще раз о двух семантиках присваивания

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

    public void TestTwoSemantics()
    {
    	Console.WriteLine("Структуры: присваивание 
    		развернутого типа!");
    	Point pt1 = new Point(3,5), pt2;
    	pt2 = pt1;
    	Console.WriteLine ("pt1: " + pt1);
    	Console.WriteLine ("pt2=pt1: " + pt2);
    	pt1.X +=10;
    	Console.WriteLine ("pt1.X =pt1.X +10: " + pt1);
    	Console.WriteLine ("pt2: " + pt2);
    	Console.WriteLine("Классы: присваивание ссылочного типа!");
    	CPoint cpt1 = new CPoint(3,5), cpt2;
    	cpt2 = cpt1;
    	Console.WriteLine ("cpt1: " + cpt1);
    	Console.WriteLine ("cpt2=cpt1: " + cpt2);
    	cpt1.X +=10;
    	Console.WriteLine ("cpt1.X =cpt1.X +10: " + cpt1);
    	Console.WriteLine ("cpt2: " + cpt2);
    }

    Результаты вычислений показаны на рис. 17.2.

    (рис 17.2) Две семантики присваивания

    Действия над объектами Point и CPoint выполняются аналогичные а результаты получаются разные: в конце вычислений pt1 и pt2 различны, а cpt1 и cpt2 совпадают.

    Перечисления

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

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

    Описание атрибутов отложим на последующие лекции. Модификаторами могут быть четыре известных модификатора доступа и модификатор new. Ключевое слово enum говорит, что определяется частный случай класса - перечисление. Список возможных значений задает те значения, которые могут получать объекты этого класса. Возможные значения должны быть идентификаторами; но допускаются в их написании и буквы русского алфавита. Можно указать также базовый для перечисления класс.

    Дело в том, что значения, заданные списком, проецируются на плотное подмножество базового класса. Реально значения объектов перечисления в памяти задаются значениями базового класса, так же, как значения класса bool реально представлены в памяти нулем и единицей, а не константами true и false, удобными для их использования программистами в тексте программ. По умолчанию, базовым классом является класс int, а подмножество проекции начинается с нуля. Но при желании можно изменить интервал представления и сам базовый класс. Естественно, на базовый класс накладывается ограничение. Он должен быть одним из встроенных классов, задающих счетное множество ( int, byte, long, другие счетные типы). Единственное исключение из этого правила - нельзя выбирать класс char в качестве базового класса. Как правило, принятый по умолчанию выбор базового класса и его подмножества вполне приемлем в большинстве ситуаций.

    Приведу примеры объявлений классов-перечислений:

    public enum Profession{teacher, engineer, businessman};
    public enum MyColors {red, blue, yellow, black, white};
    public enum TwoColors {black, white};
    public enum Rainbow {красный, оранжевый, желтый, зеленый, голубой, синий, фиолетовый};
    public enum Sex: byte {man=1, woman};
    public enum Days:long {Sun,Mon,Tue,Wed,Thu, Fri, Sat};

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

  • как и другие классы, перечисления могут быть объявлены непосредственно в пространстве имен проекта или могут быть вложены в описание класса. Последний вариант часто применяется, когда перечисление используется в одном классе и имеет атрибут доступа private ;
  • константы разных перечислений могут совпадать, как в перечислениях MyColors и TwoColors. Имя константы всегда уточняется именем перечисления ;
  • константы могут задаваться словами русского языка, как в перечислении Rainbow ;
  • разрешается задавать базовый класс перечисления. Для перечисления Days базовым классом задан класс long ;
  • разрешается задавать не только базовый класс, но и указывать начальный элемент подмножества, на которое проецируется множество значений перечисления. Для перечисления Sex в качестве базового класса выбран класс byte, а подмножество значений начинается с 1, так что хранимым значением константы man является 1, а woman - 2.
  • Рассмотрим теперь пример работы с объектами - экземплярами различных перечислений:

    public void TestEnum()
    {
    	//MyColors color1 = new MyColors(MyColors.blue);
    	MyColors color1= MyColors.white;
    	TwoColors color2;
    	color2 = TwoColors.white;
    	//if(color1 != color2) color2 = color1;
    	if(color1.ToString() != color2.ToString())
    		Console.WriteLine ("Цвета разные: {0}, {1}",
    			color1, color2);
    	else Console.WriteLine("Цвета одинаковые: {0}, 
    		{1}",color1, color2);
    	Rainbow color3;
    	color3 = (Rainbow)3;
    	if (color3 != Rainbow.красный)color3 =Rainbow.красный;
    	int num = (int)color3;
    	Sex who = Sex.man;
    	Days first_work_day = (Days)(long)1;
    	Console.WriteLine ("color1={0}, color2={1}, 
    		color3={2}",color1, color2, color3);
    	Console.WriteLine ("who={0}, first_work_day={1}",
    		who,first_work_day);
    }

    Данный пример иллюстрирует следующие особенности работы с объектами перечислений:

  • объекты перечислений нельзя создавать в объектном стиле с использованием операции new, поскольку перечисления не имеют конструкторов;
  • объекты можно объявлять с явной инициализацией, как color1, или с отложенной инициализацией, как color2. При объявлении без явной инициализации объект получает значение первой константы перечисления, так что color2 в момент объявления получает значение black ;
  • объекту можно присвоить значение, которое задается константой перечисления, уточненной именем перечисления, как для color1 и color2. Можно также задать значение базового типа, приведенное к типу перечисления, как для color3 ;
  • нельзя сравнивать объекты разных перечислений, например color1 и color2, но можно сравнивать строки, возвращаемые методом ToString, например color1.ToString() и color2.ToString() ;
  • существуют явные взаимно обратные преобразования констант базового типа и констант перечисления ;
  • Метод ToString, наследованный от класса Object, возвращает строку, задающую константу перечисления.
  • Персоны и профессии

    Рассмотрим еще один пример работы с перечислениями, приближенный к реальности. Добавим в класс Person, рассмотренный в предыдущей лекции 16, поле, определяющее профессию персоны. Вполне разумно иметь перечисление, например, Profession, задающее список возможных профессий. Сделаем это поле, как обычно, закрытым, а доступ к нему обеспечим соответствующим свойством:

    Profession prof;
    public Profession Prof
    {
    	get {return (prof);}
    	set {prof = value;}
    }

    Добавим еще в класс Person метод Analysis, анализирующий профессию, организуя традиционный разбор случаев и принимая решение на каждой ветви, в данном примере - выводя соответствующий текст:

    public void Analysis()
    {
    	switch (prof)
    		{
    		case Profession.businessman:
    			Console.WriteLine ("профессия: бизнесмен");
    			break;
    		case Profession.teacher:
    			Console.WriteLine ("профессия: учитель");
    			break;
    		case Profession.engineer:
    			Console.WriteLine ("профессия: инженер");
    			break;
    		default:
    			Console.WriteLine ("профессия: неизвестна");
    			break;
    		}
    }

    Приведу простой тестирующий пример работы с объектом Person и его профессией:

    public void TestProfession()
    {
    	Person pers1 = new Person ("Петров");
    	pers1.Prof = Profession.teacher;
    	pers1.Analysis();
    }

    Результаты работы с объектами перечислений, полученные при вызове тестов TestEnum и TestProfession, показаны на рис. 17.3.

    (рис 17.3) Результаты работы с перечислениями
    Страницы:

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

    Рассмотрим объявление объекта класса T с инициализацией:

    T x = new T();

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

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

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

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

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

    y = x;

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

    Язык программирования должен позволять программисту в момент определения класса указать, к развернутому или ссылочному типу относится класс. К сожалению, язык C# не позволяет этого сделать напрямую - в нем у класса нет модификатора, позволяющего задать развернутый или ссылочный тип. Какие же средства языка позволяют частично решить эту важную задачу? В лекции 3, где рассматривалась система типов языка C#, отмечалось, что все типы языка делятся на ссылочные и значимые. Термин "значимый" является синонимом термина "развернутый". Беда только в том, что деление на значимые и ссылочные типы предопределено языком и не управляется программистом. Напомню, к значимым типам относятся все встроенные арифметические типы, булев тип, структуры ; к ссылочным типам - массивы, строки, классы. Так можно ли в C# спроектировать свой собственный класс так, чтобы он относился к значимым типам? Ответ на этот вопрос положительный, хотя и с рядом оговорок. Для того чтобы класс отнести к значимым типам, его нужно реализовать как структуру.

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

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

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

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

    Структуры

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

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

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

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

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

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

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

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

  • Самое серьезное ограничение связано с ограничением наследования. У структуры не может быть наследников. У структуры не может быть задан родительский класс или родительская структура. Конечно, всякая структура, как и любой класс в C#, является наследником класса Object, наследуя все свойства и методы этого класса. Структура может быть наследником одного или нескольких интерфейсов, реализуя методы этих интерфейсов.
  • Второе серьезное ограничение связано с процессом создания объектов. Пусть T - структура, и дано объявление без инициализации - T x. Это объявление корректно, в результате будет создан объект без явного вызова операции new. Сущности x будет отведена память, и на этой памяти будет развернут объект. Но поля объекта не будут инициализированы и, следовательно, не будут доступны для использования в вычислениях. Об этих особенностях подробно говорилось при рассмотрении значимых типов. В этом отношении все, что верно для типа int, верно и для всех структур.
  • Если при объявлении класса его поля можно инициализировать, что найдет отражение при работе конструктора класса, то поля структуры не могут быть инициализированы.
  • Конструктор по умолчанию у структур имеется, при его вызове поля инициализируются значениями по умолчанию. Этот конструктор нельзя заменить, создав собственный конструктор без аргументов.
  • В конструкторе нельзя вызывать методы класса. Поля структуры должны быть проинициализированы до вызова методов.
  • Класс Rational или структура Rational

    Вернемся к классу Rational, спроектированному в предыдущей лекции. Очевидно, что его вполне разумно представить в виде структуры. Наследование ему не нужно. Семантика присваивания развернутого типа больше подходит для рациональных чисел, чем ссылочная семантика, ведь рациональные числа - это еще один подкласс арифметического класса. В общем, класс Rational - прямой кандидат в структуры. Зададимся вопросом, насколько просто объявление класса превратить в объявление структуры? Достаточно ли заменить слово class словом struct? В данном случае одним словом не обойтись. Есть одно мешающее ограничение на структуры. В конструкторе класса Rational вызывается метод nod, а вызов методов в конструкторе запрещен. Нетрудно обойти это ограничение, изменив конструктор, то есть явно задав вычисление общего делителя в его теле. Приведу текст этого конструктора:

    public struct Rational
    {
    	public Rational(int a, int b)
    	{
    		if(b==0) {m=0; n=1;}
    		else
    		{
    			//приведение знака
    			if( b<0) {b=-b; a=-a;}
    			//приведение к несократимой дроби
    			int p = 1, m1=a, n1 =b;
    			m1=Math.Abs(m1); n1 =Math.Abs(n1);
    			if(n1>m1){p=m1; m1=n1; n1=p;}
    			do
    			{
    				p = m1%n1; m1=n1; n1=p;
    			}while (n1!=0);
    			p=m1;
    			m=a/p; n=b/p;
    		}
    	}//Конструктор
    //поля и методы класса
    }

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

    public void TwoSemantics()
    {
    	Rational r1 = new Rational(1,3), r2 = new Rational(3,5);
    	Rational r3, r4;
    	r3 = r1+r2; r4 = r3;
    	if(r3 >1) r3 = r1+r3 + Rational.One; 
    	else r3 = r2+r3 - Rational.One;
    	r3.PrintRational("r3"); r4.PrintRational("r4");
    }

    В этом примере используются константы, работает статический конструктор, закрытый конструктор, перегруженные операции сравнения, арифметические выражения над рациональными числами. В результате вычислений r3 получит значение 8/15, r4- 14/15. Заметьте, аналогичный пример для класса Rational даст те же результаты. Для класса Rational и структуры Rational нельзя обнаружить разницу между ссылочным и развернутым присваиванием. Это связано с особенностью класса Rational - он по построению относится к неизменяемым ( immutable ) классам, аналогично классу String. Операции этого класса не изменяют поля объекта, а каждый раз создают новый объект. В этом случае можно считать, что объекты класса обладают присваиванием развернутого типа.

    Встроенные структуры

    Как уже говорилось, все значимые типы языка реализованы структурами. В библиотеке FCL имеются и другие встроенные структуры. Рассмотрим в качестве примера структуры Point, PointF, Size, SizeF и Rectangle, находящиеся в пространстве имен System.Drawing и активно используемые при работе с графическими объектами. Первые четыре структуры имеют два открытых поля X и Y ( Height и Width ), задающие для точек - структур Point и PointF - координаты, целочисленные или в форме с плавающей точкой. Для размеров - структур Size и SizeF - они задают высоту и ширину, целочисленными значениями или в форме с плавающей точкой. Структуры Point и Size позволяют задать прямоугольную область - структуру Rectangle. Конструктору прямоугольника можно передать в качестве аргументов две структуры - точку, задающую координаты левого верхнего угла прямоугольника, и размер - высоту и ширину прямоугольника.

    Между четырьмя структурами определены взаимные преобразования: точки можно преобразовать в размеры и наоборот, сложение и вычитание определено над точками и размерами, но не над точками, плавающий тип которых разными способами можно привести к целому. Ряд операций над этими структурами продемонстрирован в следующем примере:

    public void TestPointAndSize()
    {
    	Point pt1 = new Point(3,5), pt2 = new Point(7,10), pt3;
    	PointF pt4 = new PointF(4.55f,6.75f);
    	Size sz1 = new Size(10,20), sz2;
    	SizeF sz3 = new SizeF(10.3f, 20.7f);
    	pt3 = Point.Round(pt4);
    	sz2 = new Size(pt1);
    	Console.WriteLine ("pt1: " + pt1);
    	Console.WriteLine ("sz2 =new Size(pt1): " + sz2);
    	Console.WriteLine ("pt4: " + pt4);
    	Console.WriteLine ("pt3 =Point.Round(pt4): " + pt3);
    	pt1.Offset(5,7);
    	Console.WriteLine ("pt1.Offset(5,7): " + pt1);
    	Console.WriteLine ("pt2: " + pt2);
    	pt2 = pt2+ sz2;
    	Console.WriteLine ("pt2= pt2+ sz2: " + pt2);
    }//TestPointAndSize

    Результаты его выполнения показаны на рис. 17.1

    (рис 17.1) Операции над точками и размерами

    Отметим, что метод ToString, определенный для этих структур, выдает строку со значениями полей в приемлемой для восприятия форме.

    Еще раз о двух семантиках присваивания

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

    public void TestTwoSemantics()
    {
    	Console.WriteLine("Структуры: присваивание 
    		развернутого типа!");
    	Point pt1 = new Point(3,5), pt2;
    	pt2 = pt1;
    	Console.WriteLine ("pt1: " + pt1);
    	Console.WriteLine ("pt2=pt1: " + pt2);
    	pt1.X +=10;
    	Console.WriteLine ("pt1.X =pt1.X +10: " + pt1);
    	Console.WriteLine ("pt2: " + pt2);
    	Console.WriteLine("Классы: присваивание ссылочного типа!");
    	CPoint cpt1 = new CPoint(3,5), cpt2;
    	cpt2 = cpt1;
    	Console.WriteLine ("cpt1: " + cpt1);
    	Console.WriteLine ("cpt2=cpt1: " + cpt2);
    	cpt1.X +=10;
    	Console.WriteLine ("cpt1.X =cpt1.X +10: " + cpt1);
    	Console.WriteLine ("cpt2: " + cpt2);
    }

    Результаты вычислений показаны на рис. 17.2.

    (рис 17.2) Две семантики присваивания

    Действия над объектами Point и CPoint выполняются аналогичные а результаты получаются разные: в конце вычислений pt1 и pt2 различны, а cpt1 и cpt2 совпадают.

    Перечисления

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

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

    Описание атрибутов отложим на последующие лекции. Модификаторами могут быть четыре известных модификатора доступа и модификатор new. Ключевое слово enum говорит, что определяется частный случай класса - перечисление. Список возможных значений задает те значения, которые могут получать объекты этого класса. Возможные значения должны быть идентификаторами; но допускаются в их написании и буквы русского алфавита. Можно указать также базовый для перечисления класс.

    Дело в том, что значения, заданные списком, проецируются на плотное подмножество базового класса. Реально значения объектов перечисления в памяти задаются значениями базового класса, так же, как значения класса bool реально представлены в памяти нулем и единицей, а не константами true и false, удобными для их использования программистами в тексте программ. По умолчанию, базовым классом является класс int, а подмножество проекции начинается с нуля. Но при желании можно изменить интервал представления и сам базовый класс. Естественно, на базовый класс накладывается ограничение. Он должен быть одним из встроенных классов, задающих счетное множество ( int, byte, long, другие счетные типы). Единственное исключение из этого правила - нельзя выбирать класс char в качестве базового класса. Как правило, принятый по умолчанию выбор базового класса и его подмножества вполне приемлем в большинстве ситуаций.

    Приведу примеры объявлений классов-перечислений:

    public enum Profession{teacher, engineer, businessman};
    public enum MyColors {red, blue, yellow, black, white};
    public enum TwoColors {black, white};
    public enum Rainbow {красный, оранжевый, желтый, зеленый, голубой, синий, фиолетовый};
    public enum Sex: byte {man=1, woman};
    public enum Days:long {Sun,Mon,Tue,Wed,Thu, Fri, Sat};

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

  • как и другие классы, перечисления могут быть объявлены непосредственно в пространстве имен проекта или могут быть вложены в описание класса. Последний вариант часто применяется, когда перечисление используется в одном классе и имеет атрибут доступа private ;
  • константы разных перечислений могут совпадать, как в перечислениях MyColors и TwoColors. Имя константы всегда уточняется именем перечисления ;
  • константы могут задаваться словами русского языка, как в перечислении Rainbow ;
  • разрешается задавать базовый класс перечисления. Для перечисления Days базовым классом задан класс long ;
  • разрешается задавать не только базовый класс, но и указывать начальный элемент подмножества, на которое проецируется множество значений перечисления. Для перечисления Sex в качестве базового класса выбран класс byte, а подмножество значений начинается с 1, так что хранимым значением константы man является 1, а woman - 2.
  • Рассмотрим теперь пример работы с объектами - экземплярами различных перечислений:

    public void TestEnum()
    {
    	//MyColors color1 = new MyColors(MyColors.blue);
    	MyColors color1= MyColors.white;
    	TwoColors color2;
    	color2 = TwoColors.white;
    	//if(color1 != color2) color2 = color1;
    	if(color1.ToString() != color2.ToString())
    		Console.WriteLine ("Цвета разные: {0}, {1}",
    			color1, color2);
    	else Console.WriteLine("Цвета одинаковые: {0}, 
    		{1}",color1, color2);
    	Rainbow color3;
    	color3 = (Rainbow)3;
    	if (color3 != Rainbow.красный)color3 =Rainbow.красный;
    	int num = (int)color3;
    	Sex who = Sex.man;
    	Days first_work_day = (Days)(long)1;
    	Console.WriteLine ("color1={0}, color2={1}, 
    		color3={2}",color1, color2, color3);
    	Console.WriteLine ("who={0}, first_work_day={1}",
    		who,first_work_day);
    }

    Данный пример иллюстрирует следующие особенности работы с объектами перечислений:

  • объекты перечислений нельзя создавать в объектном стиле с использованием операции new, поскольку перечисления не имеют конструкторов;
  • объекты можно объявлять с явной инициализацией, как color1, или с отложенной инициализацией, как color2. При объявлении без явной инициализации объект получает значение первой константы перечисления, так что color2 в момент объявления получает значение black ;
  • объекту можно присвоить значение, которое задается константой перечисления, уточненной именем перечисления, как для color1 и color2. Можно также задать значение базового типа, приведенное к типу перечисления, как для color3 ;
  • нельзя сравнивать объекты разных перечислений, например color1 и color2, но можно сравнивать строки, возвращаемые методом ToString, например color1.ToString() и color2.ToString() ;
  • существуют явные взаимно обратные преобразования констант базового типа и констант перечисления ;
  • Метод ToString, наследованный от класса Object, возвращает строку, задающую константу перечисления.
  • Персоны и профессии

    Рассмотрим еще один пример работы с перечислениями, приближенный к реальности. Добавим в класс Person, рассмотренный в предыдущей лекции 16, поле, определяющее профессию персоны. Вполне разумно иметь перечисление, например, Profession, задающее список возможных профессий. Сделаем это поле, как обычно, закрытым, а доступ к нему обеспечим соответствующим свойством:

    Profession prof;
    public Profession Prof
    {
    	get {return (prof);}
    	set {prof = value;}
    }

    Добавим еще в класс Person метод Analysis, анализирующий профессию, организуя традиционный разбор случаев и принимая решение на каждой ветви, в данном примере - выводя соответствующий текст:

    public void Analysis()
    {
    	switch (prof)
    		{
    		case Profession.businessman:
    			Console.WriteLine ("профессия: бизнесмен");
    			break;
    		case Profession.teacher:
    			Console.WriteLine ("профессия: учитель");
    			break;
    		case Profession.engineer:
    			Console.WriteLine ("профессия: инженер");
    			break;
    		default:
    			Console.WriteLine ("профессия: неизвестна");
    			break;
    		}
    }

    Приведу простой тестирующий пример работы с объектом Person и его профессией:

    public void TestProfession()
    {
    	Person pers1 = new Person ("Петров");
    	pers1.Prof = Profession.teacher;
    	pers1.Analysis();
    }

    Результаты работы с объектами перечислений, полученные при вызове тестов TestEnum и TestProfession, показаны на рис. 17.3.

    (рис 17.3) Результаты работы с перечислениями
    Вернуться к учебному плану