Введение в C#: классы
Введение в C#: классы
Вадим Бодров
Система
классов играет важную роль в современных языках программирования. Как же они
реализованы в новом языке C#, созданном корпорацией Microsoft, и зачем нужно
изучать С#?
Ответы
на эти вопросы зависят от того, как вы собираетесь работать дальше. Если вы
хотите создавать приложения для платформы .NET, то вам, скорее всего, не
удастся избежать изучения C#. Конечно, можно использовать и Си++, и Visual
Basic или любой язык программирования, тем более что независимыми
разработчиками создаются трансляторы с APL, Кобола, Eiffel, Haskell, Оберона,
Smalltalk, Perl, Python, Паскаля и др. Однако для компилятора, способного
генерировать приложения среды .NET CLR (Common Language Runtime), только C#
является «родным» языком. Он полностью соответствует идеологии .NET и позволяет
наиболее продуктивно работать в среде CLR. В свое время для использования
виртуальной машины Java было создано множество так называемых «переходников»
(bridges) c различных языков программирования, в частности PERCobol, JPython,
Eiffel-to-JavaVM System, Tcl/Java и т.д. Подобные разработки так и не получили
должного распространения. Практика показала, что значительно проще изучить
новый язык, чем вводить дополнительные расширения в менее подходящую для данных
целей систему программирования. И не надо быть провидцем, чтобы утверждать, что
бо,льшая часть программистов, создающих приложения для платформы .NET, отдаст
предпочтение именно языку C#.
C#
является языком объектно-ориентированного программирования, поэтому классы
играют в нем основополагающую роль. Более того, все типы данных C#, как
встроенные, так и определенные пользователем, порождены от базового класса
object. Иными словами, в отличие от Java, где примитивные типы данных отделены
от объектных типов, все типы данных в C# являются классами и могут быть
разделены на две группы:
ссылочные (reference types);
обычные (value types).
Внешне
ссылочные и обычные типы очень похожи, так как аналогично Cи++ в них можно
объявлять конструкторы, поля, методы, операторы и т.д. Однако, в отличие от
Cи++, обычные типы в C# не позволяют определять классы и не поддерживают
наследования. Они описываются с помощью ключевого слова struct и в основном
используются для создания небольших объектов. Ссылочные же типы описываются с
помощью ключевого слова class и являются указателями, а экземпляры таких типов
ссылаются на объект, находящийся в куче (heap). Продемонстрируем сказанное на
примере:
using
System;
class
CValue
{
public
int val;
public CValue(int x) {val = x;}
}
class Example_1
{
public static void Main()
{
CValue p1 = new CValue(1);
CValue p2 = p1;
Console.WriteLine(”p1 = {0}, p2 =
{1}”,
p1.val, p2.val);
p2.val = 2;
Console.WriteLine(”p1 = {0}, p2 =
{1}”,
p1.val,
p2.val);
}
}
Откомпилировав
и выполнив программу, получим следующий результат:
p1
= 1, p2 = 1
p1
= 2, p2 = 2
Как
нетрудно видеть, p2 является всего лишь ссылкой на p1. Тем самым становится
очевидно, что при изменении поля val экземпляра класса p2 в действительности изменяется
значение соответствующего поля p1. Подобный подход не очень удобен при работе с
примитивными типами данных, которые должны содержать само значение, а не ссылку
на него (Complex, Point, Rect, FileInfo и т.д.). Для описания таких объектов и
предназначены типы значений:
using
System;
struct SValue
{
public int val;
public SValue(int x) {val = x;}
}
class Example_2
{
public static void Main()
{
SValue p1 = new SValue(1);
SValue p2 = p1;
Console.WriteLine(”p1 = {0}, p2 =
{1}”,
p1.val, p2.val);
p2.val = 2;
Console.WriteLine(”p1 = {0}, p2 =
{1}”,
p1.val,
p2.val);
}
}
Вот
что получится после запуска вышеприведенной программы:
p1
= 1, p2 = 1
p1
= 1, p2 = 2
Из
этого следует, что экземпляр класса p2 является самостоятельным объектом,
который содержит собственное поле val, не связанное с p1. Использование обычных
типов позволяет избежать дополнительного расходования памяти, поскольку не
создаются дополнительные ссылки, как в случае с экземплярами классов. Конечно,
экономия невелика, если у вас имеется всего несколько небольших объектов типа
Complex или Point. Зато для массива, содержащего несколько тысяч таких
элементов, картина может в корне измениться. В таблице приведены основные
отличия типов class и struct.
Интерфейсы
Классы
в языке C# претерпели довольно серьезные изменения по сравнению с языком
программирования Cи++, который и был взят за основу. Первое, что бросается в
глаза, это невозможность множественного наследования. Такой подход уже знаком
тем, кто пишет на языках Object Pascal и Java, а вот программисты Cи++ могут
быть несколько озадачены. Хотя при более близком рассмотрении данное
ограничение уже не кажется сколь-нибудь серьезным или непродуманным. Во-первых,
множественное наследование, реализованное в Cи++, нередко являлось причиной
нетривиальных ошибок. (При том что не так уж часто приходится описывать классы
с помощью множественного наследования.) Во-вторых, в C#, как и в диалекте
Object Pascal фирмы Borland, разрешено наследование от нескольких интерфейсов.
Интерфейсом
в C# является тип ссылок, содержащий только абстрактные элементы, не имеющие
реализации. Непосредственно реализация этих элементов должна содержаться в
классе, производном от данного интерфейса (вы не можете напрямую создавать
экземпляры интерфейсов). Интерфейсы C# могут содержать методы, свойства и
индексаторы, но в отличие, например, от Java, они не могут содержать
константных значений. Рассмотрим простейший пример использования интерфейсов:
using
System;
class
CShape
{
bool
IsShape() {return true;}
}
interface IShape
{
double Square();
}
class CRectangle: CShape, IShape
{
double width;
double height;
public CRectangle(double width,
double height)
{
this.width = width;
this.height = height;
}
public double Square()
{
return (width * height);
}
}
class CCircle: CShape, IShape
{
double radius;
public CCircle(double radius)
{
this.radius = radius;
}
public double Square()
{
return (Math.PI * radius * radius);
}
}
class Example_3
{
public static void Main()
{
CRectangle rect = new CRectangle(3,
4);
CCircle circ = new CCircle(5);
Console.WriteLine(rect.Square());
Console.WriteLine(circ.Square());
}
}
Оба
объекта, rect и circ, являются производными от базового класса CShape и тем
самым они наследуют единственный метод IsShape(). Задав имя интерфейса IShape в
объявлениях CRectangle и CCircle, мы указываем на то, что в данных классах
содержится реализация всех методов интерфейса IShape. Кроме того, члены
интерфейсов не имеют модификаторов доступа. Их область видимости определяется
непосредственно реализующим классом.
Свойства
Рассматривая
классы языка C#, просто нельзя обойти такое «новшество», как свойства
(properties). Надо сказать, что здесь чувствуется влияние языков Object Pascal
и Java, в которых свойства всегда являлись неотъемлемой частью классов. Что же
представляют собой эти самые свойства? С точки зрения пользователя, свойства
выглядят практически так же, как и обычные поля класса. Им можно присваивать
некоторые значения и получать их обратно. В то же время свойства имеют бо,льшую
функциональность, так как чтение и изменение их значений выполняется с помощью
специальных методов класса. Такой подход позволяет изолировать пользовательскую
модель класса от ее реализации. Поясним данное определение на конкретном
примере:
using
System;
using
System.Runtime.InteropServices;
class Screen
{
[DllImport(”kernel32.dll”)]
static extern bool
SetConsoleTextAttribute(
int hConsoleOutput, ushort
wAttributes
);
[DllImport(”kernel32.dll”)]
static extern int GetStdHandle(
uint nStdHandle
);
const uint STD_OUTPUT_HANDLE =
0x0FFFFFFF5;
static Screen()
{
output_handle =
GetStdHandle(STD_OUTPUT_HANDLE);
m_attributes = 7;
}
public static void
PrintString(string str)
{
Console.Write(str);
}
public static ushort Attributes
{
get
{
return m_attributes;
}
set
{
m_attributes = value;
SetConsoleTextAttribute(output_handle,
value);
}
}
private static ushort m_attributes;
private static int output_handle;
}
class Example_4
{
public static void Main()
{
for (ushort i = 1; i < 8; i++)
{
Screen.Attributes = i;
Screen.PrintString(”Property Demo
”);
}
}
}
Программа
выводит сообщение «Property Demo», используя различные цвета символов (от
темно-синего до белого). Давайте попробуем разобраться в том, как она работает.
Итак, сначала мы импортируем важные для нас функции API-интерфейса Windows: SetConsoleTextAttribute
и GetStdHandle. К сожалению, стандартный класс среды .NET под названием Console
не имеет средств управления цветом вывода текстовой информации. Надо полагать,
что корпорация Microsoft в будущем все-таки решит эту проблему. Пока же для
этих целей придется воспользоваться службой вызова платформы PInvoke (обратите
внимание на использование атрибута DllImport). Далее, в конструкторе класса
Screen мы получаем стандартный дескриптор потока вывода консольного приложения
и помещаем его значение в закрытую переменную output_handle для дальнейшего
использования функцией SetConsoleTextAttribute. Кроме этого, мы присваиваем
другой переменной m_attributes начальное значение атрибутов экрана (7
соответствует белому цвету символов на черном фоне). Заметим, что в реальных
условиях стоило бы получить текущие атрибуты экрана с помощью функции
GetConsoleScreenBufferInfo из набора API-интерфейса Windows. В нашем же случае
это несколько усложнило бы пример и отвлекло от основной темы.
В
классе Screen мы объявили свойство Attributes, для которого определили функцию
чтения (getter) и функцию записи (setter). Функция чтения не выполняет
каких-либо специфических действий и просто возвращает значение поля
m_attributes (в реальной программе она должна бы возвращать значение атрибутов,
полученное с помощью все той же GetConsoleScreenBufferInfo). Функция записи
несколько сложнее, так как кроме тривиального обновления значения m_attributes
она вызывает функцию SetConsoleTextAttribute, устанавливая заданные атрибуты функций
вывода текста. Значение устанавливаемых атрибутов передается специальной
переменной value. Обратите внимание на то, что поле m_attributes является
закрытым, а стало быть, оно не может быть доступно вне класса Screen.
Единственным способом чтения и/или изменения этого метода является свойство
Attributes.
Свойства
позволяют не только возвращать и изменять значение внутренней переменной
класса, но и выполнять дополнительные функции. Так, они позволяют произвести
проверку значения или выполнить иные действия, как показано в вышеприведенном
примере.
В
языке C# свойства реализованы на уровне синтаксиса. Более того, рекомендуется
вообще не использовать открытых полей классов. На первый взгляд, при таком
подходе теряется эффективность из-за того, что операции присваивания будут
заменены вызовами функций getter и setter. Отнюдь! Среда .NET сгенерирует для
них соответствующий inline-код.
Делегаты
Язык
программирования C# хотя и допускает, но все же не поощряет использование
указателей. В некоторых ситуациях бывает особенно трудно обойтись без
указателей на функции. Для этих целей в C# реализованы так называемые делегаты
(delegates), которые иногда еще называют безопасными аналогами указателей на
функцию. Ниже приведен простейший пример использования метода-делегата:
using
System;
delegate
void MyDelegate();
class Example_5
{
static void Func()
{
System.Console.WriteLine(«MyDelegate.Func()»);
}
public static void Main()
{
MyDelegate f = new MyDelegate(Func);
f();
}
}
Помимо
того что делегаты обеспечивают типовую защищенность, а следовательно, и
повышают безопасность кода, они отличаются от обычных указателей на функции еще
и тем, что являются объектами, производными от базового типа System.Delegate.
Таким образом, если мы используем делегат для указания на статический метод
класса, то он просто связывается с соответствующим методом данного класса. Если
же делегат указывает на нестатический метод класса, он связывается уже с
методом экземпляра такого класса. Это позволяет избежать нарушения принципов
ООП, поскольку методы не могут быть использованы отдельно от класса (объекта),
в котором они определены.
Еще
одним отличием делегатов от простых указателей на функции является возможность
вызова нескольких методов с помощью одного делегата. Рассмотрим это
на конкретном примере:
using System;
delegate void MyDelegate(string
message);
class Example_6
{
public static void Func1(string
message)
{
Console.WriteLine(”{0}:
MyDelegate.Func1”, message);
}
public static void Func2(string
message)
{
Console.WriteLine(”{0}: MyDelegate.Func2”,
message);
}
public static void Main()
{
MyDelegate f1, f2, f3;
f1 = new MyDelegate(Func1);
f2 = new MyDelegate(Func2);
f3 = f1 + f2;
f1(”Calling delegate f1”);
f2(”Calling delegate f2”);
f3(”Calling delegate f3”);
}
}
Откомпилировав
и выполнив вышеприведенную программу, получим следующий результат:
Calling delegate f1:
MyDelegate.Func1
Calling delegate f2:
MyDelegate.Func2
Calling delegate f3:
MyDelegate.Func1
Calling delegate f3:
MyDelegate.Func2
Из
этого следует, что вызов метода-делегата f3, полученного с помощью операции
сложения f1 + f2, приводит к последовательному выполнению обоих этих методов.
Подобно применению операции сложения с целью объединения делегатов, можно
использовать и операцию вычитания, которая, как нетрудно догадаться, выполняет
обратное действие.
Способы передачи параметров
Анализируя
особенности реализации классов языка C#, хотелось бы уделить внимание и
способам передачи параметров метода по ссылке. Иногда возникает потребность в
том, чтобы функция возвращала сразу несколько значений. Рассмотрим это на
примере программы, вычисляющей квадратный корень:
using
System;
class Example_7
{
static int GetRoots(double a, double
b, double c,
out double x1, out double x2)
{
double d = b * b - 4 * a * c;
if (d > 0)
{
x1 = -(b + Math.Sqrt(d)) / (2 * a);
x2 = -(b - Math.Sqrt(d)) / (2 * a);
return 2;
} else
if (d == 0)
{
x1 = x2 = -b / (2 * a);
return 1;
} else
{
x1 = x2 = 0;
return 0;
}
}
public static void Main()
{
double x1, x2;
int roots = GetRoots(3, -2, -5, out
x1, out x2);
Console.WriteLine(”roots #: {0}”,
roots);
if (roots == 2)
Console.WriteLine(”x1 = {0}, x2 =
{1}”, x1, x2);
else
if (roots == 1)
Console.WriteLine(”x = {0}”, x1);
}
}
Чтобы
функция GetRoots возвращала оба корня уравнения (x1 и x2), мы указали транслятору,
что переменные x1 и x2 должны быть переданы по ссылке, применив для этого
параметр out. Обратите внимание на то, что нам не обязательно инициализировать
переменные x1 и x2 перед вызовом функции GetRoots. Обозначив функцию ключевым
словом out, мы добьемся того, что ее аргументы могут использоваться только для
возврата какого-то значения, но не для его передачи внутрь функции. Таким
образом, подразумевается, что переменная будет инициализирована в теле самой
функции. В случае же, если нам по какой-то причине потребуется передать в
параметре функции некоторое значение с возможностью его последующего изменения,
можно воспользоваться параметром ref. Действие этого параметра очень похоже на
действие out, но он позволяет еще и передавать значение параметра телу функции.
Второе отличие ключевого слова ref состоит в том, что передаваемый параметр
функции должен быть инициализирован предварительно.
Такой
метод очень напоминает использование параметра var в списке аргументов функций,
принятое в языке программирования Паскаль, и является еще одним отличием от
языка Java, где параметры всегда передаются по значению.
Заключение
Язык
программирования C#, как и платформа .NET, находится в развитии. В частности, в
ближайшее время можно ожидать появления обобщенных шаблонов, которые подобно
шаблонам языка Cи++ позволят создавать сильно типизированные классы-коллекции.
В любом случае язык программирования C# уже вполне сформировался для того,
чтобы его изучить и начать применять в реальных приложениях.
Список литературы
C# Language Specification. Microsoft Corporation,
2000.
Гуннерсон
Э. Введение в C#. СПб.: Питер, 2001.
Бесплатная
версия .NET Framework SDK Beta 1: www.microsoft.com/downloads.
Обширнейшая
информация по платформе .NET: www.gotdotnet.com.
Официальная
конференция по языку C#: news://msnews.microsoft.com/
microsoft.public.dotnet.languages.csharp.
Инструментарий С#
Прежде
чем начать работу с языком программирования C#, необходимо установить на
компьютере набор инструментальных средств под названием .Net Framework SDK,
бета-версия которого доступна для бесплатной загрузки непосредственно c
Web-страницы корпорации Microsoft [3]. Кроме того, понадобится хороший
текстовый редактор, поддерживающий синтаксически настраиваемый ориентированный
режим (syntax highlight) и позволяющий выделять ключевые слова в исходных
текстах того или иного языка программирования. Я рекомендую программу
SharpDevelop (www.icsharpcode.net), распространяемую независимыми
программистами на условиях лицензии GNU. В крайнем случае можно использовать
любой редактор, способный работать с исходными текстами на языке Cи/Cи++, или
даже обычный текстовый редактор Notepad.
Основные отличия типов struct и
class
Похожие работы на - Введение в C#: классы
|