• Введение
  • Соглашения в этом приложении
  • Терминология
  • Сравнение C# и C++
  • Различия
  • Сходства
  • Новые свойства
  • Новые свойства базовых классов
  • Неподдерживаемые свойства
  • Пример Hello World
  • Инструкции #include
  • Пространства имен
  • Точка входа: Main() и main()
  • Вывод сообщения
  • Сравнение свойств
  • Архитектура программы
  • Программные объекты
  • Файловая структура
  • Точка входа программы
  • Синтаксис языка
  • Опережающие объявления
  • Отсутствие разделения определения и объявления
  • Поток выполнения программы
  • if…else
  • while и do…while
  • switch 
  • foreach
  • Переменные
  • Базовые типы данных
  • Базовые типы данных как объекты
  • Преобразования базовых типов данных
  • Проверяемое (checked) преобразование типов данных
  • Строки 
  • Последовательности кодирования
  • Типы значений и ссылочные типы
  • Инициализация переменных
  • Упаковка
  • Управление памятью
  • Оператор new
  • Методы 
  • Параметры методов
  • Перезагрузка методов
  • Свойства
  • Операторы
  • Оператор присваивания (=)
  • this
  • new
  • Классы и структуры
  • Классы 
  • Определение класса
  • Инициализация полей членов
  • Конструкторы
  • Статические конструкторы
  • Конструкторы по умолчанию
  • Списки инициализации конструктора
  • Деструкторы
  • Наследование
  • Виртуальные и невиртуальные функции
  • Структуры
  • Константы
  • Константы, ассоциированные с классом (статические константы)
  • Константы экземпляра
  • Перезагрузка операторов
  • Индексаторы
  • Определенные пользователем преобразования типов данных
  • Массивы
  • Одномерные массивы
  • Многомерные массивы
  • Проверка границ
  • Изменение размера массивов
  • Перечисления
  • Исключения
  • Указатели и небезопасный код
  • Фиксация донных в куче
  • Объявление массивов в стеке
  • Интерфейсы
  • Делегаты
  • События
  • Атрибуты
  • Директивы препроцессора
  • Пpиложeние A

    C# для разработчиков C++ 

    Введение

    Это приложение предназначено для разработчиков, которые уже хорошо знакомы с C++ и хотят узнать, какие существуют различия между C++ и C#. Мы сделаем обзор языка C#, отмечая специально те области, где он отличается от C++. Так как два языка имеют много общего в синтаксисе и методологии, хорошо подготовленные программисты C++ смогут использовать это приложение в качестве краткого курса C#.

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

    В связи со сходством между этими двумя языками программирования разработчики, использующие C++, могут обнаружить, что самый простой путь изучения C# состоит в использовании его как C++ с небольшими отличиями и в изучении этих отличий. Это приложение создано для того, чтобы в этом помочь. Мы начнем с обширного обзора, который в общих терминах дает понятия об основных различиях между двумя языками и также указывает, какие области у них совпадают. Затем мы сравним как выглядит стандартная программа "Hello, World" в каждом из этих языков. Большой объем приложения посвящен последовательномy анализу каждой из основных областей языка и подробному сравнению C# и C++. Очевидно, что приложение такого объема не может быть исчерпывающе полным, но оно создано для того чтобы охватить главные различия между языками, которые могут встретиться в ходе повседневного программирования. Отметим, что C# в большом числе областей существенно опирается на поддержку библиотеки базовых классов платформы .NET. В этом приложении мы ограничим наше внимание самим языком C# и не будем подробно рассматривать базовые классы.

    Для целей сопоставления в качестве эталона используется ANSI C++. Компания Microsoft добавила многочисленные расширения к C++, но компилятор Windows C++ имеет некоторые отличия, несовместимые со стандартом ANSI, которые будут указаны, хотя они обычно не используются при сравнении двух языков.

    Соглашения в этом приложении

    Отметим, что в данном приложении мы придерживаемся дополнительных соглашений при изображении кода. Код C# всегда выводится, как и в остальных частях книги, с серым затенением:

    // это код C#

    class MyClass : MyBaseClass {
     

    Если требуется выделить новый или важный код C#, он будет выводиться жирным шрифтом:

    // это код C#

    class MyClass : MyBaseClass // мы уже видели этот фрагмент

    {

     int x; // это интересно

    Код C++, представленный для сравнения, выглядит следующим образом:

    // это код C++

    class CMyClass : public CMyBaseClass {

    В образцах кода в этом приложении при использовании двух языков под Windows учитываются также большинство общих соглашений о наименованиях. Следовательно, имена классов в примерах C++ начинаются с C, в то время как соответствующие имена в примерах C# — нет. Также часто в образцах кода C++ используется для имен переменных "венгерский" стиль именования объектов.

    Терминология

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

    Если все это звучит путано, то следующая таблица должна в этом помочь разобраться:

    Значение Термин C++ Термин C#
    Переменная, которая является членом класса Переменная-член Поле
    Любой элемент в классе, который содержит инструкции Функция (или функция-член) Функция
    Элемент класса, который содержит инструкции и вызывается по имени с помощью синтаксиса
    DoSomething(/* параметры */)
    Функция (или функция-член) Метод
    Виртуальная функция, которая определена как член класса Метод Виртуальный метод

    Необходимо также знать о паре других различных терминов:

    Термин C++ Термин C#
    Составной оператор Блочный оператор
    lvalue Переменное выражение

    В этом приложении будет по возможности использоваться терминология, соответствующая рассматриваемому языку.

    Сравнение C# и C++

    В этом разделе мы кратко рассмотрим общие различия и сходства между двумя языками.

    Различия

    Основные области, в которых C# отливается от C++, представлены ниже:

    □ Использование компиляции. Код C++ обычно компилируется в язык ассемблера. C#, наоборот, компилируется в промежуточный язык (IL, intermediate language), который имеет некоторые сходства с байт-кодом Java. IL потоп преобразуется в собственный исполнимый код процессом оперативной компиляции (JIT) Создаваемый код IL хранится в файле или множестве файлов, называемом сборкой. Сборка по сути формирует единицу, в которой упакован код IL. Эта единица соответствует DLL или исполнимому файлу, создаваемому компилятором C++.

    □ Управление памятью. C# создан с целью освободить разработчика от проблем, связанных управлением памятью. Это означает, что в C# не требуется явно удалять память, которая была динамически выделена из кучи, как это требуется делать в C++. Вместо этого сборщик мусора периодически очищает память. Для облегчения процесса C# накладывает некоторые ограничения на то, как можно использовать переменные, которые хранятся в куче и более строгие ограничения имеет C++ в отношении безопасности типов данных.

    □ Указатели. Указатели используются в C# также, как и в C++, но только в тех блоках кода, которые специально помечены для использования указателей. Большей частью C# полагается на ссылки в стиле VB/Java для экземпляров классов, и язык был создан с таким расчетом, чтобы указатели не требовались так часто, как в C++.

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

    =
    ) для автоматической реализации перезагруженных версий комбинированных операторов (
    +=
    ).

    □ Библиотека. Как C++, так и C# зависят от наличия обширной библиотеки. Для ANSI C++ это — стандартная библиотека, для C# — множество классов, называемых базовыми классами .NET. Базовые классы .NET основываются на одиночном наследовании, а стандартная библиотека — на комбинации наследования и шаблонов. В то время как ANSI C++ держит библиотеку большей частью отдельно от самого языка, в C# такая взаимная зависимость значительно сильнее, и реализация многих ключевых слов C# прямо зависит от определенных базовых классов.

    □ Среды использования. C# специально разрабатывался для основных потребностей программирования в рабочих средах на основе GUI (не обязательно только Windows, хотя язык пока доступен только для Windows), а также в таких базовых службах, как службы Web. Это на самом деле не повлияло на сам язык, но отразилось в дизайне библиотеки базовых классов C++, в противоположность такому подходу, создавался для более общего использования, в те времена доминировали интерфейсы пользователя на основе командной строки. Ни C++, ни стандартная библиотека не включают никакой поддержки элементов GUI. В Windows paзработчики C++ прямо или косвенно используют для этого API Windows.

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

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

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

    IDisposable
    .

    □ Классы и структуры. C# формализует различия между классами (используемыми обычно для больших объектов с множеством методов) и структурами (используемыми обычно для небольших объектов, которые содержат чуть больше, чем обычные совокупности переменных). Классы и структуры хранятся неодинаково, структуры не поддерживают наследование, и есть другие различия.

    Сходства

    Области, где C# и C++ очень похожи

    □ Синтаксис. Весь синтаксис C# очень похож на синтаксис C++, хотя существуют многочисленные небольшие различия.

    □ Поток выполнения. C++ и C# имеют приблизительно схожие инструкции управления потоком выполнения. Они обычно работают одинаково в обоих языках.

    □ Исключения. Поддержка для них в C# по сути такая же как в C++, за исключением того, что C# допускает блоки

    finally
    и накладывает некоторые ограничения на тип объекта, который может порождаться.

    □ Модель наследования. Классы наследуются одинаковым образом в C# и C++. Связанные концепции, такие как абстрактные классы и виртуальные функции, реализуются одинаковым образом, хотя и существуют некоторые различия в синтаксисе. C# поддерживает также только одиночное наследование классов. Сходство иерархии классов в связи с этим означает, что программы C# будут иметь общую архитектуру, очень похожую на соответствующие программы C++.

    □ Конструкторы. Работают одинаковым образом в C# и C++, но опять же существуют некоторые различия в синтаксисе.

    Новые свойства

    C# вводит ряд новых концепций, которые не являются частью спецификации ANSI C++ (хотя большинство из них были введены компанией Microsoft как нестандартные расширения, поддерживаемые компилятором Microsoft C++). Они включают в себя следующие понятия:

    □ Делегаты. C# не поддерживает указатели на функции. Однако аналогичный результат достигается помещением ссылок на методы в классах специальной формы, называемых делегатами. Делегаты могут передаваться между методами и использоваться для вызова методов, ссылки на которые они содержат, таким же образом, как указатели на функции могут использоваться в C++. В отношении делегатов важно отметить, что они содержат в себе объектные ссылки, также как и ссылки на методы. Это означает. что в отличие от указателей на функции, делегат содержит достаточно данных для вызова экземпляра метода в классе.

    □ События. События аналогичны делегатам, но созданы специально для поддержки модели обратного вызова, в которой клиент уведомляет сервер о своем желании быть информированным, когда произойдет некоторое действие. C# использует события в качестве оболочек вокруг сообщений Windows таким же образом, как это делает VB.

    □ Свойства. Эта идея, интенсивно используемая в VB и в COM, была импортирована в C#. Свойство является в классе методом или парой методов получения/задания, которые синтаксически оформлены так, что для внешнего мира они выглядят как поле. Они позволяют написать код в виде

    MyForm.Height = 400
    вместо
    MyForm.SetHeight(400)
    .

    □ Интерфейсы. Интерфейс может рассматриваться как абстрактный класс, назначение которого состоит в определении множества методов или свойств, которые классы могут согласиться реализовать. Идея ведет свое происхождение из COM. Интерфейсы C# не такие, как интерфейсы COM — они являются просто списками методов и т.д., в то время как интерфейсы COM имеют другие связанные свойства, такие как GUID, но принцип очень похож. Это означает, что C# формально распознает принцип наследования интерфейса, посредством которого класс наследует определения функций, но без каких-либо реализаций.

    □ Атрибуты. C# позволяет дополнить классы, методы, параметры и другие элементы в коде с помощью мета-информации, называемой атрибутами. Атрибуты доступны во время выполнения и используются для определения действий, принимаемых кодом.

    Новые свойства базовых классов

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

    □ Организация поточной обработки. Язык C# включает некоторую поддержку синхронизации потоков выполнения с помощью оператора

    lock
    . (C++ не имеет встроенной поддержки для потоков выполнения и в случае необходимости вызывается соответствующая функциональность из библиотек кода.)

    □ Отражение. C# позволяет коду динамически получать информацию об определениях классов в компилированных сборках (библиотеках и исполняемом коде). Можно на самом деле написать программу на C#, которая выводит информацию о классах и методах, из которых она создана.

    Неподдерживаемые свойства

    Следующие части языка C++ не имеют никакого эквивалента в C#:

    □ Множественная реализация наследования в классах. Классы поддерживают множественное наследование только для интерфейсов.

    □ Шаблоны. Они не являются частью языка C# в настоящее время, хотя компания Microsoft утверждает, что исследует возможность поддержки шаблонов в будущих версиях C#.

    Пример Hello World

    Написание приложения 'Hello World' в мире программирования уже стало почти привычным. Но сопоставление 'Hello World' в C++ и C# может оказаться достаточно поучительным для иллюстрации некоторых различий между двумя языками. При этом сравнении сделана попытка внести немного новизны (и продемонстрировать дополнительные свойства), выводя

    Hello World
    как в командной строке, так и окне сообщения. Также сделано небольшое изменение текста сообщения в версии C++. Версия C++ выглядит следующим образом:

    #include <iostream>

    # include <Windows.h>


    using namespace std;

    int main(int argc, char *argv) {

     cout << "Goodbye, World!";

     MessageBox(NULL, "Goodbuy, World!", MB_OK);

     return 0;

    }

    А вот версия C#:

    using System;

    using System.Windows.Forms;

    namespace Console1; {

     class Class1 {

      static int Main(string[] args) {

       Console.WriteLine("Hello, World!");

       MessageBox.Show("Hello, World!");

       return 0;

      }

     }

    }

    Сравнение двух программ говорит, что синтаксис двух языков очень похож. В частности, блоки кода отмечены скобками

    {}
    , а точка с запятой используется в качестве ограничителя инструкций. Подобно C++, C# игнорирует все пробелы между инструкциями.

    Мы разберем примеры строка за строкой, рассматривая предоставляемые свойства.

    Инструкции #include

    Версия C++ 'Hello World!' начинается с пары директив препроцессора для включения некоторых заголовочных файлов.

    #include <iostream>

    #include <Windows.h>

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

    Еще момент, который необходимо отметить, состоит в том, что из двух инструкций

    #include
    в приведенном выше коде C++, первая обращается к стандартной библиотеке ANSI (часть
    iostream
    стандартной библиотеки). Вторая к специальной библиотеке Windows и используется для того, чтобы можно было вывести окно сообщения. Код C++ под Windows часто должен обращаться к API Windows, так как стандарт ANSI не имеет никаких средств создания окон. В противоположность этому базовые классы .NET — эквивалент C# стандартной библиотеки шаблонов ANSI — включает средства создания окон, и здесь используются только базовые классы .NET. Код C# в данном случае не требует никаких нестандартных средств. (Хотя и спорная, эта точка зрения уравновешивается тем фактом, что 'стандарт' C# в настоящее время доступен только в Windows.)

    Приведенный выше код C# оказался не содержащим никакой директивы

    #include
    , но это не значит, что любые препроцессорные директивы (даже и не
    #include
    ) недоступны в C#, и не сохраняется синтаксис
    #
    .

    Пространства имен

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

    Следующая часть кода в версиях C# и C++ очень похожа, в обоих используется инструкция

    using
    для указания пространства имен, в котором должны искаться все символы. Единственное различие является синтаксическим: в C# применяется инструкция
    namespace
    , в то время как в C++ используется
    using namespace
    .

    Многие разработчики C++ привыкли использовать старую библиотеку C++, что означает включения файла

    iostream.h
    , а не файла
    iostream
    , и в этом случае инструкция
    using namespace std
    является ненужной. Старая библиотека C++ официально опротестована и не будет больше поддерживаться версией Visual Studio 8 (версией, за которой последует Visual Studio.NET). Приведенный выше пример демонстрирует, как в действительности необходимо использовать библиотеку
    iostream
    в коде C++.

    Точка входа: Main() и main()

    Следующие элементы в примерах Hello World являются точками входа программ. В случае C++ это будет глобальная функция с именем

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

    Конечно, требование, чтобы все было членом класса, приводит к вопросу о том, где должна находиться точка входа программы. Ответ состоит в том, что компилятор C# ищет статический член метод с именем

    Mаin()
    . Это может быть член любого класса в исходном коде, но только один класс должен иметь такой метод. (Если более одного класса определяем этот метод, необходимо использовать ключ компилятора, чтобы указать компилятору какой из них должен быть точкой входа программы.) Подобно своему эквиваленту в C++
    Main()
    может возвращать либо
    void
    , либо
    int
    , хотя более распространено
    int
    . Также, подобно своему эквиваленту в C++,
    Main()
    получает такой же эквивалент аргументов либо множество произвольных параметров командной строки, переданных в программу как массив строк, либо не получает никаких параметров. Но как можно видеть из кода, строки определены в C# более интуитивно понятным образом, чем в C++. Каждый массив хранит число элементов, которое он содержит, а также сами элементы, поэтому нет необходимости передавать отдельно число строк в массиве в коде C#, как делает C# с помощью параметра
    argc
    .

    Вывод сообщения

    Наконец мы переходим к строкам, которые действительно выводят сообщение на консоль. а затем в окно сообщения. В обоих случаях эти строки кода используют вызов свойств из поддерживающих эти два языка библиотек. Архитектура классов в стандартной библиотеке очевидно очень отличается от архитектуры библиотеки базовых классов .NET, поэтому детали вызовов методов в этих примерах кода различны. В случае C# оба вызова делаются как вызовы статических методов на базовых классах, в то время как вывод окна сообщения в C++ должен использовать нестандартную функцию API Windows

    MessageBox()
    , которая не является объектно-ориентированной.

    Базовые классы спроектированы интуитивно понятными, существенно более понятными, чем в стандартной библиотеке. Без какого-либо знания C# сразу становится ясно, что делает

    Console.WriteLine()
    . Если не знать, то трудно понять, что означает
    cout <<
    .

    MessageBox.Show()
    получает меньше параметров, чем ее эквивалент C++ в этом примере, так как является перезагруженным. Доступны и другие перезагружаемые версии, которые получают дополнительные параметры.

    Еще один момент, который легко можно пропустить: приведенный выше код показывает, что C# использует точку, т.е. символ вместо двух двоеточий

    ::
    для разрешения области действия.
    Console
    и
    MessageBox
    являются именами классов, а не экземплярами классов. Чтобы получить доступ к статическим членам классов, C# всегда требует синтаксис
    <ИмяКласса>.<ИмяЧлена>
    , в то время как C++ дает возможность выбора между
    <ИмяКласса>::<ИмяЧлена>
    и
    <ИмяЭкземпляра>.<ИмяЧлена>
    (если экземпляр класса существует и находится в области действия).

    Сравнение свойств

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

    Архитектура программы

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

    Программные объекты

    В C++ любая программа состоит из точки входа (в ANSI C++ это функция

    main()
    , хотя для приложений Windows она обычно называется
    WinMain()
    ), а также различных классов. структур и глобальных переменных или функций, которые определены вне любого класса. Хотя многие разработчики будут считать что хороший объектно-ориентированный проект определяется тем, насколько возможно, чтобы элементы самого верхнего уровня в коде являлись объектами C++ не требует этого. Как только что было показано, C# реализует эту идею. Он утверждает существенно более объектно-ориентированную парадигму, требуя, чтобы все элементы являлись членами класса. Другими словами, единственными объектами верхнего уровня в программе являются классы (или другие элементы, которые могут рассматриваться как специальные типы классов: перечисления, делегаты и интерфейсы). В этом случае код C# оказывается более объектно-ориентированным, чем это требует C++.

    Файловая структура

    В C++ синтаксис, на котором строится программа, основывается на понятии файла как единице исходного кода. Имеются, например, файлы исходного кода (файлы

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

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

    Точка входа программы

    В стандартном ANSI C++ точка входа программы является по умолчанию функцией с именем

    main()
    , которая имеет сигнатуру:

    int main(int argc, char *argv)

    Здесь argc указывает число аргументов, передаваемых в программу, a argv является массивом строк, задающих эти аргументы. Первый аргумент всегда является командой, используемой для выполнения самой программы. Windows несколько изменяет это. Приложения Windows традиционно начинаются с точки входа, называемой

    WinMain()
    , a DLL с
    DllMain()
    . Эти методы также получают другие множества параметров.

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

    Main()
    .

    Синтаксис языка

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

    □ C++ требует точку с запятой после определения класса. C# не требует.

    □ C++ позволяет использовать выражения как инструкции, даже если они не имеют результата, например:

    i + 1;

    В C# это будет ошибкой.

    Необходимо также отметить, что подобно C++, C# различает строчные и заглавные символы. Однако, так как C# создан для взаимодействия с VB.NET (который не отличает такие символы), строго рекомендуется не использовать имена, которые разнятся только регистром символов для каких-либо объектов, видных коду вне данного проекта (другими словами, имена открытых членов классов в коде библиотеки). Если используются открытые имена, которые отличаются только регистром символов, то это не позволит коду VB.NET получить доступ к этим классам. (В случае написания какого-либо управляемого кода C++ для среды .NET применимы те же рекомендации.)

    Опережающие объявления

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

    Отсутствие разделения определения и объявления

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

    class CMyClass {

    public:

     void MyMethod(); // определение этой функции находится в файле C++,

     // если только MyMethod() не является встраиваемой функцией

     // и т.д.

    В C# этого не делают. Методы всегда определяются полностью в определении класса

    class MyClass {

     public void MyMethod() {

      // здесь реализация

    На первый взгляд может показаться, что это ведет к коду, который труднее читать. Достоинство подхода C++ в этом вопросе в конце концов состоит в том что можно просто просмотреть заголовочный файл, чтобы узнать, какие открытые функции предоставляет класс, не обращаясь к реализации этих функций. Однако это больше не требуется в C#, частично в связи с использованием современных редакторов (редактор Visual Studio.NET позволяет сворачивать реализации методов), а частично в связи с тем, что C# имеет средство автоматического создания документации для кода в формате XML.

    Поток выполнения программы

    Поток выполнения программы в C# аналогичен потоку C++. В частности, следующие инструкции работают точно таким же образом в C#, как они работают в C++, и имеют точно такой же синтаксис:

    for

    return

    goto

    break

    continue

    Существует пара синтаксических различий для инструкций

    if
    ,
    while
    ,
    do…while
    и
    switch
    , и C# предоставляет дополнительную инструкцию управления потоком выполнения
    foreach
    .

    if…else

    Инструкция

    if
    работает точно таким же образом и имеет такой же синтаксис в C#, как и в C++, кроме одного момента. Условие в каждом предложении
    if
    или
    else
    должно оцениваться как
    bool
    . Например, предположим что х является целым типом данных, а не
    bool
    , тогда следующий код C++ будет создавать ошибку компиляции в C#:

    if (х) {

    Правильный синтаксис C# выглядит так:

    if (x != 0) {

    так как оператор

    !=
    возвращает
    bool
    .

    Это требование является хорошей иллюстрацией того, как дополнительная безопасность типов в C# заранее перехватывает ошибки. Ошибки времени выполнения в C++, вызываемые написанием

    if (a=b)
    , когда предполагалось написать
    if (a==b)
    являются достаточно распространенными. В C# эти ошибки будет перехватываться во время компиляции.

    Отметим, что в C# невозможно преобразовать числовые переменные в или из

    bool
    .

    while и do…while

    Также, как и для

    if
    , эти инструкции имеют точно такой же синтаксис и назначение в C#, как и в C++, за исключением того, что условное выражение должно оцениваться как
    bool
    .

    int X;

    while (X) {/* инструкции */} // неправильно

    while (X != 0) {/* инструкции */} // правильно

    switch 

    Инструкция

    switch
    служит для тех же целей в C#, что и в C++. Она является, однако, более мощной в C#, так как используется строка в качестве проверяемой переменной, что невозможно в C++:

    string MyString; // инициализировать MyString

    switch (MyString) {

    case "Hello":

     // что-нибудь сделать

     break;

    case "Goodbye":

     // и т.д.

    Синтаксис в C# слегка отличается тем, что каждое предложение

    case
    должно явно заканчиваться. Не разрешается одному
    case
    содержать другой
    case
    , если только первый
    case
    не является пустым. Если желательно получить такой результат, используйте инструкцию
    goto
    .

    switch (MyString) {

    case "Hello":

     // что-нибудь сделать

     goto case "Goodbye"; // перейдет к выполнению инструкций

                          // в предложении "Goodbye"

    case "Goodbye":

     // сделать что-то еще

     break;

    case "Black": // здесь можно провалиться, так как он пустой

    case "White":

     // сделать что-то еще

     // выполняется, если MyString содержит

     // либо "Black", либо "White"

     break;

    default:

     int j = 3;

     break;

    }

    Компания Microsoft решила использовать инструкцию

    goto
    в этом контексте, чтобы предотвратить появление ошибок в случае, если требовалось выполнить пропущенный
    break
    , и код в инструкции
    switch
    проваливался в следующее предложение
    case
    .

    foreach

    C# предоставляет дополнительную инструкцию управления потоком выполнения

    foreach
    .
    foreach
    делает цикл по всем элементам массива или коллекции, не требуя явной спецификации индексов. Цикл
    foreach
    на массиве может выглядеть следующим образом. В этом примере предполагается, что
    MyArray
    является массивом
    double
    , и необходимо вывести каждое значение в консольном окне. Чтобы сделать это, используем следующий код:

    foreach (double SomeElement in MyArray) {

     Console.WriteLine(SomeElement);

    }

    Отметим что в этом цикле

    SomeElement
    является именем, присвоенным переменной для итерации по циклу; здесь можно выбрать любое имя не конфликтующее с другими именами переменных.

    Запишем также приведенный выше цикл следующим образом:

    foreach (double SomeElement in MyArray)

     Console.WriteLine(SomeElement);

    так как блочные инструкции в C# работают таким же образом, как составные инструкции в C++.

    Этот цикл будет иметь точно такой же результат, как и следующий:

    for (int I=0; I < MyArray.Length; I++) {

     Console.WriteLine(MyArray[i]);

    }

    (Отметим, что вторая версия иллюстрирует также, как получить число элементов в массиве в C#. Мы рассмотрим, как массив объявляется в C#, позже.)

    Отметим, однако, что в отличие от доступа к элементам массива, цикл

    foreach
    предоставляет к своим элементам доступ только для чтения. Следовательно, следующий код не будет компилироваться.

    foreach (double SomeElement in MyArray)

     SomeElement *= 2; // Неверно, для SomeElement нельзя выполнить

                       // присваивание

    Мы упомянули, что цикл

    foreach
    может использоваться для массивов или коллекций. Коллекция не имеет аналога в C++, хотя концепция стала общераспространённой в Windows благодаря ее использованию в VB и COM. Коллекция по сути является классом, который реализует интерфейс
    IEnumerable
    . Так как это включает поддержку из базовых классов, понятие коллекция объясняется в главе 7.

    Переменные

    Определения переменных следуют в основном тем же образцам в C#, что и в C++:

    int NCustomers, Result;

    double DistanceTravelled;

    double Height = 3.75;

    const decimal Balance = 344.56M;

    Хотя, как можно было ожидать, некоторые из типов различны. Как было замечено ранее, переменные могут быть объявлены только локально в методе или как члены класса. C# не имеет эквивалент, глобальных или статических (то есть с областью действия, ограниченной файлом) переменных в C++. Как уже говорилось, переменные, являющиеся членами класса, называются в C# полями.

    Отметим, что C# также строго различаем типы данных, хранимые в стеке (типы данных значений) и хранимые в куче (ссылочные типы данных). Мы позже рассмотрим этот вопрос более подробно.

    Базовые типы данных

    Как и в C++, C# имеет ряд предопределенные типов данных, и можно определять собственные типы данных, такие как классы или структуры.

    В C# и C++ предопределенные типы данных несколько различаются. Типы данных для C# приведены в таблице:

    Имя Содержит Символ
    sbyte
    8-битовое целое число со знаком  
    byte
    8-битовое целое число без знака  
    short
    16-битовое целое число со знаком  
    ushort
    16-битовое целое число без знака  
    int
    32-битовое целое число со знаком  
    uint
    32-битовое целое число без знака
    U
    long
    64-битовое целое число со знаком
    L
    ulong
    64-битовое целое число без знака
    UL
    float
    32-битовое значение с плавающей точкой со знаком
    F
    double
    64-битовое значение с плавающей точкой со знаком
    D
    bool
    true
    или
    false
     
    char
    16-битовый символ Unicode
    ''
    decimal
    Число с плавающей точкой с 28 значащими цифрами
    M
    string
    Множество символов Unicode переменной длины
    ""
    object
    Используется там, где не определен тип данных. Ближайшим эквивалентом в C++ является
    void*
    , за исключением того, что
    object
    не является указателем.
     

    В приведенной выше таблице символ в третьем столбце указывает букву, которая может быть помещена после числа, чтобы указать его тип явно, например,

    28UL
    означает число 28, хранимое как
    long
    без знака. Как и в случае C++, одиночные кавычки используются для обозначения символов, двойные кавычки для строк. Однако в C# символы всегда являются символами Unicode, а строки являются определенным ссылочным типом, а не просто массивом символов.

    Типы данных в C# используются более аккуратно, чем в C++. Например, в C++ обычно ожидается, что

    int
    будет занимать 2 байта (16 битов), но определение ANSI C++ разрешает, чтобы это зависело от платформы. Следовательно, в Windows
    int
    в C++ занимает 4 байта, столько же сколько и
    long
    . Это очевидно вызывает достаточно много проблем совместимости при переносе программ C++ между платформами. С другой стороны, в C# каждый предопределенный тип данных (за исключением
    string
    и
    object
    ) имеет явное определение занимаемой памяти.

    Так как размер каждого из примитивных типов (примитивным типом является любой из приведенных выше, за исключением

    string
    и
    object
    ) фиксирован в C#, то существует меньшая потребность в операторе
    sizeof
    , хотя он и есть в C#, но допустим только в ненадежном коде (как будет описано позже).

    Несмотря на то, что многие имена в C# аналогичны именам C++ и существует достаточно интуитивно понятное отображение между многими из соответствующих типов, некоторые вещи отличаются синтаксически. В частности,

    signed
    и
    unsigned
    не являются ключевыми словами в C# (в C++ можно использовать эти ключевые слова, также как
    long
    и
    short
    для модификации других типов данных (например,
    unsigned long
    ,
    short int
    ). Такие модификации недопустимы в C#, поэтому приведенная выше таблица является фактически полным списком предопределенных типов данных.

    Базовые типы данных как объекты

    В отличие от C++ (но как в Java) базовые типы данных в C# трактуются как объекты, чтобы вызывать на них некоторые методы. Например, в C# возможно преобразование целого числа в строку следующим образом.

    int I = 10;

    string Y = I.ToString();

    Можно даже написать:

    string Y = 10.ToString();

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

    string
    отображается в
    System.String
    ,
    int
    в
    System.Int32
    и т.д. Поэтому на самом деле в C# все является объектом. Однако отметим, что это применимо только для синтаксических целей. В реальности при выполнении кода эти типы реализуются как описанные ниже типы промежуточного языка, поэтому нет потери производительности, связанной с интерпретацией базовых типов как объектов. Здесь не будут перечисляться все методы, доступные для базовых типов данных, так как подробности представлены в MSDN. Однако необходимо отметить следующие особенности:

    □ Все типы имеют метод T

    oString()
    . Для базовых типов данных он возвращает строковое представление их значения.

    □ 

    char
    содержит большое число свойств, которые предоставляют информацию о своем содержимом (
    IsLetter
    ,
    IsNumber
    и т.д.), а также методы для выполнения преобразований (
    ToUpper()
    ,
    ToLower()
    ).

    □ 

    string
    имеет очень большое число методов и свойств. Строки будут рассмотрены отдельно.

    Также доступен ряд статических методов членов и свойств. Они включают следующие:

    □ Целые типы имеют

    MinValue
    и
    MaxValue
    , чтобы указать минимальное и максимальное значения, которые могут содержаться в типе данных.

    □ Типы данных

    float
    и
    double
    также имеют свойство
    Epsilon
    , которое указывает наименьшее возможное значение больше нуля, которое может в нем содержаться.

    □ Отдельные значения —

    NaN
    (не число, которое не определено),
    PositiveInfinity
    (положительная бесконечность) и
    NegativeInfinity
    (отрицательная бесконечность) определены для
    float
    и
    double
    . Результаты вычисления будут возвращать эти значения в подходящих ситуациях, например, деление положительного числа на ноль будет иметь в результате
    PositiveInfinity
    , в то время как деление нуля на нуль создаст
    NaN
    . Эти значения доступны как статические свойства.

    □ Многие типы, включая все числовые, имеют статический метод

    Parse()
    , который позволяет преобразование из строки:
    double D = double.Parse("20.5")
    .

    Отметим, что статические методы в C# вызываются определением имени типа данных:

    int.MaxValue
    и
    float.Epsilon
    .

    Преобразования базовых типов данных

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

    float f1 = 40.0;

    long l1 = f1; // неявно

    short s1 = (short)l1; // явно, старый стиль C

    short s2 = short(f1); // явно, новый стиль C++

    Если преобразование типа определяется явно, то это означает, что в коде явно указывается имя типа данных назначения. C++ позволяет написать явные преобразования типа любым из двух стилей — старым стилем С, в котором имя типа данных помещалось в скобки, или новым стилем, в котором имя переменной помещается в скобки. Оба стиля показаны выше и являются вопросом синтаксического предпочтения, выбор стиля не оказывает никакого влияния на код. В C++ допустимы преобразования любых базовых типов данных. Однако, если существует риск потери данных в связи с тем, что тип данных назначения имеет меньший диапазон значений, чем исходный тип данных, то компилятор может послать предупреждение в зависимости от настройки уровня предупреждений. В приведенном выше примере неявное преобразование типа может вызвать потерю данных, поэтому компилятор будет обычно порождать предупреждение. Явное определение преобразования является на самом деле способом сообщить компилятору что данное действие обдуманно, в результате это обычно приводит к подавлению всех предупреждений.

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

    Правила в C#, имеющие отношение к тому, какие базовые числовые типы данных могут быть преобразованы в другие типы данных, вполне логичны. Неявными преобразованиями будут преобразования, которые не создают риск потери данных, например,

    int
    в
    long
    или
    float
    в
    double
    . Явными преобразованиями являются такие, где может быть потеря данных в связи с ошибкой переполнения, ошибкой знака или потерей дробной части числа, например,
    float
    в
    int
    ,
    int
    в
    uint
    или
    short
    в
    ulong
    . Кроме того, так как
    char
    рассматривается несколько отдельно от других целых типов данных, можно преобразовывать только явно в или из
    char
    .

    Следующие выражения считаются допустимыми в коде C#:

    float f1 = 40.0F;

    long l1 = (long)f1; // явное, так как возможна ошибка округления

    short s1 = (short)l1; // явное, так как возможна ошибка переполнения

    int i1 = s1; // неявное — никаких проблем

    uint i2 = (uint)i1; // явное, так как возможна ошибка знака

    Отметим, что в C# явное преобразование типов данных всегда делается с помощью старого синтаксиса в стиле C. Новый синтаксис C++ использовать нельзя.

    uint i2 = uint(i1); // неверный синтаксис - это не будет компилироваться

    Проверяемое (checked) преобразование типов данных

    C# предлагает возможность выполнять преобразования типов и другие арифметические операции в проверяемом (checked) контексте. Это означает, что среда выполнения .NET будет обнаруживать возникновение переполнения и порождать исключение (конкретно,

    OverFlowException
    ). Это свойство не имеет аналога в C++.

    checked {

     int I1 = -3;

     uint I2 = (uint)I1;

    }

    В связи с контролируемостью контекста вторая строка будет порождать исключение. Если не определить

    checked
    , исключения не возникнет и переменная
    I2
    будет содержать мусор.

    Строки 

    Обработка строк выполняется значительно легче в C#, чем это было раньше в C++. Это связано с понятием строки как базового типа данных, который распознается компилятором C#. В C# нет необходимости рассматривать строки как массивы символов.

    Ближайшим эквивалентом для типа данных

    string
    в C# является класс
    string
    в C++ в стандартной библиотеке. Однако строка C# отличается от строки C++ следующими основными свойствами.

    □ Строка C# содержит символы Unicode, а не ANSI.

    □ Строка C# имеет значительно больше методов и свойств, чем версия в C++.

    □ Класс

    string
    стандартной библиотеки C++ является не более чем классом, предоставленным библиотекой, в то время как в C# синтаксис языка специально поддерживает класс
    string
    как часть языка.

    Последовательности кодирования

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

    Последовательность Имя символа Кодировка Unicode
    \'
    Одиночная кавычка
    0x0027
    \"
    Двойная кавычка
    0x0022
    \\
    Обратный слэш
    0х005C
    \0
    Null
    0x0000
    \a
    Сигнал
    0x0007
    \b
    Возврат на одну позицию
    0x0008
    \f
    Перевод страницы
    0x000C
    \n
    Новая строка
    0x000A
    \r
    Возврат каретки
    0x000D
    \t
    Горизонтальная табуляция
    0x0009
    \v
    Вертикальная табуляция
    0x000B

    Это по сути означает, что в C# используются те же коды, что и в C++, за исключением того, что C# не распознает

    \?
    .

    Имеются два отличия между символами кодирования в C++ и C#:

    □ Последовательность кодирования \0 распознается в C#. Однако она не используется как терминатор строки в C# и поэтому может встраиваться в строку. Строки C# работают, сохраняя отдельно свои длины, поэтому никакой символ не используется в качестве терминатора. Поэтому строки C# в действительности могут содержать любой символ Unicode.

    □ C# имеет дополнительную последовательность кодирования

    \uxxxx
    (или эквивалентно
    \Uxxxx
    ), где 
    xxxx
    представляет 4-символьное шестнадцатеричное число,
    \uxxxx
    представляет символ Unicode
    xxxx
    , например,
    \u0065
    представляет
    'е'
    . Однако в отличие от других последовательностей кодирования
    \uxxxx
    может использоваться в именах переменных, а также в символьных и строковых константах. Например, следующий код допустим в C#.

    int R\u0065sult; // тот же результат, что и int Result;

    Result = 10;

    Согласно документации последовательность кодирования не зависит от регистра символов:

    \uxxxx
    и
    \Uxxxx
    будут эквивалентны. Однако при написании этой книги обнаружилось, что только версия нижнего регистра успешно компилируется текущей версией .NET.

    C# имеет также альтернативный метод представления строк, который более удобен для строк, содержащих специальные символы: размещение символа

    @
    в начале строки избавляет все символы от кодирования. Эти строки называются дословными строками. Например, чтобы представить строку
    C:\Book\Chapter2
    можно написать либо
    "C:\\Book\\Chaptеr2"
    , либо
    @"C:\Book\Chapter2"
    . Интересно, что это означает также, что можно включать символы возврата каретки в дословные строки без кодирования:

    string Message = @"Это будет на первой строке,

    а это будет на следующей строке"

    Типы значений и ссылочные типы

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

    В C# тип значения действительно содержит свое значение. Все предопределенные типы данных в C# являются типами значений, за исключением

    object
    и
    string
    . Если определить свои собственные структуры и перечисления, они также будут типами значений. Это означает, что простые типы данных в C# обычно действуют точно таким же образом как в C++, когда им присваивают значения.

    int I = 10;

    long J = I; // создаёт копию значения 10

    I = 15; //не влияет на J

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

    object
    и
    string
    являются ссылочными типами, как и любые определенные самостоятельно классы. Ссылки C# могут быть переназначены для указания на другие элементы данных, по большей части таким же образом, как можно переназначить указатели C++. Также ссылкам C# можно присваивать значение
    null
    для указания, что они ни на что не ссылаются. Например, возьмем класс с именем
    MyClass
    , который имеет открытое свойство
    Width
    .

    MyClass My1 = new MyClass(); // в C# new просто вызывает конструктор

    My1.Width = 20;

    MyClass My2 = My1; // My2 указывает теперь на то же место

                       // в памяти, что и My1

    Му2.Width = 30; // Теперь My1.Width = 30, так как My1

                    // и Му2 указывают на одно место в памяти

    My2 = null; // Теперь My2 не ссылается ни на что,

                // My1 по прежнему ссылается на тот же объект

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

    Тип значения и ссылочный тип данных имеют особенности в управлении памятью, так как ссылочные типы всегда хранятся в куче, в то время как типы значений обычно хранятся в стеке. Это рассматривается более подробно в следующем разделе об управлении памятью.

    Инициализация переменных

    В C++ переменные никогда не инициализируются, если их явно не инициализировать (или в случае классов предоставить конструкторы). Если этого не сделать, переменные будут содержать какие-то случайные данные, оказавшиеся в памяти, это отражает особое внимание в C++ к производительности. C# уделяет больше внимания исключению ошибок во время выполнения и поэтому строже относится к инициализации переменных. В C# существуют следующие правила:

    □ Переменные, которые являются полями-членами, по умолчанию инициализируются с помощью нулевых значений, если они не инициализируются явно. Это означает, что числовые типы данных будут содержать нули,

    bool
    будут содержать
    false
    , а все ссылочные типы (включая строки и объекты) будут содержать ссылку
    null
    . Структуры инициализируют нулем каждый свой член.

    □ Локальные переменные методов не инициализируются по умолчанию. Однако компилятор будет давать ошибку, если локальная переменная используется до инициализации. Можно при желании инициализировать переменную, вызывая ее конструктор по умолчанию (тот, который обнуляет память).

    // локальные переменные метода

    int X1; //в этом месте X1 содержит случайные данные

    // int Y = X1; // эта закомментированная строка будет создавать ошибку

                   // компиляции, т.к. X1 используется до инициализации

    X1 = new int(); // теперь X1 будет содержать ноль.

    Упаковка

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

    int J = 10;

    object BoxedJ = (object)J;

    Упаковка действует как любое другое преобразование типов, но надо знать, что содержимое переменной скопируется в кучу и будет создана ссылка (так как объект

    BoxedJ
    является ссылочным типом).

    Обычная причина для использования упаковки значения состоит в передаче его в метод, который ожидает в качестве параметра ссылочный тип. Можно также распаковать упакованное значение, преобразуя его просто назад в первоначальный тип данных.

    int J = 10;

    object BoxedJ = (object)J;

    int K = (int)BoxedJ;

    Отметим, что процесс распаковки будет инициировать исключение, если попытаться преобразовать значение к неправильному типу данных.

    Управление памятью

    В C++ переменные (включая экземпляры классов или структур) могут храниться в стеке или в куче. Обычно переменная хранится в куче, если она или некоторый содержащий ее класс был распределен с помощью оператора

    new
    , или в противном случае помещается в стек. Это означает, что возможность выделить динамически память для переменной с помощью оператора new позволяет выбирать, будет ли переменная храниться в стеке либо в куче. (Хотя очевидно в связи со способом работы стека, что хранящиеся в стеке данные будут существовать до тех пор, пока соответствующая переменная находится в области действия.)

    C# работает совершенно по-другому. Чтобы понять как именно, рассмотрим два обычных сценария в C++. Возьмем следующее объявление двух переменных в C++:

    int j = 30;

    CMyClass *pMine = new CMyClass;

    Здесь содержимое

    j
    хранится в стеке. Это в точности та ситуация, которая существует с типами данных значений C#. Экземпляр
    MyClass
    хранится, однако, в куче, а указатель на него находится в стеке, что по сути повторяет ситуацию со ссылочными типами в C#, за исключением того, что в C# синтаксис скрывает указатель под личиной ссылки. Эквивалент в C# будет следующим:

    int J = 30;

    MyClass Mine = new MyClass();

    Этот код имеет в большой степени тот же результат, в соответствующих терминах как и приведенный выше код C++: различие состоит в том, что MyClass синтаксически используется как ссылка, а не как указатель.

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

    int* рj = new int(30);

    CMyClass Mine;

    Это приведет к тому, что

    int
    будет находиться в куче, а экземпляр
    CMyClass
    — в стеке. Этого нельзя сделать в C#, так как C# считает, что
    int
    является типом значения, в то время как любой класс всегда будет ссылочным типом.

    Другое различие состоит в том, что в C# не существует эквивалента оператора C++

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

    В C# следующие типы данных всегда являются типами значений:

    □ Все простые предопределенные типы (за исключением

    object
    и
    string
    )

    □ Все структуры

    □ Все перечисления

    Следующие типы данных всегда являются ссылочными типами:

    object

    string

    □ Все классы

    Оператор new

    Оператор

    new
    имеет совершенно другое значение в C#, чем в C++. В C++
    new
    указывает на запрос памяти из кучи. В C#
    new
    означает просто, что вызывается конструктор переменной. Однако действие аналогично в той степени, что если переменная имеет ссылочный тип то вызов ее конструктора будет неявно означать, что память выделяется в куче. Например, предположим, что имеется класс
    MyClass
    и структура
    MyStruct
    . В соответствии с правилами C# экземпляры
    MyClass
    всегда будут храниться в куче, а экземпляры
    MyStruct
    в стеке.

    MyClass Mine; // Просто объявляем ссылку. Аналогично объявлению

                  // неинициализированного указателя в C++

    Mine = new MyClass(); // создает экземпляр MyClass. Вызывает

                          // конструктор без параметров, в процессе этого

                          // выделяет память в куче

    MyStruct Struct; // создает экземпляр MyStruct, но не вызывает

                     // никакого конструкторе. Поля в MyStruct

                     // будут неинициализированы

    Struct = new MyStruct(); // вызывает конструктор, поэтому

                             // инициализирует поля, но не выделяет

                             // никакой памяти, так как Struct уже

                             // существует в стеке

    Можно использовать

    new
    для того, чтобы вызвать конструктор для предопределенных типов данных:

    int X = new int();

    Это имеет такой же результат, как:

    int X = 0;

    Отметим, что это то же самое, что и

    int X;

    Последняя инструкция оставит

    X
    неинициализированной (если переменная
    X
    является локальной переменной метода).

    Методы 

    Методы в C# определяются таким же образом, как функции в C++, с учетом факта, что методы C# всегда должны быть членами класса, и определение и объявление в C# всегда объединены:

    class MyClass {

    public int MyMethod() {

     // реализация

    Есть одно ограничение, состоящее в том, что методы-члены не могут объявляться как

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

    Параметры методов

    Как и в C++, по умолчанию параметры передаются в методы по значению. Если требуется это изменить, можно использовать ключевое слово

    ref
    , указывающее, что параметр передается по ссылке, и
    out
    , чтобы указать, что это параметр вывода (всегда передается по ссылке). Если это сделано, то необходимо объявлять этот факт как в определении метода, так и при его вызове.

    public void MultiplyByTwo(ref double d, out double square) {

     d *= 2;

     square = d*d;

    }


    // позже, при вызове метода

    double Value, Square Value = 4.0;

    MultiplyByTwo(ref Value, out Square);

    Передача по ссылке означает, что метод может изменять значение параметра. Передача по ссылке также осуществляется, чтобы улучшить производительность при работе с большими структурами, также как и в C++, передача по ссылке означает, что копируется только адрес. Отметим, однако, что, если при передаче по ссылке из соображений производительности вызываемый метод по-прежнему не изменяет значения параметра, то C# не разрешает присоединять модификатор

    const
    к параметрам, как это делает C++.

    Параметры типа

    out
    действуют по большей части так же, как ссылочные параметры. Но они предназначены для случаев, когда вызываемый метод задает значение для параметра, а не изменяет его. Следовательно, инициализации параметров будут отличаться. C# требует, чтобы параметр
    ref
    инициализировался внутри вызываемого метода до своего использования.

    Перезагрузка методов

    Методы могут быть перезагружены таким же образом, как в C++. Однако C# не допускает в методах параметров по умолчанию. Это можно смоделировать с помощью перезагружаемой версии:

    Для C++ можно сделать следующую запись:

    double DoSomething(int someData, bool Condition=true) {

     // и т.д.

    В то время как в C# необходимо выполнить такие действия:

    double DoSomething(int someData) {

     DoSomething(someData, true);

    }

    double DoSomething(int someData, bool condition) {

     // и т.д.

    Свойства

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

    length
    типа
    int
    . Тогда в C++ оно инкапсулируется с помощью методов
    GetLength()
    и
    SetLength()
    . Необходимо будет обращаться к нему извне класса:

    // MyObject является экземпляром рассматриваемого класса

    MyObject.SetLength(10);

    int Length = MyObject.GetLength();

    В C# можно реализовать эти методы, как аксессоры (методы доступа)

    get
    и
    set
    свойства
    Length
    . Тогда запишем

    // MyObject является экземпляром рассматриваемого класса

    MyObject.Length = 10;

    int length = MyObject.Length;

    Чтобы определись эти методы доступа, свойство будет определяться следующим образом:

    class MyClass {

     private int length;

     public int Length {

      get {

       return length;

      }

      set {

       Length = value;

      }

    Хотя методы доступа

    get
    и
    set
    реализованы здесь, чтобы просто возвращать или задавать поле
    length
    , в эти методы можно поместить любой другой требуемый код C# так же, как это обычно делается в методе. Например, добавить некоторую проверку данных в метод доступа
    set
    . Отметим, что метод доступа
    set
    возвращает
    void
    и получает дополнительный неявный параметр с именем
    value
    .

    Можно опустить любой из методов доступе

    get
    или
    set
    из определения свойства, и в этом случае свойство осуществляет соответственно либо только запись, либо только чтение.

    Операторы

    Значение и синтаксис операторов в большинстве случаев те же в C#, что и в C++. Следующие операторы по умолчанию имеют в C# такое же значение и синтаксис как и в C++:

    □ Бинарные арифметические операторы

    +
    ,
    -
    ,
    *
    ,
    /
    ,
    %

    □ Соответствующие арифметические операторы присваивания

    +=
    ,
    -=
    ,
    *=
    ,
    /=
    ,
    %=

    □ Унарные операторы

    ++
    и
    --
    (обе — префиксная и постфиксная формы)

    □ Операторы сравнения

    !=
    ,
    ==
    ,
    <
    ,
    <=
    ,
    >=

    □ Операторы сдвига

    >>
    и
    <<

    □ Логические операторы

    &
    ,
    |
    ,
    &&
    ,
    ||
    ,
    ~
    ,
    ^
    ,
    !

    □ Операторы присваивания, соответствующие логическим операторам:

    >>=
    ,
    <<=
    ,
    &=
    ,
    |=
    ,
    ^=

    □ Тернарный (условный) оператор

    Символы

    ()
    ,
    []
    , и
    ,
    (запятая) также имеют в общих чертах такой же эффект в C#, как и в C++.

    Необходимо быть осторожным со следующими операторами, так как они действуют в C# иначе, чем в C++:

    □ Присваивание (

    =
    ),
    new
    ,
    this
    .

    Оператор разрешения области видимости в C# представлен

    .
    , а не
    ::
    (
    ::
    не имеет смысла в C#). Также в C# не существуют операторы
    delete
    и
    delete[]
    . Они не нужны, так как сборщик мусора автоматически управляет очисткой памяти в куче. Однако C# предоставляет также три других оператора, которые не существуют в C++, а именно,
    is
    ,
    as
    и
    typeof
    . Эти операторы связаны с получением информации о типе объекта или класса.

    Оператор присваивания (=)

    Для простых типов данных = просто копирует данные. Однако при определении своих собственных классов C++ считает в большой степени, что обязанность разработчика указать значение

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

    В C# правила, определяющие, что означает оператор присваивания, значительно проще. Вообще не разрешается перезагружать =, его значение неявно определено во всех ситуациях.

    Ситуация в C# будет следующая:

    □ Для простых типов данных

    =
    просто копирует значения, как в C++.

    □ Для структур

    =
    делает поверхностное копирование структуры — прямую копию памяти данных в экземпляре структуры. Это аналогично поведению в C++.

    □ Для классов

    =
    копирует ссылку, то есть адрес, а не объект. Это не соответствует поведению в C++.

    Если требуется скопировать экземпляры классов, обычный способ в C# состоит в переопределении метода

    MemberwiseCopy()
    , который все классы в C# по умолчанию наследуют из класса
    System.Object
    — общего класса-предка, из которого неявно выводятся все классы C#.

    this

    Оператор

    this
    имеет то же самое значение, что и в C++, но это скорее ссылка, а не указатель. Например, в C++ можно записать:

    this->m_MyField = 10;

    В C# это будет выглядеть так:

    this.MyField = 10;

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

    new

    Как сообщалось ранее, оператор

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

    Классы и структуры

    В C++ классы и структуры очень похожи. Формально единственное различие состоит в том, что члены структуры являются по умолчанию открытыми, в то время как члены класса являются по умолчанию закрытыми. На практике, однако, многие программисты предпочитают использовать структуры и классы различным образом, сохраняя использование структур для объектов данных, которые содержат только члены-переменные (другими словами, без функций членов или явных конструкторов).

    C# отражает это традиционное различие использования. В C# класс — это совершенно другой тип объектов, по сравнению со структурой, поэтому нет необходимости тщательно рассматривать, будет ли лучше определить заданный объект как класс или как структуру. Наиболее важные различия между классами C# и структурами C# следующие:

    □ Структуры не поддерживают наследование, кроме того факта, что они являются производными из

    System.ValueType
    . Невозможно наследовать от структуры и структура не может наследовать от другой структуры или класса.

    □ Структуры являются типами данных значений. Классы всегда являются ссылочными типами данных.

    □ Структуры позволяют организовать способ размещения полей в памяти и определяют эквивалент объединений C++.

    □ Конструктор структуры по умолчанию (без параметров; всегда поставляется компилятором и не может быть заменен.

    Поскольку классы и структуры сильно отличаются в C#, они в этом приложении рассматриваются по отдельности.

    Классы 

    Классы в C# следуют в основном тем же самым принципам, что и в C++, однако существует разница в свойствах и синтаксисе. Мы рассмотрим отличия между классами C++ и классами C# в этом разделе.

    Определение класса

    Классы определяются в C# с помощью синтаксиса, который на первый взгляд выглядит как синтаксис C++:

    class MyClass : MyBaseClass {

     private string SomeField;

      public int SomeMethod() {

       return 2;

     }

    }

    За этим первоначальным сходством скрываются многочисленные различия в деталях.

    □ Не существует модификатора доступа по имени базового класса. Наследование всегда открытое.

    □ Класс может быть выведен только из одного базового класса (хотя из любого числа интерфейсов). Если базовый класс явно не определен, то класс будет автоматически выводиться из

    System.Object
    , который предоставит ему всю функциональность
    System.Object
    , из которой чаще всего используется
    ToString()
    .

    □ Каждый член явно объявляется с модификатором доступа. Не существует эквивалента синтаксису C++, где один модификатор доступа может применяться к нескольким членам.

    public: // нельзя использовать этот синтаксис в C#

     int MyMethod();

     int MyOtherMethod();

    □ Методы не могут объявляться как

    inline
    . Это связано с тем, что C# компилируется в промежуточный язык (IL). Любые вставки кода происходят на второй стадии компиляции, когда JIT-компилятор выполняет преобразование из IL в собственный код машины. JIT-компилятор имеет доступ ко всей информации в IL при определении, какие методы могут подходить для вставки без необходимости каких-либо указаний в исходном коде разработчика.

    □ Реализация методов всегда помещается вместе с определением. Невозможно написать реализацию вне класса, как позволяет C++.

    □ В то время как в ANSI C++ единственными типами члена класса являются переменные, функции, конструкторы, деструкторы и перезагружаемые версии операторов, C# имеет в наличии также делегатов, события и свойства.

    □ Модификаторы доступа

    public
    ,
    private
    и
    protected
    обладают тем же самым значением, как и в C++, но существуют два дополнительных модификатора доступа:

     □ 

    internal
    ограничивает доступ к другому коду внутри той же сборки.

     □ 

    protected internal
    ограничивает доступ к производным классам, которые находятся внутри той же сборки.

    □ Инициализация переменных разрешается в C# в определении класса.

    □ В C++ ставится точка с запятой после закрывающейся фигурной скобки в конце определения класса. Это не требуется в C#.

    Инициализация полей членов

    Синтаксис, используемый для инициализации полей членов в C#, очень отличается от синтаксиса C++, хотя конечный результат одинаковый.

    Члены экземпляра

    В C++ поля членов экземпляра обычно инициализируются в списке инициализации конструктора:

    MyClass::MyClass() : m_MyField(6) {

     // и т.д.

    В C# этот синтаксис недопустим. Можно помещать в инициализатор конструктора (который является эквивалентом C# списка инициализации конструктора в C++) другой конструктор. Вместо этого инициализированное значение помечается с помощью определения члена в определении класса:

    class MyClass {

     private int MyField = 6;

    Отметим, что в C++ это будет ошибкой, так как C++ использует примерно такой же синтаксис для определения чисто виртуальных функций. В C# такое действие считается нормой, так как C# не применяет синтаксис

    =0
    для этой цели (он использует вместо этого ключевое слово
    abstract
    ).

    Статические поля

    В C++ статические поля инициализируются с помощью отдельного определения вне класса:

    int MyClass:MyStaticField = 6;

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

    Class MyClass {

     private static int MyStaticField = 6;

    Конструкторы

    Синтаксис объявления конструкторов в C# такой же, как синтаксис для встраиваемых конструкторов, заданных в определении класса в C++.

    class MyClass {

     publiс MyClass() {

      // код конструктора

     }

    Как и в C++, можно определить столько конструкторов, сколько потребуется, при условии, что они получают различное число или типы параметров. (Отметим, что, как и в методах, параметры по умолчанию не допускаются, необходимо моделировать это с помощью нескольких перезагружаемых версий.)

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

    System.Object
    ) выполняется первым, за ним следуют конструкторы в порядке, определяемом деревом иерархии.

    Статические конструкторы

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

    class MyClass {

     static MyClass() {

      // код статического конструкторе

     }

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

    Отметим, что статический конструктор не имеет спецификатора доступа, он не объявляется как открытый, закрытый или как-нибудь еще. Спецификатор доступа не будет иметь смысла, так как статический конструктор вызывается только средой выполнения .NET, когда загружается определение класса. Он не может вызываться никаким другим кодом C#.

    C# не задает точно, когда будет выполнен статический конструктор, за исключением только того, что это произойдет после инициализации всех статических полей, но перед тем, как будет создан какой-либо объект класса, или там, где статические методы класса реально используются.

    Конструкторы по умолчанию

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

    Как и в C++ можно обойтись без создания экземпляров класса, объявляя закрытый конструктор единственным.

    class MyClass {

     private MyClass() {

     }

    Это также не позволяет создавать экземпляры любых производных классов. Однако, если класс или методы в нем объявлены абстрактными, то нельзя создать экземпляр этого класса, причем не обязательно производного класса.

    Списки инициализации конструктора

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

    base
    и
    this
    соответственно:

    class MyClass : MyBaseClass {

     MyClass(int X)

     : base(X) // выполняет конструктор MyBaseClass с одним параметром

     {

      // здесь другая инициализация

     }

     MyClass()

     : this(10) // выполняет конструктор MyClass с одним параметром,

                // передавая в него значение 10

     {

      // здесь другая инициализация

     }

    Если явно не задан никакой список инициализации конструктора, то компилятор будет неявно использовать список из элемента

    base()
    . Другими словами, инициализатор по умолчанию вызывает конструктор по умолчанию базового класса. Это поведение совпадает с C++.

    В отличие от C++ нельзя поместить переменные члены в список инициализации конструктора. Однако это только вопрос синтаксиса, так как эквивалент C# должен отметить свои начальные значения в определении класса.

    Более серьезным различием является тот факт, что можно поместить только один иной конструктор в список. Это влияет на способ разработки конструкторов, хотя несомненно полезно, так как заставляет использовать хорошо определенную и эффективную парадигму организации конструкторов. Эта парадигма указана в приведенном выше коде. Все конструкторы следуют единому порядку, в котором выполняются различные конструкторы.

    Деструкторы

    C# реализует отличную от C++ модель программирования деструкторов. Это связано с тем, что механизм сборки мусора в C# предполагает следующее:

    □ Существует меньшая необходимость в деструкторах, так как динамически распределенная память будет удаляться автоматически.

    □ Так как невозможно предсказать, когда сборщик мусора реально разрушит заданный объект, то если для класса предоставляется деструктор, невозможно предсказать в точности, когда этот деструктор будет выполнен.

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

    1. Класс должен выводиться из интерфейса

    IDisposable
    и реализовывать метод
    Dispose()
    . Этот метод предназначен для явного вызова с помощью кода клиента для указания, что он закончил работать с объектом и требуется очистить ресурсы. (Интерфейсы будет рассмотрены позже в этом приложении.)

    2. Класс должен отдельно реализовать деструктор, который рассматривается как запасной механизм, на случай, если клиент не вызывает

    Dispose()
    .

    Обычная реализация

    Dispose()
    выглядит следующим образом:

    public void Dispose() {

     // очистка ресурсов

     System.GC.SuppressFinalize(this);

    }

    System.GC
    является базовым классом, представляющим сборщика мусора.
    SuppressFinalize()
    является методом, который информирует сборщика мусора, что нет необходимости вызывать деструктор для разрушаемого объекта. Вызов
    SuppressFinalize()
    важен, так как имеется снижение производительности, если в объекте есть деструктор, который нужно вызывать в то время, когда сборщик мусора выполняет свою работу. Следствием этого является то, что реальное освобождение управляемой памяти, занимаемой этим объектом, будет существенно задерживаться.

    Синтаксис деструктора по сути такой же в C#, как и в C++. Отметим, что в C# не требуется объявлять деструктор виртуальным, компилятор будет это подразумевать. Не требуется также предоставлять модификатор доступа:

    Class MyClass {

     ~MyClass() {

      // очистка ресурсов

     }

    }

    Хотя метод

    Dispose()
    обычно явно вызывается клиентами, C# допускает альтернативный синтаксис, который гарантирует, что компилятор примет меры, чтобы он был вызван. Если переменная объявлена внутри блока
    using()
    , то ее область действия совпадает с блоком
    using
    и ее метод
    Dispose()
    будет вызываться при выходе из блока:

    using (MyClass MyObject = new MyClass()) {

     // код

    } // MyObject.Dispose() будет неявно вызван при выходе из этого блока

    Отметим, что приведенный выше код будет компилироваться успешно только если

    MyClass
    выводится из интерфейса
    IDisposable
    и реализует метод
    Dispose()
    . Если нежелательно использовать синтаксис
    using
    , то можно опустить один или оба из двух шагов в последовательности деструктора (реализация
    Dispose()
    и реализация деструктора), но обычно реализуются оба шага. Можно также реализовать
    Dispose()
    , без привлечения интерфейса
    IDisposable
    , но если это делается, то снова невозможно использовать синтаксис
    using
    , чтобы для экземпляров этого класса
    Dispose()
    вызывался автоматически.

    Наследование

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

    Class MyClass : MyBaseClass {

     // и т.д.

    В C++ указатель на класс может дополнительно указывать на экземпляр производного класса. (Виртуальные функции в конце концов зависят от этого факта.) В C# классы доступны через ссылки, но правило остается тем же. Ссылка на класс может ссылаться на экземпляры этого класса или на экземпляры любого производного класса.

    MyBaseClass Mine;

    Mine = new MyClass(); // все нормально, если MyClass будет производным

                          // от MyBaseClass

    Если желательно, чтобы ссылка ссылалась на произвольный объект (эквивалент

    void*
    в C++), можно определить ее как
    object
    в C#, так как C# отображает
    object
    в класс
    System.Object
    , из которого выводятся все другие классы.

    object Mine2 = new MyClass();

    Виртуальные и невиртуальные функции

    Виртуальные функции поддерживаются в C# таким же образом, как и в C++. Однако в C# существуют некоторые синтаксические отличия, которые созданы, чтобы исключить возможную неоднозначность в C++. Это означает, что некоторые типы ошибок, которые появляются в C++ только во время выполнения, будут идентифицированы в C# во время компиляции.

    Отметим также, что в C# классы всегда доступны по ссылке (что эквивалентно доступу через указатель в C++).

    Если в C++ требуется, чтобы функция была виртуальной необходимо просто определить ключевое слово

    virtual
    в базовом и производном классах. В противоположность этому в C# необходимо объявить функцию как
    virtual
    в базовом классе и как
    override
    в версиях производных классов.

    class MyBaseClass {

     public virtual void DoSomething(int X) {

      // и т.д.

     }

     // и т.д.

    }


    class MyClass : MyBaseClass {

     public override void DoSomething(int X) {

      // и т.д.

     }

     // и т.д.

    }

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

    override
    , и компилятор не сможет идентифицировать ее версию в каком-либо базовом классе.

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

    В случае, если в C# версия функции в производном классе скрывает соответствующую функцию в базовом классе, можно явно указать это с помощью ключевого слова

    new
    .

    class MyBaseClass {

     public void DoSomething(int X) {

      // и т.д.

     }

     // и т.д.

    }


    class MyClass : MyBaseClass {

     public new void DoSomething(int X) {

      // и т.д.

     } и т.д.

    }

    Если не пометить новую версию класса явно как

    new
    , то код по-прежнему будет компилироваться, но компилятор будет выдавать предупреждение. Предупреждение служит для защиты от любых трудноуловимых ошибок, возникающих во время выполнения. Например, когда написана новая версия базового класса, в которой добавлен метод, имеющий, оказывается, такое же имя, как и существующий метод в производном классе.

    В C# можно объявить абстрактную функцию, также как это делается в C++ (в C++ она называются еще чисто виртуальной функцией), но в C# синтаксис будет отличаться: вместо использования

    =0
    в конце определения применяется ключевое слово
    abstract
    .

    C++:

    public:

     virtual void DoSomething(int X) = 0;

    C#:

    public abstract void Dosomething(int X);

    Как и в C++, можно создать экземпляр класса, только если он сам не содержит абстрактных методов и предоставляет реализации всех абстрактных методов, которые были определены в любом из его базовых классов.

    Структуры

    Синтаксис определения структур в C# соответствует синтаксису определения классов.

    struct MyStruct {

     private SomeField;

     public int SomeMethod() {

      return 2;

     }

    }

    Наследование и связанные концепции, виртуальные и абстрактные функции не допускаются. В остальном базовый синтаксис идентичен синтаксису классов, за исключением того, что ключевое слово

    struct
    заменяет в определении
    class
    .

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

    В отличие от классов в C#, структуры являются типом данных значений. Это означает, что такая инструкция как:

    MyStruct Mine;

    реально создает экземпляр

    MyStruct
    в стеке. Однако в C# этот экземпляр не инициализируется, если конструктор не вызван явно:

    MyStruct Mine = new MyStruct();

    Если все поля-члены из

    MyStruct
    являются открытыми, можно альтернативно инициализировать структуру, преобразуя каждое поле-член по отдельности.

    Константы

    Ключевое слово

    const
    в C++ имеет достаточно большой диапазон использования. Например, можно объявить переменные как
    const
    , тем самым указывая, что их значения обычно задаются во время компиляции и не могут изменяться никакой инструкцией присваивания во время выполнения (хотя существует небольшой элемент гибкости, так как значение члена переменной
    const
    может быть задано в списке инициализации конструктора, а это предполагает в данном случае, что значение может вычисляться во время выполнения). Можно также применять
    const
    к указателям и ссылкам, чтобы запретить их использование для изменения данных, на которые они указывают, и можно также использовать ключевое слово
    const
    для модификации определений параметров, передаваемых в функции. В этом случае
    const
    показывает, что переменная, которая была передана по ссылке или через указатель, не должна изменяться функцией. Как упоминалось ранее, сами члены функции тоже могут быть объявлены как
    const
    , чтобы подчеркнуть, что они не изменяют содержащий их экземпляр класса.

    C# позволяет также использовать ключевое слово

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

    Синтаксис объявления констант различается в C# и C++, поэтому мы рассмотрим его более подробно. Синтаксис C# использует два ключевых слова —

    const
    и
    readonly
    . Ключевое слово
    const
    предполагает, что значение задается во время компиляции, в то время как
    readonly
    предполагает, что оно задается однажды во время выполнения в конструкторе.

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

    Константы, ассоциированные с классом (статические константы)

    Обычный способ определения статической константы в C++ состоит в записи члена класса как

    static const
    . C# делает это похожим образом, но с помощью более простого синтаксиса:

    Синтаксис C++:

    int CMyClass::MyConstant = 2;


    class CMyClass {

    public:

     static const int MyConstant;

    Синтаксис C#:

    class MyClass {

     public const int MyConstant = 2;

    Отметим, что в C# константа не определяется явно как

    static
    , если это сделать, то возникнет ошибка компиляции. Она является, конечно, неявно статической, так как не существует возможности задать значение константы более одного раза, и, следовательно, она всегда должна быть доступна как статическое поле.

    int SomeVariable = MyClass.MyConstant;

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

    const
    . В случае C# статические константы инициализируются во время выполнения. Поле определяется как
    readonly
    и инициализируется в статическом конструкторе.

    class MyClass {

     public static readonly int MyConstant;

     static MyClass() {

      // определяет и присваивает начальное значение MyConstant

     }

    Константы экземпляра

    Константы, которые ассоциированы с экземплярами класса, всегда инициализируются значениями, вычисленными во время выполнения. (Если их значения были вычислены во время компиляции, то, по определению, это делает их статическими.)

    В C++ такие константы должны быть инициализированы в списке инициализации конструктора класса. Это в некоторой степени ограничивает гибкость при вычислении значений этих констант, так как начальное значение должно быть таким, чтобы его можно было записать как выражение в списке инициализации конструктора.

    class CMyClass {

    public:

     const int MyConstInst;

     CMyClass() : MyConstInst(45); {

    В C# принцип похож, но константа объявляется как

    readonly
    , а не как
    const
    . Таким образом, ее значение задается в теле конструктора, придавая гибкость процессу, так как можно использовать любые инструкции C# при вычислении начального значения. (Вспомните, что в C# невозможно задать значения переменных в инициализаторе конструктора, только вызвать другой конструктор.)

    class MyClass {

     public readonly int MyConstInst;

     MyClass() {

      // определяет и инициализирует здесь MyConstInst

    Если поле в C# объявлено как

    readonly
    , то ему можно присвоить значение только в конструкторе.

    Перезагрузка операторов

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

    +
    и отдельно
    +=
    . В C# можно перезагрузить только
    +
    . Компилятор всегда будет использовать перезагруженную версию
    +
    , чтобы автоматически определить значение
    +=
    для этого класса или структуры.

    Следующие операторы могут перезагружаться в C# также, так и в C++:

    □ Бинарные арифметические операторы

    +
    ,
    -
    ,
    *
    ,
    /
    ,
    %

    □ Унарные операторы

    ++
    и 
    --
    (только префиксная форма)

    □ Операторы сравнения

    !=
    ,
    ==
    ,
    <
    ,
    <=
    ,
    >=

    □ Побитовые операторы

    &
    ,
    |
    ,
    ~
    ,
    ^
    !

    □ Булевы значения

    true
    и
    false

    Следующие операторы, перезагружаемые в C++, нельзя перезагружать в C#.

    □ Арифметические операторы присваивания

    *=
    ,
    /=
    ,
    +=
    ,
    -=
    ,
    %=
    . (Они определяются компилятором из соответствующих арифметических операторов и оператора присваивания, который не может перезагружаться.) Постфиксные операторы увеличения на единицу. Они определяются компилятором из перезагруженных версий соответствующих префиксных операторов. (Реализуются с помощью вызова соответствующей перезагруженной версии префиксного оператора, но возвращают первоначальное значение операнда вместо нового значения.)

    □ Операторы побитового присваивания

    &=
    ,
    |
    =,
    ^=
    ,
    >>=
    и
    <<=
    .

    □ Булевы операторы

    &&
    ,
    ||
    . (Они определяются компилятором из соответствующих побитовых операторов.)

    □ Оператор присваивания

    =
    . Значение этого оператора в C# фиксировано.

    Существует также ограничение в том, что операторы сравнения должны перезагружаться парами, другими словами, при перезагрузке

    ==
    необходимо перезагрузить также
    !=
    и наоборот. Аналогично, если перезагружается один из операторов
    <
    и
    <=
    , то необходимо перезагрузить оба оператора и так же для
    >
    и
    >=
    . Причина этого состоит в необходимости обеспечения согласованной поддержки для любых типов данных базы данных, которые могут иметь значение
    null
    и для которых поэтому, например,
    ==
    не обязательно имеет результат, противоположный
    !=
    .

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

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

    что
    CMyClass имеет член 
    x
    и сложение экземпляров означает сложение членов
    x
    .). Код может выглядеть следующим образом (предполагается, что перезагруженная версия является явной вставкой кода):

    static CMyClass operator+(const CMyClass &lhs, const CMyClass &rhs) {

     CMyClass Result;

     Result.x = lhs.x + rhs.x;

     return Result;

    }

    Отметим, что оба параметра объявлены как

    const
    и передаются по ссылке, чтобы обеспечить оптимальную производительность. Это само по себе не слишком плохо. Однако теперь для возвращения результата, необходимо создать временный экземпляр
    CMyClass
    внутри перезагруженной версии оператора. Конечная инструкция
    return Result
    выглядит безопасной, но она будет компилироваться только в том случае, если доступен оператор присваивания для копирования
    Result
    из функции.

    Это само по себе является нетривиальной задачей, так как если ссылки используются неправильно при определении, то очень легко случайно определить ссылку, которая рекурсивно вызывает себя, пока не будет получено переполнение стека. Перезагрузка операторов в C++ является задачей для опытных программистов. Нетрудно видеть, почему компания Microsoft решила сделать некоторые операторы неперезагружаемыми в C#.

    В C# практика будет другой. Здесь нет необходимости явно передавать по ссылке, так как классы C# являются ссылочными переменными в любом случае (а для структур передача по ссылке снижает производительность). И возвращение значения является легкой задачей. Будет ли это класс или структура, надо просто вернуть значение временного результата, а компилятор C# гарантирует, что в результате будут  скопированы либо поля-члены (для типов данных значений), либо адреса (для ссылочных типов). Единственный недостаток заключается в том, что нельзя использовать ключевое слово

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

    static MyClass operator+(MyClass lhs, CMyClass rhs) {

     MyClassResult = new MyClass();

     Result.x = lhs.x + rhs.x;

     return Result;

    }

    Индексаторы

    C# cтрого не разрешает перезагружать

    []
    . Однако он позволяет определить так называемые индексаторы (indexer) класса, что обеспечивает такой же результат.

    Синтаксис определении индексатора очень похож на синтаксис свойства. Предположим, что необходимо использовать экземпляры

    MyClass
    как массив, где каждый элемент индексируется с помощью
    int
    и возвращает
    long
    . Тогда можно сделать следующую запись:

    class MyClass {

     public long this[int x] {

      get {

       // код для получения элемента

      }

      set {

       // код для задания элемента, например X = value;

      }

     }

     // и т.д.

    Код внутри блока

    get
    выполняется всякий раз, когда
    Mine[x]
    стоит с правой стороны выражения (при условии, что выражение
    Mine
    является экземпляром
    MyClass
    и 
    x
    будет
    int
    ), в то время как
    блок
    set выполняется только тогда, когда
    Mine[x]
    указывается с левой стороны выражения. Блок
    set
    ничего не может вернуть и использует ключевое слово
    value
    для указания величины, которая появится с правой стороны выражения. Блок
    get
    должен вернуть тот же тип данных, который имеет индексатор.

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

    Определенные пользователем преобразования типов данных

    Так же как для индексаторов и

    []
    , C# формально не рассматривает
    ()
    как оператор, который может перезагружаться, однако C# допускает определяемые пользователем преобразования типов данных, которые имеют тот же результат. Например, предположим, что имеются два класса (или структуры) с именами
    MySource
    и
    MyDest
    и необходимо определить преобразование типа из
    MySource
    в
    MyDest
    . Синтаксис этого выглядит следующим образом:

    public static implicite operator MyDest(MySource Source) {

     // код для выполнения преобразования. Должен возвращать экземпляр MyDest

    }

    Преобразование типа данных определяется как статический член класса

    MyDest
    или класса
    MySource
    . Оно должно также объявляться любо как
    implicit
    , либо как
    explicit
    . Если преобразование объявлено как
    implicit
    , то оно используется неявно:

    MySource Source = new MySource();

    MyDest Dest = MySource;

    Если преобразование объявлено как explicit, то оно может использоваться только явным образом:

    MySource Source = new MySource();

    MyDest Dest = (MyDest)MySource;

    Необходимо определять неявные преобразования типов данных в том случае, когда они всегда работают, а явные преобразования типов только тогда, когда может произойти отказ в связи с потерей данных или порождением исключения.

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

    C# не позволяет определить преобразования типов данных между классами, которые являются производными друг друга. Такие преобразования уже доступны — неявно из производного класса в базовый класс и явно из базового класса в производный.

    Отметим, что если попробовать выполнить преобразование ссылки базового класса в ссылку производного класса, и при этом рассматриваемый объект не является экземпляром производного класса (или какого-нибудь производного из него), то будет порождаться (генерироваться) исключение. В C++ нетрудно преобразовать указатель на объект в "неправильный" класс объектов. Это просто невозможно в C# с помощью ссылок. По этой причине преобразование типов в C# считается более безопасным, чем в C++.

    // пусть MyDerivedClass получен из MyBaseClass

    MyBaseClass MyBase = new MyBaseClass();

    MyDerivedClass MyDerived = (MyDerivedClass) MyBase; // это приведет

     // к порождению исключения

    Если нежелательно преобразовывать что-то в производный класс, но нежелательно также, чтобы генерировалось исключение, можно применить ключевое слово

    as
    . При использовании as, если преобразование отказывает, будет возвращаться
    null
    .

    // пусть MyDerivedClass получен из MyBaseClass

    MyBaseClass MyBase = new MyBaseClass();

    MyDerivedClass MyDerived as (MyDerivedClass)MyBase; // это

                                                        // возвратит null

    Массивы

    Массивы являются одной из областей, в которой внешнее сходство в синтаксисе между C++ и C# скрывает то, что реально происходящее "за сценой" существенно различается в этих двух языках. В C++ массив является по сути множеством переменных, упакованных вместе в памяти и доступных через указатель. В C#, с другой стороны, массив является экземпляром базового класса

    System.Array
    , и поэтому выступает полноценным объектом, хранящимся в куче под управлением сборщика мусора. Для доступа к методам этого класса C# использует синтаксис типа C++ способом, который создает иллюзию доступа к массиву. Недостаток этого подхода состоит в том, что накладные расходы для массивов больше, чем в C++, но преимуществом является то, что массивы C# более гибкие и при этом проще кодируются. В качестве примера: все массивы C# имеют свойство
    Length
    , которое задает число элементов массива, не требуя тем самым хранить его отдельно. К тому же массивы C# значительно безопаснее в использовании — так, проверка границ индекса выполняется автоматически.

    В C# возможно сделать простой массив без накладных расходов класса

    System.Array
    , но для этого понадобится использовать указатели и ненадежные блоки кода.

    Одномерные массивы

    Для одномерных массивов (терминология C#: массивы ранга 1) синтаксис доступа в обоих языках идентичен — с квадратными скобками, используемыми для индикации элементов массива. Массивы начинаются с нулевого индекса в обоих языках.

    Например, чтобы умножить каждый элемент массива

    float
    на 2:

    // массив, объявлен как массив float

    // этот код работает в C++ и C# без каких-либо изменений

    for (int i = 0; i < 10; i++) Array[i] = 2.0f;

    Однако, как упоминалось ранее, массивы C# поддерживают свойство

    Length
    , которое используется для определения числа элементов в массиве.

    // массив, объявлен как массив float

    // этот код компилируется только в C#

    for (int i = 0; i < Array.Length; i++) Array[i] *= 2.0f;

    В C# можно также использовать инструкцию

    foreach
    для доступа к элементам массива, что рассматривалось ранее.

    Синтаксис объявления массивов в C# слегка отличается, так как массивы C# всегда объявляются как ссылочные объекты.

    double [] Array; // простое объявление ссылки без реального

                     // создания экземпляра массива

    Array = new double[10]; // реально создается экземпляр объекта

                            // System.Array и задается размер 10.

    Или, объединяя эти инструкции, запишем:

    double [] array = new double[10];

    Отметим, что размер массива задается только вместе с созданием его экземпляра. Объявление ссылки использует просто квадратные скобки для указания, что размерность (ранг) массива будет единица. В C# ранг считается частью типа массива, в отличие от числа элементов.

    Ближайший эквивалент в C++ приведенного выше определения будет выглядеть так:

    double *pArray = new double[10];

    Эта инструкция C++ действительно дает достаточно близкую аналогию, так как обе версии C++ и C# размещаются в куче. Отметим, что версия C++ является просто областью памяти, которая содержит 10

    double
    , в то время как версия C# создает экземпляр полноценного объекта. Более простая стековая версия C++:

    doublе pArray[10];

    не имеет аналога в C#, который использует реальный массив C#, хотя инструкция C#

    stackalloc
    может создать эквивалент этой инструкции с помощью указателей. Об этом мы будем говорить позже в разделе, в котором рассматривается ненадежный код.

    Массивы в C# можно явно инициализировать при создании экземпляра:

    double [] Array = new double[10] {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 9.0, 10.0};

    Существует также более короткая форма:

    double [] Array = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 9.0, 10.0};

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

    new
    в куче (хотя C++ допускает это для массивов на основе стека).

    Многомерные массивы

    C# существенно отклонился от C++ в вопросе многомерных массивов, так как C# поддерживает как прямоугольные, так и неровные массивы.

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

    int [,] MyArray2d;

    MyArray2d = new int[2, 3] {{1, 0}, {3, 6}, {9, 12}};

    Синтаксис здесь является интуитивно понятным расширением синтаксиса одномерных массивов. Список инициализации в таком коде может отсутствовать. Например:

    int [,,] MyArray3d = new int [2, 3, 2];

    Это приведет к вызову конструктора по умолчанию для каждого элемента и к инициализации каждого

    int
    нулем. В этом частном примере проиллюстрировано создание трехмерного массива. Общее число элементов в массиве равно 2×3×2 = 12. Характеристика прямоугольных массивов состоит в том, что все строки имеют одинаковое число элементов.

    Элементы прямоугольного массива доступны с помощью следующего синтаксиса.

    int X = MyArray3d[1, 2, 0] + MyArray2d[0, 1];

    Прямоугольный массив C# не имеет прямых аналогов в C++. Однако неровные массивы в C# соответствуют достаточно точно многомерным массивам C++. Например, если объявить в C++ массив следующим образом:

    int MyCppArray[3][5];

    то реально объявляется не массив 3×5, а массив массивов — массив размера 3, каждый элемент которого является массивом размера 5. Это будет, возможно, понятнее, если сделать то же самое динамически. Запишем:

    int pMyCppArray = new int[3];

    for (int i=0; i<3; i++) pMyCppArray[i] = new int[5];

    Из этого кода должно быть видно, что теперь не существует причины, чтобы каждая строка содержала одинаковое число элементов (хотя это вполне может быть, как в данном примере). В качестве примера неровного массива в C++, который имеет различное число элементов в каждой строке, можно написать:

    int pMyCppArray = new int[3];

    for (int i=0; i<3; i++) pMyCppArray[i] = new int[2*i + 2];

    Соответствующие строки этого массива имеют размерности 2, 4 и 6. C# делает те же самые вещи почти таким же образом, хотя в случае C# синтаксис указывает число размерностей более явно:

    int[][] MyJaggedArray = new int[3][];

    for (int i = 0; i < 3, i++) MyJaggedArray[i] = new int[2*i + 2];

    Доступ к членам неровного массива следует точно тому же синтаксису, что и в C++.

    int X = MyJaggedArray[1][3];

    Здесь показан неровный массив ранга 2. Однако так же, как и в C++, можно определить неровный массив с любым рангом, необходимо просто добавить прямоугольные скобки в его определение.

    Проверка границ

    Одной из областей, где объектная сущность массивов C# становится явной, является проверка границ. Если обратиться к элементу массива C#, указывая индекс, который не находится в границах массива, то это будет обнаружено во время выполнения и породит исключение

    IndexOutOfBoundsException
    . В C++ этого не происходит, в результате появляются трудноуловимые ошибки. C# выполняет дополнительную проверку ошибок за счет производительности. Хотя можно было бы ожидать, что это создаст потерю производительности, на самом деле здесь содержится преимущество, так как среда выполнения .NET способна контролировать код, чтобы гарантировать, что он является безопасным в том смысле, что не будет пытаться обратиться к памяти, которая не выделена для его переменных. Это обеспечивает выигрыш производительности, так как различные приложения могут, например, выполняться в одном процессе, и все равно есть уверенность, что эти приложения будут изолированы друг от друга. Имеется также выигрыш и в безопасности, так как возможно более точное предсказание, что данная программа будет или не будет пытаться делать.

    С другой стороны, теперь нет ничего необычного в том, что программисты C++ используют какой-либо из различных классов оболочек массивов в стандартной библиотеке или в MFC, в основном для линейных массивов, чтобы получить тот же контроль границ и различные другие свойства, хотя в этом случае — без выигрыша в безопасности и производительности, связанных с возможностью анализа программы до ее выполнения.

    Изменение размера массивов

    Массивы C# являются динамическими, то есть можно определить число элементов в каждой размерности во время компиляции (также, как в динамически выделяемых массивах в C++). Однако невозможно изменить их размер после того, как были созданы их экземпляры. Если требуется это сделать, необходимо рассмотреть другие связанные с этим классы в пространстве имен

    System.Collections
    в библиотеке базовых классов, таких как
    System.Collections.ArrayList
    . Однако в этом отношении C# не отличается от C++. Обычные массивы C++ не допускают изменение размера, но существует ряд классов стандартной библиотеки, которые предоставляют это свойство.

    Перечисления

    В C# можно определить перечисление с помощью синтаксиса, аналогичного синтаксису C++.

    // допустимо в C++ или C#

    enum TypeOfBuilding {Shop, House, OfficeBlock, School};

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

    // допустимо только в C#

    enum TypeOfBuilding {Shop, House, OfficeBlock, School}

    Однако в C# перечисление должно быть именованным, в то время как в C++ задание имени для перечисления является необязательным. Также как в C++, элементы перечисления в C# нумеруются от нуля в сторону увеличения, если только специально не определено, что элемент должен иметь определенное значение.

    enum TypeOfBuilding {Shop, House=5, OfficeBlock, School = 10}

    // Shop будет иметь значение 0, OfficeBlock будет иметь значение 6

    Способ, с помощью которого происходит доступ к значениям элементов, отличается в C#, так как в C# необходимо определять имя перечисления.

    Синтаксис C++:

    TypeOfBuilding MyHouse = House;

    Синтаксис C#:

    TypeOfBuilding MyHouse = TypeOfBuilding.House;

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

    System.Enum
    ) и поэтому имеет некоторые методы. В частности, для любого перечисленного значения можно сделать следующее:

    TypeOfBuilding MyHouse = TypeOfBuilding.House;

    string Result = MyHouse.ToString(); // Result будет содержать "House"

    Это почти невозможно сделать в C++.

    В C# это делается и другим способом, с помощью статического метода

    Parse()
    класса
    System.Enum
    , хотя синтаксис будет чуть более запутанным

    TypeOfВuilding MyHouse = (TypeOfBuilding)Enum.Parse(typeof(TypeOfBuilding), "House", true);

    Enum.Parse()
    возвращает объектную ссылку и должен быть явно преобразован (распакован) обратно в соответствующий тип
    enum
    . Первым параметром в
    Parse()
    является объект
    System.Тyре
    , который описывает, какое перечисление должна представлять строка. Второй параметр является строкой, а третий параметр указывает, должен ли игнорироваться регистр символов. Вторая перегружаемая версия опускает третий параметр и не игнорирует регистр символов.

    C# позволяет также выбрать описанный ниже тип данных, используемый для хранения

    enum
    :

    enum TypeOfBuiding : short {Shop, House, OfficeBlock, School};

    Если тип не указан, компилятор будет предполагать по умолчанию

    int
    .

    Исключения

    Исключения используются в C# таким же образом, как и в C++, кроме двух следующих различий:

    □ C# определяет блок

    finally
    , который содержит код, всегда выполняющийся в конце блока
    try
    независимо от того, порождалось ли какое-либо исключение. Отсутствие этого свойства C++ явилось причиной недовольства среди разработчиков C++. Блок
    finally
    выполняется, как только управление покидает блок
    catch
    или
    try
    , и содержит обычно код очистки выделенных в блоке
    try
    ресурсов.

    □ В C++ класс, порожденный в исключении, может быть любым классом. C#, однако, требует, чтобы исключение было классом, производным от

    System.Exception
    .

    Правила выполнения программы в блоках

    try
    и
    catch
    идентичны в C++ и C#. Используемый синтаксис также одинаков, за исключением одного различия: в C# блок catch, который не определяет переменную для получения объекта исключения, обозначается самой инструкцией
    catch
    . Синтаксис C++:

    catch (...) {

    Синтаксис C#.

    catch {

    В C# этот вид инструкции

    catch
    может быть полезен для перехвата исключений, которые порождаются кодом, написанным на других языках (и которые поэтому могут не быть производными от
    System.Exception
    , компилятор C# отметит ошибку, если попробовать определить такой объект-исключение, но это не имеет значения для других языков программирования).

    Полный синтаксис для

    try…catch…finally
    в C# выглядит следующим образом:

    try {

     // обычный код

    } catch (MyException e) { // MyException выводится из System.Exception

     // код обработки ошибки

    }

    // необязательные дополнительные блоки catch

    finally {

     // код очистки

    }

    Отметим, что блок

    finally
    является необязательным. Также допустимо не иметь блоков
    catch
    , в этом случае конструкция
    try…finally
    служит просто способом обеспечения, чтобы код в блоке
    finally
    всегда выполнялся, когда происходит выход из блока
    try
    . Это может быть полезно, например, если блок
    try
    содержит несколько инструкций
    return
    и требуется выполнить очистку ресурсов, прежде чем метод реально возвратит управление.

    Указатели и небезопасный код

    Указатели в C# используются почти таким же образом, как и в C++. Однако они могут объявляться и использоваться только в блоке небезопасного (ненадежного) кода. Любой метод можно объявить небезопасным (

    unsafe
    ):

    public unsafe void MyMethod() {

    Можно альтернативно объявить любой класс или структуру небезопасными и:

    unsafe class MyClass {

    Объявление класса или структуры ненадежными означает, что все члены рассматриваются как ненадежные. Можно также объявить любое поле-член (но не локальные переменные) как ненадежное, если имеется поле-член типа указателя:

    private unsafe int* рХ;

    Можно также пометить блочный оператор как ненадежный следующим образом: 

    unsafe {

     // инструкции, которые используют указатели

    }
     

    Синтаксис для объявления, доступа, разыменования и выполнения арифметических операций с указателями такой же, как и в C++:

    // этот код будет компилироваться в C++ или C#

    // и имеет одинаковый результат в обоих языках

    int X = 10, Y = 20;

    int *рХ = &Х;

    *рХ = 30;

    pХ = &Y;

    ++рХ; // добавляет sizeof(int) к рХ

    Отметим, однако, следующие моменты.

    □ В C# не допускается разыменовывать указатели

    void*
    , также нельзя выполнять арифметические операции над указателями
    void*
    . Синтаксис указателя
    void*
    был сохранен для обратной совместимости, для вызова внешних функций API, которые не знают о .NET и которые требуют указателей
    void*
    в качестве параметров.

    □ Указатели не могут указывать на ссылочные типы (классы или массивы). Также они не могут указывать на структуры, которые содержат встроенные ссылочные типы в качестве членов. Это в действительности попытка защитить данные, используемые сборщиком мусора и средой выполнения .NET (хотя в C#, также как и в C++, если начать использовать указатели, почти всегда можно найти способ обойти любые ограничения, выполняя арифметические операции на указателях и затем разыменовывая их).

    □ Помимо объявления соответствующих частей кода как ненадежных, необходимо также определять для компилятора флаг

    /unsafe
    при компиляции кода, который содержит указатели.

    □ Указатели не могут указывать на переменные, которые встроены в ссылочные типы данных (например, членов класса), если только они не объявлены внутри инструкции

    fixed
    .

    Фиксация донных в куче

    Разрешается присвоить адрес типа данных значения указателю, даже если этот тип встроен как поле-член в ссылочный тип данных. Однако такой указатель должен быть объявлен внутри инструкции

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

    class MyClass {

     public int X; // и т.д.

    }


    // где-то в другом месте кода

    MyClass Mine = new MyClass(); // выполнить обработку

    fixed (int *pX = Mine.X) {

     // можно использовать рХ в этом блоке

    }

    Возможно вкладывание блоков

    fixed
    для объявления более одного указателя. Можно также объявить более одного указателя в одной инструкции
    fixed
    при условии, что оба указателя имеют один тип объекта ссылки.

    fixed (int *рХ = Mine.X, *рХ2 = Mine2.X) {

    Объявление массивов в стеке

    C# предоставляет оператор

    stackalloc
    , который используется в соединении с указателями для объявления массива в стеке без накладных расходов. Массив, размещаемый таким образом, не является полным объектом
    System.Array
    в стиле C#, он является просто массивом чисел, аналогичным одномерному массиву C++. Элементы этого массива не инициализируются и доступны с помощью такого же синтаксиса, как и в C++, с использованием квадратных скобок для указателя.

    Оператор

    stackalloc
    требует спецификации типа данных и числа размещаемых элементов.

    Синтаксис C++:

    unsigned long рМуArray[20];

    Синтаксис C#:

    ulong *pMyArray = stackalloc ulong[20];

    Отметим, однако, что хотя эти массивы похожи, версия C# позволяет определить размер во время выполнения:

    int X;

    // инициализировать X

    ulong *pMyArray = stackalloc ulong[X];

    Интерфейсы

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

    Интерфейс в C# не совсем такой, как интерфейс COM, так как он не имеет связанного с ним GUID, не является производным из

    IUnknown
    и не имеет связанных с ним записей в реестре (хотя можно отобразить интерфейс C# на интерфейс COM). Интерфейс C# является просто множеством определений функций и свойств. Он может рассматриваться как аналог абстрактного класса и определяется с помощью синтаксиса аналогичного класса.

    interface IMyInterface {

     void MyMethod(int X);

    }

    Можно заметить, однако, следующие синтаксические различия с определением класса:

    □ Методы не имеют модификаторов доступа.

    □ Методы никогда не реализуются в интерфейсе.

    □ Методы не объявляются виртуальными или явно абстрактными. Выбор методов принадлежит классу, который реализует этот интерфейс.

    Класс реализует интерфейс, наследуя из него. Класс может быть производным только от одного класса, но от любого числа интерфейсов. Если класс реализует интерфейс, то он должен предоставить реализации всех методов, определенных этим интерфейсом.

    class MyClass : MyBaseClass, IMyInterface, IAnotherInterface // и т.д.

    {

     public virtual void MyMethod(int X) {

      // реализация

     }

     // и т.д.

    В этом примере выбрана реализация

    MyMethod
    как виртуального метода с открытым доступом.

    Интерфейсы могут также выводиться из других интерфейсов, и в этом случае производный интерфейс содержит свои собственные методы, а также методы базового интерфейса.

    interface IMyInterface : IBaseInterface

    Можно проверить, что объект реализует интерфейс, используя либо оператор

    is
    , либо оператор
    as
    для преобразования его в этот интерфейс. Альтернативно можно преобразовать его напрямую, но в этом случае будет получено исключение, если объект не реализует интерфейс, поэтому этот подход годится только в том случае, если известно, что преобразование пройдет успешно. Можно использовать полученную таким образом ссылку на интерфейс, чтобы вызывать методы на этом интерфейсе (реализация будет предоставляться экземпляром класса).

    IMyInterface MyInterface;

    MyClass Mine = new MyClass();

    MyInterface = Mine as IMyInterface;

    if (MyInterface != null) MyInterface.MyMethod(10);

    Основные применения интерфейсов следующие:

    □ Взаимодействовать и устанавливать обратную совместимость с компонентами COM.

    □ Служить в качестве контракта для других классов .NET. Интерфейс может использоваться для указания, что класс реализует некоторые свойства. Например, цикл C# foreach

    работает
    внутренне, проверяя, что класс, в котором он используется, реализует интерфейс
    IEnumerate
    , и вызывая затем методы, определенные этим интерфейсом.

    Делегаты

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

    null
    для статического метода). Это означает, что в отличие от указателя на функцию в C++, делегат C# содержит достаточно информации для вызова метода экземпляра.

    Формально делегат является классом, который выводится из класса

    System.Delegate
    . Следовательно, создание экземпляра делегата включает два этапа: определение этого производного класса и объявление переменной соответствующего типа. Определение класса делегата включает данные полной сигнатуры (с возвращаемым типом) метода, который содержит делегат.

    Основное использование делегатов состоит в передаче и вызове ссылок на методы: ссылки на методы нельзя передавать непосредственно, но они могут передаваться внутри делегата. Делегат обеспечивает безопасность типа данных, не позволяя вызывать метод с неверной сигнатурой. Метод, который содержит делегат, может вызываться синтаксически как вызов делегата. Следующий код показывает общие принципы. Первое, необходимо определить класс делегата:

    // определить класс делегата, который представляет метод,

    // получающий int и возвращающий void

    delegate void MyOp(int X);

    Затем, для целей этого примера объявим класс, который содержит вызываемый метод:

    // затем определение класса

    class MyClass {

     void MyMethod(int X) {

      // и т.д.

     }

    }

    Еще позже, может быть при реализации некоторого другого класса, имеется метод, которому должна быть передана ссылка на метод с помощью делегата:

    void MethodThatTakesDelegate(MyOp Op) {

     // вызвать метод, передавая ему значение 4

     Oр(4);

    }

    // и т.д.

    И, наконец, код, который реально использует делегата:

    MyClass Mine = new MyClass();

    // Создать экземпляр делегата MyOp. Настроить его,

    // чтобы он указывал на метод MyMethod из Mine.

    MyOp DoIt = new MyOp(Mine.MyMethod);

    После объявления переменной делегата можно вызвать метод с помощью делегата:

    DoIt();

    Или передать его в другой метод:

    MethodThatTakesDelegate(DoIt);

    В частном случае, когда делегат представляет метод, который возвращает

    void
    , этот делегат является широковещательным делегатом и может одновременно представлять более одного метода. Вызов делегата заставляет все методы вызываться по очереди. Можно использовать операторы
    +
    и
    +=
    для добавления метода делегату, а
    -
    и
    -=
    — для удаления метода, который уже находится в делегате. Делегаты рассматриваются более подробно в главе 6.

    События

    События являются специальной формой делегатов, которые используются для поддержки модели уведомления о событии с помощью обратного вызова. Событие имеет следующую сигнатуру:

    delegate void EventClass(obj Sender, EventArgs e);

    Это сигнатура, которую должен иметь любой обработчик событий с обратным вызовом. Ожидается, что

    Sender
    будет ссылкой на объект, который инициирует событие, в то время как
    System.EventArgs
    (или любой класс, производный из
    EventArgs
    , который также допустим в качестве параметра) является классом, используемым средой выполнения .NET для передачи базовой информации, имеющей отношение к деталям события.

    Для объявления события используется специальный синтаксис:

    public event EventClass OnEvent;

    Клиенты используют синтаксис

    +=
    широковещательных делегатов для информирования, что они хотят получить уведомление.

    // EventSource ссылается на экземпляр класса, который содержит событие

    EventSource.OnEvent += MyHandler;

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

    OnEvent(this, new EventArgs());

    Атрибуты

    Концепция атрибутов не имеет эквивалента в ANSI C++, однако атрибуты поддерживаются компилятором Microsoft C++ как специальное расширение Windows. В версии C# имеются классы .NET, которые выводятся из

    System.Attribute
    . Они могут применяться к различным элементам кода C# (классам, перечислениям, методам, параметрам и т.д.) для создания дополнительной документирующей информации в компилированной сборке. Кроме того, некоторые атрибуты распознаются компилятором C# и будут иметь влияние на компилированный код. Они включают следующие:

    Атрибут Описание
    DllImport
    Указывает, что метод определен во внешней DLL.
    StructLayout
    Позволяет расположить содержимое структуры в памяти. Позволяет получить эквивалент
    union
    в C#.
    Obsolete
    Создает ошибку компилятора или предупреждение, если используется этот метод.
    Conditional
    Заставляет выполнить условную компиляцию. Этот метод и все ссылки на него будут игнорироваться, если присутствует определенный символ препроцессора.

    Существует большое число других атрибутов, а также возможно задать свои собственные специальные атрибуты. Использование атрибутов рассматривается в главах 6 и 7.

    Согласно синтаксису атрибуты указываются непосредственно перед объектом, к которому они применимы, в квадратных скобках. Это такой же синтаксис, как у атрибутов Microsoft C++.

    [Conditional("Debug")]

    void DisplayValuesOfImportantVariables() {

     // и т.д.

    Директивы препроцессора

    C# поддерживает директивы препроцессора таким же образом, как C++, за исключением того, что их значительно меньше. В частности, C# не поддерживает обычно используемую директиву C++

    #include
    . (Она не требуется, так как в C# не используется предварительное объявление.)

    Синтаксис директив препроцессора в C# такой же, как в C++. В C# поддерживаются следующие директивы:

    Директива Значение
    #define/#undef
    Так же как в C++, за исключением того, что они должны появиться в начале файла, до кода C#.
    #if/#elif/#else/#endif
    То же самое, что в C++
    #ifdef/#elif/#else/#endif
    .
    #line
    То же самое, что в C++
    #line
    .
    #warning/#error
    То же самое, что в C++
    #warning/#error
    .
    #region/#endregion
    Помечает блок кода как область. Области распознаются некоторыми редакторами (такими, как редактор VisualStudio.NET) и поэтому могут использоваться для улучшения компоновки кода, представленного пользователю при редактировании.






     

    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх