2.5. Статические методы и поля
§ 24. Статические методы. Иногда методу класса не требуется доступ к состоянию объекта. Так, в рассмотренном в главе 2.1 примере расчета площади треугольника, метод расчета SquareGeron не использует поля класса и, соответственно, доступ к переменной this ему не нужен. То есть метод имеет доступ к заведомо не нужным для его работы данным. Чтобы ограничить доступ такого рода методов к полям объекта используют статические методы.
Статический метод (static) класса – метод, не имеющий доступа к состоянию (полям) объекта, то есть к переменной this.
Для объявления статического метода используется ключевое слово static:
private static float SquareGeron (float a, float b, float c) { /* … */ }
Статический метод может быть вызван как через экземпляр класса, так и через имя класса. Например, из методов класса Triangle мы можем обратиться к статическому методу SquareGeron следующими способами:
class Triangle
{
public void SomeMethod ()
{
// 1 - Неявно через объект, то есть через переменную this.
SquareGeron (1,2,2);
// 2 - Явно через объект, то есть через переменную this.
this.SquareGeron (1,2,2);
// 3 - Через имя класса, без использования объекта.
Square.SquareGeron (1,2,2);
}
}
Аналогично, извне класса, при условии, что мы сделаем метод SquareGeron открытым:
Square sq = new Square();
// 2 - Через объект.
sq.SquareGeron (1,2,2);
// 3 - Через имя класса.
Square.SquareGeron(1,2,3);
Подчеркнем, что во всех перечисленных случаях метод не имеет доступа к переменной this, то есть даже при вызове через экземпляр, ему не передается, как для обычных методов, параметр this, он не знает, какой именно объект его вызвал и, соответственно, у него нет доступа к полям объекта. Также, как следствие, из статического метода нельзя вызвать нестатический метод того же класса для вызванного объекта. При этом, статический метод, как и любой другой метод класса, имеет доступ к полям и методам объектов этого класса независимо от их уровня видимости:
class Vector
{
private float x;
private float y;
public Vector()
{
}
private static float Length (float x0, float y0, float x1, float у1)
{
/* ... */
}
public float Length ()
{
// Из нестатического метода можно вызывать статический метод.
return Length (0, 0, x, y);
}
public static Vector Sum (Vector a, Vector b)
{
Vector sum = new Vector();
// Статические метод, как и нестатический,
// имеет доступ к полям, в том числе закрытым (private)
// объектов того же класса:
sum.x = a.x + b.x;
sum.y = a.y + b.y;
return sum;
}
}
Слово «статический» используется в том смысле, что статические методы не относятся к динамике объекта, не используют и не меняют его состояния.
Вспомним, что мы уже использовали статический метод для вычисления квадратного корня Math.Sqrt. Класс System.Math реализует в виде статических методов и другие математические функции. Однако есть множество причин, почему в большинстве случаев следует избегать использования статических методов.
Программисты, не имеющие опыта ООП, часто начинают широко использовать статические методы как способ программировать на объектно-ориентированном языке в процедурном стиле. Действительно, для статических методов их класс – лишь способ синтаксической группировки. Более того, использование статических методов – это всегда в некотором смысле отход от ООП, так как он делает невозможным использование всех ключевых элементов объектно-ориентированного программирования: абстрактных типов данных, наследования, полиморфизма. Сформулируем следующее правило: в первом приближении статическими следует делать только 1) небольшие 2) вспомогательные 3) закрытые (private) методы класса. Практически всегда методы, не удовлетворяющие приведенному правилу и не обращающиеся к полям объекта, можно и нужно вынести в отдельный класс. Например, если бы метод SquareGeron был большим, то следовало бы создать класс SquareGeronCalculator и создать там открытый метод Calc.
§ 25. Статические поля. Аналогично тому, как статический метод не привязан к объекту, мы можем объявить статическое поле, не являющееся частью никакого объекта, которое будет создаваться в одном экземпляре и будет доступно из любого объекта класса или через имя класса.
Например, следующий код считает число объектов типа Point, созданных с момента запуска приложения:
class Point
{
private static long newCount;
public Point()
{
// Эквивалентно newCount++, но корректно работает в многопоточном окружении.
Interlocked.Increment(ref newCount);
}
}
Экземпляр статической переменной создается автоматически до первого ее использования (когда именно – не регламентируется), а при создании экземпляров класса память для статических переменных не выделяется. Таким образом, в приведенном примере в некоторый момент времени после запуска приложения и до вызова команды увеличения значения newCount будет создан ровно один экземпляр этой переменной, а каждый создаваемый объект Point будет увеличивать ее значение в своем конструкторе.
Применительно к статическим полям также можно сформулировать правило: в первом приближении следует избегать использовать статические полей.
Причин тут много, в частности, создаются неявные связи между объектами одного класса. Также статические переменные не обрабатываются сборщиком мусора и существуют до закрытия программы, поэтому в крупных приложениях статические поля могут «связать» ощутимый объем памяти, освобождаемый только при закрытии программы.
Тем не менее, статические поля, как и статические методы, используются достаточно широко. Рассмотрим следующий типичный пример.
Положим, мы хотим реализовать авторизацию пользователей, то есть управлять доступностью функциональных возможностей приложения в зависимости от роли текущего пользователя. Для этого в приложении фиксируется перечень возможностей (features) и при вызове соответствующих методов, выполняется проверка, разрешен ли текущему пользователю доступ к запрашиваемой функциональной возможности. В первом приближении мы могли бы задать уникальное название для каждой функциональной возможности и привязывать к текущему пользователю перечень строк – список разрешенных возможностей. Тогда авторизация могла бы выглядеть следующим образом:
public class ACL // access control list
{
// Список возможностей, доступных текущему пользователю.
private string[] currentUserAllowedFeatures;
public void Authorize (string feature)
{
// Если у пользователь нет запрашиваемой возможности…
if (!currentUserAllowedFeatures.Contains (feature))
// …формируем исключение.
throw new Exception ();
}
}
// Использование:
public class SomeClass
{
private ACL acl;
// Объект acl создается и инициализируется
// где-то при запуске программы.
public SomeClass (ACL acl)
{
this.acl = acl;
}
public void DoSomeJob ()
{
// Сначала проверяем, если ли у текущего пользователя доступ
// к указанной возможноти (ADMINISTRATOR).
// Если доступа нет, исключение прервет выполнение метода.
acl.Authorize (“ADMINISTRATOR”);
// ...
}
}
Проблема этого решения в том, что легко допустить ошибку в написании названия функциональной возможности. Однако мы можем перечислить допустимые названия как открытые неизменяемые статические поля некоторого класса:
public Feature {
public static readonly string Administrator = “ADMINISTRATOR”;
public static readonly string Guest = “GUEST”;
// ...
}
// Фрагмент из предыдущего листинга:
public void DoSomeJob ()
{
acl.Authorize (Feature.Administrator);
}
Такое решение лучше, но оно все еще позволяет нам передать в метод Authorize произвольную строку, не используя класс Feature. Проанализируйте следующий код:
public Feature {
private string code;
private Feature (string code)
{
this.code = code;
}
public string GetCode()
{
return code;
}
public static readonly Feature Administrator = new Feature (“ADMINISTRATOR);
public static readonly Feature Guest = new Feature (“GUEST”);
// ...
}
//class ACL
public void Authorize (Feature feature)
{
if (!currentUserAllowedFeatures.Contains (feature.GetCode()))
throw new Exception ();
}
// class SomeClass
public void DoSomeJob ()
{
acl.Authorize (Feature.Administrator);
}
Теперь экземпляры класса Feature представляют отдельные функциональные возможности, но так как единственный конструктор этого класса объявлен как закрытый (private), то они не могут быть созданы извне класса. В статических открытых полях сохраняем фиксированный перечень экземпляров этого же класса. Таким образом, в метод Authorize мы передаем объект типа Feature, но не можем создать его самостоятельно, а используем перечень фиксированных, «зашитых» в классе Feature объектов.
Мы можем пойти еще дальше, сохраняя в классе ACL не массив строк, а массив объектов Features:
class ACL
{
private Feature[] currentUserAllowedFeatures;
public void Authorize (Feature feature)
{
for (int i = 0; i < currentUserAllowedFeatures.Length; i++)
{
if (currentUserAllowedFeatures[i].GetCode() == feature.GetCode())
return;
}
throw new Exception ();
}
}
Такое решение делает безопасным не только передачу параметра в Authorize, но и формирование списка currentUserAllowedFeatures, который теперь также гарантированно не будет содержать произвольных строк.
В заключение, повторимся, что хотя механизм статических полей и методов широко применяется в современной практике объектно-ориентированного программирования, использовать эти возможности следует крайне осмотрительно. Дополнительные аргументы против статических методов и полей мы рассмотрим в последующих главах.
Вопросы и задания
Что такое статические методы, статические поля?
Верно ли говорить о статических классах или статических объектов? В чем их отличие (если верно) от неизменяемых классов и объектов?
Статический метод не имеет доступ к полям объекта, но имеет ли он доступ к полям объекта того же класса, переданного в параметрах этого метода?
В каком порядке следует использовать ключевые слова public/private и static?
Сравните следующие поля класса:
public class SomeClass
{
static int field1 = 1;
const int field2 = 2;
readonly int field3 = 3;
static readonly int field4 = 4;
int field5 = 5;
}
Когда происходит выделение памяти, создаются ли экземпляры полей для каждого экземпляра объекта, возможно ли изменение значений полей? Какие еще есть отличия между ними?
Статические поля и методы часто применяются для реализации объектов-одиночек35. Объект-одиночка (singleton) – объект, который должен существовать в программе в одном экземпляре. К примеру, это может быть объект, хранящий глобальные параметры приложения. Разберите следующие реализации:
// 1 вариант
public class Settings
{
public string TempWorkspacePath() { /* ... */ }
public static Settings Singleton = new Settings();
}
// Использование:
string path = Settings.Singleton.TempWorkspacePath();
// 2 вариант
public class Settings
{
public string TempWorkspacePath() { /* ... */ }
private static Settings singleton;
private Settings ()
{
singleton = new Settings();
}
public Settings Get ()
{
if (singleton == null)
singleton = new Settings();
return singleton;
}
}
// Использование:
string path = Settings.Get().TempWorkspacePath();
Объясните, почему вторая реализация лучше? Почему во втором варианте используется закрытый (private) конструктор? Почему не используется ключевое слово readonly для поля singleton?
* Объясните почему использование newCount++, вместо метода Interlocked.Increment может привести к ошибкам (неверном подсчету).
* Изучите механизм перечислений в C# (enum).
Примечания:
35. Отметим попутно, что шаблон «одиночка» в современной практике часто стремятся заменять на шаблон «инверсии управления».↩︎