Прочитав эту главу, вы научитесь:
• определять класс, содержащий родственный набор методов и элементов данных;
• управлять доступностью компонентов класса с помощью ключевых слов public и private;
• создавать объекты с помощью ключевого слова new для привлечения к их созданию конструктора;
• записывать и вызывать собственные конструкторы;
• создавать с помощью ключевого слова static методы и данные, которые могут совместно использоваться экземплярами одного и того же класса;
• разбираться в способах создания безымянных классов.
Среда выполнения Windows, а также среда Microsoft .NET Framework содержат тысячи классов. Некоторыми из них, включая Console и Exception, вы уже пользовались. Классы обеспечивают удобный механизм для моделирования тех сущностей, с которыми работают приложения. Сущность может представлять собой конкретный элемент, например клиента, или что-либо более абстрактное, например транзакцию. Часть процесса проектирования любой системы осоставляют определение сущностей, играющих важную роль для реализуемых системой процессов, а затем анализ с целью определения информации, которая должна содержаться в той или иной сущности, и операций, которые должны выполняться этими сущностями. Информация, содержащаяся в классе, хранится в виде полей, а для реализации операций, которые могут выполняться классом, используются методы.
В слове «классификация» корнем является «класс». При проектировании класса вы систематизируете информацию и поведение, создавая их осмысленное единство. Это упорядочение является актом классификации, которым занимаются не только программисты. Например, у всех автомобилей одинаковое поведение (ими можно управлять, их можно останавливать, ускорять и т.д.) и одинаковые свойства (у них есть руль, двигатель и пр.). Люди используют слово «машина» для обозначения объекта, обладающего этими общими характеристиками поведения и свойствами. Пока все соглашаются со значением данного слова, система работает хорошо и сложные, но точные мысли вы можете выражать в краткой форме. Трудно представить, как без классификации люди вообще смогли бы размышлять или общаться друг с другом.
Учитывая, что классификация так глубоко укоренилась в нашем способе мышления и общения, есть смысл попробовать создавать программы, классифицируя различные понятия, присущие задаче и ее решению, и моделируя затем эти классы в языке программирования. Именно это позволяют сделать объектно-ориентированные языки, к которым относится и Microsoft Visual C#.
При определении классов применяется такое важное понятие, как инкапсуляция. Идея инкапсуляции заключается в том, что программа, использующая класс, не должна брать в расчет то, что делается внутри класса, — она просто создает экземпляр класса и вызывает методы, содержащиеся в этом классе. Пока эти методы работают в строгом соответствии со своим предназначением, программе не нужно знать подробности их реализации. Например, когда вызывается метод Console.WriteLine, вам не хочется вникать во все сложности того, как класс Console физически организован для данных, выводимых на экран. Классу, возможно, потребуется поддерживать все виды внутренней информации о состоянии, позволяющем выполнить имеющиеся в нем разнообразные методы. Эта дополнительная информация о состоянии и внутренняя работа скрыты от программы, использующей класс. Поэтому инкапсуляцию иногда называют сокрытием информации. Инкапсуляция преследует две цели:
• объединить методы и данные внутри класса, то есть поддержать классификацию;
• управлять доступностью методов и данных, то есть контролировать использование класса.
Для определения нового класса в C# используется ключевое слово class. Данные и методы класса помещаются в его тело между двумя фигурными скобками. В следующем примере показан класс по имени Circle, в котором содержится один метод (для вычисления площади круга) и один элемент данных (радиус окружности):
class Circle
{
int radius;
double Area()
{
return Math.PI * radius * radius;
}
}
ПРИМЕЧАНИЕ В классе Math содержатся методы для выполнения математических вычислений и поля с математическими константами. В поле Math.PI содержится значение 3,14159265358979, приблизительно равное значению числа π.
В теле класса содержатся обычные методы (такие, как Area) и поля (такие, как radius). Ранее уже упоминалось, что переменные в классе называются полями. Как объявляются переменные, было показано в главе 2 «Работа с переменными, операторами и выражениями», а как создаются методы, вы видели в главе 3 «Создание методов и применение областей видимости», поэтому незнакомый синтаксис здесь практически отсутствует.
Класс Circle можно использовать так же, как вы использовали другие типы, уже встречавшиеся при чтении книги. Создается переменная, в качестве типа которой указывается Circle, а затем эта переменная инициализируется какими-нибудь подходящими данными, например:
Circle c; // Создание Circle-переменной
c = new Circle(); // Ее инициализация
В этом коде заслуживает внимания использование ключевого слова new. Ранее при инициализации переменной, имеющей тип int или float, вы просто присваивали ей значение:
int i;
i = 42;
С переменной типа класса сделать то же самое невозможно. Одной из причин является то, что в C# не предоставляется синтаксис для присвоения переменным значений литерала класса. Вы не можете написать следующую инструкцию:
Circle c;
c = 42;
Кроме всего прочего, каким тогда будет Circle-эквивалент числа 42? Есть еще одна причина, имеющая отношение к способу, который используется средой выполнения для выделения памяти переменным типов классов и управления этой памятью. Она будет рассмотрена в главе 8 «Основные сведения о значениях и ссылках». А пока просто примите как данность то, что ключевое слово new создает новый экземпляр класса, который чаще всего называют объектом.
Но присвоить экземпляр класса другой переменной того же типа можно напрямую:
Circle c;
c = new Circle();
Circle d;
d = c;
И тем не менее по причинам, рассматриваемым в главе 8, все не так просто, как может показаться на первый взгляд.
ВНИМАНИЕ Не следует путать понятия класса и объекта. Класс является определением типа. А объект является экземпляром этого типа, создаваемым при выполнении программы. Экземплярами одного и того же класса могут быть несколько различных объектов.
Как ни удивительно, но в данном виде класс Circle практически бесполезен. Изначально, когда методы и данные инкапсулируются внутри класса, этот класс отгораживается от внешнего мира. Определенные в классе поля (такие, как radius) и методы (такие, как Area) могут использоваться другими методами внутри класса, но только не во внешнем мире, то есть они являются закрытой собственностью класса. Получается, что, несмотря на то что вы можете создать в программе объект Circle, вы не можете обратиться к его полю radius или вызвать его метод Area, и поэтому класс для вас пока что бесполезен! Но используя ключевые слова public и private, можно изменить определение поля или метода и управлять их доступностью за пределами экземпляра класса.
• Методы или поля являются закрытыми (private), если они доступны только внутри класса. Чтобы объявить метод или поле закрытым, перед его объявлением нужно указать ключевое слово private. Как уже упоминалось, этот статус приобретается по умолчанию, но во избежание путаницы лучше все же определять закрытый статус полей и методов явным образом.
• Методы или поля являются открытыми (public), если они доступны как внутри класса, так и за его пределами. Чтобы объявить метод или поле открытыми, перед их определением нужно указать ключевое слово public.
Вернемся к классу Circle. На этот раз Area объявлен открытым методом, а radius объявлен закрытым полем:
class Circle
{
private int radius;
public double Area()
{
return Math.PI * radius * radius;
}
}
ПРИМЕЧАНИЕ Если у вас есть опыт программирования на C++, имейте в виду, что после ключевых слов public и private двоеточие не ставится. Ключевое слово должно повторяться для каждого объявления поля и метода.
Хотя radius объявлен закрытым полем, недоступным за пределами класса, доступ к нему можно получить из класса Circle. Метод Area находится внутри класса Circle, следовательно, из тела метода Area имеется доступ к полю radius.
Но практическая ценность класса все еще невелика, поскольку в нем отсутствует способ инициализации поля radius. Чтобы исправить положение, можно воспользоваться конструктором.
СОВЕТ Запомните, что переменные, объявленные в методе, изначально пребывают в неинициализированном состоянии. А поля в классе в зависимости от их типа автоматически инициализируются в нуль, false или null. Но все же лучше предоставлять инициализируемым полям явные значения.
Задание имен и доступность
У многих организаций есть свой фирменный стиль, и они требуют от разработчиков придерживаться его при написании кода. Частью этого стиля обычно являются правила подбора имен идентификаторов. Чаще всего такие правила призваны облегчить сопровождение кода. Следующие рекомендации носят общий характер и связаны с соглашением о задании имен для полей и методов на основе доступности компонентов класса, хотя в C# подобные требования не выдвигаются.
Идентификаторы для открытых компонентов класса должны начинаться с прописной буквы. Например, Area начинается с «A» (а не с «а»), потому что это открытый метод. Эта система называется схемой задания имен PascalCase, потому что впервые она была применена в языке Pascal.
Идентификаторы для закрытых компонентов (включая локальные переменные) должны начинаться с буквы в нижнем регистре. Например, radius начинается с «r» (а не с «R»), потому что это закрытое поле. Эта система называется схемой задания имен в смешанном регистре (camelCase).
ПРИМЕЧАНИЕ В некоторых организациях схема camelCase используется только для методов, а в отношении имен закрытых полей придерживаются схемы задания имен, начинающихся с символа подчеркивания, например, _radius. Но в примерах данной книги для задания имен закрытых методов и полей мы будем придерживаться схемы, использующей смешанный регистр.
Из этого правила есть только одно исключение: имена классов должны начинаться с прописной буквы и конструкторы должны в точности соответствовать имени своего класса, поэтому имя закрытого конструктора должно начинаться с заглавной буквы.
ВНИМАНИЕ Не следует объявлять два открытых компонента класса, чьи имена различаются только регистром символов. Если поступить таким образом, то разработчики, использующие другие языки, нечувствительные к регистру символов (например, Microsoft Visual Basic), могут лишиться возможности интегрировать ваш класс в свои решения.
Когда ключевое слово new используется для создания объекта, среде выполнения нужно сконструировать этот объект, используя определение класса. Она должна забрать часть памяти у операционной системы, заполнить ее полями, определенными в классе, а затем для выполнения любой требуемой инициализации воспользоваться конструктором.
Конструктор — это специальный метод, автоматически запускаемый при создании экземпляра класса. У него такое же имя, что и у класса, и он может принимать параметры, но не может возвращать значение (даже типа void). Конструктор должен быть у каждого класса. Если он не будет написан, компилятор автоматически создаст для вас пассивный конструктор. (Но созданный компилятором пассивный конструктор практически ничего не делает.) Вы можете, не прилагая особых усилий, создать собственный пассивный конструктор. Просто добавьте открытый метод, не возвращающий значение, и дайте ему такое же имя, как у класса. В следующем примере показан класс Circle с пассивным конструктором, инициализирующим поле radius значением 0:
class Circle
{
private int radius;
public Circle() // пассивный конструктор
{
radius = 0;
}
public double Area()
{
return Math.PI * radius * radius;
}
}
ПРИМЕЧАНИЕ В манере выражений, свойственных C#, пассивным считается конструктор, не принимающий никаких параметров. Неважно, чем или кем он был создан, компилятором или вами, это все равно пассивный конструктор. Вы можете также написать активные конструкторы (принимающие параметры), что и будет показано в разделе «Перегрузка конструкторов».
В данном примере конструктор помечен как открытый (public). Если это ключевое слово не указано, конструктор будет закрытым, как и любые другие метод или поле. Если конструктор закрытый (private), он не может быть использован за пределами класса, что не дает вам возможности создавать объекты Circle из методов, не являющихся частью класса Circle. Поэтому можеть сложиться мнение, что закрытые конструкторы не имеют особой ценности. Им находится применение, но этот вопрос выходит за рамки рассматриваемой темы.
После добавления открытого конструктора вы можете воспользоваться классом Circle и на деле испытать имеющийся у него метод Area. Обратите внимание на форму записи с точкой, применяемую для вызова метода Area в отношении Circle-объекта:
Circle c;
c = new Circle();
double areaOfCircle = c.Area();
Но это еще не все. Теперь вы можете объявить переменную, имеющую тип Circle, воспользоваться ею для ссылки на только что созданный Circle-объект, а затем вызвать метод Area. Осталась еще одна, последняя проблема. Площадь всех Circle-объектов всегда будет равна нулю, поскольку пассивный конструктор устанавливает значение поля radius в нуль и оно так и остается с нулевым значением. Поле radius является закрытым, поэтому у нас нет простого способа изменить его значение после инициализации. Конструктор является всего лишь специальной разновидностью метода, и он, как и все другие методы, может быть перегружен. Точно так же, как существует несколько версий метода Console.WriteLine, каждая из которых принимает разные параметры, могут существовать и различные созданные вами версии конструктора. То есть вы можете добавить к классу Circle еще один конструктор с параметрами, указывающими на используемый радиус окружности:
class Circle
{
private int radius;
public Circle() // пассивный конструктор
{
radius = 0;
}
public Circle(int initialRadius) // перегруженный конструктор
{
radius = initialRadius;
}
public double Area()
{
return Math.PI * radius * radius;
}
}
ПРИМЕЧАНИЕ Порядок следования конструкторов в классе не имеет никакого значения, конструкторы можно определять в наиболее удобном для вас порядке.
Затем этим конструктором можно воспользоваться при создании нового объекта Circle:
Circle c;
c = new Circle(45);
При сборке приложения компилятор определяет, какой конструктор должен быть вызван, основываясь на параметрах, указанных для оператора new. В данном примере было передано целое число, поэтому компилятор создаст код, вызывающий конструктор, принимающий int-параметр.
Вам нужно знать о важной особенности языка C#: если вы создаете для класса свой собственный конструктор, компилятор не создает пассивный конструктор. Поэтому если вы написали собственный конструктор, принимающий один или несколько параметров, и вам нужен еще и пассивный конструктор, то его придется создавать отдельно.
В следующем упражнении вами будет объявлен класс, моделирующий точку в двумерном пространстве. Класс будет состоять из двух закрытых полей для хранения x- и y-координат точки и предоставлять конструкторы для инициализации этих полей. Для создания экземпляров класса вы воспользуетесь ключевым словом new и вызовом конструкторов.
Разделяемые классы
В классе могут содержаться несколько методов, полей и конструкторов, а также другие элементы, рассматриваемые в следующих главах. Класс, имеющий высокоразвитые функциональные возможности, может стать слишком большим. Программируя на C#, вы можете разбить исходный код класса на несколько отдельных файлов, получив возможность организовать определение большого класса в небольших частях, с которыми проще работать. Это свойство используется средой Visual Studio 2015 для приложений, разрабатываемых под универсальную платформу Windows (Universal Windows Platform (UWP)), где исходный код, который разработчик может редактировать, сохраняется в отдельном файле, обособляясь таким образом от кода, создаваемого средой Visual Studio при каждом изменении разметки формы.
При разбиении класса на несколько файлов части класса определяются с использованием в каждом файле ключевого слова partial. Например, если класс Circle разбит на два файла с именами circ1.cs (в этом файле содержатся конструкторы) и circ2.cs (в этом файле содержатся методы и поля), то содержимое circ1.cs выглядит следующим образом:
partial class Circle
{
public Circle() // пассивный конструктор
{
this.radius = 0;
}
public Circle(int initialRadius) // перегруженный конструктор
{
this.radius = initialRadius;
}
}
А содержимое circ2.cs имеет следующий вид:
partial class Circle
{
private int radius;
public double Area()
{
return Math.PI * this.radius * this.radius;
}
}
При компиляции класса, разбитого на два отдельных файла, компилятору следует предоставить все файлы.
Откройте в среде Visual Studio 2015 проект Classes, который находится в папке \Microsoft Press\VCSBS\Chapter 7\Classes вашей папки документов.
Дважды щелкните в обозревателе решений на файле Program.cs, чтобы его код был выведен в окно редактора. Найдите в классе Program метод Main. Этот метод вызывает метод doWork, заключенный в блок try, за которым следует обработчик исключения. Используя блок try-catch, вы можете написать код, который обычно помещается в Main, не в этом методе, а в методе doWork, и быть в полной уверенности, что этот блок перехватит и обработает любые исключения. Метод doWork на данный момент не содержит ничего, кроме комментария // TODO:.
СОВЕТ Комментарии TODO часто используются разработчиками в качестве напоминания о том, что они отложили работу над фрагментом кода и к ней нужно будет вернуться. В этих комментариях часто содержится описание той работы, которую нужно сделать, например, // TODO: Реализовать метод doWork. Visual Studio распознает эту форму комментария, и вы можете быстро найти такие комментарии в любом месте приложения, воспользовавшись окном Список задач. Чтобы вывести это окно на экран, нужно щелкнуть в меню Вид на пункте Список задач. Изначально окно с этим названием открывается ниже окна редактора (рис. 7.1). В нем будут выведены списком все комментарии TODO. Затем, чтобы перейти непосредственно к соответствующему коду, который будет отображен в окне редактора, можно будет дважды щелкнуть на любом из этих комментариев.
Рис. 7.1
Выведите в окно редактора файл Point.cs. В этом файле определяется класс по имени Point, который будет вами использоваться для представления местонахождения точки в двумерном пространстве, определяемого парой координат x и y. Класс Point на данный момент не содержит ничего, кроме еще одного комментария // TODO:.
Вернитесь к файлу Program.cs. Отредактируйте в классе Program тело метода doWork, заменив комментарий // TODO: следующей инструкцией:
Point origin = new Point();
Эта инструкция создает новый экземпляр класса Point и запускает его пассивный конструктор.
Щелкните в меню Сборка (Build) на пункте Собрать решение (Build Solution).
Код пройдет сборку без ошибок, потому что компилятор автоматически создает код для пассивного конструктора класса Point. Но вы не можете видеть код C# для этого конструктора, потому что компилятор не создает на этом языке никаких исходных инструкций.
Вернитесь в класс Point, который находится в файле Point.cs. Замените комментарий // TODO: открытым конструктором, принимающим два int-аргумента с именами x и y, а затем вызовите метод Console.WriteLine для отображения в консоли значений этих аргументов. Все изменения в следующем примере кода выделены жирным шрифтом:
class Point
{
public Point(int x, int y)
{
Console.WriteLine($"x:{x}, y:{y}");
}
}
Щелкните в меню Сборка на пункте Собрать решение. Теперь компилятор выдаст сообщение об ошибке: Отсутствует аргумент, соответствующий требуемому формальному параметру "x" из "Point.Point(int, int)".
Смысл этого весьма многословного сообщения состоит в том, что вызов пассивного конструктора в методе doWork теперь неприемлем, поскольку такого конструктора больше нет. Вы создали собственный конструктор для класса Point, поэтому компилятор не создает пассивный конструктор. Теперь вам нужно устранить эту проблему, написав собственный пассивный конструктор.
Отредактируйте класс Point, добавив открытый пассивный конструктор, вызывающий инструкцию Console.WriteLine для записи в консоль строки «Default constructor called» («Вызван пассивный конструктор»). Добавляемый код выделен жирным шрифтом, а класс Point должен теперь приобрести следующий вид:
class Point
{
public Point()
{
Console.WriteLine("Default constructor called");
}
public Point(int x, int y)
{
Console.WriteLine($"x:{x}, y:{y}");
}
}
Щелкните в меню Сборка на пункте Собрать решение. Теперь сборка программы пройдет успешно.
Отредактируйте в файле Program.cs тело метода doWork. Объявите переменную по имени bottomRight с типом Point и инициализируйте ее новым объектом Point, воспользовавшись конструктором с двумя аргументами. Соответствующий код показан ниже и выделен жирным шрифтом. Предоставьте конструктору значения 1366 и 768 — координаты правой нижней точки экрана, имеющего разрешение 1366×768, которое встречается на многих планшетных устройствах. Теперь метод doWork должен приобрести следующий вид:
static void doWork()
{
Point origin = new Point();
Point bottomRight = new Point(1366, 768);
}
Щелкните в меню Отладка на пункте Запуск без отладки.
Программа пройдет сборку и будет запущена, в результате чего на консоль будет выведено следующее сообщение (рис. 7.2).
Нажмите клавишу Ввод, чтобы завершить работу программы, и вернитесь в среду Visual Studio 2015.
Теперь вы добавите к классу Point два int-поля для представления принадлежащих точке координат x и y и внесете изменения в конструктор для инициализации этих полей. Отредактируйте в файле Point.cs класс Point, добавив к нему два закрытых поля int-типа с именами x и y. Соответствующий код показан ниже и выделен жирным шрифтом. Класс Point должен приобрести следующий вид:
class Point
{
private int x, y;
public Point()
{
Console.WriteLine("default constructor called");
}
public Point(int x, int y)
{
Console.WriteLine($"x:{x}, y:{y}");
}
}
Рис. 7.2
Теперь вы отредактируете второй конструктор Point для инициализации полей x и y значениями параметров x и y. Но здесь вас поджидает потенциальная ловушка. Если проявить невнимательность, код конструктора может стать похожим на следующий:
public Point(int x, int y) // Не набирайте этот код!
{
x = x;
y = y;
}
Несмотря на то что этот код пройдет компиляцию, используемые в нем инструкции представляются неоднозначными. Как из инструкции x = x; компилятор узнает, что первый идентификатор x представляет собой поле, а второй является параметром? Ответ такой: он просто не сможет этого сделать! Параметр метода с таким же именем, как и у поля, скрывает поле для всех инструкций в методе. Все, что фактически делает этот код, — это присваивает значения параметров им же самим, с полями он вообще ничего не делает. Разумеется, это совсем не то, что вы хотели получить.
Решение проблемы заключается в использовании ключевого слова this, чтобы уточнить, что здесь является параметрами, а что полями. Указание перед именем переменной ключевого слова this означает «поле этого объекта».
Отредактируйте конструктор Point, принимающий два параметра, заменив инструкцию Console.WriteLine следующим кодом, выделенным жирным шрифтом:
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
Отредактируйте пассивный конструктор Point для инициализации полей x и y значениями –1. Показанные далее изменения выделены жирным шрифтом. Хотя здесь и нет параметров, которые могли бы вызвать путаницу, лучше все же с помощью ключевого слова this указать ссылки на поля:
public Point()
{
this.x = -1;
this.y = -1;
}
Щелкните в меню Сборка на пункте Собрать решение. Убедитесь в том, что код компилируется без ошибок или предупреждений. (Его можно запустить, но никакого вывода на экран он не выполнит.)
Методы, принадлежащие классу и совершающие операции с данными, принадлежащими конкретному экземпляру класса, называются методами экземпляра. (Сведения о других типах методов будут даны в этой главе чуть позже.) В следующем упражнении вы создадите метод экземпляра для класса Point, называющийся DistanceTo и вычисляющий расстояние между двумя точками.
В среде Visual Studio 2015 добавьте к классу Point в проекте Classes сразу после конструкторов следующий открытый метод экземпляра под названием DistanceTo. Этот метод принимает один Point-аргумент по имени other и возвращает значение, имеющее тип данных double.
Метод DistanceTo должен иметь следующий вид:
class Point
{
...
public double DistanceTo(Point other)
{
}
}
При выполнении следующих этапов к телу метода экземпляра DistanceTo будет добавлен код для вычисления и возвращения расстояния между объектом типа Point, задействованным для совершения вызова, и объектом типа Point, переданным в качестве параметра. Для этого нужно вычислить разницу между координатами x и y.
Объявите в методе DistanceTo локальную int-переменную по имени xDiff и проинициализируйте ее разницей значений this.x и other.x. Соответствующий код, показанный далее, выделен жирным шрифтом:
public double DistanceTo(Point other)
{
int xDiff = this.x - other.x;
}
Объявите еще одну локальную int-переменную по имени yDiff и проинициализируйте ее разницей значений this.y и other.y. Новый код выделен жирным шрифтом:
public double DistanceTo(Point other)
{
int xDiff = this.x - other.x;
int yDiff = this.y - other.y;
}
ПРИМЕЧАНИЕ Хотя поля x и y являются закрытыми, доступ к ним все же может быть получен из других экземпляров того же класса. Важно понимать, что понятие закрытости (private) действует на уровне класса, а не на уровне объекта: два объекта, являющиеся экземплярами одного и того же класса, могут иметь доступ к закрытым данным друг друга, а объекты, являющиеся экземплярами другого класса, такого доступа иметь не могут.
Для вычисления расстояния можно воспользоваться теоремой Пифагора и найти квадратный корень из суммы квадратов xDiff и yDiff. Метод Sqrt, который можно использовать для вычисления квадратных корней, предоставляется классом System.Math.
Объявите переменную по имени distance, имеющую тип данных double, и воспользуйтесь ею для сохранения результата только что рассмотренного вычисления:
public double DistanceTo(Point other)
{
int xDiff = this.x - other.x;
int yDiff = this.y - other.y;
double distance = Math.Sqrt((xDiff * xDiff) + (yDiff * yDiff));
}
Добавьте к концу метода DistanceTo инструкцию return и возвратите значение переменной distance:
public double DistanceTo(Point other)
{
int xDiff = this.x - other.x;
int yDiff = this.y - other.y;
double distance = Math.Sqrt((xDiff * xDiff) + (yDiff * yDiff));
return distance;
}
А теперь займемся тестированием метода DistanceTo.
Вернитесь к методу doWork в классе Program. После инструкций, объявляющих Point-переменные origin и bottomRight, объявите переменную по имени distance с типом данных double. Инициализируйте эту double-переменную результатом, получаемым при вызове метода DistanceTo, в отношении объекта origin после передачи этому методу в качестве аргумента значения переменной bottomRight.
Теперь метод doWork должен приобрести следующий вид:
static void doWork()
{
Point origin = new Point();
Point bottomRight = new Point(1366, 768);
double distance = origin.DistanceTo(bottomRight);
}
ПРИМЕЧАНИЕ Как только после слова origin будет набран символ точки, система Microsoft IntelliSense должна вывести название метода DistanceTo.
Добавьте к методу doWork еще одну инструкцию, записывающую значение переменной distance в консоль с помощью метода Console.WriteLine.
В окончательном виде метод doWork должен выглядеть следующим образом:
static void doWork()
{
Point origin = new Point();
Point bottomRight = new Point(1366, 768);
double distance = origin.DistanceTo(bottomRight);
Console.WriteLine($"Distance is: {distance}");
}
Щелкните в меню Отладка на пункте Запуск без отладки.
Убедитесь в том, что в окно консоли записано значение 1568,45465347265, а затем нажмите Ввод, чтобы закрыть приложение и вернуться в среду Visual Studio 2015.
В предыдущем упражнении был использован метод Sqrt, принадлежащий классу Math. Также, когда рассматривался класс Circle, из класса Math производилось чтение поля PI. Если приглядеться, то способ вызова метода Sqrt или чтения поля PI был немного странным. Вы вызывали метод или читали поле самого класса, а не объекта типа Math. Это все равно что в коде, который был добавлен в предыдущем упражнении, попытаться написать Point.DistanceTo вместо origin.DistanceTo. Так что же все-таки произошло и как это работает?
Вам часто придется сталкиваться с тем, что не все методы естественным образом принадлежат экземпляру класса, есть еще и методы общего пользования, считающиеся таковыми по причине реализации полезной функции, не зависящей от какого-либо конкретного экземпляра класса. Типичным примером может послужить метод WriteLine класса Console, повсеместно используемый в данной книге. В качестве еще одного примера можно привести метод Sqrt. Если бы Sqrt был методом экземпляра класса Math, вам пришлось бы создавать объект Math и вызывать Sqrt в отношении этого объекта:
Math m = new Math();
double d = m.Sqrt(42.24);
Это было бы слишком громоздко. Объект Math не играл бы никакой роли в вычислении квадратного корня. Все вводимые данные, необходимые Sqrt, представлены в списке параметров, а результат передан назад вызывающему методу с помощью имеющегося в методе возвращаемого значения. Объекты здесь совершенно ни к чему, поэтому пихать Sqrt в экземпляр не имеет никакого смысла.
ПРИМЕЧАНИЕ Кроме того что в классе Math содержатся метод Sqrt и поле PI, там имеется множество других полезных математических методов, например Sin, Cos, Tan и Log.
В C# все методы должны быть объявлены внутри класса. Но если объявить метод или поле в качестве статического, то вызывать метод или обращаться к полю можно с использованием имени класса. Вот как выглядит объявление метода Sqrt класса Math:
class Math
{
public static double Sqrt(double d)
{
...
}
...
}
А метод Sqrt можно вызвать следующим образом:
double d = Math.Sqrt(42.24);
Статический метод не зависит от экземпляра класса и не может обращаться к каким-либо определенным в классе методам экземпляра или полям экземпляра, он может использовать только поля и другие методы, которые обозначены ключевым словом static.
Определение поля в качестве статического позволяет создавать единственный экземпляр поля, совместно используемый всеми объектами, созданными из одного и того же класса. (Нестатические поля являются для каждого экземпляра объекта локальными.) В следующем примере при каждом создании нового объекта Circle значение статического поля NumCircles в классе Circle увеличивается конструктором Circle на единицу:
class Circle
{
private int radius;
public static int NumCircles = 0;
public Circle() // пассивный конструктор
{
radius = 0;
NumCircles++;
}
public Circle(int initialRadius) // перегруженный конструктор
{
radius = initialRadius;
NumCircles++;
}
}
Все Circle-объекты совместно используют один и тот же экземпляр поля NumCircles, поэтому инструкция NumCircles++; при создании каждого нового экземпляра увеличивает на единицу одни и те же данные. Следует заметить, что вы не можете использовать для NumCircles префикс из ключевого слова this, поскольку NumCircles не принадлежит конкретному объекту.
Как показано в следующем примере, доступ к полю NumCircles можно получить за пределами класса, указав не Circle-объект, а класс Circle:
Console.WriteLine($"Number of Circle objects: {Circle.NumCircles}");
ПРИМЕЧАНИЕ Следует иметь в виду, что статические методы называются также методами класса. А вот статические поля полями класса обычно не называют — они просто называются статическими полями (иногда статическими переменными).
Используя для поля префикс, состоящий из ключевого слова const, можно объявить поле статическим, но при этом его значение уже никогда не может быть изменено. Ключевое слово const является сокращением от слова constant — константа. Ключевое слово static не используется при определении const-поля, но оно все равно является статическим. По причинам, которые в данной книге не рассматриваются, поле может быть объявлено как const только в том случае, если оно относится к числовому типу (например, int или double), является строкой или перечислением. (Перечисления будут рассматриваться в главе 9 «Создание типов значений с использованием перечислений и структур».) Вот как, к примеру, в качестве поля const в классе Math объявляется константа PI:
class Math
{
...
public const double PI = 3.14159265358979;
}
Еще одной особенностью языка C# является возможность объявлять класс статическим. Статический класс может содержать только статические элементы. (Все объекты, создаваемые с использованием класса, совместно используют одну и ту же копию этих элементов.) Статические классы предназначены для применения исключительно в качестве хранилищ совместно используемых методов и полей. Статический класс не может содержать какие-либо данные или методы экземпляров, и нет никакого смысла пытаться с помощью оператора new создавать из статического класса объект. Фактически вам не удастся использовать new для создания экземпляра объекта, даже если вы этого захотите. (При такой попытке компилятор выдаст отчет об ошибке.) Если вам нужно выполнить любую инициализацию, у статического класса должен быть пассивный конструктор, если, конечно, он тоже объявлен с использованием ключевого слова static. Любые другие типы конструктора недопустимы, о чем и будет объявлено компилятором.
Если вами определена собственная версия класса Math, содержащая только статические элементы, она должна иметь следующий вид:
public static class Math
{
public static double Sin(double x) {...}
public static double Cos(double x) {...}
public static double Sqrt(double x) {...}
...
}
ПРИМЕЧАНИЕ Настоящий класс Math таким образом не определен, поскольку в нем все-таки имеются методы экземпляров.
При вызове статического метода или при ссылке на статическое поле нужно указывать класс, которому принадлежат эти метод или поле, например Math.Sqrt или Console.WriteLine. Инструкции использования статических элементов (using) позволяют вводить класс в область видимости и не указывать имя класса при доступе к статическим элементам. Эти инструкции работают практически так же, как и обычные инструкции using, помещающие пространства имен в область видимости. Порядок их использования показан в следующем примере:
using static System.Math;
using static System.Console;
...
var root = Sqrt(99.9);
WriteLine($"The square root of 99.9 is {root}");
Обратите внимание на использование с инструкцией using ключевого слова static. В этом примере статические методы классов System.Math и System.Console помещаются в область видимости (вы должны указать классы с их пространствами имен в развернутой форме). Затем можно будет просто вызывать методы Sqrt и WriteLine. Компилятор сам определит, какой метод какому классу принадлежит. Но за всем этим кроется потенциальная проблема сопровождения кода. Стремясь уменьшить объем набираемого кода, вам нужно выдерживать разумный баланс, учтывая те дополнительные усилия, которые потребуется приложить кому-то другому, кто будет сопровождать этот код, поскольку теперь уже непонятно, к какому классу какой метод принадлежит. В какой-то мере помогает система IntelliSense, имеющаяся в среде Visual Studio, но для разработчика, вычитывающего код при попытке выяснить причину ошибки, это обстоятельство может все запутать. Инструкции использования статических элементов следует применять с оглядкой, — сам автор предпочитает с ними не работать, но выбор всегда остается за вами!
В заключительном упражнении этой главы вам предстоит добавить к классу Point закрытое статическое поле и инициализировать его нулевым значением. Инкремент этого поля счетчика будет осуществляться в обоих конструкторах. В завершение вы напишете открытый статический метод для возвращения значения закрытого статического поля, с помощью которого сможете определить количество созданных Point-объектов.
В среде Visual Studio 2015 выведите в окно редактора класс Point. Добавьте к нему непосредственно перед конструкторами закрытое статическое int-поле по имени objectCount и инициализируйте его во время объявления нулевым значением:
class Point
{
...
private static int objectCount = 0;
...
}
ПРИМЕЧАНИЕ При объявлении такого поля, как objectCount, ключевые слова private и static можно размещать в любом порядке. Но предпочтительнее сначала указать private, а затем static.
Добавьте к обоим Point-конструкторам инструкции для увеличения значения поля objectCount на единицу (в следующем примере они выделены жирным шрифтом). Теперь класс Point должен приобрести следующий вид:
class Point
{
private int x, y;
private static int objectCount = 0;
public Point()
{
this.x = -1;
this.y = -1;
objectCount++;
}
public Point(int x, int y)
{
this.x = x;
this.y = y;
objectCount++;
}
public double DistanceTo(Point other)
{
int xDiff = this.x - other.x;
int yDiff = this.y - other.y;
double distance = Math.Sqrt((xDiff * xDiff) + (yDiff * yDiff));
return distance;
}
}
При каждом создании объекта вызывается его конструктор. Поскольку значение objectCount увеличивается на единицу в каждом конструкторе, включая пассивный, в objectCount будет содержаться определенное количество созданных на данный момент объектов. Эта стратегия работает благодаря тому, что objectCount является совместно используемым статическим полем. Если бы objectCount был полем экземпляра, то у каждого объекта было бы свое персональное поле objectCount с установленным для него значением 1.
А теперь возникает вопрос: как пользователи класса Point смогут определить количество созданных Point-объектов? На данный момент поле objectCount является закрытым, к нему нет доступа за пределами класса. Решение открыть публичный доступ к полю objectCount было бы неразумным, поскольку тогда была бы нарушена инкапсуляция класса и у вас после этого не было бы никаких гарантий относительно корректности значения поля objectCount, поскольку его мог бы изменить кто угодно. Более удачным решением будет предоставить открытый статический метод, возвращающий значение поля objectCount. Именно этим вы сейчас и займетесь.
Добавьте к классу Point открытый статический метод по имени ObjectCount, возвращающий int-значение, но не принимающий никаких параметров. Этот метод, выделенный в следующем примере жирным шрифтом, должен возвращать значение поля objectCount:
class Point
{
...
public static int ObjectCount() => objectCount;
}
Выведите в окно редактора код класса Program. Добавьте к методу doWork инструкцию для вывода на экран значения, возвращаемого методом ObjectCount класса Point (выделена жирным шрифтом):
static void doWork()
{
Point origin = new Point();
Point bottomRight = new Point(1366, 768);
double distance = origin.distanceTo(bottomRight);
Console.WriteLine($"Distance is: {distance}");
Console.WriteLine($"Number of Point objects: {Point.ObjectCount()}");
}
Метод ObjectCount вызывается за счет ссылки на Point, то есть на имя класса, а не на имя Point-переменной, такой как origin или bottomRight. Поскольку на момент вызова ObjectCount уже были созданы два Point-объекта, метод должен возвратить значение 2.
Щелкните в меню Отладка на пункте Запуск без отладки. Убедитесь в том, что в окно консоли выводится сообщение «Number of Point objects: 2» (после сообщения, отображающего значение переменной distance). Нажмите Ввод, чтобы закрыть программу и вернуться в среду Visual Studio 2015.
Безымянным называется класс, не имеющий имени. Звучит, конечно, странно, но как будет показано далее, в некоторых ситуациях использовать такие классы весьма удобно, особенно когда используются выражения запросов. (Эти выражения будут рассматриваться в главе 20 «Отделение логики приложения и обработка событий».) А сейчас в пользу их применения нужно просто поверить.
Безымянный класс создается путем использования ключевого слова new и пары фигурных скобок, внутри которых определяются содержащиеся в классе поля и значения:
myAnonymousObject = new { Name = "John", Age = 47 };
Этот класс содержит открытое поле Name, которое инициализируется строкой «John», и открытое поле Age, которое инициализируется целым числом 47. Компилятор определяет типы полей по типам данных, указанных при их инициализации.
Когда определяется безымянный класс, компилятор создает для него свое собственное имя, но какое именно, вам не сообщает. В связи с этим возникает, казалось бы, парадоксальная ситуация: если не известно имя класса, то как можно создать объект соответствующего типа и присвоить ему экземпляр класса? Тогда каким в показанном ранее примере кода должен быть тип переменной myAnonymousObject? Ответ такой: вы об этом не знаете, в чем и заключается характерная особенность безымянных классов!
Но если вы воспользуетесь ключевым словом var и объявите myAnonymousObject как переменную с неявно указанным типом, как показано в следующем примере, то проблемы не будет:
var myAnonymousObject = new { Name = "John", Age = 47 };
Вспомним, что ключевое слово var заставляет компилятор создавать переменную того же типа, к которому относится инициализирующее ее выражение. В данном случае типом выражения станет то самое имя, которое компилятор создаст для безымянного класса.
Доступ к полям объекта можно получить, используя уже известную форму записи с точкой:
Console.WriteLine($"Name: {myAnonymousObject.Name} Age: {myAnonymousObject.Age}"};
Можно даже создавать другие экземпляры одного и того же безымянного класса, но с разными значениями, например:
var anotherAnonymousObject = new { Name = "Diana", Age = 46 };
Для определения того, относятся ли два экземпляра безымянного класса к одному и тому же типу, компилятор C# использует имена, типы и порядок следования полей. В данном случае переменные myAnonymousObject и anotherAnonymousObject имеют одинаковое количество полей с одинаковыми именами и типами, которые стоят в одном и том же порядке, значит, обе переменные являются экземплярами одного и того же безымянного класса. Это означает, что вы можете использовать следующие инструкции присваивания:
anotherAnonymousObject = myAnonymousObject;
ПРИМЕЧАНИЕ Имейте в виду, что эта инструкция присваивания может не выполнить то, что вы от нее ожидаете. Более подробно вопрос о присваивании объектов-переменных будет рассмотрен в главе 8.
На содержимое безымянных классов накладывается довольно много ограничений. Например, безымянные классы могут содержать только открытые поля, все поля должны быть инициализированы, они не могут быть статическими, и вы не можете определять для этих классов какие-либо методы. Безымянные классы будут время от времени использоваться в данной книге, по мере чтения вы сможете с ними поближе познакомиться.
В данной главе вы увидели, как определяются новые классы. Узнали, что по умолчанию поля и методы класса являются закрытыми и недоступными для кода, находящегося за пределами класса, но чтобы открыть поля и методы для доступа к ним из внешнего мира, можно воспользоваться ключевым словом public. Вы увидели, как для создания нового экземпляра класса используется ключевое слово new и как определяются конструкторы, которые могут инициализировать экземпляры класса. И наконец, вы увидели, как создаются статические поля и методы для предоставления данных и операций, которые не зависят от конкретных экземпляров класса.
Если хотите продолжить работу и изучить следующую главу, оставьте открытой среду Visual Studio 2015 и переходите к главе 8.
Если вы хотите выйти из среды Visual Studio 2015, то в меню Файл щелкните на пункте Выход. Если увидите диалоговое окно с предложением сохранить изменения, щелкните на кнопке Да и сохраните проект.
Чтобы | Сделайте следующее |
Объявить класс | Напишите ключевое слово class, за которым укажите имя класса, а затем поставьте открывающую и закрывающую фигурные скобки. Методы и поля класса объявляются между этими фигурными скобками, например: class Point { ... } |
Объявить конструктор | Напишите метод, имя которого совпадает с именем класса, а возвращаемый тип (даже void) отсутствует, например: class Point { public Point(int x, int y) { ... } } |
Вызвать конструктор | Воспользуйтесь ключевым словом new и укажите конструктор с соответствующим набором параметров, например: Point origin = new Point(0, 0); |
Объявить статический метод | Напишите перед объявлением метода ключевое слово static, например: class Point { public static int ObjectCount() { ... } } |
Вызвать статический метод | Напишите имя класса, после него поставьте точку, а далее укажите имя метода, например: int pointsCreatedSoFar = Point.ObjectCount(); |
Объявить статическое поле | Поставьте ключевое слово static перед типом поля, например: class Point { ... private static int objectCount; } |
Объявить поле const | Напишите перед объявлением поля ключевое слово const и не указывайте ключевое слово static, например: class Math { ... public const double PI = ...; } |
Получить доступ к статическому полю | Напишите имя класса, поставьте после него точку, а далее укажите имя статического поля, например: double area = Math.PI * radius * radius; |