Тверской государственный университет
Опубликован: 02.12.2009 | Доступ: свободный | Студентов: 3450 / 677 | Оценка: 4.41 / 4.23 | Длительность: 09:18:00
ISBN: 978-5-9963-0259-8
Лекция 2:

Типы и классы. Переменные и объекты

Аннотация: Вводятся понятия типа данных и класса. Рассматривается связь между этими понятиями. Показано, как объявляются переменные и объекты. Приводятся примеры. Большое внимание уделяется вводу и выводу переменных разных типов, контролю ввода. Предлагаются задачи на эту тему.
Ключевые слова: система типов, динамический тип, ПО, тип integer, переменная, связывание, множества, операции, значение, определение, объектный язык, свойства объектов класса, класс, объект, тип данных, встроенные типы, int, string, CAR, программирование, net, FCL, сложный тип, базис, строковый тип, статический тип, память, system, стек, очередь, список, деление, значимый тип, ссылка, Паскаль, Java, Visual Basic, логический, знание, базисный тип, стандарт языка, типизированный указатель, куча, heap, доступ, область действия, динамическая переменная, длина, слово, ссылочный тип, наследование, развернутый тип, double, long, программа, имя переменной, тело оператора цикла, вывод, главная кнопочная форма, файл, метка, заголовок метода, механизм делегатов, охраняемые блоки, классы исключений, выбрасывание исключений, универсальный обработчик, управляющий ввод, объявление с отложенной инициализацией, garbage collector, события элемента управления, константы, синтаксис, инициализация, выражение, интерфейс, статические методы, поле, контейнер, архитектура, командная кнопка, модальная форма, Visual Studio

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

Общий взгляд

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

В первых языках программирования понятие класса отсутствовало - рассматривались только типы данных. При определении типа явно задавалось только множество возможных значений, которые могут принимать переменные этого типа. Например, утверждалось, что тип integer задает целые числа в некотором диапазоне. Неявно с типом всегда связывался и набор разрешенных операций. В строго типизированных языках, к которым относится большинство языков программирования, каждая переменная в момент ее объявления связывалась с некоторым типом. Связывание переменной x с типом Т означало, что переменная x может принимать только значения из множества, заданного типом T, и к ней применимы операции, разрешенные этим типом. Таким образом, тип определял, говоря современным языком, свойства и поведение переменных. Значение переменной задавало ее свойства, а операции над ней - ее поведение, то есть то, что можно делать с этой переменной.

Классы и объекты впервые появились в программировании в языке Симула 67. Произошло это спустя 10 лет после появления первого алгоритмического языка Фортран. Определение класса наряду с описанием данных уже тогда содержало четкое определение операций или методов, применимых к данным. Классы стали естественным обобщением понятия типа, а объекты - экземпляры класса - стали естественным обобщением понятия переменной. Сегодня определение класса в C# и других объектных языках содержит:

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

Так есть ли различие между основополагающими понятиями - типом и классом, переменной и объектом? Такое различие существует. Программистам нужны все эти понятия. Но определить это различие не так-то просто. Мне до сих пор не удается точно описать все ситуации, в которых следует использовать только понятие "тип", и ситуации, в которых приемлемо только применение понятия "класс". Во многих ситуациях эти понятия становятся синонимичными. Если, например, есть объявление <T x;>, то можно говорить, что объявлена переменная типа T, но столь же справедливо утверждение, что данное объявление задает объект x класса T. На первых порах можно считать, что класс - это хорошо определенный тип данных, объект - хорошо определенная переменная.

Есть традиционные предпочтения. Базисные встроенные типы, такие, как int или string, предпочитают называть по-прежнему типами, а их экземпляры - переменными. Когда же речь идет о создании собственных типов, моделирующих, например, такие абстракции данных, как множество автомобилей или множество служащих, то естественнее говорить о классах Car и Employee, а экземпляры этих классов называть объектами.

Объектно-ориентированное программирование, доминирующее сегодня, построено на классах и объектах. Тем не менее, понятия типа и переменной все еще остаются центральными при описании языков программирования, что характерно и для языка C#. Заметьте, что в Framework .Net предпочитают говорить о системе типов, хотя все типы библиотеки FCL являются классами.

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

Есть и другие критерии классификации типов. Так, типы разделяются на встроенные типы и типы, определенные программистом (пользователем). Встроенные типы изначально принадлежат языку программирования и составляют его базис. В основе системы типов любого языка программирования всегда лежит базисная система типов, встроенных в язык. На их основе программист может строить собственные, им самим определенные типы данных. Но способы (правила) создания таких типов являются базисными, встроенными в язык. Принято считать, что встроенными в язык C# являются арифметические типы, булевский и строковый тип, что также в язык встроен механизм построения массивов из переменных одного типа. Эти встроенные типы будем называть базисными. Базисные встроенные типы должны быть реализованы любым компилятором, отвечающим стандарту языка C#.

Язык C#, рассматриваемый в данном курсе, изначально предполагает реализацию, согласованную с Framework .Net. Это означает, что все базисные встроенные типы проецируются на соответствующие типы библиотеки FCL. Библиотека FCL реализует базис языка C#. Но помимо этого, она предоставляет в распоряжение программиста множество других полезных типов данных. Так что для нашей реализации языка C# встроенных типов огромное число - вся библиотека FCL. Знать все типы из этой библиотеки практически невозможно, но умение ориентироваться в ней необходимо.

Типы данных разделяются также на статические и динамические. Для переменных статического типа память под данные отводится в момент объявления, требуемый размер данных известен при их объявлении. Для динамического типа размер данных в момент объявления обычно не известен, и память им выделяется динамически в процессе выполнения программы. Многие динамические типы, доступные разработчикам проектов на C#, реализованы как встроенные типы в библиотеке FCL. Например, в пространстве имен этой библиотеки System.Collections находятся классы Stack, Queue, ListArray и другие классы, описывающие широко распространенные динамические типы данных - стек, очередь, список, построенный на массиве.

Возможно, наиболее важным для C# программистов является деление типов на значимые и ссылочные. Для значимых типов, называемых также развернутыми, значение переменной (объекта) является неотъемлемой собственностью переменной (точнее, собственностью является память, отводимая значению, само значение может изменяться). Значимый тип принято называть развернутым, подчеркивая тем самым, что значение объекта развернуто непосредственно в памяти, отводимой объекту.

Для ссылочных типов значением служит ссылка на некоторый объект в памяти, расположенный обычно в динамической памяти - "куче". Объект, на который указывает ссылка, может быть разделяемым. Это означает, что несколько ссылочных переменных могут указывать на один и тот же объект и разделять его значения. О ссылочных и значимых типах еще предстоит обстоятельный разговор.

Для большинства процедурных языков, реально используемых программистами, - Паскаль, C++, Java, Visual Basic, C#, - базисная система встроенных типов более или менее одинакова. Всегда в языке присутствуют арифметический, логический (булев), символьный типы. Арифметический тип всегда разбивается на подтипы. Всегда допускается организация данных в виде массивов и записей (структур). Внутри арифметического типа всегда допускаются преобразования, всегда есть функции, преобразующие строку в число и обратно. Так что знание, по крайней мере, одного из процедурных языков позволяет построить общую картину базисной системы типов и для языка C#. Язык C# многое взял от языка C++, системы базисных типов этих двух языков близки и совпадают вплоть до названия типов и областей их определения. Но отличия, в том числе принципиального характера, есть и здесь.

Система типов

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

  1. Логический тип ( bool ).
  2. Символьный тип ( char ).
  3. Целые типы. Целые типы могут быть одного из трех размеров - short, int, long, сопровождаемые описателем signed или unsigned, который указывает, как интерпретируется значение - со знаком или без оного.
  4. Типы с плавающей точкой. Эти типы также могут быть одного из трех размеров - float, double, long double.

    Кроме того, в языке есть

  5. Тип void, используемый для указания на отсутствие информации.

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

  6. Указатели (например, int* - типизированный указатель на переменную типа int).
  7. Ссылки (например, double& - типизированная ссылка на переменную типа double).
  8. Массивы (например, char[] - массив элементов типа char).

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

  9. Перечислимые типы ( enum ) для представления значений из конкретного множества.
  10. Структуры ( struct ).
  11. Классы ( class ).

Первые три вида типов называются интегральными или счетными. Значения их перечислимы и упорядочены. Целые типы и типы с плавающей точкой относятся к арифметическому типу. Типы подразделяются также на встроенные и определенные пользователем.

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

  • типы-значения ( value ), или значимые типы;
  • ссылочные ( reference );
  • указатели ( pointer );
  • тип void.

Эта классификация основана на том, где и как хранятся значения типов. Переменные или, что то же в данном контексте, - объекты, хранят свои значения в памяти компьютера, которую будем называть памятью типа "Стек" (Stack). Другой вид памяти, также связанный с хранением значений переменной, будем называть памятью типа "Куча" (Heap). Об особенностях этих двух видов памяти поговорим позже. Сейчас для нас важно понимать следующее. Для значимого типа значение переменной хранится непосредственно в стеке. Поскольку значение может быть сложным и состоять, например, из множества скалярных значений, говорят, что значение разворачивается в стеке. По этой причине значимый тип называют также развернутым типом. Для ссылочного типа значение в стеке задает ссылку на область памяти в "куче", где хранятся собственно данные, задающие значение. Данные, хранящиеся в куче, в этом случае называют объектом, а значение, хранящееся в стеке - ссылкой на объект. Самое важное в этой модели хранения значений - это то, что разные ссылки в стеке могут указывать на один и тот же объект из кучи. И тогда у этого объекта существует много разных имен (псевдонимов), каждое из которых позволяет получить доступ к полям объекта и изменять хранящиеся там значения.

В отдельную категорию выделены указатели, что подчеркивает их особую роль в языке. Указатели и ссылки в языке C# хотя и возможны, но в большинстве проектов используются редко. Отказ от этих средств делает программы более простыми, а самое главное более надежными. В нашем курсе эти средства практически появляться не будут. Ситуация похожа на ситуацию с оператором Goto - оператор доступен в языке, но не рекомендован к использованию. Если программист C# действительно хочет применять эти средства, то ему придется предпринять для этого определенные усилия, поскольку указатели имеют ограниченную область действия и могут использоваться только в небезопасных блоках, помеченных как unsafe.

Особый статус имеет и тип void, указывающий на отсутствие какого-либо значения.

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

Со структурами дело сложнее. Структуры C# представляют частный случай класса. Два объявления типа данных могут отличаться лишь одним ключевым словом, начинающим это объявление, - class или struct. В зависимости от того, какое ключевое слово использовано, данное объявление будет задавать класс (ссылочный тип) или структуру (значимый тип). Определив тип как структуру, программист получает возможность отнести класс к значимым типам, что иногда бывает крайне полезно. Замечу, что в хорошем объектном языке Eiffel программист может любой класс объявить развернутым (expanded), что эквивалентно отнесению к значимому типу. У программиста C# только благодаря структурам появляется возможность управлять отнесением класса к значимым или ссылочным типам. Правда, это не совсем полноценное средство, поскольку на структуры накладываются дополнительные и довольно жесткие ограничения по сравнению с обычными классами. В частности, для структур разрешено только наследование интерфейсов, и структура не может иметь в качестве родителя класс или структуру. Все развернутые типы языка C# - int, double и прочие - реализованы как структуры.

Все базисные встроенные типы C# однозначно отображаются, а фактически совпадают с системными типами каркаса Net Framework, размещенными в пространстве имен System. Поэтому всюду, где можно использовать имя типа, например, int, с тем же успехом можно использовать и имя System.Int32.

Замечание: следует понимать тесную связь и идентичность базисных встроенных типов языка C# и типов каркаса. Какими именами типов следует пользоваться в программных текстах - это спорный вопрос. Джеффри Рихтер в своей известной книге "Программирование на платформе Framework .Net" рекомендует использовать системные имена. Другие авторы считают, что следует пользоваться именами типов, принятыми в языке. Возможно, в модулях, предназначенных для межъязыкового взаимодействия, разумны системные имена, а в остальных случаях - имена конкретного языка программирования.

В заключение этого раздела приведу таблицу, содержащую описание базисных встроенных типов языка C# и их основные характеристики.

Таблица 2.1. Базисные встроенные типы языка C#
Логический тип
Имя типа Системный тип Значения Размер
bool System.Boolean true, false 8 бит
Арифметические целочисленные типы
Имя типа Системный тип Диапазон Размер
sbyte System.SByte [-128, 127] Знаковое, 8-бит
byte System.Byte [0, 255] Беззнаковое, 8-бит
short System.Int16 [-32768, 32767] Знаковое, 16-бит
ushort System.UInt16 [0, 65535] Беззнаковое, 16-бит
int System.Int32 [-231, 231] Знаковое, 32-бит
uint System.UInt32 [0, 232] Беззнаковое, 32-бит
long System.Int64 [-263 , 263] Знаковое, 64-бит
ulong System.UInt64 \approx(0, 2^{64}) Беззнаковое, 64-бит
Арифметический тип с плавающей точкой
Имя типа Системный тип Диапазон (по модулю) Точность
float System.Single [10-45, 1038] 7 цифр
double System.Double [10-324, 10308] 15-16 цифр
Арифметический тип с фиксированной точкой
Имя типа Системный тип Диапазон (по модулю) Точность
decimal System.Decimal [10-28, 1028] 28-29 значащих цифр
Символьные типы
Имя типа Системный тип Диапазон Точность
char System.Char [U+0000, U+ffff] 16-бит Unicode символ
string System.String Строка из символов Unicode
Объектный тип
Имя типа Системный тип Примечание
object System.Object Прародитель всех встроенных и пользовательских типов

Система базисных встроенных типов языка C# не только содержит практически все встроенные типы (за исключением long double) стандарта языка C++, но и перекрывает его разумным образом. В частности, тип string является встроенным в язык, что вполне естественно. В области совпадения сохранены имена типов, принятые в C++, что облегчает жизнь тем, кто привык работать на C++, но уже перешел на язык C#.

Переменные, объекты и сущности

Уже говорилось о том, что одна из главных ролей, которую играют классы в ОО-языках, - это роль типа данных. Класс, рассматриваемый как тип данных, задает описание свойств, поведения и событий некоторого множества элементов, называемых экземплярами класса, а чаще переменными или объектами. Заметьте, класс - это описание, это текст - статическая конструкция. Чтобы программа могла выполняться, в ней должны быть определены переменные или, что то же, объекты класса, создаваемые динамически в ходе выполнения программы. Напомню: в первой лекции объяснялось, что в начальный момент работы программы - в момент "большого взрыва" - создается первый объект, который становится текущим объектом, и начинает работать метод Main - точка входа в программу. Все дальнейшее зависит от содержания метода Main: какие новые объекты создаются, какие методы и свойства вызываются этими объектами.

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

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

Синтаксис объявления

Неформально уже отмечалось, что в момент объявления переменной указывается ее тип, имя и, возможно, значение. Давайте попробуем выразить это более формально. Синтаксис объявления сущностей в C# может быть задан следующей синтаксической формулой:

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

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

<имя> | <имя> = <инициализатор>

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

О типах языка C# мы уже кое-что знаем, так что готовы уточнить, как строятся объявления сущностей в простых случаях.

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

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

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

new <имя конструктора>([<список аргументов>])

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

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

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

Федор Антонов
Федор Антонов

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

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

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

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

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

Добрый день!

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