Статический конструктор у класса может быть только один

Обновлено: 16.05.2024

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

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

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

В данном случае класс Person имеет два поля: age (хранит возраст человека) и retirementAge (хранит пенсионный возраст). Однако поле retirementAge является статическим. Оно относится не к конкретному человеку, а ко всем людям. (В данном случае для упрощения пренебрежем тем фактом, что в зависимости от пола и профессии пенсионный возраст может отличаться.) Таким образом, поле retirementAge относится не к отдельную объекту и хранит значение НЕ отдельного объекта класса Person, а относится ко всему классу Person и хранит общее значение для всего класса.

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

Но если мы хотим обратиться к этому полю вне своего класса, то мы можем обращаться к этому полю по имени класса:

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

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

Статические свойства

Подобным образом мы можем создавать и использовать статические свойства:

В данном случае доступ к статической переменной retirementAge опосредуется с помощью статического свойства RetirementAge .

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

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

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

Статические методы

Статические методы определяют общее для всех объектов поведение, которое не зависит от конкретного объекта. Для обращения к статическим методам также применяется имя класса / структуры:

В данном случае в классе Person определен статический метод CheckRetirementStatus() , который в качестве параметра принимает объект Person и проверяет его пенсионный статус.

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

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

Кроме обычных конструкторов у класса также могут быть статические конструкторы. Статические конструкторы имеют следующие отличительные черты:

Статические конструкторы не должны иметь модификатор доступа и не принимают параметров

Как и в статических методах, в статических конструкторах нельзя использовать ключевое слово this для ссылки на текущий объект класса и можно обращаться только к статическим членам класса

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

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

Определим статический конструктор:

В данном случае с помощью встроенной структуры DateTime получаем текущий год. Для этого используется свойство DateTime.Now.Year . если он равен 2022, устанавливаем один пенсионный возраст. При другом значении текущего года устанавливается другое значение пенсионного возраста.

Статические классы

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

Статический класс в основном такой же, как и нестатический класс, но имеется одно отличие: нельзя создавать экземпляры статического класса. Другими словами, нельзя использовать оператор new для создания переменной типа класса. Поскольку нет переменной экземпляра, доступ к членам статического класса осуществляется с использованием самого имени класса. Например, если есть статический класс, называемый UtilityClass , имеющий открытый статический метод с именем MethodA , вызов метода выполняется, как показано в следующем примере:

Ниже приведены основные возможности статического класса.

Содержит только статические члены.

Создавать его экземпляры нельзя.

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

Статические классы запечатаны, поэтому их нельзя наследовать. Они не могут наследовать ни от каких классов, кроме Object. Статические классы не могут содержать конструктор экземпляров. Однако они могут содержать статический конструктор. Нестатические классы также должен определять статический конструктор, если класс содержит статические члены, для которых нужна нетривиальная инициализация. Дополнительные сведения см. в разделе Статические конструкторы.

Пример

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

Статический члены

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

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

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

Несмотря на то, что поле не может быть объявлено как static const , поле const по своему поведению является статическим. Он относится к типу, а не к экземплярам типа. Поэтому к полям const можно обращаться с использованием той же нотации ClassName.MemberName , что и для статических полей. Экземпляр объекта не требуется.

Для объявления статических методов класса используется ключевое слово static перед возвращаемым типом члена, как показано в следующем примере:

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

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

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

Общая концепция
  • полям
  • свойствам
  • методам
  • операторам
  • событиям
  • конструктору
  • классам

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

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

  • Нельзя создавать экземпляр класса, используя ключевое слово new.
  • Не разрешается использовать не статические члены этого же класса.
  • Он не поддерживает наследование.
  • Невозможно перегрузить методы.
  • Не разрешается использовать не статические члены этого же класса из статических. Конечно же, вам никто не мешает создать экземпляр класса в статическом методе.
  • Наследование и полиморфизм для статических членов не поддерживаются.
Больше деталей

Выше мы не рассматривали такую конструкцию, как статический конструктор. Один из достаточно интересных вопросов, на мой взгляд, когда происходит вызов статического конструктор у классов?

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


Можно поиграться, закомментировав любую из строк в которых происходит обращение к классу Box. Теперь немного изменим код, и подправим наше утверждение

Общие рассуждения об использовании статики

Существует относительно много мнений, когда использовать статические классы и когда не стоит так поступать. Исходя из своего опыта, отмечу, статические классы — любимое оружие начинающих разработчиков. Попользовал и забыл — хорошая концепция.
Чтобы разобраться в хитросплетениях о применяемости статики, следует вернуться к понятиям ООП. Представьте что у вас есть велосипед, но велосипед есть так же и у вашего соседа, и у соседа соседа, и т.д. В данном случае статика неприемлема. Т.к. велосипеды могут быть разного цвета, веса, обладать разным количеством колёс. То-бишь различные экземпляры одного и того же вида. Статика же применима для каких-то глобальных объектов\действий, когда не подразумевается создание экземпляров класса(часто для каких-то служебных методов: вывод на консоль — Console.WriteLine(), сортировка массива Array.Sort). Зачастую классы могут предоставлять как статическую, так и не статическую функциональность. Когда же у вас возникают сомнения, остановитесь и подумайте, понадобится ли вам экземпляр “этого”. Если же вы так хотите контролировать создание экземпляров класса или же вообще иметь только один, то для этих целей замечательно пригодится паттерн Singleton. В рамках ООП статика обладает рядом недостатков. Чем же она так плоха?

Полиморфизм

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

Тестирование

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

Единственная ответственность


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

Давайте рассмотрим одну из наиболее простых и популярных реализаций паттерна Синглтон (*), основанную на инициализаторе статического поля:

public sealed class Singleton
private static readonly Singleton instance = new Singleton();

public static Singleton Instance < get < return instance; >>
>

* This source code was highlighted with Source Code Highlighter .

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

Статический конструктор и инициализаторы полей

Итак, давайте рассмотрим следующий код:

public static string S = Echo( "Field initializer" );

public static string Echo( string s)
Console .WriteLine(s);
return s;
>
>

static void Main( string [] args)
Console .WriteLine( "Starting Main. " );
if (args.Length == 1)
Console .WriteLine(Singleton.S);
>
Console .ReadLine();
>
>

* This source code was highlighted with Source Code Highlighter .

Как видно, что статическое поле будет проинициализировано, хотя сам тип в приложении не используется. Практика показывает, что в большинстве случаев при отсутствии явного конструктора, JIT-компилятор вызывает инициализатор статических переменных непосредственно перед вызовом метода, в котором используется эта переменная. Если раскомментировать статический конструктор класса Singleton, то поведение будет именно таким, которое ожидает большинство разработчиков – инициализатор поля вызван не будет и при запуске приложения на экране будет только одна строка: Starting Main…”.

Статические конструкторы и взаимоблокировка

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

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

class Program
static Program()
var thread = new Thread(o => < >);
thread.Start();
thread.Join();
>

static void Main()
// Этот метод никогда не начнет выполняться,
// поскольку дедлок произойдет в статическом
// конструкторе класса Program
>
>

* This source code was highlighted with Source Code Highlighter .

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

Бага в реальном приложении

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

Итак, вот симптомы реальной проблемы, с которой я столкнулся. У нас есть сервис, который прекрасно работает в консольном режиме, а также не менее прекрасно работает в виде сервиса, если собрать его в Debug-е. Однако если собрать его в релизе, то он запускается через раз: один раз запускается успешно, а во второй раз запуск падает по тайм-ауту (по умолчанию SCM прибивает процесс, если сервис не запустился за 30 секунд).

В результате отладки было найдено следующее. (1) У нас есть класс сервиса, в конструкторе которого происходит создание счетчиков производительности; (2) класс сервиса реализован в виде синглтона с помощью инициализации статического поля без явного статического конструктора, и (3) этот синглтон использовался напрямую в методе Main для запуска сервиса в консольном режиме:

// Класс сервиса
partial class Service : ServiceBase
// "Кривоватая" реализаци Синглтона. Нет статического конструктора
public static readonly Service instance = new Service();
public static Service Instance < get < return instance; >>

public Service()
InitializeComponent();

// В конструкторе инициализирутся счетчики производительности
var counters = new CounterCreationDataCollection();

if (PerformanceCounterCategory.Exists(category))
PerformanceCounterCategory.Delete(category);

PerformanceCounterCategory.Create(category, description,
PerformanceCounterCategoryType.SingleInstance, counters);
>

// Метод запуска сервиса
public void Start()
<>

>

* This source code was highlighted with Source Code Highlighter .

Заключение

Дополнительные ссылки

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

(***) Если интересно, какие такие проблемы таятся в изменяемых значимых типах, то вполне подойдет предыдущая заметка «О вреде изменяемых значимых типов», ну а если интересно, что же такого плохого в вызове Thread.Abort, то тут есть даже две заметки: «О вреде вызова Thread.Abort», а также перевод интересной статьи Криса Селлза «Изучение ThreadAbortExcpetion с помощью Rotor».


Всем привет. На одном из код-ревью я столкнулся с мыслью, что многие, а чего скрывать и я сам, не то чтобы хорошо понимаем когда нужно использовать ключевое слова static. В данной статье я хотел бы поделиться своими знаниями и информацией по поводу ключевого слова static. Статья будет полезна как начинающим программистам, так и людям, работающим с языком С++. Для понимания статьи у вас должны быть знания о процессе сборки проектов и владение языком С/С++ на базовом уровне. Кстати, static используется не только в С++, но и в С. В этой статье я буду говорить о С++, но имейте в виду, что всё то, что не связано с объектами и классами, в основном применимо и к языку С.

Что такое static?

Static - это ключевое слово в C++, используемое для придания элементу особых характеристик. Для статических элементов выделение памяти происходит только один раз и существуют эти элементы до завершения программы. Хранятся все эти элементы не в heap и не на stack, а в специальных сегментах памяти, которые называются .data и .bss (зависит от того инициализированы статические данные или нет). На картинке ниже показан типичный макет программной памяти.


Где используется?

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


А теперь я постараюсь детально описать все то, что изображено на схеме. Поехали!

Статические переменные внутри функции

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

Если не использовать static в строке 4, выделение памяти и инициализация переменной count происходит при каждом вызове функции counter(), и уничтожается каждый раз, когда функция завершается. Но если мы сделаем переменную статической, после инициализации (при первом вызове функции counter()) область видимости count будет до конца функции main(), и переменная будет хранить свое значение между вызовами функции counter().

Статические объекты класса

Статический объект класса имеет такие же свойства как и обычная статическая переменная, описанная выше, т.е. хранится в .data или .bss сегменте памяти, создается на старте и уничтожается при завершении программы, и инициализируется только один раз. Инициализация объекта происходит, как и обычно — через конструктор класса. Рассмотрим пример со статическим объектом класса.

В строке 3 мы создаем класс Base с конструктором (строка 5) и деструктором (строка 8). При вызове конструктора либо деструктора мы выводим название метода класса в консоль. В строке 14 мы создаем статический объект obj класса Base. Создание этого статического объекта будет происходить только при первом вызове функции foo() в строке 18.

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


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

Статические члены класса

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


В нашем примере мы создали класс А (строка 3) и класс В (строка 9) со статическими членами класса (строка 15). Мы предполагаем, что при создании объекта b в строке 19 будет создан объект a в строке 15. Так бы и произошло, если бы мы использовали нестатические члены класса. Но вывод программы будет следующим:

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


Теперь, после того как мы определили наш статический член класса в строке 18, мы можем увидеть следующий результат программы:

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

Constructor A
Constructor B1
Constructor B2
Constructor B3
Destructor B3
Destructor B2
Destructor B1
Destructor A

Статические функции

Статические функции пришли в С++ из С. По умолчанию все функции в С глобальные и, если вы захотите создать две функции с одинаковым именем в двух разных .c(.cpp) файлах одного проекта, то получите ошибку о том, что данная функция уже определена (fatal error LNK1169: one or more multiply defined symbols found). Ниже приведен листинг трех файлов одной программы.

В этом случае вы говорите компилятору, что доступ к статическим функциям ограничен файлом, в котором они объявлены. И он имеет доступ только к функции sum() из math.cpp файла. Таким образом, используя static для функции, мы можем ограничить область видимости этой функции, и данная функция не будет видна в других файлах, если, конечно, это не заголовочный файл (.h).

Статические функции-члены класса (методы)

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

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


В классе A в строке 8 у нас есть статическая функция-член foo(). В строке 14, мы вызываем функцию используя имя класса и оператор разрешения области видимости и получаем следующий результат программы:

Из вывода видно, что никакого создания объекта нет и конструктор/деструктор не вызывается.

Если бы метод foo() был бы нестатическим, то компилятор выдал бы ошибку на выражение в строке 14, т.к. нужно создать объект для того, чтобы получить доступ к его нестатическим методам.

Заключение

В одной статье в интернете я нашел совет от автора – «Используйте static везде, где только можно». Я хотел бы написать, почему так делать не стоит, а стоит использовать только в случае необходимости.

Читайте также: