Книга: Microsoft Visual C#. Подробное руководство. 8-е издание
Назад: 8. Основные сведения о значениях и ссылках
Дальше: 10. Использование массивов

9. Создание типов значений с использованием перечислений и структур

Прочитав эту главу, вы научитесь:

объявлять перечисляемый тип;

создавать и использовать перечисляемый тип;

объявлять структурный тип;

создавать и использовать структурный тип;

объяснять разницу в поведении между структурой и классом.

В главе 8 «Основные сведения о значениях и ссылках» было дано описание двух базовых типов, имеющихся в Microsoft Visual C#: типов значений и ссылочных типов. Вспомним, что переменные типа значения содержат свои значения непосредственно в стеке, а переменные ссылочного типа содержат ссылки на объект, который находится в динамической памяти (куче). В главе 7 «Создание классов и объектов и управление ими» было показано, как путем определения классов создаются собственные ссылочные типы. В этой главе вы изучите способы со­здания собственных типов значений.

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

Работа с перечислениями

Предположим, вам нужно представить в программе времена года. Для представления весны, лета, осени и зимы можно воспользоваться целыми числами 0, 1, 2 и 3 соответственно. Такая система будет работать, но вряд ли можно назвать ее интуитивно понятной. Если в коде используется целочисленное значение 0, то совсем не очевидно, что какое-то конкретное нулевое значение представляет именно весну. Надежность такого решения вызывает большие сомнения. Например, если объявляется переменная по имени season, то ничто не помешает назначить ей любое допустимое целочисленное значение за пределами набора 0, 1, 2 или 3. Язык C# предлагает более подходящее решение. Вы можете создать перечисление (иногда называемое enum-типом), чьи значения ограничиваются набором символьных имен.

Объявление перечисления

Перечисление определяется с помощью ключевого слова enum, за которым следует набор заключенных в фигурные скобки символов, идентифицирующих допустимые для типа значения. Вот как объявляется перечисление по имени Season, чьи литеральные значения ограничиваются символьными именами Spring, Summer, Fall и Winter:

enum Season { Spring, Summer, Fall, Winter }

Использование перечисления

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

enum Season { Spring, Summer, Fall, Winter }

 

class Example

{

    public void Method(Season parameter) // пример параметра метода

    {

        Season localVariable;            // пример локальной переменной

        ...

    }

    private Season currentSeason;        // пример поля

}

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

Season colorful = Season.Fall;

Console.WriteLine(colorful); // записывается 'Fall'

173606.png

ПРИМЕЧАНИЕ Как и в случае со всеми другими типами значений, воспользовавшись модификатором в виде вопросительного знака (?), вы можете создать версию переменной перечисления, допускающую пустое значение. Затем переменной можно будет присвоить значение null — точно так же, как и значения, определяемые перечислением:

Season? colorful = null;

Обратите внимание на необходимость записи Season.Fall, а не просто Fall. Все литеральные имена перечисления находятся в области видимости своего перечисляемого типа, что позволяет различным перечислениям содержать литералы с одинаковыми именами.

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

string name = colorful.ToString();

Console.WriteLine(name); // также записывает 'Fall'

В отношении переменных перечислений (кроме побитовых операторов и операторов сдвига, которые будут рассматриваться в главе 16 «Использование индексаторов») могут использоваться и многие стандартные операторы, пригодные для применения в отношении целочисленных переменных. Например, определить равенство двух перечислений одного и того же типа можно с помощью оператора (==), а в отношении переменных перечислений можно даже выполнять арифметические операции, хотя определенный практический смысл у результата бывает не всегда!

Выбор литеральных значений перечислений

В своем внутреннем механизме перечисляемый тип ассоциирует с каждым элементом перечисления целочисленное значение. По умолчанию нумерация начинается с нуля для первого элемента и возрастает на единицу с каждым следующим элементом. Это исходное целочисленное значение переменной перечисления можно извлечь. Для этого нужно привести это значение к его основному типу. При рассмотрении механизма распаковки в главе 8 говорилось, что при приведении типа данные преобразуются из одного типа в другой при условии, что это преобразование допустимо и имеет какой-либо смысл. В следующем примере кода записывается не слово Fall, а значение 2 (вспомним, что в Season-перечислении Spring — это 0, Summer — 1, Fall — 2 и Winter — 3):

enum Season { Spring, Summer, Fall, Winter }

...

Season colorful = Season.Fall;

Console.WriteLine((int)colorful); // записывается '2'

При необходимости с литералом перечисления (например, со Spring) можно ассоциировать конкретную целочисленную константу (например, 1):

enum Season { Spring = 1, Summer, Fall, Winter }

174878.png

ВНИМАНИЕ Целочисленное значение, которым инициализируется литерал перечисления, должно быть на момент компиляции значением константы (таким, как 1).

Если литералу перечислений постоянное целочисленное значение явным образом не задается, компилятор дает ему значение на единицу больше значения предыдущего литерала перечисления, за исключением самого первого литерала перечисления, которому компилятор дает исходное значение 0. Применительно к предыдущему примеру основными целочисленными значениями для Spring, Summer, Fall и Winter теперь являются 1, 2, 3 и 4.

Разрешается одному и тому же основному значению задавать более одного литерала перечисления. Например, в Великобритании время года fall (осень) называют autumn. Угодить обеим вариантам английского можно следующим образом:

enum Season { Spring, Summer, Fall, Autumn = Fall, Winter }

Выбор основного типа перечислений

При объявлении перечисления литералам перечисления задаются значения типа int. Можно также выбрать за основу перечисления другой базовый целочисленный тип. Например, чтобы объявить основной тип для Season не int, а short, можно использовать следующий код:

enum Season : short { Spring, Summer, Fall, Winter }

Основной смысл использования short заключается в экономии памяти: int занимает больше памяти, чем short, и если вы не нуждаетесь в полном диапазоне значений, доступных в int, практичнее будет воспользоваться типом данных с более скромными запросами.

Заложить основу перечисления можно с помощью любого из восьми целочисленных типов: byte, sbyte, short, ushort, int, uint, long или ulong. Значения всех литералов перечисления должны входить в диапазон избранного основного типа. Например, если за основу перечисления выбрать тип данных byte, у вас может быть максимум 256 литералов (начиная с нуля).

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

Создание и использование перечисления

Откройте в среде Microsoft Visual Studio 2015 проект StructsAndEnums, который находится в папке \Microsoft Press\VCSBS\Chapter 9\StructsAndEnums вашей папки документов.

Выведите в окно редактора файл Month.cs. В исходном файле нет ничего, кроме объявления пространства имен StructsAndEnums и комментария // TODO:.

Замените комментарий // TODO: выделенным в следующем примере кода жирным шрифтом перечислением по имени Month, находящемся в пространстве имен StructsAndEnums. Это перечисление моделирует месяцы года. В качестве 12 литералов перечисления Month фигурируют названия месяцев от January до December.

namespace StructsAndEnums

{

    enum Month

    {

        January, February, March, April,

        May, June, July, August,

        September, October, November, December

    }

}

Выведите в окно редактора файл Program.cs. Метод Main, как и в упражнениях из предыдущих глав, вызывает метод doWork и перехватывает любые возникающие исключения.

Добавьте в окне редактора к методу doWork еще одну инструкцию, объявляющую Month-переменную по имени first, и инициализируйте ее значением Month.January. Добавьте еще одну инструкцию для записи значения переменной first в консоль. Метод doWork должен приобрести следующий вид:

static void doWork()

{

    Month first = Month.January;

    Console.WriteLine(first);

}

173613.png

ПРИМЕЧАНИЕ Как только после Month будет набрана точка, среда Microsoft IntelliSense автоматически выведет все значения, имеющиеся в перечислении Month.

Щелкните в меню Отладка на пункте Запуск без отладки. Среда Visual Studio 2015 выполнит сборку и запуск программы. Убедитесь в том, что в консоль будет записано слово January.

Нажмите Ввод, чтобы закрыть программу и вернуться в среду программирования Visual Studio 2015.

Добавьте к методу doWork еще две инструкции (в следующем примере кода выделены жирным шрифтом), предназначенные для выполнения операции инкремента переменной first и вывода ее нового значения на консоль:

static void doWork()

{

    Month first = Month.January;

    Console.WriteLine(first);

    first++;

    Console.WriteLine(first);

}

Щелкните в меню Отладка на пункте Запуск без отладки. Среда Visual Studio 2015 произведет сборку и запуск программы. Убедитесь в том, что в консоль записаны слова January и February.

Заметьте, что выполнение математической операции, такой как инкремент, в отношении переменной перечисления изменяет внутреннее целочисленное значение переменной. Когда переменная записывается в консоль, на экран выводится соответствующее значение перечисления. Нажмите Ввод, чтобы закрыть программу и вернуться в среду программирования Visual Studio 2015.

Измените, как показано жирным шрифтом в следующем примере, первую инструкцию в методе doWork, чтобы инициализировать переменную first значением Month.December:

static void doWork()

{

    Month first = Month.December;

    Console.WriteLine(first);

    first++;

    Console.WriteLine(first);

}

Щелкните в меню Отладка на пункте Запуск без отладки. Среда Visual Studio 2015 произведет сборку и запуск программы. На этот раз в консоль будет записано слово December, за которым будет следовать число 12 (рис. 9.1).

09_01.tif 

Рис. 9.1

Выполнять арифметические операции в отношении перечисления вполне допустимо, но если результат операции находится вне диапазона значений, определенных для перечисления, то все, что может сделать среда выполнения, — это интерпретировать значение переменной как соответствующее целочисленное значение. Нажмите Ввод, чтобы закрыть программу и вернуться в среду программирования Visual Studio 2015.

Работа со структурами

В главе 8 было показано, что классы определяют ссылочные типы, которые всегда создаются в динамической памяти. В некоторых случаях класс может содержать настолько малые по объему данные, что издержки на управление динамической памятью становятся неоправданно высокими. В таких случаях лучше определить тип как структуру. Структура относится к типу значений. Поскольку структура хранится в стеке, то, пока она невелика, издержки на управление памятью зачастую снижаются.

Подобно классу, у структуры могут быть свои собственные поля, методы и (за важным исключением, рассматриваемым далее) конструкторы.

Общие типы структур

Хотя вы этого и не осознавали, в предыдущих примерах книги структуры уже использовались. В C# такие простые числовые типы, как int, long и float, являются псевдонимами структур System.Int32, System.Int64 и System.Single соответственно. У этих структур имеются поля и методы, и эти методы могут вызываться в отношении переменных и литералов этих типов. Например, все эти структуры предоставляют метод ToString, проводящий преобразование числового значения в его строковое представление. В C# вполне допустимы все следующие инструкции:

int i = 55;

Console.WriteLine(i.ToString());

Console.WriteLine(55.ToString());

float f = 98.765F;

Console.WriteLine(f.ToString());

Console.WriteLine(98.765F.ToString());

Слишком часто видеть такое использование метода ToString не приходилось, поскольку метод Console.WriteLine вызывает его автоматически по мере надобности. Чаще используются предоставляемые этими структурами статические методы. Например, в предыдущих главах использовался метод int.Parse, занимающийся преобразованием строки в соответствующее ей целочисленное значение. Фактически при этом вызывался метод Parse структуры Int32:

string s = "42";

int i = int.Parse(s); // абсолютно то же самое, что и Int32.Parse

Эти структуры включают также ряд полезных статических полей. Например, Int32.MaxValue является максимальным значением, которое может содержаться в int-переменной, а Int32.MinValue является соответственно минимально возможным значением такой переменной.

В следующей таблице показаны простые типы, имеющиеся в C#, и эквивалентные им типы в среде Microsoft .NET Framework. Обратите внимание на то, что типы string и object являются классами (ссылочными типами), а не структурами.

Ключевое слово

Эквивалент типа

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

bool

System.Boolean

Структура

byte

System.Byte

Структура

decimal

System.Decimal

Структура

double

System.Double

Структура

float

System.Single

Структура

int

System.Int32

Структура

long

System.Int64

Структура

object

System.Object

Класс

sbyte

System.SByte

Структура

short

System.Int16

Структура

string

System.String

Класс

uint

System.UInt32

Структура

ulong

System.UInt64

Структура

ushort

System.UInt16

Структура

Объявление структуры

Для объявления своей собственной структуры используется ключевое слово struct, за которым следуют имя типа и заключенное в фигурные скобки тело структуры. Синтаксически процесс похож на объявление класса. Вот как, к примеру, выглядит структура по имени Time, содержащая три открытых int-поля с именами hours, minutes и seconds:

struct Time

{

    public int hours, minutes, seconds;

}

Как и при объявлении классов, в большинстве случаев указывать поля открытыми нежелательно, поскольку управлять значениями, содержащимися в открытых полях, невозможно. Например, установить для minutes или seconds значение, превышающее 60, может кто угодно. Лучше сделать поля закрытыми и обеспечить структуру конструкторами и методами для инициализации этих полей и управления ими:

struct Time

{

    private int hours, minutes, seconds;

    ...

    public Time(int hh, int mm, int ss)

    {

        this.hours = hh % 24;

        this.minutes = mm % 60;

        this.seconds = ss % 60;

    }

 

    public int Hours()

    {

        return this.hours;

    }

}

173619.png

ПРИМЕЧАНИЕ Автоматически использовать многие типовые операторы в ваших собственных структурах не получится. Например, к переменным, относящимся к вашей собственной структуре, невозможно применять операторы равенства (==) и неравенства (!=). Но для сравнения переменных типа структуры можно использовать встроенный метод Equals(), предоставляемый всеми структурами, также можно для типов своей собственной структуры объявить явным образом и реализовать операторы. Используемый при этом синтаксис рассматривается в главе 21 «Запрос данных, находящихся в памяти, с помощью выражений в виде запросов».

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

175194.png

СОВЕТ Используйте структуры для реализации простых понятий, чьей основной характеристикой является их значение, а не предоставляемые ими функциональные возможности.

Основные сведения о том, чем структуры отличаются от классов

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

Для структуры невозможно объявить пассивный конструктор (конструктор без параметров). Следующий пример был бы скомпилирован, если бы Time был классом, но он не пройдет компиляцию, потому что Time — это структура:

struct Time

{

    public Time() { ... } // ошибка в ходе компиляции

    ...

}

Причина, по которой вы не можете объявить для структуры свой собственный пассивный конструктор, заключается в том, что компилятор всегда создает такой конструктор самостоятельно. В классе компилятор создает пассивный конструктор только в том случае, если вы не создали его сами. Созданный компилятором пассивный конструктор для структуры всегда устанавливает для полей значения 0, false или null. Он делает это точно так же, как и для класса. Поэтому нужно убедиться в том, что значение структуры, созданное пассивным конструктором, ведет себя логично и имеет с этими исходными значениями вполне определенный смысл. Имеющиеся разновидности будут исследованы в следующем упражнении.

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

struct Time

{

    private int hours, minutes, seconds;

    ...

    public Time(int hh, int mm)

    {

        this.hours = hh;

        this.minutes = mm;

    }     // ошибка в ходе компиляции: поле seconds не проинициализировано

}

В классе можно инициализировать поля экземпляра в тех же местах, где они объявляются. В структуре этого делать нельзя. Следующий пример был бы откомпилирован, если бы объявление Time относилось к классу, но в ходе компиляции возникнет ошибка, поскольку Time объявляется структурой:

struct Time

{

    private int hours = 0; // ошибка в ходе компиляции

    private int minutes;

    private int seconds;

    ...

}

Основные отличия структур от классов сведены в табл. 9.1.

Таблица 9.1

Вопрос

Структура

Класс

Это тип значения или ссылочный тип?

Структура относится к типу значения

Класс относится к ссылочному типу

Где размещается экземпляр, в стеке или в динамической памяти?

Экземпляры структуры называются значениями и размещаются в стеке

Экземпляры классов называются объектами и размещаются в динамической памяти

Можно ли объявить пассивный конструктор?

Нет

Да

Если вы объявите собственный конструктор, будет ли компилятор непременно создавать пассивный конструктор?

Да

Нет

Если вы в собственном конструкторе не проинициализируете поле, проинициализирует ли компилятор это поле автоматически?

Нет

Да

Разрешено ли выполнять инициализацию полей экземпляра в месте их объявления?

Нет

Да

Есть и другие отличия классов от структур, касающиеся наследования. Они будут рассмотрены в главе 12 «Работа с наследованием».

Объявление переменных структуры

После определения типа структуры им можно воспользоваться абсолютно так же, как и любым другим типом. Например, если была определена структура Time, можно создать переменные, поля и параметры типа Time:

struct Time

{

    private int hours, minutes, seconds;

    ...

}

class Example

{

    private Time currentTime;

 

    public void Method(Time parameter)

    {

        Time localVariable;

        ...

    }

}

173627.png

ПРИМЕЧАНИЕ Как и в случае с перечислениями, можно создать версию структурной переменной, допускающую пустые значения, воспользовавшись для этого модификатором в виде вопросительного знака (?). После чего переменной можно будет присвоить пустое значение.

Time? currentTime = null;

Представление об инициализации структуры

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

Time now = new Time();

Состояние полей в этой структуре показано на рис. 9.2.

184830.png 

Рис. 9.2

Но поскольку структуры относятся к типам значений, можно также создавать структурные переменные, не вызывая конструктор:

Time now;

На этот раз переменная создана, но поля остаются в своем не прошедшем инициализацию состоянии. На рис. 9.3 показано состояние полей переменной now. Любая попытка обращения к значениям этих полей приведет к ошибке в ходе компиляции.

184869.png 

Рис. 9.3

Заметьте, что в обоих случаях переменная now создается в стеке.

Написав свой собственный конструктор структуры, можете использовать его также для инициализации структурной переменной. В соответствии с теми объяснениями, которые уже давались в этой главе, конструктор структуры всегда должен инициализировать все свои поля, например:

struct Time

{

    private int hours, minutes, seconds;

    ...

    public Time(int hh, int mm)

    {

        hours = hh;

        minutes = mm;

        seconds = 0;

    }

}

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

Time now = new Time(12, 30);

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

Пора применить эти знания на практике. В следующем упражнении будет создана и использована структура для представления даты.

184903.png 

Рис. 9.4

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

Выведите в окно редактора файл Date.cs, принадлежащий проекту StructsAndEnums. Замените комментарий TODO структурой по имени Date, находящейся в пространстве имен StructsAndEnums.

В этой структуре должны содержаться три закрытых поля: одно типа int с именем year, другое типа Month с именем month (с использованием созданного в предыдущем упражнении перечисления) и третье типа int с именем day. Структура Date должна иметь следующий вид:

struct Date

{

    private int year;

    private Month month;

    private int day;

}

Рассмотрим пассивный конструктор, который компилятор должен создать для Date. Этот конструктор установит для year значение 0, для month — значение 0 (это значение соответствует литералу January) и для day — значение 0. Значение 0 для year не подойдет, поскольку такого года не существует, не подойдет оно и для day, так как каждый месяц начинается с первого числа. Одним из способов устранения этой проблемы является перевод значений year и day путем такой реализации структуры Date, при которой поле year содержит значение Y, которое представляет собой год Y + 1900 (или, если нужно, можно выбрать другое столетие), а поле day содержит значение D, представляющее день D + 1. Тогда пассивный конструктор назначит трем полям значения, представляющие дату 1 января 1900 года.

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

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

Добавьте к структуре Date открытый конструктор. Этот конструктор должен принимать три параметра: int-параметр по имени ccyy для поля year, Month-параметр по имени mm для месяца и int-параметр по имени dd для дня. Используйте эти три параметра для инициализации соответствующих полей. Поле year со значением Y представляет год Y + 1900, поэтому вам нужно инициализировать поле year значением ccyy 1900. Поле day со значением D представляет день D + 1, следовательно, нужно инициализировать поле day значением dd 1.

Теперь структура Date должна выглядеть следующим образом (конструктор выделен жирным шрифтом):

struct Date

{

    private int year;

    private Month month;

    private int day;

 

    public Date(int ccyy, Month mm, int dd)

    {

        this.year = ccyy - 1900;

        this.month = mm;

        this.day = dd - 1;

    }

}

Добавьте к структуре Date после конструктора открытый метод ToString. Этот метод не получает аргументов и возвращает строку, представляющую дату. Вспомним, что значение поля year представляет year + 1900, а значение поля day представляет day + 1.

Метод ToString должен выглядеть следующим образом:

struct Date

{

    ...

    public override string ToString()

    {

        string data = $"{this.month} {this.day + 1} {this.year + 1900}";

        return data;

    }

}

173633.png

ПРИМЕЧАНИЕ Метод ToString немного отличается от тех методов, которые вы видели до сих пор. Любой тип, включая определяемые вами структуры и классы, хотите вы того или нет, автоматически уже имеет метод ToString. Изначально он преобразует данные, хранящиеся в переменной, в строку, являющуюся представлением этих данных. Иногда это исходное поведение имеет вполне определенный смысл, а иногда смысл особо не прослеживается. Например, исходное поведение метода ToString, созданного для структуры Date, просто заключается в создании строки «StructsAndEnums.Date». По выражению Зафода Библброкса, персонажа книги «Автостопом по галактике», автором которой является Дуглас Адамс, это «проникновенно, но скучновато». Вам нужно с помощью ключевого слова override определить новую версию этого метода, переопределяющую исходное поведение. Более подробно перегружаемые методы рассматриваются в главе 12.

В этом методе создается отформатированная строка, использующая текстовое представление значения поля month, а также выражения this.day + 1 и this.year + 1900. В качестве результата метод ToString возвращает отформатированную строку.

Выведите в окно редактора файл Program.cs. Закомментируйте в методе doWork четыре имеющиеся в нем инструкции. Добавьте к методу doWork инструкции, объявляющие локальную переменную по имени defaultDate, и проинициализируйте ее Date-значением, сконструированным путем использования пассивного Date-конструктора. Добавьте к методу doWork еще одну инструкцию, выводящую значение переменной defaultDate на консоль с помощью вызова метода Console.WriteLine.

173639.png

ПРИМЕЧАНИЕ Метод Console.WriteLine для форматирования своего аргумента в виде строки автоматически вызывает для него метод ToString.

Теперь метод doWork должен приобрести следующий вид:

static void doWork()

{

    ...

    Date defaultDate = new Date();

    Console.WriteLine(defaultDate);

}

173647.png

ПРИМЕЧАНИЕ После набора new Date() система IntelliSense автоматически определяет, что типу Date доступны два конструктора.

Щелкните в меню Отладка на пункте Запуск без отладки, чтобы выполнить сборку и запуск программы. Убедитесь в том, что на консоль будет записана дата January 1 1900.

Нажмите Ввод для возвращения в среду программирования Visual Studio 2015.

Вернитесь в окне редактора к методу doWork и добавьте еще две инструкции. В первой инструкции объявите локальную переменную по имени weddingAnniversary и инициализируйте ее значением July 4 2015. (Я действительно женился в День независимости, хотя это было много лет назад.) Во второй инструкции запишите в консоль значение weddingAnniversary.

Теперь метод doWork должен выглядеть так:

static void doWork()

{

    ...

    Date weddingAnniversary = new Date(2015, Month.July, 4);

    Console.WriteLine(weddingAnniversary);

}

Щелкните в меню Отладка на пункте Запуск без отладки, а затем убедитесь в том, что в консоль ниже предыдущей информации записана дата July 4 2015. Нажмите Ввод и вернитесь в среду Visual Studio 2015.

Копирование структурных переменных

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

Date now = new Date(2012, Month.March, 19);

Date copy = now;

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

Следующий пример не пройдет компиляцию, потому что переменная now не инициализирована:

Date now;

Date copy = now; // ошибка в ходе компиляции: now не присвоено значение

При копировании структурной переменной каждое поле той переменной, которая находится слева от оператора присваивания, получает значение непосредственно из соответствующего ему поля переменной, расположенной справа от него. Копирование осуществляется в виде быстрой единой операции, копирующей содержимое всей структуры, и при этом никогда не выдается исключение. Сравните такое поведение с тем, которое наблюдалось бы, будь Time классом, при котором обе переменные, now и copy, в конечном итоге указывали бы на один и тот же объект в динамической памяти.

185005.png 

Рис. 9.5

173654.png

ПРИМЕЧАНИЕ При наличии опыта программирования на C++ вы должны заметить, что поведение при копировании настройке не поддается.

В заключительном упражнении данной главы вы сопоставите поведение при копировании структуры с поведением при копировании класса.

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

Выведите в окно редактора файл Date.cs, принадлежащий проекту StructsAndEnums. Добавьте к структуре Date следующий метод, увеличивающий дату структуры на один месяц. Если после этого значение поля month выйдет за рамки December, код переключит month на January и увеличит значение поля year на единицу:

struct Date

{

    ...

    public void AdvanceMonth()

    {

        this.month++;

        if (this.month == Month.December + 1)

        {

            this.month = Month.January;

            this.year++;

        }

    }

}

Выведите в окно редактора файл Program.cs. Закомментируйте в методе doWork первые две незакомментированные инструкции, создающие и выводящие на экран значение переменой defaultDate.

Добавьте следующий код, выделенный далее жирным шрифтом, в конец метода doWork. Этот код создает копию переменной weddingAnniversary под названием weddingAnniversaryCopy и выводит значение новой переменной на экран:

static void doWork()

{

    ...

    Date weddingAnniversaryCopy = weddingAnniversary;

    Console.WriteLine($"Value of copy is {weddingAnniversaryCopy}");

}

Добавьте следующие выделенные далее жирным шрифтом инструкции в конец метода doWork. Эти инструкции вызывают метод AdvanceMonth переменной weddingAnniversary, а затем выводят значения переменных weddingAnniversary и weddingAnniversaryCopy на экран:

static void doWork()

{

    ...

    weddingAnniversary.AdvanceMonth();

    Console.WriteLine($"New value of weddingAnniversary is

                                     {weddingAnniversary}");

    Console.WriteLine($"Value of copy is still {weddingAnniversaryCopy}");

}

Щелкните в меню Отладка на пункте Запуск без отладки, чтобы выполнить сборку и запуск приложения. Убедитесь в том, что в окно консоли выводятся следующие сообщения:

July 4 2015

Value of copy is July 4 2015

New value of weddingAnniversary is August 4 2015

Value of copy is still July 4 2015

В первом сообщении выводится исходное значение переменной weddingAnni­versary (July 4 2015). Во втором сообщении выводится значение переменной weddingAnniversaryCopy. Вы можете увидеть, что оно содержит те же данные, которые хранятся в переменной weddingAnniversary (July 4 2015). В третьем сообщении выводится значение переменной weddingAnniversary после изменения месяца на август (August 4 2015). И в последнем сообщении выводится значение переменной weddingAnniversaryCopy. Обратите внимание на то, что по сравнению с исходным значением July 4 2015 оно не изменилось.

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

Нажмите Ввод и вернитесь в среду Visual Studio 2015.

Выведите в окно редактора файл Date.cs. Измените структуру Date, превратив ее, как показано жирным шрифтом, в класс:

class Date

{

    ...

}

Щелкните в меню Отладка на пункте Запуск без отладки, чтобы снова выполнить сборку и запуск приложения. Убедитесь, что в окно консоли выведены следующие сообщения:

July 4 2015

Value of copy is July 4 2015

New value of weddingAnniversary is August 4 2015

Value of copy is still August 4 2015

Первые три сообщения совпадают с предыдущими. А вот четвертое сообщение показывает, что значение переменной weddingAnniversaryCopy изменилось на August 4 2015.

Нажмите Ввод, чтобы вернуться в среду Visual Studio 2015.

Структуры и совместимость со средой выполнения Windows

Все приложения C# выполняются с использованием общеязыковой среды выполнения (common language runtime (CLR)), относящейся к среде .NET Framework. CLR отвечает за предоставление для кода вашего приложения безопасной во всех отношениях среды в виде виртуальной машины (если у вас есть опыт работы с Java, то эта концепция должна быть вам знакома). При компиляции приложения на C# компилятор преобразует ваш код C# в набор инструкций, используя псевдомашинный код, который называется общим промежуточным языком (Common Intermediate Language (CIL)). Эти инструкции хранятся в сборке. При запуске приложения на C# CLR отвечает за преобразование CIL-инструкций в реальные инструкции машины, понимаемые и выполняемые центральным процессором вашего компьютера. Все это окружение известно под именем управляемой среды выполнения, и программы на C# зачастую называют управляемым кодом. Управляемый код можно создавать и на других языках, поддерживаемых .NET Framework, например Visual Basic и F#.

На Windows 7 и более ранних версий можно было кроме этого создавать неуправляемые приложения, известные также как внутренний код, основанный на Win32 API-интерфейсах, взаимодействующих непосредственно с операционной системой Windows. (Если вы запускаете управляемое приложение, CLR также преобразует многие функции в .NET Framework в вызовы Win32 API, хотя этот процесс совершенно открыт для вашего кода.) Для этого можно воспользоваться языком C++. Среда .NET Framework позволяет интегрировать управляемый код в неуправляемые приложения посредством набора технологий обеспечения взаимодействия. Подробности работы этих технологий и порядок их использования в данной книге не рассматриваются, и нам вполне достаточно знать, что они не всегда носят простой и понятный характер.

Последние версии Windows предоставляют альтернативную стратегию в виде среды выполнения Windows Runtime, или WinRT. Эта среда является надстройкой над Win32 API и другими избранными исходными API-интерфейсами Windows, предоставляющей однородную функциональность на различных типах оборудования, от серверов до смартфонов. При создании приложения универсальной платформы (Universal Windows Platform (UWP)) используются API-интерфейсы, предоставляемые WinRT, а не Win32. Аналогично этому CLR на Windows 10 также использует WinRT: весь управляемый код, написанный с использованием C# или любого другого управляемого языка, по-прежнему выполняется с помощью CLR, но в ходе выполнения CLR преобразует ваш код в вызовы WinRT API, а не в вызовы Win32. CLR и WinRT совместно отвечают за безопасное управление вашим кодом и его выполнение.

Основным назначением WinRT является упрощение языкового взаимодействия, направленное на облегчение интегрирования компонентов, разработанных с использованием различных языков программирования, в единое цельное приложение. Но за эту простоту приходится платить, и вы должны быть готовы идти на ряд компромиссов, основанных на доступности различных функциональных наборов разных языков. В частности, в силу исторических причин язык C++ хоть и поддерживает структуры, но не распознает принадлежащие им функции. В понятиях C# входящая в состав структуры функция является методом экземпляра. Следовательно, если выполняется сборка структур C#, которые требуется упаковать в библиотеку, сделав их доступными для разработчиков, программирующих на C++ (или на других неуправляемых языках), эти структуры не должны содержать никаких методов экземпляров. Аналогичные ограничения действуют в структурах и по отношению к статическим методам. Если нужно включить методы экземпляров или статические методы, следует преобразовать вашу структуру в класс. Кроме того, структуры не могут содержать закрытые поля, а все открытые поля должны относиться к простым типам C#, соответствующим типам значений или строкам.

WinRT налагает и некоторые другие ограничения на те классы и структуры C#, которые требуется сделать доступными для обычных приложений. Дополнительные сведения по этому вопросу можно будет найти в главе 12.

Выводы

В этой главе вы увидели, как создаются и используются перечисления и структуры. Узнали, в чем сходство и различия структуры и класса, и увидели, как определяются конструкторы для инициализации полей структуры. Вы также увидели, как можно представить структуру в виде строки путем переопределения метода ToString.

Если хотите продолжить работу и изучить следующую главу, оставьте открытой среду Visual Studio 2015 и переходите к главе 10 «Использование массивов».

Если хотите выйти из среды Visual Studio 2015, то в меню Файл щелкните на пункте Выход. Если увидите диалоговое окно с предложением сохранить изменения, щелкните на кнопке Да и сохраните проект.

Краткий справочник

Чтобы

Сделайте следующее

Объявить перечисление

Напишите ключевое слово enum, за ним укажите имя типа, а затем в фигурных скобках напишите через запятые список литеральных имен перечисления, например:

enum Season { Spring, Summer, Fall, Winter }

Объявить переменную перечисления

Напишите слева название перечисления, а затем имя переменной, поставив после него точку с запятой, например:

Season currentSeason;

Присвоить значение переменной перечисления

Напишите имя литерала перечисления, указав также имя того перечисления, которому оно принадлежит, например:

currentSeason = Spring;        // ошибка

currentSeason = Season.Spring; // правильно

Объявить структурный тип

Напишите ключевое слово struct, после которого укажите имя структурного типа, а затем укажите тело структуры (конструкторы, методы и поля), например:

struct Time

{

    public Time(int hh, int mm, int ss)

    { ... }

    ...

    private int hours, minutes, seconds;

}

Объявить структурную переменную

Напишите имя структурного типа, за ним имя переменной, поставив после него точку с запятой, например:

Time now;

Инициализировать структурную переменную значением

Инициализируйте переменную структурным значением путем вызова конструктора структуры, например:

Time lunch = new Time(12, 30, 0);

Назад: 8. Основные сведения о значениях и ссылках
Дальше: 10. Использование массивов

Антон
Перезвоните мне пожалуйста 8(812)642-29-99 Антон.
Антон
Перезвоните мне пожалуйста по номеру 8(904) 332-62-08 Антон.
Антон
Перезвоните мне пожалуйста, 8 (904) 606-17-42 Антон.
Антон
Перезвоните мне пожалуйста по номеру. 8 (953) 367-35-45 Антон
Ксения
Текст от профессионального копирайтера. Готово через 1 день. Консультация бесплатно. Жми roholeva(точка)com