Разработка приложений на C# в среде Visual Studio 2005

Обзор среды .NET Framework

Разбить на страницы
Показывать лекцию целиком

В упрощенном виде разработка и выполнение приложений .NET состоит из следующих этапов:

  • Создание программы на любом подходящем языке .NET
  • Создание байт-кода, т.е. компиляция исходного кода в модуль на промежуточном языке ( IL - Intermediate Language )
  • Объединение управляемых модулей ( managed module ) и создание сборки ( assembly )
  • Развертывание ( deploy ) или установка сборки на предназначенную платформу
  • Обращение к среде CLR ( CLR - Common Language Runtime ), которая загружает, компилирует байт-код в базовый и выполняет его.
  • CLR - это среда исполнения промежуточного кода MSIL. При загрузке приложения на выполнение она загружается в память первой и в последующем управляет работой приложения. В неуправляемом коде готовый машинный код сразу исполняется операционной системой. CLR же - это программа, которая включает в себя целый набор средств: JIT -компилятор, Manager Memory, Garbage Collector и другие. Все они, как посредники между приложением и операционной системой, обеспечивают выполнение приложения.

    Чтобы проделать всю эту работу, необходимо задействовать несколько компонентов платформы .NET Framework, которые представлены на рисунке

    Спецификация общего языка ( CLS - Common Language Specification ) - это набор правил для всех .NET - совместимых языков для успешной их работы в платформе .NET Framework.

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

    Среда CLR обеспечивает выполнение приложений .NET, а также предоставляет службы, необходимые этим приложениям во время выполнения. При обращении среда CLR сама выполняет все необходимые операции, включая компиляцию, распределение памяти и управление кодом обратившегося к ней приложения.

    Среда выполнения CLR работает с промежуточным представлением исходного кода, написанного на любом .NET -совместимом языке, удовлетворяющем спецификации CLS. Промежуточный код сгенерирован при компиляции исходного кода языка высокого уровня в код промежуточного языка MSIL или просто IL ( MSIL (IL) - Microsoft Intermediate Language ) и содержится в файле, называемом управляемым модулем или PE -файлом ( PE - Portable Executable - переносимый исполняемый файл Windows ).

    PE-файл состоит из 4-х частей

  • Заголовок PE
  • Заголовок CLR. Этот заголовок содержит информацию, необходимую для самой среды выполнения.
  • Метаданные. Метаданные состоят из таблиц, описывающих типы данных, членов модуля и ссылок на другие модули.
  • Управляемый код. Код, который выполняется под управлением среды CLR.
  • Несмотря на то, что управляемые модули являются переносимыми исполняемыми файлами, оно не могут быть запущены автономно, сначала их необходимо включить в сборку и передать на выполнение среде CLR, т.е. в операционной системе должен быть движок CLR.

    Язык MSIL ( IL ) является промежуточным языком низкого уровня, наподобие ассемблера, и подвергается повторной компиляции при выполнении приложения. Компилятор времени выполнения приложения .NET называют JIT - компилятором ( JIT - Just In Time - как раз во время, в свое время). Последовательность загрузки, компиляции и выполнения изображена на рисунке

    Сборки ( Assembly ) - развертываемый блок приложений .NET, содержащий в себе все необходимое для нормальной работы. Любое приложение .NET является сборкой. Сборки могут содержать один или несколько управляемых модулей, файлы ресурсов (рисунки, HTML ). Сборки позволяют улучшить защиту и доступ к коду, развертывание и контроль версий. Различные сборки могут располагаться в одной и той же системе и два разных приложения могут использовать две различные версии одной сборки. Проблемы DLL больше нет, когда замена DLL на более новую влечет за собой переделку кода приложений. Для установки сборки достаточно скопировать ее в нужный каталог.

    Типы данных C#

    Встроенные (элементарные) типы

    Системные типы данных и их псевдонимы в C#
    Псевдоним C# Соответствие CLS Системный тип Диапазон Пояснения
    void + Void Пусто Применяется для соблюдения синтаксиса
    sbyte - SByte -128 —127 Знаковое 8 бит
    byte + Byte 0 —255 Беззнаковое 8 бит
    short + Int16 -32'768 —32'767 Знаковое 16 бит
    ushort - UInt16 0 —65'535 Беззнаковое 16 бит
    int + Int32 -2'147'483'648 —2'147'483'647 Знаковое 32 бит (4 байта)
    uint - UInt32 0 —4'294'967'295 Беззнаковое 32 бит
    long + Int64 -9'223'372'036'854'775'808 —9'223'372'036'854'775'807 Знаковое 64 бит (8 байт)
    ulong - UInt64 0 —18'446'774'073'709'551'615 Беззнаковое 64 бит (8 байт)
    char + Char U+0000 —U+FFFF Для описания только одного символа Unicode 16 бит
    float + Single 1.5x10-45 —3.4x1038 Знаковое с плавающей точкой 32 бит (4 байта)
    double + Double 5.0x10-324— 1.7x10308 Знаковое с плавающей точкой 64 бит (8 байт)
    bool + Boolean true или false Логическое
    decimal + Decimal 100 —1028 Знаковое 96 бит (12 байт)
    string + String Ограничено только системной памятью Для описания строки символов Unicode любой длины
    object + Object Практически все что угодно. Все типы происходят от System.Object, поэтому объектом является все Класс, базовый для всех типов в .NET

    Утилита ILDasm - для исследования сборок

    Полезное средство для исследования сборок - утилита ILDasm ( ILDasm - Intermediate Language Disassembler utility ), Ее вызов можно подключить к оболочке Visual Studio 2005 с помощью команды:

    Tools/External Tools...

    Утилита находится в

    C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\ildasm.exe

    После запуска утилиты в ее меню File/Open можно открыть нужную сборку, например, системную

    C:\WINNT\Microsoft.NET\Framework\v2.0.50727\mscorlib.dll

    Для просмотра метаданных типов достаточно загрузить сборку в ILDasm и нажать клавиши Ctrl-M. Команда меню Dump TreeView утилиты выгружает иерархию исследуемой сборки в текстовый файл. Графические значки будут заменены соответствующими текстовыми аббревиатурами.

    Подключение ссылок на внешние сборки

    Через панель Solution Explorer/References подключим сборку system.windows.forms.dll

  • Создайте новый проект консольного приложения с именем MyApp командой File/New/Project, выберите, при этом, нужное его размещение в раскрывающемся списке Location
  • Если открыть панель Object Browser, то можно увидеть подключенные библиотечные сборки, которые оболочка посчитала (по мнению ее разработчиков) необходимым присоединить к заготовке проекта

    Удалим ссылки на сборки System.Data и System.Xml, которые нужны при работе с базами данных и языком XML. Через панель Object Browser это сделать нельзя (она только для просмотра), только в панели Solution Explorer.

  • В панели Solution Explorer раскройте узел References, выделите обе сборки System.Data и System.Xml, затем командой Remove контекстного меню удалите их из проекта
  • Командой Project/Add Reference откройте окно Add Reference и добавьте в проект ссылку на библиотечную сборку System.Windows.Forms
  • Каждая сборка содержит множество различных пространств имен, которые нужны для однозначной адресации к типам, имеющим возможно одинаковые имена. Добавленную сборку System.Windows.Forms мы будем использовать для создания графического окна Windows в нашем консольном приложении.

    Упражнение 1

  • Через панель Solution Explorer откройте файл-заготовку Program.cs и откорректируйте его следующим образом
  • // Подключение пространств имен 
    // для сокращенной адресации классов
    using System;
    using System.Windows.Forms;
        
    // Пространство имен в проекте можно и не объявлять
    // Тогда будет использовано глобальное пространство имен
    namespace MyApp
    {
        class MyClass
        {
            // Точка входа может быть без аргументов
            static void Main(string[] args)
            {
                // Настройка консольного окна сборки mscorlib.dll
                Console.Title = "Это консольное окно";
                Console.BackgroundColor = ConsoleColor.Blue;
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WindowWidth = 50; // Ширина в знакоместах
                Console.WindowHeight = 10; // Высота в текстовых строках
                Console.CursorVisible = false;
                Console.WriteLine("Простая C# - программа");
        
                // Графическое окно сборки System.Windows.Forms.dll
                MessageBox.Show("Привет из графического окна сообщений!!!");
        
                Console.ReadLine();
            }
        }
    }

    Результат выполнения приложения будет таким

    Ключевые слова в C#

    abstract as base bool break byte case
    catch char checked class const continue decimal
    default delegate do double else enum event
    explicit extern false finally fixed float for
    foreach goto if implicit in int interface
    internal is lock long namespace new null
    object operator out override params private protected
    public readonly ref return sbyte sealed short
    sizeof stackalloc static string struct switch this
    throw true try typeof uint ulong unchecked
    unsafe ushort using virtual void volatile while

    Разработка кода программы C# сводится к последовательному развитию типов, главным из которых является класс. Каждый класс рекомендуется размещать, как самостоятельный фрагмент кода программы, в отдельном файле с расширением *.cs. Но и в одном файле можно размещать сколько угодно классов, просто неудобно в них разбираться.

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

    Точка входа приложения

    Аргументами метода Main() являются параметры командной строки, передаваемые в период запуска программы. Мы вольны использовать их по своему усмотрению или игнорировать. Могут быть следующие вариации заголовка метода Main()

    Варианты метода Main()
    static void Main(string[] args) {...}
    public static void Main(string[] args) {...}
    public static void Main() {...}
    private static void Main(string[] args) {...}
    private static void Main() {...}
    static int Main(string[] args) {... return 0; }
    public static int Main(string[] args) {... return 0; }
    public static int Main() {... return 0; }
    private static int Main(string[] args) {... return 0; }
    private static int Main() {... return 0; }

    Главное требование - функция Main() должна быть статической, поскольку запускается тогда, когда еще не существует ни одного экземпляра класса. А объявлена она общедоступной или приватной - система ее всегда найдет.

    Обработка параметров командной строки в приложении (Упражнение 2)

    Вернемся к медоту Main() нашего приложения, вернее, к его заголовку.

  • Скорректируйте файл Program.cs, чтобы он выглядел так
  • using System;
    using System.Windows.Forms;
        
    namespace MyApp
    {
        class MyClass
        {
            // Точка входа может быть без аргументов
            static void Main(string[] args)
            {
                Console.Title = "";
                Console.ForegroundColor = ConsoleColor.White;
                Console.WriteLine("Простая C# - программа");
                Console.ReadLine();// Для задержки окна
            }
        }
    }

    Использование цикла for для перебора параметров

  • Измените метод Main() так, чтобы внутри него перебирались параметры командной строки
  • using System;
    using System.Windows.Forms;
        
    namespace MyApp
    {
        class MyClass
        {
            // Точка входа может быть без аргументов
            static void Main(string[] args)
            {
                // Настройка консоли
                Console.Title = "";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
        
                // Выводим параметры на консоль
                Console.WriteLine("Печать аргументов:");
                for (int i = 0; i < args.Length; i++)
                {
                    Console.WriteLine("Arg: {0}", args[i]);
                }
        
                Console.ReadLine();// Для задержки окна
            }
        }
    }

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

    Запуск приложения с аргументами из командной строки Windows

    Необходимо выполнить следующее:

  • Вызвать окно запуска программ Windows через меню Пуск/ Выполнить
  • Через кнопку Обзор найти исполнимую сборку проекта
  • Добавить аргументы в конец командной строки. Если путь к сборке или аргументы имеют пробелы, то их нужно заключать в двойные кавычки
  • Например, командная строка

    C:\Tmp\Test.exe -arg1 -arg2 "-Привет семье" -345

    при запуске нашего последнего примера даст результат

    Задание аргументов в окне командной строки оболочки

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

  • Чтобы отобразить окно команд, выполните опцию меню View/Windows Other/Command Window
  • Введите в окне команд после промптера строку, состоящую из вызова процессора команд ( Shell ), полного пути нахождения нашей сборки, имени сборки и аргументов, разделенных символами пробела. У меня это будет выглядеть так
  • Если путь содержит составные имена папок, разделенные пробелами, то вызов нужно заключать в двойные кавычки, например

  • После завершения команды нажатием клавиши Enter запустится консольное окно со следующим результатом
  • Нажмите клавишу Enter, чтобы прервать работу консольного приложения
  • Перебор введенных ранее команд в окне Command Window выполняется стрелками "вверх/вниз" клавиатуры .

    Задание аргументов командной строки в режиме отладки

    Для задания аргументов в режиме Debug сделайте следующее:

  • В панели Solution Explorer щелкните на пиктограмме Properties
  • То же самое можно сделать, если выполнить команду меню Project/MyApp Properties или для узла проекта вызвать контекстное меню и выбрать опцию Properties

  • Настройте окно свойств проекта для вкладки Debug так
  • Выполните команду Debug/Start Debugging или щелкните на одноименной кнопке окна оболочки
  • Получим результат

    Использование цикла foreach для перебора параметров

  • Замените в методе Main() цикл for на foreach, получив такой код
  • using System;
    using System.Windows.Forms;
        
    namespace MyApp
    {
        class MyClass
        {
            // Точка входа может быть без аргументов
            static void Main(string[] args)
            {
                // Настройка консоли
                Console.Title = "";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
        
                // Выводим параметры на консоль
                Console.WriteLine("Печать аргументов:");
                foreach (string arg in args)
                {
                    Console.WriteLine("Arg: {0}", arg);
                }
        
                Console.ReadLine();// Для задержки окна
            }
        }
    }
  • Постройте приложение - результат будет прежним
  • Извлечение параметров методом Environment.GetCommandLineArgs()

    using System;
    using System.Windows.Forms;
        
    namespace MyApp
    {
        class MyClass
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = "";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
        
                // Выводим параметры на консоль
                Console.WriteLine("Печать аргументов:");
        
                // Аргументы командной строки 
                // извлекаются классом Environment
                string[] theArgs = Environment.GetCommandLineArgs();
                Console.WriteLine("Путь к приложению: {0}", theArgs[0]);
                for (int i = 1; i < theArgs.Length; i++)
                    Console.WriteLine("Аргумент: {0} ", theArgs[i]);
        
                Console.ReadLine();// Для задержки окна
            }
        }
    }
  • Постройте приложение - результат будет прежним
  • Создание объектов и их инициализация

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

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

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

    Имена конструкторов должны совпадать с именем класса. Конструкторы должны имет модификатор доступа public.

    Для иллюстрации сказанного продолжим модификацию нашего проекта консольного приложения MyApp в части объявления класса MyClass.

    Упражнение 3

  • Модифицируйте класс следующим образом
  • using System;
    using System.Windows.Forms;
        
    namespace MyApp
    {
        class MyClass
        {
            // Внутренние переменные - поля
            private int x, y;
        
            // Конструктор по умолчанию
            public MyClass() 
            {
                Console.WriteLine("Конструктор по умолчанию");
            }
        
            // Конструктор с параметрами
            public MyClass(int a, int b) 
            {
                Console.WriteLine("Конструктор с параметрами");
                x = a;
                y = b;
            }
        
            // Сервисы класса
            public int GetX() { return x; }
            public int GetY() { return y; }
        
            // Точка входа в программу без аргументов
            static void Main()
            {
                // Настройка консоли
                Console.Title = "";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
        
                // Создаем два объекта-экземпляра класса
                MyClass ob1 = new MyClass(); // Вызов конструктора по умолчанию
                Console.WriteLine("ob1: x = {0}; y = {1} \n", ob1.GetX(), ob1.GetY());
        
                MyClass ob2 = new MyClass(10, 15); // Вызов параметризованного конструктора
                Console.WriteLine("ob2: x = {0}; y = {1}", ob2.GetX(), ob2.GetY());
                Console.Read();
            }
        }
    }
  • Постройте проект, чтобы получить результат
  • Мы видим, что поля x и y в первом объекте не остались брошенными, как это бы было в C++, а инициализировались нулями в соответствии с их типом. Такой порядок инициализации по умолчанию смысловым типом не относится к неинициализированным ссылкам. Их нужно обязательно инициализировать нулевым адресом с помощью ключевого слова null.

    Деструкторы в C# применять не нужно, хотя они и предусмотрены синтаксисом. Программист не должен заботиться об освобождении затребованной у системы памяти на куче. Об этом позаботиться специальный механизм, который называется сборщиком мусора ( GC - Garbage Collector ). Сборщик мусора платформы .NET освободит память автоматически.

    Вынесение точки входа в самостоятельный класс

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

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

  • Измените код файла MyClass.cs так, как показано ниже
  • using System;
    using System.Windows.Forms;
        
    namespace MyApp
    {
        class MyClass
        {
            // Внутренние (по умолчанию) переменные - поля
            int x, y;
        
            // Конструктор с параметрами
            public MyClass(int x, int y) 
            {
                this.x = x;
                this.y = y;
            }
        
            // Общедоступные свойства класса
            public int X
            {
                get { return x; }
                set { x = value; }
            }
        
            public int Y
            {
                get { return y; }
                set { y = value; }
            }
        
            // Точка входа в программу без аргументов
            static void Main()
            {
                // Настройка консоли
                Console.Title = "";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
        
                // Создаем экземпляр класса
                MyClass ob = new MyClass(10, 15);
                Console.WriteLine("x = {0}; y = {1}", ob.X, ob.Y);
        
                Console.ReadLine();
            }
        }
    }

    Операторы get и set называются аксессорами, а ключевое слово value способно иметь значение любого типа. Свойство можно объявить "только для чтения", используя ключевое слово readonly, а можно просто убрать блок аксессора set.

  • Запустите приложение, чтобы получить результат
  • Теперь обращение к внутренним полям класса осуществляется через общедоступные свойства.

    Упакуем функцию Main() в отдельный класс с именем EntryPoint, для этого

  • Выделите код функции Main() и выполните для выделенного фрагмента опцию контекстного меню Surround With (охватить с помощью)
  • В появившемся всплывающем меню выберите тип охватывающего блока class
  • Оболочка создаст встроенный класс с дежурным именем MyClass. Встроенный класс по умолчанию будет иметь область видимости внутри охватывающего класса, если к нему не добавить модификатор доступа public, т.е. по нему можно создавать объекты только внутри внешнего класса. Кроме того, синтаксис C# не допускает в одном пространстве имен существование типов с одинаковыми именами.

  • Переименуйте новый класс в EntryPoint и запустите приложение
  • Мы видим, что функция Main() доступна и из внутреннего класса.

  • Вынесите класс EntryPoint в пространство имен проекта и запустите приложение. Все остается прежним!
  • Для структурирования отступов в коде уничтожте последнюю закрывающую фигурную скобку пространства имен и вновь ее создайте. Код будет выровнен!
  • Размещение классов в отдельных файлах

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

    Разработчики оболочки предполагали, что в файле Program.cs будет находиться функция Main(), а пользовательский код желательно (но не обязательно) размещать в других файлах. Согласимся с ними.

  • Выберите опцию главного меню Project/Add New Item и заполните диалоговое окно мастера так
  • Перенесите в новый файл содержимое класса MyClass
  • После этого коды файлов будут такими

    using System;
        
    namespace MyApp
    {
        class EntryPoint
        {
            // Точка входа в программу без аргументов
            static void Main()
            {
                // Настройка консоли
                Console.Title = "";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
        
                // Создаем экземпляр класса
                MyClass ob = new MyClass(10, 15);
                Console.WriteLine("x = {0}; y = {1}", ob.X, ob.Y);
        
                Console.ReadLine();
            }
        }
    }
    using System;
        
    namespace MyApp
    {
        class MyClass
        {
            // Внутренние (по умолчанию) переменные - поля
            int x, y;
        
            // Конструктор с параметрами
            public MyClass(int x, int y)
            {
                this.x = x;
                this.y = y;
            }
        
            // Общедоступные свойства класса
            public int X
            {
                get { return x; }
                set { x = value; }
            }
        
            public int Y
            {
                get { return y; }
                set { y = value; }
            }
        }
    }

    Инициализация полей без конструктора

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

  • Измените код класса MyClass нашего примера так, как показано ниже
  • using System;
        
    namespace MyApp
    {
        class MyClass
        {
            // Внутренние (по умолчанию) переменные - поля
            int x = 10, y = 15;
        
            // Конструктор по умолчанию
            public MyClass() { }
        
            // Конструктор с параметрами
            public MyClass(int x, int y)
            {
                this.x = x;
                this.y = y;
            }
        
            // Общедоступные свойства класса
            public int X
            {
                get { return x; }
                set { x = value; }
            }
        
            public int Y
            {
                get { return y; }
                set { y = value; }
            }
        }
    }
    using System;
        
    namespace MyApp
    {
        class EntryPoint
        {
            // Точка входа в программу без аргументов
            static void Main()
            {
                // Настройка консоли
                Console.Title = "";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
        
                // Создаем экземпляр класса
                MyClass ob = new MyClass();
                Console.WriteLine("x = {0}; y = {1}", ob.X, ob.Y);
        
                // Создаем еще один экземпляр, 
                // используя ту же ссылочную переменную
                // Прежний объект будет утерян
                ob = new MyClass(100, 150);
                Console.WriteLine("x = {0}; y = {1}", ob.X, ob.Y);
        
                Console.ReadLine();
            }
        }
    }

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

    После построения проекта получим результат

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

    Виды данных в C#

    В C# различают два вида типов: значимые ( value-based ) и ссылочные ( reference-based ). Переменная значимого типа представляет закрепленное за ней само значение оперативной памяти. Переменная ссылочного типа содержит лишь адрес области оперативной памяти с размещаемым в этой области значением. Все структурные переменные размещаются на стеке, а ссылочные - на управляемой куче ( managed heap ). К структурным типам относятся перечисления и структуры, а к ссылочным - классы, делегаты, интерфейсы.

    Такое разделение принято для экономии памяти и быстродействия. Значимые типы представляют собой малообъемные данные, которые смысла нет размещать в управляемой области памяти. Представьте целочисленный счетчик цикла, который находится в удаленной памяти и существует только на период работы цикла (если объявлен внутри заголовка). Одна только ссылка на него будет занимать столько же памяти, что и само значение. И сам процесс адресации может занять порядочно времени. Да еще потом останется работа для сборщика мусора.

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

  • Добавьте к нашему проекту файл с именем MyStruct.c s типа C #, выполнив команду Project/Add Clas s, и скопируйте в него код из файла MyClass.cs. Исправьте программу, чтобы она была такой
  • using System;
        
    namespace MyApp
    {
        struct MyStruct
        {
            // Внутренние (по умолчанию) переменные - поля
            int x, y;
        
            // Конструктор с параметрами
            public MyStruct(int x, int y)
            {
                this.x = x;
                this.y = y;
            }
        
            // Конструктор по умолчанию перегружать нельзя
            // public MyStruct() { }
        
            // Общедоступные свойства класса
            public int X
            {
                get { return x; }
                set { x = value; }
            }
        
            public int Y
            {
                get { return y; }
                set { y = value; }
            }
        }
    }
    using System;
        
    namespace MyApp
    {
        class MyClass
        {
            // Внутренние (по умолчанию) переменные - поля
            int x, y;
            
            // Конструктор с параметрами
            public MyClass(int x, int y)
            {
                this.x = x;
                this.y = y;
            }
        
            // Конструктор по умолчанию
            public MyClass() { }
        
            // Общедоступные свойства класса
            public int X
            {
                get { return x; }
                set { x = value; }
            }
        
            public int Y
            {
                get { return y; }
                set { y = value; }
            }
        }
    }
    using System;
        
    namespace MyApp
    {
        class EntryPoint
        {
            // Точка входа в программу без аргументов
            static void Main()
            {
                // Настройка консоли
                Console.Title = "";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
        
                // Создаем две структуры
                MyStruct struct1 = new MyStruct(10, 15);
                MyStruct struct2 = new MyStruct();
        
                // Создаем два объекта
                MyClass object1 = new MyClass(10, 15);
                MyClass object2 = new MyClass();
        
                // Печатаем
                Console.WriteLine("Структуры до изменений:");
                Console.WriteLine("x = {0}; y = {1}", struct1.X, struct1.Y);
                Console.WriteLine("x = {0}; y = {1}", struct2.X, struct2.Y);
                Console.WriteLine("\nОбъекты до изменений:");
                Console.WriteLine("x = {0}; y = {1}", object1.X, object1.Y);
                Console.WriteLine("x = {0}; y = {1}", object2.X, object2.Y);
        
                // Присваиваем значения
                struct1 = struct2;// Копируются значения, struct1 существует
                object1 = object2; // Копируется адрес, object1 теряется  
        
                // Изменяем экземпляры
                struct2.X = 20; struct2.Y = 30;
                object2.X = 20; object2.Y = 30;
        
                // Печатаем
                Console.WriteLine("\nСтруктуры после изменений:");
                Console.WriteLine("x = {0}; y = {1}", struct1.X, struct1.Y);
                Console.WriteLine("x = {0}; y = {1}", struct2.X, struct2.Y);
                Console.WriteLine("\nОбъект после изменений:");
                Console.WriteLine("x = {0}; y = {1}", object1.X, object1.Y);
                Console.WriteLine("x = {0}; y = {1}", object2.X, object2.Y);    
        
                Console.ReadLine();
            }
        }
    }

    Результат выполнения будет таким

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

    Общие сравнения значимых и ссылочных типов

    Поставим некоторые предварительные вопросы и ответы на них в таблице

    Сравнение структурных и ссылочных типов
    Вопрос Значимые типы Ссылочные типы
    Место размещения Стек Управляемая куча
    Как будет представлена переменная, которой в качестве значения присвоен этот тип В виде локальной побитовой копии типа В виде указателя на область оперативной памяти, относящейся к объекту этого типа
    Что может являться для них базовым типом Эти типы могут производиться только напрямую от типа System.ValueType Могут производиться от любого другого типа, кроме System.ValueType, если этот тип не является закрытым
    Может ли этот тип выступать производителем другого типа Нет. Значимые типы всегда закрыты и дополнение их другими свойствами не предусмотрено. Поэтому они не могут выступать при наследовании в качестве базового для других типов Да, если этот ссылочный тип не определен как закрытый
    Как производится передача параметров этого типа в функцию Только по значению, т.е. вызывающей функции передаются локальные копии значений переменных Передаются как ссылки, т.е. вызывающей функции передаются адреса размещения соответствующих объектов в оперативной памяти
    Существует ли возможность переопределить встроенный метод Object.Finalize() Нет. Значимые типы никогда не размещаются в куче, поэтому к ним нельза применить функцию освобождения памяти Да, но не напрямую
    Можно ли определить конструкторы для этого типа Да, но конструктор по умолчанию зарезервирован, поэтому все определяемые конструкторы должны иметь параметры Да, сколько угодно, лишь бы они различались сигнатурой
    Когда происходит разрушение переменной данного типа Когда происходит выход из области видимости Во время процесса сборки мусора в управляемой куче с помощью механизма Garbage Collector (сборщик мусора)
    Можно ли напрямую инициализировать данные при объявлении типа Нет Да
    Можно ли перегружать конструктор по умолчанию Нет, он зарезервирован за системой Да, и обязательно, если определен хоть один конструктор с параметрами

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

    Страницы:

    В упрощенном виде разработка и выполнение приложений .NET состоит из следующих этапов:

  • Создание программы на любом подходящем языке .NET
  • Создание байт-кода, т.е. компиляция исходного кода в модуль на промежуточном языке ( IL - Intermediate Language )
  • Объединение управляемых модулей ( managed module ) и создание сборки ( assembly )
  • Развертывание ( deploy ) или установка сборки на предназначенную платформу
  • Обращение к среде CLR ( CLR - Common Language Runtime ), которая загружает, компилирует байт-код в базовый и выполняет его.
  • CLR - это среда исполнения промежуточного кода MSIL. При загрузке приложения на выполнение она загружается в память первой и в последующем управляет работой приложения. В неуправляемом коде готовый машинный код сразу исполняется операционной системой. CLR же - это программа, которая включает в себя целый набор средств: JIT -компилятор, Manager Memory, Garbage Collector и другие. Все они, как посредники между приложением и операционной системой, обеспечивают выполнение приложения.

    Чтобы проделать всю эту работу, необходимо задействовать несколько компонентов платформы .NET Framework, которые представлены на рисунке

    Спецификация общего языка ( CLS - Common Language Specification ) - это набор правил для всех .NET - совместимых языков для успешной их работы в платформе .NET Framework.

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

    Среда CLR обеспечивает выполнение приложений .NET, а также предоставляет службы, необходимые этим приложениям во время выполнения. При обращении среда CLR сама выполняет все необходимые операции, включая компиляцию, распределение памяти и управление кодом обратившегося к ней приложения.

    Среда выполнения CLR работает с промежуточным представлением исходного кода, написанного на любом .NET -совместимом языке, удовлетворяющем спецификации CLS. Промежуточный код сгенерирован при компиляции исходного кода языка высокого уровня в код промежуточного языка MSIL или просто IL ( MSIL (IL) - Microsoft Intermediate Language ) и содержится в файле, называемом управляемым модулем или PE -файлом ( PE - Portable Executable - переносимый исполняемый файл Windows ).

    PE-файл состоит из 4-х частей

  • Заголовок PE
  • Заголовок CLR. Этот заголовок содержит информацию, необходимую для самой среды выполнения.
  • Метаданные. Метаданные состоят из таблиц, описывающих типы данных, членов модуля и ссылок на другие модули.
  • Управляемый код. Код, который выполняется под управлением среды CLR.
  • Несмотря на то, что управляемые модули являются переносимыми исполняемыми файлами, оно не могут быть запущены автономно, сначала их необходимо включить в сборку и передать на выполнение среде CLR, т.е. в операционной системе должен быть движок CLR.

    Язык MSIL ( IL ) является промежуточным языком низкого уровня, наподобие ассемблера, и подвергается повторной компиляции при выполнении приложения. Компилятор времени выполнения приложения .NET называют JIT - компилятором ( JIT - Just In Time - как раз во время, в свое время). Последовательность загрузки, компиляции и выполнения изображена на рисунке

    Сборки ( Assembly ) - развертываемый блок приложений .NET, содержащий в себе все необходимое для нормальной работы. Любое приложение .NET является сборкой. Сборки могут содержать один или несколько управляемых модулей, файлы ресурсов (рисунки, HTML ). Сборки позволяют улучшить защиту и доступ к коду, развертывание и контроль версий. Различные сборки могут располагаться в одной и той же системе и два разных приложения могут использовать две различные версии одной сборки. Проблемы DLL больше нет, когда замена DLL на более новую влечет за собой переделку кода приложений. Для установки сборки достаточно скопировать ее в нужный каталог.

    Типы данных C#

    Встроенные (элементарные) типы

    Системные типы данных и их псевдонимы в C#
    Псевдоним C# Соответствие CLS Системный тип Диапазон Пояснения
    void + Void Пусто Применяется для соблюдения синтаксиса
    sbyte - SByte -128 —127 Знаковое 8 бит
    byte + Byte 0 —255 Беззнаковое 8 бит
    short + Int16 -32'768 —32'767 Знаковое 16 бит
    ushort - UInt16 0 —65'535 Беззнаковое 16 бит
    int + Int32 -2'147'483'648 —2'147'483'647 Знаковое 32 бит (4 байта)
    uint - UInt32 0 —4'294'967'295 Беззнаковое 32 бит
    long + Int64 -9'223'372'036'854'775'808 —9'223'372'036'854'775'807 Знаковое 64 бит (8 байт)
    ulong - UInt64 0 —18'446'774'073'709'551'615 Беззнаковое 64 бит (8 байт)
    char + Char U+0000 —U+FFFF Для описания только одного символа Unicode 16 бит
    float + Single 1.5x10-45 —3.4x1038 Знаковое с плавающей точкой 32 бит (4 байта)
    double + Double 5.0x10-324— 1.7x10308 Знаковое с плавающей точкой 64 бит (8 байт)
    bool + Boolean true или false Логическое
    decimal + Decimal 100 —1028 Знаковое 96 бит (12 байт)
    string + String Ограничено только системной памятью Для описания строки символов Unicode любой длины
    object + Object Практически все что угодно. Все типы происходят от System.Object, поэтому объектом является все Класс, базовый для всех типов в .NET

    Утилита ILDasm - для исследования сборок

    Полезное средство для исследования сборок - утилита ILDasm ( ILDasm - Intermediate Language Disassembler utility ), Ее вызов можно подключить к оболочке Visual Studio 2005 с помощью команды:

    Tools/External Tools...

    Утилита находится в

    C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\ildasm.exe

    После запуска утилиты в ее меню File/Open можно открыть нужную сборку, например, системную

    C:\WINNT\Microsoft.NET\Framework\v2.0.50727\mscorlib.dll

    Для просмотра метаданных типов достаточно загрузить сборку в ILDasm и нажать клавиши Ctrl-M. Команда меню Dump TreeView утилиты выгружает иерархию исследуемой сборки в текстовый файл. Графические значки будут заменены соответствующими текстовыми аббревиатурами.

    Подключение ссылок на внешние сборки

    Через панель Solution Explorer/References подключим сборку system.windows.forms.dll

  • Создайте новый проект консольного приложения с именем MyApp командой File/New/Project, выберите, при этом, нужное его размещение в раскрывающемся списке Location
  • Если открыть панель Object Browser, то можно увидеть подключенные библиотечные сборки, которые оболочка посчитала (по мнению ее разработчиков) необходимым присоединить к заготовке проекта

    Удалим ссылки на сборки System.Data и System.Xml, которые нужны при работе с базами данных и языком XML. Через панель Object Browser это сделать нельзя (она только для просмотра), только в панели Solution Explorer.

  • В панели Solution Explorer раскройте узел References, выделите обе сборки System.Data и System.Xml, затем командой Remove контекстного меню удалите их из проекта
  • Командой Project/Add Reference откройте окно Add Reference и добавьте в проект ссылку на библиотечную сборку System.Windows.Forms
  • Каждая сборка содержит множество различных пространств имен, которые нужны для однозначной адресации к типам, имеющим возможно одинаковые имена. Добавленную сборку System.Windows.Forms мы будем использовать для создания графического окна Windows в нашем консольном приложении.

    Упражнение 1

  • Через панель Solution Explorer откройте файл-заготовку Program.cs и откорректируйте его следующим образом
  • // Подключение пространств имен 
    // для сокращенной адресации классов
    using System;
    using System.Windows.Forms;
        
    // Пространство имен в проекте можно и не объявлять
    // Тогда будет использовано глобальное пространство имен
    namespace MyApp
    {
        class MyClass
        {
            // Точка входа может быть без аргументов
            static void Main(string[] args)
            {
                // Настройка консольного окна сборки mscorlib.dll
                Console.Title = "Это консольное окно";
                Console.BackgroundColor = ConsoleColor.Blue;
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WindowWidth = 50; // Ширина в знакоместах
                Console.WindowHeight = 10; // Высота в текстовых строках
                Console.CursorVisible = false;
                Console.WriteLine("Простая C# - программа");
        
                // Графическое окно сборки System.Windows.Forms.dll
                MessageBox.Show("Привет из графического окна сообщений!!!");
        
                Console.ReadLine();
            }
        }
    }

    Результат выполнения приложения будет таким

    Ключевые слова в C#

    abstract as base bool break byte case
    catch char checked class const continue decimal
    default delegate do double else enum event
    explicit extern false finally fixed float for
    foreach goto if implicit in int interface
    internal is lock long namespace new null
    object operator out override params private protected
    public readonly ref return sbyte sealed short
    sizeof stackalloc static string struct switch this
    throw true try typeof uint ulong unchecked
    unsafe ushort using virtual void volatile while

    Разработка кода программы C# сводится к последовательному развитию типов, главным из которых является класс. Каждый класс рекомендуется размещать, как самостоятельный фрагмент кода программы, в отдельном файле с расширением *.cs. Но и в одном файле можно размещать сколько угодно классов, просто неудобно в них разбираться.

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

    Точка входа приложения

    Аргументами метода Main() являются параметры командной строки, передаваемые в период запуска программы. Мы вольны использовать их по своему усмотрению или игнорировать. Могут быть следующие вариации заголовка метода Main()

    Варианты метода Main()
    static void Main(string[] args) {...}
    public static void Main(string[] args) {...}
    public static void Main() {...}
    private static void Main(string[] args) {...}
    private static void Main() {...}
    static int Main(string[] args) {... return 0; }
    public static int Main(string[] args) {... return 0; }
    public static int Main() {... return 0; }
    private static int Main(string[] args) {... return 0; }
    private static int Main() {... return 0; }

    Главное требование - функция Main() должна быть статической, поскольку запускается тогда, когда еще не существует ни одного экземпляра класса. А объявлена она общедоступной или приватной - система ее всегда найдет.

    Обработка параметров командной строки в приложении (Упражнение 2)

    Вернемся к медоту Main() нашего приложения, вернее, к его заголовку.

  • Скорректируйте файл Program.cs, чтобы он выглядел так
  • using System;
    using System.Windows.Forms;
        
    namespace MyApp
    {
        class MyClass
        {
            // Точка входа может быть без аргументов
            static void Main(string[] args)
            {
                Console.Title = "";
                Console.ForegroundColor = ConsoleColor.White;
                Console.WriteLine("Простая C# - программа");
                Console.ReadLine();// Для задержки окна
            }
        }
    }

    Использование цикла for для перебора параметров

  • Измените метод Main() так, чтобы внутри него перебирались параметры командной строки
  • using System;
    using System.Windows.Forms;
        
    namespace MyApp
    {
        class MyClass
        {
            // Точка входа может быть без аргументов
            static void Main(string[] args)
            {
                // Настройка консоли
                Console.Title = "";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
        
                // Выводим параметры на консоль
                Console.WriteLine("Печать аргументов:");
                for (int i = 0; i < args.Length; i++)
                {
                    Console.WriteLine("Arg: {0}", args[i]);
                }
        
                Console.ReadLine();// Для задержки окна
            }
        }
    }

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

    Запуск приложения с аргументами из командной строки Windows

    Необходимо выполнить следующее:

  • Вызвать окно запуска программ Windows через меню Пуск/ Выполнить
  • Через кнопку Обзор найти исполнимую сборку проекта
  • Добавить аргументы в конец командной строки. Если путь к сборке или аргументы имеют пробелы, то их нужно заключать в двойные кавычки
  • Например, командная строка

    C:\Tmp\Test.exe -arg1 -arg2 "-Привет семье" -345

    при запуске нашего последнего примера даст результат

    Задание аргументов в окне командной строки оболочки

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

  • Чтобы отобразить окно команд, выполните опцию меню View/Windows Other/Command Window
  • Введите в окне команд после промптера строку, состоящую из вызова процессора команд ( Shell ), полного пути нахождения нашей сборки, имени сборки и аргументов, разделенных символами пробела. У меня это будет выглядеть так
  • Если путь содержит составные имена папок, разделенные пробелами, то вызов нужно заключать в двойные кавычки, например

  • После завершения команды нажатием клавиши Enter запустится консольное окно со следующим результатом
  • Нажмите клавишу Enter, чтобы прервать работу консольного приложения
  • Перебор введенных ранее команд в окне Command Window выполняется стрелками "вверх/вниз" клавиатуры .

    Задание аргументов командной строки в режиме отладки

    Для задания аргументов в режиме Debug сделайте следующее:

  • В панели Solution Explorer щелкните на пиктограмме Properties
  • То же самое можно сделать, если выполнить команду меню Project/MyApp Properties или для узла проекта вызвать контекстное меню и выбрать опцию Properties

  • Настройте окно свойств проекта для вкладки Debug так
  • Выполните команду Debug/Start Debugging или щелкните на одноименной кнопке окна оболочки
  • Получим результат

    Использование цикла foreach для перебора параметров

  • Замените в методе Main() цикл for на foreach, получив такой код
  • using System;
    using System.Windows.Forms;
        
    namespace MyApp
    {
        class MyClass
        {
            // Точка входа может быть без аргументов
            static void Main(string[] args)
            {
                // Настройка консоли
                Console.Title = "";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
        
                // Выводим параметры на консоль
                Console.WriteLine("Печать аргументов:");
                foreach (string arg in args)
                {
                    Console.WriteLine("Arg: {0}", arg);
                }
        
                Console.ReadLine();// Для задержки окна
            }
        }
    }
  • Постройте приложение - результат будет прежним
  • Извлечение параметров методом Environment.GetCommandLineArgs()

    using System;
    using System.Windows.Forms;
        
    namespace MyApp
    {
        class MyClass
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = "";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
        
                // Выводим параметры на консоль
                Console.WriteLine("Печать аргументов:");
        
                // Аргументы командной строки 
                // извлекаются классом Environment
                string[] theArgs = Environment.GetCommandLineArgs();
                Console.WriteLine("Путь к приложению: {0}", theArgs[0]);
                for (int i = 1; i < theArgs.Length; i++)
                    Console.WriteLine("Аргумент: {0} ", theArgs[i]);
        
                Console.ReadLine();// Для задержки окна
            }
        }
    }
  • Постройте приложение - результат будет прежним
  • Создание объектов и их инициализация

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

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

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

    Имена конструкторов должны совпадать с именем класса. Конструкторы должны имет модификатор доступа public.

    Для иллюстрации сказанного продолжим модификацию нашего проекта консольного приложения MyApp в части объявления класса MyClass.

    Упражнение 3

  • Модифицируйте класс следующим образом
  • using System;
    using System.Windows.Forms;
        
    namespace MyApp
    {
        class MyClass
        {
            // Внутренние переменные - поля
            private int x, y;
        
            // Конструктор по умолчанию
            public MyClass() 
            {
                Console.WriteLine("Конструктор по умолчанию");
            }
        
            // Конструктор с параметрами
            public MyClass(int a, int b) 
            {
                Console.WriteLine("Конструктор с параметрами");
                x = a;
                y = b;
            }
        
            // Сервисы класса
            public int GetX() { return x; }
            public int GetY() { return y; }
        
            // Точка входа в программу без аргументов
            static void Main()
            {
                // Настройка консоли
                Console.Title = "";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
        
                // Создаем два объекта-экземпляра класса
                MyClass ob1 = new MyClass(); // Вызов конструктора по умолчанию
                Console.WriteLine("ob1: x = {0}; y = {1} \n", ob1.GetX(), ob1.GetY());
        
                MyClass ob2 = new MyClass(10, 15); // Вызов параметризованного конструктора
                Console.WriteLine("ob2: x = {0}; y = {1}", ob2.GetX(), ob2.GetY());
                Console.Read();
            }
        }
    }
  • Постройте проект, чтобы получить результат
  • Мы видим, что поля x и y в первом объекте не остались брошенными, как это бы было в C++, а инициализировались нулями в соответствии с их типом. Такой порядок инициализации по умолчанию смысловым типом не относится к неинициализированным ссылкам. Их нужно обязательно инициализировать нулевым адресом с помощью ключевого слова null.

    Деструкторы в C# применять не нужно, хотя они и предусмотрены синтаксисом. Программист не должен заботиться об освобождении затребованной у системы памяти на куче. Об этом позаботиться специальный механизм, который называется сборщиком мусора ( GC - Garbage Collector ). Сборщик мусора платформы .NET освободит память автоматически.

    Вынесение точки входа в самостоятельный класс

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

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

  • Измените код файла MyClass.cs так, как показано ниже
  • using System;
    using System.Windows.Forms;
        
    namespace MyApp
    {
        class MyClass
        {
            // Внутренние (по умолчанию) переменные - поля
            int x, y;
        
            // Конструктор с параметрами
            public MyClass(int x, int y) 
            {
                this.x = x;
                this.y = y;
            }
        
            // Общедоступные свойства класса
            public int X
            {
                get { return x; }
                set { x = value; }
            }
        
            public int Y
            {
                get { return y; }
                set { y = value; }
            }
        
            // Точка входа в программу без аргументов
            static void Main()
            {
                // Настройка консоли
                Console.Title = "";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
        
                // Создаем экземпляр класса
                MyClass ob = new MyClass(10, 15);
                Console.WriteLine("x = {0}; y = {1}", ob.X, ob.Y);
        
                Console.ReadLine();
            }
        }
    }

    Операторы get и set называются аксессорами, а ключевое слово value способно иметь значение любого типа. Свойство можно объявить "только для чтения", используя ключевое слово readonly, а можно просто убрать блок аксессора set.

  • Запустите приложение, чтобы получить результат
  • Теперь обращение к внутренним полям класса осуществляется через общедоступные свойства.

    Упакуем функцию Main() в отдельный класс с именем EntryPoint, для этого

  • Выделите код функции Main() и выполните для выделенного фрагмента опцию контекстного меню Surround With (охватить с помощью)
  • В появившемся всплывающем меню выберите тип охватывающего блока class
  • Оболочка создаст встроенный класс с дежурным именем MyClass. Встроенный класс по умолчанию будет иметь область видимости внутри охватывающего класса, если к нему не добавить модификатор доступа public, т.е. по нему можно создавать объекты только внутри внешнего класса. Кроме того, синтаксис C# не допускает в одном пространстве имен существование типов с одинаковыми именами.

  • Переименуйте новый класс в EntryPoint и запустите приложение
  • Мы видим, что функция Main() доступна и из внутреннего класса.

  • Вынесите класс EntryPoint в пространство имен проекта и запустите приложение. Все остается прежним!
  • Для структурирования отступов в коде уничтожте последнюю закрывающую фигурную скобку пространства имен и вновь ее создайте. Код будет выровнен!
  • Размещение классов в отдельных файлах

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

    Разработчики оболочки предполагали, что в файле Program.cs будет находиться функция Main(), а пользовательский код желательно (но не обязательно) размещать в других файлах. Согласимся с ними.

  • Выберите опцию главного меню Project/Add New Item и заполните диалоговое окно мастера так
  • Перенесите в новый файл содержимое класса MyClass
  • После этого коды файлов будут такими

    using System;
        
    namespace MyApp
    {
        class EntryPoint
        {
            // Точка входа в программу без аргументов
            static void Main()
            {
                // Настройка консоли
                Console.Title = "";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
        
                // Создаем экземпляр класса
                MyClass ob = new MyClass(10, 15);
                Console.WriteLine("x = {0}; y = {1}", ob.X, ob.Y);
        
                Console.ReadLine();
            }
        }
    }
    using System;
        
    namespace MyApp
    {
        class MyClass
        {
            // Внутренние (по умолчанию) переменные - поля
            int x, y;
        
            // Конструктор с параметрами
            public MyClass(int x, int y)
            {
                this.x = x;
                this.y = y;
            }
        
            // Общедоступные свойства класса
            public int X
            {
                get { return x; }
                set { x = value; }
            }
        
            public int Y
            {
                get { return y; }
                set { y = value; }
            }
        }
    }

    Инициализация полей без конструктора

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

  • Измените код класса MyClass нашего примера так, как показано ниже
  • using System;
        
    namespace MyApp
    {
        class MyClass
        {
            // Внутренние (по умолчанию) переменные - поля
            int x = 10, y = 15;
        
            // Конструктор по умолчанию
            public MyClass() { }
        
            // Конструктор с параметрами
            public MyClass(int x, int y)
            {
                this.x = x;
                this.y = y;
            }
        
            // Общедоступные свойства класса
            public int X
            {
                get { return x; }
                set { x = value; }
            }
        
            public int Y
            {
                get { return y; }
                set { y = value; }
            }
        }
    }
    using System;
        
    namespace MyApp
    {
        class EntryPoint
        {
            // Точка входа в программу без аргументов
            static void Main()
            {
                // Настройка консоли
                Console.Title = "";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
        
                // Создаем экземпляр класса
                MyClass ob = new MyClass();
                Console.WriteLine("x = {0}; y = {1}", ob.X, ob.Y);
        
                // Создаем еще один экземпляр, 
                // используя ту же ссылочную переменную
                // Прежний объект будет утерян
                ob = new MyClass(100, 150);
                Console.WriteLine("x = {0}; y = {1}", ob.X, ob.Y);
        
                Console.ReadLine();
            }
        }
    }

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

    После построения проекта получим результат

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

    Виды данных в C#

    В C# различают два вида типов: значимые ( value-based ) и ссылочные ( reference-based ). Переменная значимого типа представляет закрепленное за ней само значение оперативной памяти. Переменная ссылочного типа содержит лишь адрес области оперативной памяти с размещаемым в этой области значением. Все структурные переменные размещаются на стеке, а ссылочные - на управляемой куче ( managed heap ). К структурным типам относятся перечисления и структуры, а к ссылочным - классы, делегаты, интерфейсы.

    Такое разделение принято для экономии памяти и быстродействия. Значимые типы представляют собой малообъемные данные, которые смысла нет размещать в управляемой области памяти. Представьте целочисленный счетчик цикла, который находится в удаленной памяти и существует только на период работы цикла (если объявлен внутри заголовка). Одна только ссылка на него будет занимать столько же памяти, что и само значение. И сам процесс адресации может занять порядочно времени. Да еще потом останется работа для сборщика мусора.

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

  • Добавьте к нашему проекту файл с именем MyStruct.c s типа C #, выполнив команду Project/Add Clas s, и скопируйте в него код из файла MyClass.cs. Исправьте программу, чтобы она была такой
  • using System;
        
    namespace MyApp
    {
        struct MyStruct
        {
            // Внутренние (по умолчанию) переменные - поля
            int x, y;
        
            // Конструктор с параметрами
            public MyStruct(int x, int y)
            {
                this.x = x;
                this.y = y;
            }
        
            // Конструктор по умолчанию перегружать нельзя
            // public MyStruct() { }
        
            // Общедоступные свойства класса
            public int X
            {
                get { return x; }
                set { x = value; }
            }
        
            public int Y
            {
                get { return y; }
                set { y = value; }
            }
        }
    }
    using System;
        
    namespace MyApp
    {
        class MyClass
        {
            // Внутренние (по умолчанию) переменные - поля
            int x, y;
            
            // Конструктор с параметрами
            public MyClass(int x, int y)
            {
                this.x = x;
                this.y = y;
            }
        
            // Конструктор по умолчанию
            public MyClass() { }
        
            // Общедоступные свойства класса
            public int X
            {
                get { return x; }
                set { x = value; }
            }
        
            public int Y
            {
                get { return y; }
                set { y = value; }
            }
        }
    }
    using System;
        
    namespace MyApp
    {
        class EntryPoint
        {
            // Точка входа в программу без аргументов
            static void Main()
            {
                // Настройка консоли
                Console.Title = "";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
        
                // Создаем две структуры
                MyStruct struct1 = new MyStruct(10, 15);
                MyStruct struct2 = new MyStruct();
        
                // Создаем два объекта
                MyClass object1 = new MyClass(10, 15);
                MyClass object2 = new MyClass();
        
                // Печатаем
                Console.WriteLine("Структуры до изменений:");
                Console.WriteLine("x = {0}; y = {1}", struct1.X, struct1.Y);
                Console.WriteLine("x = {0}; y = {1}", struct2.X, struct2.Y);
                Console.WriteLine("\nОбъекты до изменений:");
                Console.WriteLine("x = {0}; y = {1}", object1.X, object1.Y);
                Console.WriteLine("x = {0}; y = {1}", object2.X, object2.Y);
        
                // Присваиваем значения
                struct1 = struct2;// Копируются значения, struct1 существует
                object1 = object2; // Копируется адрес, object1 теряется  
        
                // Изменяем экземпляры
                struct2.X = 20; struct2.Y = 30;
                object2.X = 20; object2.Y = 30;
        
                // Печатаем
                Console.WriteLine("\nСтруктуры после изменений:");
                Console.WriteLine("x = {0}; y = {1}", struct1.X, struct1.Y);
                Console.WriteLine("x = {0}; y = {1}", struct2.X, struct2.Y);
                Console.WriteLine("\nОбъект после изменений:");
                Console.WriteLine("x = {0}; y = {1}", object1.X, object1.Y);
                Console.WriteLine("x = {0}; y = {1}", object2.X, object2.Y);    
        
                Console.ReadLine();
            }
        }
    }

    Результат выполнения будет таким

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

    Общие сравнения значимых и ссылочных типов

    Поставим некоторые предварительные вопросы и ответы на них в таблице

    Сравнение структурных и ссылочных типов
    Вопрос Значимые типы Ссылочные типы
    Место размещения Стек Управляемая куча
    Как будет представлена переменная, которой в качестве значения присвоен этот тип В виде локальной побитовой копии типа В виде указателя на область оперативной памяти, относящейся к объекту этого типа
    Что может являться для них базовым типом Эти типы могут производиться только напрямую от типа System.ValueType Могут производиться от любого другого типа, кроме System.ValueType, если этот тип не является закрытым
    Может ли этот тип выступать производителем другого типа Нет. Значимые типы всегда закрыты и дополнение их другими свойствами не предусмотрено. Поэтому они не могут выступать при наследовании в качестве базового для других типов Да, если этот ссылочный тип не определен как закрытый
    Как производится передача параметров этого типа в функцию Только по значению, т.е. вызывающей функции передаются локальные копии значений переменных Передаются как ссылки, т.е. вызывающей функции передаются адреса размещения соответствующих объектов в оперативной памяти
    Существует ли возможность переопределить встроенный метод Object.Finalize() Нет. Значимые типы никогда не размещаются в куче, поэтому к ним нельза применить функцию освобождения памяти Да, но не напрямую
    Можно ли определить конструкторы для этого типа Да, но конструктор по умолчанию зарезервирован, поэтому все определяемые конструкторы должны иметь параметры Да, сколько угодно, лишь бы они различались сигнатурой
    Когда происходит разрушение переменной данного типа Когда происходит выход из области видимости Во время процесса сборки мусора в управляемой куче с помощью механизма Garbage Collector (сборщик мусора)
    Можно ли напрямую инициализировать данные при объявлении типа Нет Да
    Можно ли перегружать конструктор по умолчанию Нет, он зарезервирован за системой Да, и обязательно, если определен хоть один конструктор с параметрами

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

    Вернуться к учебному плану