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, который теперь также гарантированно не будет содержать произвольных строк.

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

Вопросы и задания

  1. Что такое статические методы, статические поля?

  2. Верно ли говорить о статических классах или статических объектов? В чем их отличие (если верно) от неизменяемых классов и объектов?

  3. Статический метод не имеет доступ к полям объекта, но имеет ли он доступ к полям объекта того же класса, переданного в параметрах этого метода?

  4. В каком порядке следует использовать ключевые слова public/private и static?

  5. Сравните следующие поля класса:


public class SomeClass
{
  static int field1 = 1;
  const int field2 = 2;
  readonly int field3 = 3;
  static readonly int field4 = 4;
  int field5 = 5;
}
  1. Когда происходит выделение памяти, создаются ли экземпляры полей для каждого экземпляра объекта, возможно ли изменение значений полей? Какие еще есть отличия между ними?

  2. Статические поля и методы часто применяются для реализации объектов-одиночек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?

  1. * Объясните почему использование newCount++, вместо метода Interlocked.Increment может привести к ошибкам (неверном подсчету).

  2. * Изучите механизм перечислений в C# (enum).


Примечания:

  1. 35.  Отметим попутно, что шаблон «одиночка» в современной практике часто стремятся заменять на шаблон «инверсии управления».↩︎

О сайте, об авторе, контакты, оставить отзыв.

Телеграм-канал: newobjx.

© Тимофей Усов, 2019—2020.