div.main {margin-left: 20pt; margin-right: 20pt}
Введение в 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 Demon”);
}
}
}
Программа выводит сообщение «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# уже вполне сформировался для того, чтобы
его изучить и начать применять в реальных приложениях.
Литература и Internet-ресурсы
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
|
Тип class |
Тип struct |
Представление экземпляра типа |
указатель |
значение |
Местоположение объекта |
куча |
стек |
Значение по умолчанию |
null |
заполняется нулями |
Результат операции присваивания для экземпляров типа |
копируется указатель |
копируется сам объект |
Базовый тип |
встроенный тип string |
встроенный тип int |
C# и Java
Язык программирования C# часто и небезосновательно сравнивают с
Java. Оба языка были созданы для аналогичных целей и имеют много
общего, в том числе синтаксис, базирующийся на Cи++. В то же время
есть и множество различий, относящихся к базовым типам, классам,
способам передачи параметров, реализации интерфейсов и т. д.
Основным же несходством между C# и Java является то, что
Java-приложения работают со средой Java Frameworks and Runtime, а
C#-приложения — со средой .NET Framework and Runtime. В полном
объеме концепция .NET будет реализована только в новой операционной
системе Windows XP (также известна как Whistler), хотя она уже около
года активно продвигается корпорацией Microsoft. Похоже, если вы
планируете создавать приложения, совместимые с платформой Microsoft,
явно стоит поближе познакомиться с Microsoft .NET. Лучшим же языком
для создания .NET-приложений, по утверждению самой корпорации
Microsoft, является C#.
От двух до...
Исходный текст любого исполняемого приложения, написанного на
языке программирования C#, содержит статический метод Main(), —
аналог знакомой программистам Си/Си++ функции main(). Именно с этого
метода начинается выполнение программы.
Что же произойдет, если исходный текст будет содержать два или
более методов Main(), как показано ниже? using System;
class SayHello
{
public static void Main()
{
Console.WriteLine(”Hello friend!”);
}
}
class SayBye
{
public static void Main()
{
Console.WriteLine(”Bye, bye...”);
}
}
Разумеется, компиляция этого примера вызовет сообщение об ошибке,
так как классы SayHello и SayBye абсолютно «равноправны» с точки
зрения транслятора. Процесс компиляции будет прерван. Однако
существует специальный ключ компилятора /main, с помощью которого
можно указать класс, содержащий нужный нам метод Main().
Вышеприведенный пример, откомпилированный с ключом /main:SayHello,
напечатает сообщение: Hello friend!
Если же откомпилировать тот же самый пример, указав ключ
/main:SayBye, то текст будет иным: Bye, bye...
|