Найти тему

Ключевое слово static. Когда его использовать, а когда нет

Оглавление

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

Я уже писал про статические методы, но теперь подробно про всю статику.

Введение

Давайте сначала разберёмся, что такое статические члены (static members) класса, и чем они отличаются от нестатических (далее — экземплярных).

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

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

Обращения к полям и методам происходит аналогично.

Жизненный цикл объекта

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

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

  • Окно View
  • ViewModel
  • Метод, назначающий значения параметрам
  • Guid общего параметра
  • Счётчик цикла i, в котором мы проходимся по элементам
  • Переменная element, отображающая текущий элемент цикла

И давайте подумаем, какой у них будет жизненный цикл:

  • Окно будет разным для каждого запуска команды (вдруг у нас поменялись категории), но единым в пределах одного запуска.
  • ViewModel отражает окно, так что тоже разная для каждого запуска
  • Метод, назначающий значения, всегда один и тот же. Он не существует как конкретный объект, но не меняется со временем
  • Guid общего параметра всегда одинаковый, для всех запусков команды и для всех сеансов Revit
  • Счётчик цикла i существует внутри метода, его жизненный цикл ограничен вызовом метода
  • Переменная element существует внутри одной конкретной итерации цикла

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

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

Ключевые слова static, readonly и const и их разница.

Для неизменяемых величин мы можем использовать модификаторы static, const и readonly. Давайте посмотрим, в чём разница.


const — константа этапа компиляции. По сути, везде, где она есть в коде, она будет заменена на своё значение при сборке проекта. Код, где мы пытаемся изменить константу, не скомпилируется. При этом константа не занимает места в памяти, она по сути влияет только на размер скомпилированного кода.

Константа C# скомпилировалась в её значение в IL-коде
Константа C# скомпилировалась в её значение в IL-коде

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

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

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

Из примера выше мы можем хранить Guid guid1 как const string для экономии памяти, а при обращении к ней создавать новый Guid из строки. А метод мы можем сделать статическим (методы сами по себе не расходуют память, так что тут это не так важно, но поскольку он не меняется и не обращается к изменяемым полям (мы передаём ему изменяемые поля как аргументы), почему бы не сделать его статическим?

Когда следует использовать статические поля и свойства в плагинах для Revit?

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

Примеры правильного использования:

1. В своём коде я часто использую статический класс RevitAPI. По сути, я взял идею из шаблона Романа Карповича (где у него этот класс теперь называется Context. В чём суть: ссылки на активный документ, UiDocument, Application, ActiveView (а у Романа теперь ещё и на то, находимся ли мы в контексте Ревита или нет):

https://github.com/Nice3point/RevitToolkit/blob/main/source/Nice3point.Revit.Toolkit/Context.cs
https://github.com/Nice3point/RevitToolkit/blob/main/source/Nice3point.Revit.Toolkit/Context.cs

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

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

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

Тут есть важный нюанс: я не пишу, что мы должны делать именно так, я пишу, что мы можем так сделать. А можем сделать иначе и сделать все данные свойства экземплярными. Да, может быть будет неудобно прокидывать документ из класса в класс, но по сути это будет просто ссылка на один и тот же документ. Хэндлер можно создать перед использованием, а гуиды создавать из констант, как я описал выше, и всё это будет правильно (но не всегда удобно, и не всегда оптимально). Лично я использую статики из первых двух случаев, а в третьем предпочитаю константы и новые гуиды.

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

Примеры неправильного использования

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

Тут прям всё неправильно. Во-первых, значение останется в памяти между запусками команды. Во-вторых, при повторном запуске что-то может пойти не так, значение не перезапишется, но вместо NullReferenceException мы получим использование значение из прошлого запуска (оно сохраняется между командами). Результат может быть любым, абсолютно неожиданным.

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

2. Ревит-команда записывает результаты своей работы в статический список.

А потом при последующем запуске список не пустой, а со значениями. И ладно она неправильный отчёт выведет: иногда на этот список завязана логика. И плагин у пользователя 3 раза сработает, а потом не сработает. А у вас в addin-менеджере всё будет работать, и вы не будете понимать причину бага.

Повторюсь:

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

3. Вы где-то услышали, что Модель для бизнес-логики должна быть static и содержать только методы по работе с данным и не зависеть от View и ViewModel. Но вы не знаете, как передать в static класс Document, не создавая экземпляр класса, и создаёте статическое свойство для него.

В принципе, да, Модель для бизнес-логики должна содержать только методы по работе с данным и не зависеть от View и ViewModel, но она не должна быть статической (но может, об этом ниже). А документ мы берём из класса Context или передаём в метод как аргумент, а не создаём 100 моделей со 100 статическими свойствами Document.

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

Когда и зачем следует использовать статические методы

Я решил изучить вопрос, чем отличаются статические методы от нестатических с точки зрения скомпилированного кода. И я не нашёл разницы, оба типа методов при одинаковом коде компилируются одинаково. Хотя я встретил в интернете мнение, что в экземплярный метод неявно передаётся this, из-за чего он работает чуть медленнее, я не могу это подтвердить после своих экспериментов с IL-кодом. Поэтому давайте предположим, что с точки зрения скомпилированного кода, между статическим и нестатическим методом разницы нет (а если и есть, то едва заметная).

С точки зрения использования тут есть 2 разницы:

1. Экземплярные методы могут обращаться ко всем членам класса, статические — только к статическим (константы считаются статическими).

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

И нужно обратить внимание на ещё один важный нюанс:

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

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

Статическая реализация:

Math.Sin() // не создаём объект, не выделяем память, просто считаем

Экземплярная реализация (выдуманная):

var math = new Math(); // создали объект, выделили память
math.Sin(); // посчитали

Как видите, результат лучше в первом случае — нет выделения лишней памяти и нет излишних затрат времени на это. Издержки — нет доступа к экземплярным полям. Впрочем, класс Math статический, поэтому его экземпляр создать нельзя при всём желании, а экземплярных полей у него нет.

Когда стоит создавать статические методы

1. Для статических классов типа Utils. Например, мы часто используем методы, которые взаимодействуют с путями к файлам. Мы создали статический класс PathUtils, определили нам нужную реализацию, и вызываем эти методы без создания объекта. Точно также можно вынести какие-либо повторяющиеся методы взаимодействия с Ревитом. Вынесли в отдельный класс (а иногда и в отдельный проект Common) и переиспользуем. Переиспользуем в качестве функций, который принимают аргументы, выдают результат, и не меняют состояние своего класса-списка таких функций.

2. Для создания статической модели с вынесенной бизнес-логикой.

Почему бы и нет. Допустим, у нас есть та же задача из начала статьи по назначению параметра всем элементам категории. Создаём статический класс ParameterSetter, внутри него метод статический метод Set(BuiltInCategory category). Во ViewModel у нас будет команда (экземплярная, потому что статическую команду делать не стоит), которая вызовет ParameterSetter.Set(SelectedCategory) без создания объекта.

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

var setter = new ParameterSetter();
setter.Set(SelectedCategory);

Такое решение тоже правильное.

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

3. Если ваша IDE (например, JetBrains Rider) предлагает сделать вам метод статическим, то почему бы и не сделать. В принципе, разницы особой не будет (если вы в итоге не сведёте свой класс к статическому классу, тогда не надо будет создавать объект), но хуже вы точно не сделаете. Может быть, потом вам потребуется переиспользовать метод, и тогда вы вынесете его в какой-нибудь Utils-класс.

Заключение

Конечно, при всём желании данную тему невозможно осветить на 100 процентов. Я ещё не рассказал про некоторые минусы статических классов (их нельзя наследовать, поэтому нельзя переопределить их логику), но я старался рассмотреть данный вопрос с точки зрения здравого смысла (не надо использовать статические свойства везде, где захотелось), быстродействия и выделения памяти.
Если у вас есть что добавить по этой теме, смело делитесь мыслями в комментариях! И не забывайте подписываться на
мой телеграм-канал о Revit API, на меня на GitHub, ставить лайки статьям и звёздочки репозиториям. До новых встреч!

-4