Типы и классы. Переменные и объекты
Переменные. Область видимости и время жизни
Давайте рассмотрим, где могут появляться объявления переменных, какую роль они играют в зависимости от уровня, на котором они объявлены. Рассмотрим такие важные характеристики переменных, как время их жизни и область видимости. Зададимся вопросом, как долго живут объявленные переменные и в какой области программы видимы их имена? Ответ зависит от того, где и как, в каком контексте объявлены переменные. В языке C# не так уж много возможностей для объявления переменных, пожалуй, меньше, чем в любом другом языке. Открою "страшную" тайну - здесь вообще нет настоящих глобальных переменных. Их отсутствие не следует считать некоторым недостатком C#, это достоинство языка. Но обо всем по порядку.
Поля класса
Первая важнейшая роль переменных - задавать свойства классов. В языке C#, как и в других ОО-языках, такие переменные называются полями (fields) класса. О классах и полях предстоит еще обстоятельный разговор, а сейчас сообщу лишь некоторые минимальные сведения, связанные с рассматриваемой темой.
Все переменные, объявленные на уровне класса при его описании, являются полями этого класса.
Поскольку класс, как уже многократно говорилось, задает описание типа данных, то поля класса задают представление этих данных. Необходимо крайне внимательно относиться к проектированию полей класса - всякие "лишние" объявления на этом уровне крайне нежелательны.
Когда конструктор класса создает очередной объект - экземпляр класса, то он в памяти создает набор полей, определяемых классом, и записывает в них значения, характеризующие свойства данного конкретного экземпляра. Так что каждый объект в памяти можно рассматривать как набор соответствующих полей класса со своими значениями. Заметьте, для классов, представленных структурой, объект создается аналогичным способом, но разворачивается в стеке.
Объекты в динамической памяти, с которыми не связана ни одна ссылочная переменная, становятся недоступными. Реально они оканчивают свое существование, когда сборщик мусора (garbage collector) выполнит чистку "кучи". Для значимых типов, к которым принадлежат экземпляры структур, жизнь оканчивается при завершении блока, в котором они объявлены.
Есть одно важное исключение. Некоторые поля могут жить дольше. Если при объявлении класса поле объявлено с модификатором static, то такое поле является частью модуля, связанного с классом, и не входит в состав его экземпляров. Поэтому static -поля живут так же долго, как и сам класс. Более подробно эти вопросы будут обсуждаться при рассмотрении классов, структур, интерфейсов.
Наследование и поля
Наследование классов - это одно из важнейших отношений, существующих между классами одного проекта. О нем мы будем говорить подробно, а сейчас рассмотрим только одну сторону наследования - что происходит с полями классов в процессе наследования. Пусть класс В является наследником класса А. Тогда класс В наследует все поля класса А. Наследник не может ни удалить поле родительского класса, ни изменить его тип. Наследник может лишь добавить собственные поля к уже имеющимся полям родителя. Таким образом, объекты класса наследника обладают всеми свойствами родителя и возможно дополнительным набором свойств.
Как создаются объекты класса наследника? Конструктор этого класса первым делом вызывает конструктор родителя, и тот создает объект родителя - коробочку с набором родительских полей. Только после этого конструктор наследника добавляет, если они есть, собственные поля к уже созданному объекту.
Все сказанное относится лишь к "настоящим" классам, представляющим ссылочный тип. Для развернутых классов, заданных структурой, отношение наследования не определено. Структуры могут иметь в качестве родительских классов лишь интерфейсы.
Область видимости полей класса
Поля класса являются глобальными переменными класса. Они видимы во всех методах этого класса. Каждый метод класса может читать и изменять значение любого поля класса независимо от того, какие атрибуты доступа установлены для полей и методов класса.
Если в теле метода объявлена локальная переменная, имя которой совпадает с именем поля класса, то такая ситуация не приводит к ошибке, поскольку конфликт имен разрешим. К полю класса можно добраться, используя уточненное имя поля с префиксом this, задающим имя текущего объекта.
Поля класса видимы не только в пределах самого класса. Если в некотором классе В объявлен и создан объект класса А, то класс В является клиентом класса А. В классе клиенте у объекта видны лишь те поля класса, для которых в момент объявлении был задан атрибут доступа public, что делает эти поля общедоступными. Для классов наследников у объекта видны поля с атрибутами public или protected, но недоступны поля с атрибутом private.
Имеет место следующая иерархия доступности полей объекта в зависимости от значения атрибута доступа - public, protected, private. В самом классе доступны все поля. У наследников не доступны закрытые поля с атрибутом private, у клиентов не доступны закрытые поля и защищенные поля - поля с атрибутами private и protected.
Глобальные переменные уровня модуля. Существуют ли они в C#?
Где еще могут объявляться переменные? Во многих языках программирования переменные могут объявляться на уровне модуля. Такие переменные называются глобальными. Их область действия распространяется, по крайней мере, на весь модуль. Глобальные переменные играют важную роль, поскольку они обеспечивают весьма эффективный способ обмена информацией между различными частями модуля. Обратная сторона эффективности аппарата глобальных переменных - их опасность. Если какая-либо процедура, в которой доступна глобальная переменная, некорректно изменит ее значение, то ошибка может проявиться в другой процедуре, использующей эту переменную. Найти причину ошибки бывает чрезвычайно трудно. В таких ситуациях приходится проверять работу многих компонентов модуля.
В языке C# роль модуля играют классы, пространства имен, проекты, решения. Поля классов, о которых шла речь выше, могут рассматриваться как глобальные переменные класса. Но здесь у них особая роль. Данные, хранимые в полях класса, являются тем центром, вокруг которого вращается мир класса. Методы класса в этом мире, можно сказать, играют второстепенную роль - они обрабатывают данные. Заметьте, каждый экземпляр класса - это отдельный мир. Поля экземпляра (открытые, защищенные и закрытые) - это глобальная информация, которая доступна всем методам класса.
Статические поля класса хранят информацию, общую для всех экземпляров класса. Они представляют определенную опасность, поскольку каждый экземпляр способен менять их значения.
В других видах модуля - пространствах имен, проектах, решениях - нельзя объявлять переменные. В пространствах имен в языке C# разрешено только объявление классов и их частных случаев: структур, интерфейсов, делегатов, перечислений. Поэтому глобальных переменных уровня модуля, в привычном для других языков программирования смысле, в языке C# нет. Классы не могут обмениваться информацией, используя глобальные переменные. Все взаимодействие между ними обеспечивается способами, стандартными для объектного подхода. Между классами могут существовать два типа отношений - клиентские и наследования, а основной способ инициации вычислений - это вызов метода для объекта-цели или вызов обработчика события. Поля класса и аргументы метода позволяют передавать и получать нужную информацию. Устранение глобальных переменных на уровнях более высоких, чем класс, существенно повышает надежность создаваемых на языке C# программных продуктов, поскольку устраняется источник опасных, трудно находимых ошибок.
Локальные переменные
Перейдем теперь к рассмотрению локальных переменных. Во всех языках программирования, в том числе и в C#, основной контекст, в котором появляются переменные, - это процедуры и функции - методы класса. Тело метода, заключенное в фигурные скобки, будем называть процедурным блоком. Переменные, объявленные в процедурном блоке, называются локальными - они локализованы в методе.
В некоторых языках, например в Паскале, локальные переменные должны быть объявлены в вершине процедурного блока. Иногда это правило заменяется менее жестким, но, по сути, аналогичным правилом - где бы внутри процедурного блока ни была объявлена переменная, она считается объявленной в вершине блока и ее область видимости распространяется на весь процедурный блок. В C# принята другая стратегия. Переменную можно объявлять в любой точке процедурного блока. Область ее видимости распространяется от точки объявления до конца процедурного блока.
На самом деле, ситуация с процедурным блоком в C# не так проста. Процедурный блок имеет сложную структуру; в него могут быть вложены другие блоки, связанные с операторами выбора, цикла и так далее. В каждом таком блоке, в свою очередь, допустимы вложения блоков. В каждом внутреннем блоке допустимы объявления переменных. Переменные, объявленные во внутренних блоках, локализованы именно в этих блоках, их область видимости и время жизни определяются этими блоками. Локальные переменные, объявленные в любом внутреннем блоке, существуют от точки объявления до конца соответствующего блока.
Рассмотрим ситуацию с возможными конфликтами имен, появляющихся в различных блоках. Уже говорилось, что имя локальной переменной может совпадать с именем поля класса. Этот конфликт разрешен, поскольку для поля класса можно использовать уточненное имя. Чтобы избежать других конфликтов, не разрешается во внутреннем блоке метода объявлять локальную переменную, имя которой совпадает с именем формального параметра метода или с именем локальной переменной, объявленной в охватывающем блоке.
Класс TestingLocals
Добавим в наш проект новый класс TestingLocals. Зададим в этом классе два поля и один метод. Поля будут играть роль глобальных переменных для метода класса, а во внутренних блоках метода появятся объявления локальных переменных. Это поможет нам обсудить на примере области действия и существования объявленных переменных. Вот код этого класса:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace SimpleVariables { class TestingLocals { //fields string s; int n; const string POINT = "Point_1"; //Constructor public TestingLocals(string s, int n) { this.s = s; this.n = n; } //Method public int Test(int x) { int result = 0; int n = 9; if (s == POINT) { //static int sc =7; const int cc = 6; int u = 7, v = u + 2; x += u + v - cc; for (int i = 0; i < x; i++) { result += i * i; } //x += i; } else { //int n = 5; //int x = 10; int u = 7, v = u + 2; x += u + v - n + this.n; for (int i = 0; i < x; i++) { result += i * i; } } return result; } } }
Тест 2. Локальные и глобальные переменные класса
Создадим интерфейс пользователя для работы с классом TestingLocals. С этой целью добавим в проект интерфейсный класс FormLocals - наследник класса Form. На рис. 2.7 показано, как выглядит спроектированный интерфейс в процессе работы с формой.
Назначение формы поясняется в специальном текстовом окне. В разделе "исходные данные" два текстовых окна позволяют задать данные, необходимые для формирования объекта класса TestingLocals. Две командные кнопки позволяют создать объект этого класса и вызвать метод Test, тело которого представляет систему вложенных внутренних блоков, содержащих объявление локальных переменных. Большой раздел в интерфейсе формы занимают советы, подсказывающие разработчику, что он может делать при объявлении локальных переменных и что является недопустимым.
Напомню, что код интерфейсного класса, создаваемый по умолчанию, состоит из двух частей. Одна часть предназначена для Дизайнера форм, и код в ней появляется автоматически, отражая проектирование дизайна формы, выполняемое руками. Другая часть ориентирована на разработчика интерфейса. В эту часть класса добавляются поля, необходимые для обмена информацией с элементами управления, расположенными на форме. Здесь же находится поле, в котором объявлен объект класса TestingLocals. Заметьте, если создается интерфейсный класс, обеспечивающий поддержку работы с одним или несколькими содержательными классами, то в интерфейсном классе должны быть поля с объектами этих классов. Так интерфейсный класс становится клиентом содержательного класса. В нашем случае интерфейсный класс FormLocals становится клиентом класса TestingLocals и эти два класса связываются отношением "клиент - поставщик".
Приведу код той части интерфейсного класса, которая создается разработчиком:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; namespace SimpleVariables { public partial class FormLocals : Form { //поля класса - константы и переменные const string YOU_CAN_1 = "Объявить локальную переменную с именем, " + "совпадающим с именем поля класса!"; const string YOU_CAN_2 = "В непересекающихся блоках объявлять " + "локальные переменные с совпадающими именами!"; const string YOU_CAN_3 = "Объявить локальную переменную в любой точке блока!"; const string YOU_CAN_4 = "Объявлять константы в блоках!"; const string YOU_CANNOT_1 = "Объявлять глобальные переменные " + "уровня Решения, Проекта, Пространства Имен!"; const string YOU_CANNOT_2 = "Объявить локальную переменную метода с именем," + "совпадающим с именем формального параметра!"; const string YOU_CANNOT_3 = "Объявить локальную переменную с атрибутом static!"; const string YOU_CANNOT_4 = "Объявить переменную во внутреннем блоке, " + "если в охватывающем блоке уже объявлена " + "локальная переменная с тем же именем!"; TestingLocals testing; string s; int n; public FormLocals() { InitializeComponent(); } } }
Помимо упомянутых полей класса, в нем определены строковые константы для вывода советов по использованию локальных переменных.
В этой части класса появляются и обработчики событий элементов управления формы. Вот как выглядит код обработчика события Click командной кнопки, создающей объект класса TestingLocals:
private void buttonCreateObject_Click(object sender, EventArgs e) { s = textBoxS.Text; n = Convert.ToInt32(textBoxN.Text); testing = new TestingLocals(s, n); textBoxS.Text = "Point"; textBoxN.Text = "0"; }
Устроен он достаточно просто: вначале из текстовых полей формы читается информация, необходимая конструктору класса TestingLocals для создания объекта, затем этот объект создается, а текстовые окна формы получают новое значение, которое может быть изменено конечным пользователем в процессе работы с формой.
Чуть более сложно устроен обработчик события Click командной кнопки, вызывающей метод Test класса TestingLocals:
private void buttonTest_Click(object sender, EventArgs e) { int x = Convert.ToInt32(textBoxX.Text); textBoxResult.Text = testing.Test(x).ToString(); textBoxCan1.Text = YOU_CAN_1; textBoxCan2.Text = YOU_CAN_2; textBoxCan3.Text = YOU_CAN_3; textBoxCan4.Text = YOU_CAN_4; textBoxCannot1.Text = YOU_CANNOT_1; textBoxCannot2.Text = YOU_CANNOT_2; textBoxCannot3.Text = YOU_CANNOT_3; textBoxCannot4.Text = YOU_CANNOT_4; }
И здесь из текстового окна формы читается значение аргумента, заданное конечным пользователем, затем созданный объект testing вызывает открытый ( public ) метод класса Test, и результат работы метода выводится в соответствующее текстовое окно, поддерживая необходимую связь с конечным пользователем. В качестве побочного эффекта в текстовые поля формы выводятся советы по использованию локальных переменных.
Помимо советов, анализируя текст метода Test, следует обратить внимание при использовании локальных переменных на следующие моменты.
Аргументы метода (его формальные параметры) считаются объявленными в начале блока, задающего тело метода. Таким образом, область их действия распространяется на весь этот блок и ни в одном из внутренних блоков нельзя объявлять локальную переменную с именем, совпадающим с именем аргумента.
Параметр цикла считается объявленным в блоке, задающем тело цикла. Поэтому область его действия распространяется на весь этот блок и во внутренних блоках тела цикла нельзя объявлять локальную переменную с именем, совпадающим с именем параметра цикла. Заметьте, после окончания цикла параметр цикла перестает существовать и не может быть использован. В нашем примере оператор, в котором делается попытка использовать параметр цикла после завершения цикла, закомментирован.
В параллельных блоках (в нашем примере две ветви оператора if ) разрешается объявлять локальные переменные с одинаковыми именами, поскольку области существования этих переменных не пересекаются.
Поскольку объявлять локальную переменную можно в любой точке блока, хорошим стилем считается объявление локальной переменной как можно ближе к точке ее непосредственного использования. Нет смысла объявлять локальную переменную в начале блока, если она будет использована где-то в конце блока.
Глобальные переменные уровня процедуры. Существуют ли?
Поскольку процедурный блок - блок тела метода - имеет сложную структуру с вложенными внутренними блоками, то и здесь возникает тема глобальных переменных. Переменная, объявленная во внешнем блоке, рассматривается как глобальная по отношению к внутренним блокам. В большинстве известных языков программирования во внутренних блоках разрешается объявлять переменные с именем, совпадающим с именем глобальной переменной. Конфликт имен снимается за счет того, что локальное внутреннее определение сильнее внешнего. Поэтому область видимости внешней глобальной переменной сужается и не распространяется на те внутренние блоки, где объявлена переменная с подобным именем. Внутри блока действует локальное объявление этого блока, при выходе восстанавливается область действия внешнего имени. В языке C# этот гордиев узел конфликтующих имен разрублен - во внутренних блоках запрещено использование имени, совпадающего с именем, уже использованном во внешнем блоке. В нашем примере незаконная попытка объявить во внутреннем блоке уже объявленное имя закомментирована.
Обратите внимание, что подобные решения, принятые создателями языка C#, не только упрощают жизнь разработчикам транслятора. Они способствуют повышению эффективности программ, а самое главное - повышают надежность программирования на C#.
Отвечая на вопрос, вынесенный в заголовок, следует сказать, что глобальные переменные на уровне процедуры в языке C#, конечно же, есть, но нет конфликта имен между глобальными и локальными переменными на этом уровне. Область видимости глобальных переменных процедурного блока распространяется на весь блок, в котором они объявлены, начиная от точки объявления, и не зависит от существования внутренних блоков. Когда говорят, что в C# нет глобальных переменных, то, прежде всего, имеют в виду их отсутствие на уровне модуля. Уже во вторую очередь речь идет об отсутствии конфликтов имен на процедурном уровне.