Statmod.Ru
НовостиНовости
ФорумФорум
МатМех СпбГУWiki-страницы
О кафедреСтудентам I-IIБакалавры и магистрыСтудентам III-VВыпускникам
Главная -> Студентам III-V -> Программирование -> C#: первое впечатление

C#. Первое впечатление

Объектом данного сочинения является язык программирования C#. Я опишу первое впечатление от знакомства с ним. Основой для этого сделаю сравнение с C++. Не только и не столько потому, что эти языки похожи, а в силу того, что на данный момент лучше всего я знаком с C++ и обладаю некоторым опытом в разработке программ именно на последнем. Кроме того, иногда я буду ссылаться на Java, на который C# похож не меньше, а то и больше, чем на C++.

C# является объектно-ориентированным языком в стиле C++. Спецификацию этого языка можно найти в MSDN. В нем, в дополнение к встроенным типам, также используются встроенные интерфейсы для поддержки некоторых важных методов программирования. Синтаксис C# требует от программиста более четко указывать свои намерения, чем синтаксис C++, унаследованный последним от чистого C. Стоит отметить, что популярность языка во многом зависит от наличия тех или иных библиотек. C# ориентирован на использование на платформе .NET, для которой имеются обширный библиотеки разных классов. Я же сосредоточусь на самом языке, безотносительно этих библиотек, а именно, на некоторых идеологических основах и базовых конструкциях языка.

Отличия от C++

Хотелось бы начать с одного из основных отличий C# от C++, дающего начало ряду основных возможностей языка. А именно, если программа, написанная на C++, компилятором и компоновщиком преобразуется в машинный код конкретной платформы, то программа на C# компилируется в промежуточный MSIL (Microsoft Intermediate Language) код. И уже этот MSIL код позже преобразуется в код под конкретную платформу. Причем это может происходить в любой момент, от получения кода до запуска приложения. Это относится не столько к C#, сколько к CLR (Common Language Runtime), среде исполнения MSIL кода, поэтому здесь я не буду останавливаться, а подробнее об этом можно узнать в соответствующем разделе CLR.

Использование абстрактной машины для C# приводит к необходимости отдельно разбираться с управлением памятью. В C#CLR в целом) для этой цели используется сборщик мусора (garbage collector). Аналогичное решение используется и в Java. Преимущество такого подхода, кроме разных способов работы с памятью на разных платформах, заключается еще и в отсутствии необходимости явного выделения и освобождения памяти программистом. Сборка мусора осуществляется либо в моменты простоя программы (когда она ожидает команд пользователя), либо по мере необходимости в свободной памяти (сборку мусора можно вызвать и в коде). Сборщик мусора отслеживает состояние объектов — ссылается ли на них кто-нибудь или нет. Если он находит изолированную (от остальной программы) группу объектов, то он запускает процедуру освобождения занимаемой ими памяти. Здесь детально это рассматривать я не буду. К сожалению, помимо плюсов у сборки мусора есть и минусы. Первый — это снижение быстродействия, отслеживание состояния объектов отнюдь не ускоряет работу программы. Второе — потеря ясности в том, когда происходит удаление объектов (вызов деструктора в смысле C++). Помимо этого, это усложняет использование одной из методологий ООП, когда в конструкторе объекта происходит захват ресурса, а в деструкторе происходит его освобождение. Данный метод удобен и эффективен в плане уменьшения количества ошибок, так как вызов парного метода происходит автоматически. Разработчики C# сочли этот метод настолько эффективным и удобным, что он поддерживается на уровне языка — посредством реализации интерфейса IDisposable. (Java такого механизма не имеет.)

Стоит отметить, что в C# очень большое внимание уделяется безопасности кода. Это выражается в запрещении потенциально некорректных операций. В случае же невозможности запретить операцию такого рода (например, приведение типов), проверка на ошибки осуществляется динамически и в случае ее возникновения выбрасывается соответствующее исключение. Такой подход приводит к уменьшению возможностей языка. В C# выход из этой ситуации заключается в использовании интерфейсов, поддержанных на уровне языка. Примером такого интерфейса может служить уже упоминавшийся IDisposable (в данном случае сборку мусора можно рассматривать как решение проблем с управлением памятью).

Вернуться в начало

Классы и Структуры (Classes and Structures)

Как и в C++, классы и структуры являются основными рабочими объектами языка. Эти две конструкции языка представляют одинаковые понятия, за исключением одного свойства. Переменные структур являются типами-значениями, а классов — типами-ссылками. То есть присвоение одной структуры другой вызывает операцию копирования содержимого одной из них в другую, а после присвоения переменной типа класса другой приводит к тому, что обе они ссылаются на один объект.

Подобно классам в C++, классы в C# могут содержать методы и поля. Кроме того, классы могут содержать некоторые другие конструкции языка, такие, как события и свойства. У класса может быть только один базовый класс, вернее — всегда один, так как если класс не наследует никакие пользовательские классы, то он неявно наследуется от базового класса object. В то же время, C# допускает реализацию нескольких интерфейсов в одном классе.

Все сказанное о классах справедливо и для структур, за исключением наследования. В отличие от классов все структуры неявно наследуются от типа ValueType, а наследование от самих структур запрещено. При этом структуры, как и классы, могут реализовывать один или несколько интерфейсов.

Вернуться в начало

Интерфейсы (Interfaces)

В целом весь C# ориентирован на использование интерфейсов. В нем интерфейсы представлены отдельной структурой языка. Интерфейс объявляется с ключевым словом interface и является почти такой же структурой языка, как и класс, за несколькими исключениями.

В отличие от класса интерфейс не может содержать полей. Интерфейс может содержать все остальные конструкции языка, которые могут содержаться в классе. Такие, как свойства, события и методы. Стоит отметить, что интерфейс не может содержать определений методов, свойств или событий. В терминах C++ интерфейс представляет собой абстрактный класс. Все методы интерфейса должны быть реализованы в классах, наследующих данный интерфейс.

Отказ от использования полей в интерфейсе позволяет поддерживать множественное наследование от интерфейсов, в то время как наличие полей в классах препятствует множественному наследованию классов. Такой подход можно рассматривать как вполне разумное сокращение возможностей C++. При множественном наследовании классов возникает много проблем, как у разработчиков компиляторов, так и у рядовых программистов. Разработчикам компиляторов необходимо заботиться о правильном размещении таких классов в памяти и передавать указатели на базовые классы, плюс необходимо принимать не самые простые решения в случае виртуального наследования. У остальных программистов возникают проблемы в случае наследования от классов, наследующих один базовый, да и в некоторых других тоже. Разрешение множественного наследования только от интерфейсов позволяет избежать этих проблем.

В Java интерфейс имеет подобную смысловую нагрузку и подобные свойства. Кроме этого, в Java для наследования от интерфейсов введено специальное ключевое слово implements (реализовывает), которое достаточно точно характеризует смысл такого отношения между классом и интерфейсом. Интерфейс определяет контракт работы классов, его реализующих, а классы обязаны его выполнить-реализовать.

Вернуться в начало

Свойства (Properties)

В C# представлена интересная идея свойств объекта. С точки зрения пользователя класса свойства представляют собой обычное поле, в то время как внутри класса каждое свойство представляется методами доступа, так называемыми getter’ом и/или setter’ом. Геттер и сеттер представляют пару методов для чтения/записи данных. В зависимости от типа поля, у свойства может быть только один такой метод (read-only — только для чтения, write-only — только для записи) или оба (read-write — чтение и запись). Каждый из методов является безымянным, то есть вызов каждого из них происходит только неявно, через обращение к свойству.

Вернуться в начало

Делегаты и События (Delegates and Events)

Часто в процессе написания метода возникает необходимость вызвать другой метод, неизвестный в этом, а определяемый в месте вызова (или определяемый ранее в процессе выполнения программы). Обычно это бывает необходимо для обработки объектов в коллекциях, например, получения какой-либо информации из них, или получения информации в процессе выполнения долгой операции. В C++ это можно сделать двумя способами: использовать специально определенный интерфейс (наследуя от него конкретные классы и реализовывая необходимые методы), или callback, обычно это указатель на функцию. И если в C# первый способ вполне уместен и поддерживается, то второй потенциально опасен. Поэтому для поддержки второго способа в язык были введены делегаты. Делегаты представляют из себя абстракцию указателя на функцию. Тип делегата определяется сигнатурой метода, который может быть вызван через каждый конкретный экземпляр. Делегат можно использовать, как обычную переменную. Вызов делегата синтаксически такой же, как и вызов обычной функции.

Другой проблемой, решаемой с помощью делегатов, является передача информации об изменении одного объекта другому объекту без необходимости установления непосредственной связи между ними. Эта проблема появляется достаточно часто при связи различных компонент, когда изменение одной опосредованно должно отразиться на другой. Особенно часто такая проблема возникает при реализации пользовательского интерфейса, где интерфейсные классы (различные окна (windows) и редакторы (editors)) достаточно сильно взаимодействуют с кодом, обрабатывающим данные. Для данной проблемы характерны условия, в которых объектов, получающих уведомления об изменениях, несколько, а их состав может меняться динамически. В таком случае необходимо иметь коллекцию делегатов, которая поддержана в языке в виде Событий. События поддерживают добавление и удаление делегатов, а также вызов последних с конкретными параметрами. Уведомление о событии (вызов всех добавленных делегатов) выглядит, как вызов обычной функции. Стоит отметить, что Событие может быть вызвано только из класса, где оно определено.

Вернуться в начало

Common Language Runtime

Программа, написанная на C++, сначала компилируется в набор отдельных компонент, которые потом связываются (link) компоновщиком (linker) в программу (или отдельную компоненту) предназначенную для работы на машине с определенной архитектурой и операционной системой. Промежуточные результаты в большинстве случаев никого не интересуют. Проект на C# компилируется сразу в готовый модуль на «промежуточном» бинарном языке MSIL (Microsoft Intermediate Language). В первую очередь MSIL предназначен для абстракции объектно-ориентированного кода, написанного на конкретном языке для конкретной платформы. В идеале это позволяет скомпилированный код запускать на любой машине.

Такое решение не было новым. Еще раньше в основу Java была положена та же идея — запуск единожды собранной программы на любой платформе. И для работы готовой программы, написанной на Java, необходимо иметь только платформу с установленной виртуальной машиной Java (Java Virtual Machine) (в байт-код для которой как раз и компилируется исходный код программы). Первые реализации виртуальной машины Java представляли собой интерпретаторы, на лету исполняя байт-код. Однако такой подход оказался чреват сильной потерей производительности (что и понятно — эмулирование работы одной машины на другой скорости не добавляет). Поэтому через какое-то время появились новые реализации JVM, перед запуском компилировавшие байт-код в родной («нативный», native) для конкретной машины код. Такой подход называется Just-In-Time компиляцией, то есть компиляцией во время исполнения.

При разработке MSIL были учтены эти проблемы. Поэтому MSIL программа работает в среде CLR (Common Language Runtime), и перед запуском исходный MSIL код компилируется в родной код конкретной платформы. Сейчас обычно это делается сразу после генерации MSIL кода, но это может происходить и в момент установки программы, или непосредственно перед первым запуском. При этом последние два варианта могут дать ускорение производительности программы, так как в момент инсталляции (или первого запуска программы) уже известны все особенности платформы, на которой будет работать программа, и компилятор «нативного» кода может воспользоваться этим для оптимизации программы. Естественно, что такого рода действия конечная платформа должна поддерживать.

Сейчас CLR поддерживается в трех версиях платформы .NET под Windows: 1.0, 1.1, 2.0. Программы под них можно писать на Microsoft Visual Studio версий .NET (7.0), 2003 (7.1), 2005 (8.0) соответственно. Версия 1.0 почти нигде сейчас не используется. Кроме C# генерация MSIL кода осуществляется также с языков Managed C++ (расширение от Microsoft языка C++ для CLR), Visual Basic, Visual J#, входящих в упоминавшиеся пакеты Visual Studio. В силу ориентации на CLR имеется возможность удобного сочетания любого из названных языков. Кроме того, имеется возможность вызова непереносимого кода, написанного под какую-либо конкретную платформу. Как правило, это реализуется через вызов из Managed C++ кода, написанного на Unmanaged C++ (обычный C++), хотя такая возможность имеется и в других языках.

Программы, написанные на C++, также можно перенести на разные платформы, но для этого приходится перекомпилировать программу. При этом появляется проблема компиляции программы на разных компиляторах для разных платформ. Для программ, поставляющихся с исходным кодом, это является только техническим ограничением. Но для коммерческих программ, поставляемых в виде бинарной программы, это является практически непреодолимым ограничением, если разработчик не имеет версии для нужной платформы.

Если говорить о переносимости программ, написанных под .NET, то несмотря на перспективность CLR, эти программы, в основном, ориентированы только на Windows. Насколько я знаю, разрабатывается Open Source проект DotGNU (www.dotgnu.org) для поддержки .NET на Linux, но сведений о его реальном использовании у меня нет.

Павел Потапов aka Scavenger (выпуск 2003 СМ)

Вернуться в начало