Введение в программирование на C# 2.0

Объекты

Показывать лекцию целиком

В соответствии с принципами объектно-ориентированного программирования решение поставленной задачи сводится к разработке модели (объявлению класса) и созданию экземпляров (объектов), представляющих реализацию этой модели.

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

Создание объекта. Конструктор

Конструктором называется множество операторов кода, которому передается управление при создании объекта. Синтаксис объявления конструктора аналогичен объявлению метода — те же спецификаторы доступа, имя, список параметров. Особенности конструктора заключаются в том, что:

  • конструктор НЕ ИМЕЕТ НИКАКОГО возвращаемого спецификатора, даже void ;
  • имя конструктора полностью совпадает с именем класса или структуры;
  • в классе и в структуре можно объявлять множество вариантов конструкторов. Они должны отличаться списками параметров. В структуре невозможно объявить конструктор с пустым списком параметров;
  • не существует выражения вызова для конструктора, управление в конструктор передается посредством выполнения специальной операции new.
  • Операция new

    Операция new используется для создания объектов и передачи управления конструкторам, например:

    Class1 myVal = new Class1(); // Объект ссылочного типа. Создается в куче.

    new также используется для обращения к конструкторам объектов типа значений:

    int myInt = new int(); // Объект типа int размещается в стеке!

    При определении объекта myInt ему было присвоено начальное значение 0, которое является значением по умолчанию для типа int. Следующий оператор имеет тот же самый эффект:

    int myInt = 0;

    Конструктор БЕЗ ПАРАМЕТРОВ (конструктор умолчания) обеспечивает инициализацию переменной предопределенным значением. Со списком предопределенных значений, которыми инициализируются объекты предопределенных типов, можно ознакомиться в Default Values Table.

    У структуры конструктор умолчания (конструктор без параметров) НЕ ПЕРЕОПРЕДЕЛЯЕТСЯ! Для них объявляются только параметризованные конструкторы.

    А вот для типов — значений конструкторов с параметрами в принципе нет!

    int q = new int();
    //int q = new int(125); // Такого нет.

    Сколько конструкторов может иметь структура? При использовании правил перегрузки – неограниченное количество.

    Сколько конструкторов может иметь класс? Всегда на один больше, чем структура.

    В следующем примере создаются с использованием операции new и конструктора объекты — представители класса и структуры. Для инициализации полей — членов класса используются параметры конструкторов. Присвоение значений осуществляется в теле конструктора с использованием операций присвоения:

    // cs_operator_new.cs
    // The new operator
    using System;
    class NewTest 
    {
    struct MyStruct 
     {
    public int x;
    public int y;
    
    public MyStruct (int x, int y) 
     {
    this.x = x;
    this.y = y;
     }
     }
    class MyClass 
     {
    public string name;
    public int id;
    
    public MyClass () 
     {
     }
    
    public MyClass (int id, string name) 
     {
    this.id = id;
    this.name = name;
     } 
     }
    
    public static void Main() 
     {
    // Create objects using default constructors:
    MyStruct Location1 = new MyStruct();
    MyClass Employee1 = new MyClass();
    
    // Display values:
    Console.WriteLine("Default values:");
    Console.WriteLine(" Struct members: {0}, {1}", 
    Location1.x, Location1.y);
    Console.WriteLine(" Class members: {0}, {1}", 
    Employee1.name, Employee1.id);
    // Create objects using parameterized constructors::
    MyStruct Location2 = new MyStruct(10, 20);
    MyClass Employee2 = new MyClass(1234, "John Martin Smith");
    // Display values:
    Console.WriteLine("Assigned values:");
    Console.WriteLine(" Struct members: {0}, {1}", 
    Location2.x, Location2.y);
    Console.WriteLine(" Class members: {0}, {1}", 
    Employee2.name, Employee2.id);
     }
    }

    Кто строит конструктор умолчания

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

    Class X
    {
    
    }
    ::::::::::
    X x = new X(); // Работает конструктор умолчания.

    Однако попытка объявления ЛЮБОГО варианта конструктора (с параметрами или без) приводит к тому, что транслятор перестает заниматься построением собственных версий конструкторов. Отныне в классе нет больше конструктора умолчания. Теперь все зависит от соответствия оператора определения объекта построенному нами конструктору. Объявим в производном классе оба варианта конструкторов.

    Class X
    {
    public X(int key){}
    }
    ::::::::::
    X x0 = new X(125); // Работает конструктор с параметрами.
    X x1 = new X(); // Не дело! Конструктора умолчания уже нет!

    Однажды взявшись за дело объявления конструкторов, разработчик класса должен брать на себя ответственность за создание ВСЕХ без исключения вариантов конструкторов, которые могут потребоваться для решения поставленной задачи. Таковы правила.

    Class X
    {
    public X(int key){}
    public X(){}
    }
    ::::::::::
    X x0 = new X(125); // Работает конструктор с параметрами.
    X x1 = new X(); // Работает новый конструктор без параметров.

    this в конструкторе, деструкторе, методе, свойстве

    Нестатический метод класса определяет индивидуальное поведение объекта и поэтому вызывается "от имени" данного объекта. В большинстве объектно-ориентированных языков программирования реализована возможность непосредственного обращения из нестатического метода (а также свойства) к объекту, который обеспечил выполнение кода данного метода или свойства. Аналогичная возможность обращения к объекту реализуется в теле нестатических конструкторов и деструктора.

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

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

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

    Для метода ToString все ссылки на объекты, представляющие данный класс, неразличимы. Следующий пример подтверждает тот факт, что this в нестатическом методе является всего лишь ссылкой на объект — представитель данного класса.

    using System;
    
    namespace XXX
    {
    class TEST
    {
    int x;
    int y;
    public string str;
    
    public TEST(int xKey, int yKey) 
    {
    // В конструкторе this – это всегда ссылка
    // на создаваемый объект
    this.x = xKey; 
    y = yKey;
    str = "Ha-Ha-Ha";
    }
    
    public TEST f1(string strKey)
    {
    this.str = this.str + strKey + this.ToString();
    // Здесь можно будет посмотреть,
    // что собой представляет значение ссылки на объект.
    return this; // А вот здесь без выражения this просто
    // ничего не получается.
    }     
    }
    
    class Program
    {
    static void Main(string[] args)
    {
    TEST t, tM;
    t = new TEST(10, 10);
    Console.WriteLine(t.str);
    tM = t.f1(" from ");
    t = new TEST(100, 100);
    Console.WriteLine(t.str);
    Console.WriteLine(t.ToString());
    Console.WriteLine(tM.str);
    Console.WriteLine(tM.ToString());
    }
    }
    }

    this в заголовке конструктора

    Конструктор не вызывается. Передача управления конструктору осуществляется при выполнении операции new.

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

    Для сокращения размера кода можно оставить ОДИН конструктор, выполняющий ВСЮ "черновую" обязательную работу по созданию объектов. Тонкую настройку объектов можно производить после выполнения кода конструктора, непосредственно вызывая соответствующие методы-члены. При этом методы вызываются именно ПОСЛЕ того, как отработает конструктор.

    Можно также определить множество специализированных конструкторов, после выполнения которых следует вызывать дополнительный метод инициализации для общей "достройки" объекта.

    И то и другое — некрасиво. Код получается сложный.

    В C# реализован другой подход, который состоит в следующем:

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

    Следующий фрагмент программного кода демонстрирует объявление нескольких конструкторов с передачей управления "золушке":

    class Point2D
    {
    private float x, y;
    public Point2D(float xKey, float yKey)
     {
    Console.WriteLine("Point2D({0}, {1}) is here!", xKey, yKey);
    // Какой-нибудь сложный обязательный
    // код инициализации данных – членов класса.
    
    int i = 0;
    while (i < 100)
     {
    x = xKey;
    y = yKey;
    i++;
     }	
     }
    
    // А все другие конструкторы в обязательном порядке предполагают
    // регламентные работы по инициализации значений объекта – и делают при этом
    // еще много чего...
    public Point2D():this(0,0)
     {
    int i;
    for (i = 0; i < 100; i++)
     {
    // Хорошо, что значения уже проинициализированы! 
    // Здесь своих проблем хватает.
     }
    
    Console.WriteLine("Point2D() is here!");
     }
    
    public Point2D(Point2D pKey):this(pKey.x, pKey.y)
     {
    int i;
    for (i = 0; i < 100; i++)
     {
    // Здесь тоже заблаговременно  позаботились о значениях.
    // Хорошо.
     }
    Console.WriteLine("Point2D({0}) is here!", pKey.ToString());
     }
    }

    Уничтожение объектов в управляемой памяти. Деструктор

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

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

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

    Простой пример, иллюстрирующий принципы функционирования управляемой памяти и работы деструктора:

    using System;
    
    namespace MemoryTest01
    {
    
    // Объекты этого класса регистрируют деятельность конструктора
    // при создании объекта и деструктора при его уничтожении.
     public class MemElem
     {
      public static long AllElem = 0;
      long N = 0;
    public MemElem(long key)
    {
    AllElem++;
    N = key;
    Console.WriteLine("{0} element was created. {1} in memory!", N, AllElem);
    }
    
    
    // Объявление деструктора.
    ~MemElem()
    {
    AllElem--;
    Console.WriteLine("{0} was destroyed by GC. {1} in memory!", N, AllElem);
    }
    }
    
    class Program
    {
    
    // Порождение объектов-представителей класса MemElem.
    // Партиями по 50 штук.
    // Периодичность активности GC (Garbage Cleaner) неизвестна.
    
    static void Main(string[] args)
     {
        MemElem mem;
        string s;
        long N = 0;
        int i = 0;
        for ( ; ; )
        {
         Console.WriteLine("_________________________");
         Console.Write("x for terminate >> ");
         s = Console.ReadLine();
         if (s.Equals("x")) break;
         else N += i;
         for (i = 0; i < 50; i++)
         {
           mem = new MemElem(N+i);
         }
       }
     }
    }
    }

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

    Класс GC

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

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

    Public-свойства
    MaxGeneration Максимальное количество поддерживаемых в управляющей памяти поколений. Статическое
    Public-методы
    AddMemoryPressure Уведомление среды выполнения о резервировании большого объема неуправляемой памяти, который необходимо учесть при планировании работы сборщика мусора. Статический
    Collect Перегруженный. Активизирует процесс сборки мусора. Сборка происходит в отдельном потоке. Поэтому время начала деятельности сборщика остается неопределенным. Статический
    CollectionCount Определяет общее количество проходов сборщика для данного поколения объектов. Статический
    Equals Определение эквивалентности объектов.
    GetGeneration Перегруженный. Возвращает значение номера поколения, содержащего данный объект. Статический
    GetTotalMemory Возвращает количество байт, занятых под объекты в управляющей памяти. В зависимости от значения параметра типа bool учитываются (или не учитываются) результаты деятельности сборщика мусора в момент выполнения метода. Сборщик мусора работает в теневом потоке, и в принципе можно немножко подождать результатов его работы. Статический
    KeepAlive Ссылается на указанный объект, делая его недоступным для сборщика мусора с момента начала текущей программы до вызова этого метода. Статический
    RemoveMemoryPressure Информирует среду выполнения об освобождении области неуправляемой памяти. Эта информация может быть полезна сборщику мусора для планирования работы. Статический
    SuppressFinalize Метод обеспечивает уведомление сборщика мусора о том, что данный объект (представляется ссылкой в параметре метода) не подлежит удалению. Статический
    ReRegisterForFinalize Сначала защищаем объект от GC путем вызова метода SuppressFinalize. А теперь снимаем с объекта эту защиту. Статический
    WaitForPendingFinalizers Приостанавливает текущий поток до тех пор, пока поток, обрабатывающий очередь финализаторов, не обработает всю очередь. Статический

    Далее демонстрируются методы управления сборкой мусора. Используется модифицированный класс MemElem:

    using System;
    
    namespace GCExample
    {
    // Объекты этого класса регистрируют деятельность конструктора
    // при создании объекта и деструктора при его уничтожении.
    
    public class MemElem
    {
     public static long AllElem = 0;
     long N = 0;
     public MemElem(long key)
     {
     AllElem++;
     N = key;
     Console.WriteLine("{0} was created. {1} in memory!", N, AllElem);
     }
    
    ~MemElem()
    {
    AllElem--;
    Console.WriteLine("{0} was destroyed. {1} in memory!", N, AllElem);
                
    // Смотрим общее количество байтов, занимаемых объектами в куче.
    // Значение параметра (false) свидетельствует о том, что эта информация
    // требуется незамедлительно, вне зависимости от того, работает ли в данный
    // момент в теневом потоке сборщик мусора или нет.
    // А в этом случае значения true мы готовы подождать окончания работы
    // сборщика мусора, если он в данный момент разбирает завалы.
    // Заменить false на true и осознать разницу!
    Console.WriteLine("Total Memory: {0}", GC.GetTotalMemory(false));
    }
    }
        
    class GCTestClass
    {
    private const long maxGarbage = 50;
    
    static void Main()
    {
    // Можно посмотреть максимальное количество поколений
    // (maximum number of generations),
    // которое в данный момент поддерживается
    // системой сборки мусора.
    Console.WriteLine("The highest generation is {0}", GC.MaxGeneration);
    
    // Создан объект - представитель класса GCTestClass. 
    GCTestClass gct = new GCTestClass();              
    
    // Начали забивать память всяким хламом.
    gct.MakeSomeGarbage();
    
    // Можно посмотреть, в каком "поколении" расположился объект.
    Console.WriteLine("gct is in Generation: {0}", GC.GetGeneration(gct));
                       
    // Активизировали сборщик мусора для объектов генерации 0.
    // Время начала сборки не определено.
    GC.Collect(0);
    // Смотрим, в каком поколении располагается данный объект. Переместился.
    Console.WriteLine("gct is in Generation: {0}", GC.GetGeneration(gct));
    
    // Perform a collection of all generations up to and including 2.
    // Сборка мусора во всех поколениях, включая второе.
    // Время начала сборки не определено.
    GC.Collect(2);
    
    // А в каком поколении gct сейчас? Ну надо же... опять переместился.
    Console.WriteLine("gct is in Generation: {0}", GC.GetGeneration(gct));
    // И как дела с памятью? 
    Console.WriteLine("Total Memory: {0}", GC.GetTotalMemory(false));
    Console.Read();
    }
    
    
    // Метод, который всего лишь забивает память всякой ненужной ерундой.
    void MakeSomeGarbage()
    {
    // В данном случае в качестве мусора используются объекты - 
    // представители двух замечательных классов... 
    //Version vt;
    //Object ob;
    MemElem mem;           
    for (int i = 0; i < maxGarbage; i++)
    {
    //vt = new Version();
    //ob = new Object();
    mem = new MemElem(i);
    Console.WriteLine("mem is in Generation: {0}", GC.GetGeneration(mem));
    }
    }
    }
    }

    Деструктор и метод Finalize

    Проблема освобождения неуправляемых ресурсов в .NET решается за счет применения метода Finalize, который наследуется любым классом от object. По умолчанию метод Object.Finalize

    void Finalize()...

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

    В .NET метод Finalize занимает особое место, и его использование регламентируется следующими ограничениями:

  • предполагается, что управление этому методу передается непосредственно сборщиком мусора при удалении данного объекта;
  • НЕ РЕКОМЕНДУЕТСЯ вызывать метод Finalize из методов, непосредственно не связанных с уничтожением объекта, тем более из методов других классов. Поэтому метод Finalize должен объявляться со спецификатором protected. При этом единственным корректным вызовом метода считается вызов метода Finalize для базового класса с использованием нотации base (...base.Finalize();...) ;
  • в методе Finalize должны освобождаться только те ресурсы, которыми владеет уничтожаемый объект.
  • Транслятор C# знает об особом статусе метода Finalize и о проблемах, связанных с его использованием. Соответствующее объявление метода в классе сопровождается предупреждением (warning), которое гласит:

    Introducing a 'Finalize' method can interfere with destructor invocation.
    Did you intend  to declare a destructor?

    То есть не лучше ли заменить этот метод деструктором? Дело в том, что для C# транслятора следующие строки кода являются эквивалентными:

    ~MyClass() // объявление деструктора.
    {
      // Здесь реализуются алгоритмы освобождения ресурсов.
    }
    
    
    protected override void Finalize() // Объявление метода финализации.
    {
      try
      {
        // Здесь реализуются алгоритмы освобождения ресурсов.
      }
      finally
      {
        base.Finalize(); // Вызов Finalize для базового класса.
      }
    }

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

    protected override void Finalize()...

    в том же классе воспринимается как ошибка.

    Таким образом, освобождение ресурсов в программе на C# возлагается на Finalize, точнее, на деструктор, который запускается непосредственно GC.

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

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

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