Система классов играет важную роль в современных языках программирования. Как же они реализованы в новом языке 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# уже вполне сформировался для того, чтобы его изучить и начать применять в реальных приложениях.
Литература и 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.
Тип 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...