вторник, 28 июня 2011 г.

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



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

Статический конструктор нельзя вызывать напрямую. Если обычный конструктор вызывается в момент создания экземпляра класса, то статический – вызывается автоматически перед созданием первого экземпляра класса или перед использованием ссылки на какие-либо статические члены. Инициализация статических переменных в коде C# компилируется CLR в статический конструктор, различие его от конструктора, заданного в явном виде лишь в одном атрибуте MS IL, как раз и определяющего логику вызова (отложено/при обращении). 

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

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

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

Чтобы объявить статический конструктор, достаточно просто добавить модификатор static перед объявлением конструктора. Статический конструктор не может иметь параметров.
class MyClass {
   static MyClass() {
      //...
   }
}
Для деструкторов никаких статических аналогов не существует.

Теперь, когда нам известна теория, попробуем сделать задание. Рассмотрим следующий код и попробуем ответить на следующие вопросы: Что произойдет при запуске программы?
           1. Ошибка компиляции. В каком месте?
           2. Ошибка выполнения. В каком месте? Что успеет вывестись на консоль?
           3. Ошибок не будет. Что будет выведено на консоль?

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.Threading;
using System.Runtime.InteropServices;

namespace Tests
{
    class Program
    {
        #region StaticConstructor&Inherit

        public interface IWritable
        {
            string Write();
        }

        public class A : IWritable
        {
            public static int intVarA;

            static A()
            {
                intVarA = C.intVarC + B.intVarB + 5;
            }

            public virtual string Write()
            {
                return "A.Write()";
            }
        }

        public class B : A, IWritable
        {
            public static int intVarB;

            static B()
            {
                intVarB = C.intVarC + A.intVarA + 5;
            }

            public override string Write()
            {
                return "B.Write()";
            }
        }

        public class C : B, IWritable
        {
            public static int intVarC;

            static C()
            {
                intVarC = B.intVarB + A.intVarA + 5;
            }

            public new string Write()
            {
                return "C.Write()";
            }
        }

        #endregion



        static void Main(string[] args)
        {

            #region StaticConstructor&Inherit

            Console.WriteLine(A.intVarA);
            Console.WriteLine(B.intVarB);
            Console.WriteLine(C.intVarC);
            Console.WriteLine();

            #endregion

            Console.ReadLine();
        }
    }
}

Ответ: На консоли будет выведено три числа: 20, 5, 10.

Почему вывод в консоли будет именно такой, давайте разберёмся. Сначала когда вызывается строчка Console.WriteLine(A.intVarA); в консоль должно вывестись значение статической переменной A.intVarA, по умолчанию переменные типа int инициализируются нулём но в классе A присутствует статический конструктор, который как мы уже знаем будет запущен до обращения к любому члену класса A.
            static A()
            {
                intVarA = C.intVarC + B.intVarB + 5;
            }

В этом конструкторе используются статические переменные C.intVarC и B.intVarB. Классы B и С также имеют статические конструкторы и мы заходим в них до инициализации статических переменных. Сначала в конструктор класса С.

            static C()
            {
                intVarC = B.intVarB + A.intVarA + 5;
            }

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

            static B()
            {
                intVarB = C.intVarC + A.intVarA + 5;
            }

Здесь мы опять видим переменные C.intVarC и A.intVarA, похоже на зацикливание. J НО как мы помним, статический конструктор вызывается только один раз! А у  нас в программе все статические конструкторы уже были вызваны, поэтому больше вызываться статические конструкторы не будут. Вспомним чему у нас равна переменная C.intVarC= B.intVarB + A.intVarA + 5 = 0 + 0 = 5. Т.к. по умолчанию int инициализируются нулём. Аналогично можно вычислить intVarC = B.intVarB + A.intVarA + 5 = 5 + 0 + 5 = 10. Осталось вычислить intVarA = C.intVarC + B.intVarB + 5 = 10 + 5 + 5 = 20. Теперь, я надеюсь, всё понятно, почему программа вывела такие числа на консоль. Если объяснение не очень понятно, то попробуете подебажить программу по шагам и всё поймёте.

Надеюсь, статья была познавательной и интересной для вас. Следующая статья будет про наследование в C#.


4 комментария:

  1. Сначала же заходим в конструктор класса B, получается intVarB = C.intVarC + A.intVarA + 5 = 0 + 0 + 5 = 5, далее идем в конструктор класса С, intVarC = B.intVarB + A.intVarA + 5 = 0 + 5 + 5 = 10. Ну и в последний заходим в А, intVarA = C.intVarC + B.intVarB + 5 = 10 + 5 + 5 = 20, и получаем логичный ответ 20, 5, 10.

    ОтветитьУдалить
    Ответы
    1. Суть примера в том, чтобы понять что статический конструктор вызывается только один раз до обращения к любому члену класса. Поэтому не будет ошибки или зацикливания.

      Удалить
  2. Этот комментарий был удален автором.

    ОтветитьУдалить
  3. В теории суть ясна, но осознание пришло на идеальном примере.

    ОтветитьУдалить