Программирование PC Ключевое слово static на языке C# Fri, April 19 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

Ключевое слово static на языке C# Печать
Добавил(а) microsin   

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

Csharp static analogy

В памяти компьютера есть множество отделенных друг от друга мест, что делает похожей эту память на солнечную систему. В пространстве памяти объект static (как и планета Земля в нашей галактике) является исключительным. На языке C# ключевое слово static используется при объявлении методов, переменных и классов. Static обозначает сущности, которые не могут быть повторены. Они не являются частью какого-либо экземпляра класса. Static часто увеличивает производительность программ, но делает их менее гибкими.

В следующем примере программа показывает статический класс, статическое поле и статический метод. Это показано в их синтаксисе. Функция Main() это специальный случай статического метода.

// Программа на C#, использующая ключевое слово static (.NET 2017)
using System;
 
static class Perls
{
   public static int _value = 5;
}
 
class Program
{
   static void Main()
   {
      // Программа выведет в консоль число 6.
      Console.WriteLine(++Perls._value);
   }
}

Вывод:

6

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

Класс: в статическом классе все поля и методы также должны быть статическими. Это полезное ограничение.

Следующий пример показывает статические методы. Они вызываются через имя типа. При этом не требуется использовать экземпляр, что немного ускоряет вызов. Статические методы могут быть public или private. Когда статические методы используют ключевое слово static, оно обычно идет первым, или вторым после ключевого слова public.

Предупреждение: статические методы не могут получить доступ к не статическим членам уровня класса. У этого нет указателя "this".

Экземпляр (instance): метод экземпляра может получить доступ к статическим членам, но он должен быть вызван через инстанцированный объект. Это добавляет косвенное обращение (indirection).

// Программа C#, которая использует экземпляр и статические методы.
using System;
 
class Program
{
    static void MethodA()
    {
        Console.WriteLine("Static method");
    }
 
    void MethodB()
    {
        Console.WriteLine("Instance method");
    }
 
    static char MethodC()
    {
        Console.WriteLine("Static method");
        return 'C';
    }
 
    char MethodD()
    {
        Console.WriteLine("Instance method");
        return 'D';
    }
 
    static void Main()
    {
        // Вызов 2 статических методов типа Program.
        Program.MethodA();
        Console.WriteLine(Program.MethodC());
 
        // Создание нового экземпляра класса Program, и вызов двух
        // методов этого экземпляра.
        Program programInstance = new Program();
        programInstance.MethodB();
        Console.WriteLine(programInstance.MethodD());
    }
}

Вывод:

Static method
Static method
C
Instance method
Instance method
D

Public, private. Утилитарные (вспомогательные) классы часто содержат публичные статические методы. В результате не теряется производительность. Эти методы доступны в любом месте программы.

Член класса, определенный с ключевым словом public, может вызываться из других мест программы, определенных вне этого класса, т. е. из других классов, других файлов. Это не доступ по умолчанию для декларации членов, классов и методов (по умолчанию доступ private). Мы часто используем ключевое слово public вместе с ключевым словом static - публичные методы могут быть статическими, и это означает, что они подсоединены к самому типу.

// Пример программы C#, использующей методы public.
using System;
 
public class Example
{
   static int _fieldStatic = 1;  // частное (private), статическое поле класса
   int _fieldInstance = 2;       // частное поле, которое будет в экземплярах класса
 
   public static void DoStatic()
   {
      // Тело публичного статического метода.
      Console.WriteLine("DoAll called");
   }
   public static int SelectStatic()
   {
      // Публичный статический метод, возвращающий значение.
      return _fieldStatic * 2;
   }
   public void DoInstance()
   {
      // Тело публичного метода, принадлежащего к экземпляру класса Example.
      Console.WriteLine("SelectAll called");
   }
   public int SelectInstance()
   {
       // Публичный метод, принадлежащий экземпляру класса Example
       // и возвращающий значение.
       return _fieldInstance * 2;
   }
}
 
class Program
{
    static void Main()
    {
        // Сначала запустим публичные static-методы через указание типа:
        Example.DoStatic();
        Console.WriteLine(Example.SelectStatic());
 
        // Создадим экземпляр типа Example, и вызовем для этого экземпляра
        // его публичные методы:
        Example example = new Example();
        example.DoInstance();
        Console.WriteLine(example.SelectInstance());
    }
}

Вывод:

DoAll called
2
SelectAll called
4

Эта программа содержит 2 класса: класс Example, который содержит public-методы, и класс Program, который содержит главную точку входа в программу (процедура Main). В классе Example реализовано 4 публичных метода. Два из этих методов статические, и два не статические, и половина из них не возвращает значения.

В процедуре Main мы вызываем DoStatic и SelectStatic через сам тип (тип это Example, т. е. тип и класс это одно и то же). Класс Example может также создавать свой экземпляр вызовом конструктора с помощью оператора new. Через экземпляр класса могут быть вызваны oInstance и SelectInstance, так как они не статические.

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

Тип сам по себе не сохраняет никакого состояния. Он просто указывает размещение объектов, которые могут сохранять состояние.

Статические поля и статические данные фактически не являются частью типа, и могут рассматриваться как экземпляр, который нельзя создать больше одного раза. Среда выполнения .NET (.NET Framework execution engine) является объект-ориентированной даже на низком уровне инструкций.

По умолчанию, если не указаны ключевые слова public и static, методы неявно являются частными (private) и не статическими (instance, т. е. принадлежащими только экземпляру). Если это необходимо, Вы должны явно указать любые девиации этого состояния по умолчанию. Любые методы, которые должны быть публичными, должны декларироваться с модификатором public. И соответственно статические методы (привязанные к типу, не к экземпляру) должны быть декларированы с модификатором static.

Техника "по умолчанию" является обычной практикой сокрытия информации в программировании. Это улучшает качество программного обеспечения, однако для новичков создает путаницу. Имейте в виду, что public/private также служат технике сокрытия информации, и слишком большое количество методов и полей, декларированных с модификатором public, говорит о плохом дизайне приложения.

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

Ключевое слово internal (внутренний) ограничивает доступ к членам класса. Есть несколько модификаторов доступности, которые можно применить к типу. Модификатор internal, наподобие других таких модификаторов, как public и private, меняет ограничения на то, где еще можно получить доступ к типу.

Давайте рассмотрим пример ключевого слова internal в программе на C#. Вы можете добавить модификатор internal непосредственно перед ключевым словом class, чтобы создать внутренний класс. Любой тип члена класса может также быть модифицирован с помощью internal.

Совет: у Вас могут быть internal поля, свойства или методы. Эта программа может инстанцировать тип Test, потому что он находится в той же программе (пример основа на .NET 2017).

// Программа на C#, которая использует модификатор internal.
class Program
{
   static void Main()
   {
      // В этой же программе можно получить доступ к типу internal:
      Test test = new Test();
      test._a = 1;
   }
}
 
// Пример internal-типа.
internal class Test
{
   public int _a;
}

Что стандарт говорит о модификаторе internal? Спецификация C# очень кратко затрагивает этот вопрос. Она устанавливает, что "интуитивный смысл ключевого слова internal в том, что доступ ограничен этой программой" (The intuitive meaning of internal is 'access limited to this program.').

Другими словами, другая программа не может получить доступ к типу internal (?..).

Примечание: доступность на языке C# определяет, каким кускам текста программы разрешено получить доступ к определенному члену.

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

Это означает, что если у Вас есть программа на C#, которая содержит другой двоичный файл, такой как DLL или EXE, то из него нельзя будет получить доступ к члену internal. Это улучшает пригодность кода для обслуживания.

Совет: в большинстве программ модификатор internal не нужен. Для больших, более сложных программ он становится более полезным. Материал по модификаторам public и private поможет лучше понять смысл ключевого слова internal и его эффекты.

Итак, ключевое слово internal позволяет скрывать доступ к информации на границах программы. Это модификатор доступности (accessibility modifier) в языке C#. В большинстве программ это не нужно, но может упростить поддержку больших программ.

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

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

Для чего это нужно? Программы становятся проще для дальнейшей поддержки и тестирования. Класс доступа private установлен по умолчанию, если не указаны никакие модификаторы (default accessibility).

Рассмотрим пример. Программа декларирует 2 класса, один из которых содержит private-методы, и другой содержит функцию Main. Пример показывает, как модификатор private влияет на то, как может быть вызван метод.

Примечание: программа вызывает private-методы изнутри тел public-методов. Это показывает методы экземпляра (instance methods) и частный статический метод (private static method).

// Программа C#, использующая private-методы.
using System;
 
class Test
{
   // Приватный метод экземпляра (private instance method),
   //  который что-то вычисляет:
   private int Compute1()
   {
      return 1;
   }
 
   // Публичный метод экземпляра (public instance method):
   public int Compute2()
   {
      return this.Compute1() + 1;
   }
 
   // Приватный статический метод (private static method),
   //  который что-то вычисляет:
   private static int Compute3()
   {
      return 3;
   }
 
   // Публичный статический метод (public static method):
   public static int Compute4()
   {
      return Compute3() + 1;
   }
}
 
class Program
{
   static void Main()
   {
      // Создание нового экземпляра класса Test.
      // Для него Вы можете вызвать только публичный метод
      //  экземпляра Compute2.
      Test test = new Test();
      Console.WriteLine(test.Compute2());
      // Вызов публичного статического метода. Вы не сможете
      //  получить доступ к приватному статическому методу.
      Console.WriteLine(Test.Compute4());
   }
}

Вывод:

2
4

Здесь у класса Test есть 4 декларации метода: 2 private-метода и 2 public-метода. Из класса Program Вы можете получить доступ только к public-метода. Программа также запускает private -метод из public-метода. Доступность private влияет только на то, как внешние источники получают доступ к класса.

Замечание: доступность public-метода не транзитивна через вызов public-метода.

Доступность private основывается не лексике - она влияет на члены, основываясь на позиции в тексте исходного кода и на иерархии типов.

Что такое доступность вообще? Модификатор private является частью грамматики спецификации языка C#. Он может быть указан во многих частях синтаксиса.

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

Implicit private (неявное определение частного). Язык C# автоматически считает для методов экземпляра (не статических), что они частные (не публичные). Т. е. если ключевое слово private опущено, и не присутствует любое другое слово по управлению доступом, то подразумевается, что здесь указано private. Так что Вы только можете поменять private-доступность, только лишь указав явно другое ключевое слово - public или protected.

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

Примечание: система, принятая языком C#, уменьшает симметрию в исходном тексте программ.

Подведем итог. Мы использовали методы, которые приватные (private). Они могут быть вызваны из других иерархий класса. Доступность не транзитивна, и вместо этого основывается на лексике языка. Это означает, что private-метод может быть вызван из тела public-метода. Ключевое слово private неявно добавляется ко всем методам, у которых не указано альтернативный модификатор доступности.

Как уже упоминалось, статические методы позволяют добиться повышения производительности. Они обычно быстрее для запуска на стеке вызовов (call stack), чем методы экземпляра. Важное замечание: методы экземпляра в действительности используют указатель на экземпляр "this" в качестве первого параметра. Это всегда добавляет дополнительную нагрузку. Методы экземпляра также реализованы с инструкцией callvirt, что также вводит небольшую лишнюю нагрузку.

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

Статические свойства подобны статическим методам. В метаданных свойства имеют слово "get_" или "set_" в качестве префикса к их идентификаторам.

Вы исследуете древние руины, и видите кувшин. У него есть форма, он сделан из определенного материала, имеет некий объем. Все это материальные свойства объекта.

С помощью программных свойств (properties) Вы могли бы описать этот кувшин. Он сделана из глины (clay) - свойство Material вернет "clay". Его размер может быть целым числом.

Get, set. Ниже показан класс Example. В нем есть одно целочисленное поле, используемое для хранения свойства Number.

Number это свойство типа int. Number предоставляет реализации для get { } и set { }.

Get: реализация get { } должна включать оператор return. Через реализацию get любой член класса может получить доступ к свойству на чтение.

Set: реализация set { } принимает неявный аргумент "value". Это то значение, которое присваивается свойству.

// Программа C#, которая использует свойство public int:
using System;
 
class Example
{
   int _number;
   
   public int Number
   {
      get
      {
         return this._number;
      }
      set
      {
         this._number = value;
      }
   }
}
 
class Program
{
   static void Main()
   {
      Example example = new Example();
      example.Number = 5;                 // set { }
      Console.WriteLine(example.Number);  // get { }
   }
}

Вывод:

5

Перечисление (enum). Этот пример показывает тип перечисления DayOfWeek в свойстве. Мы также добавили код в методе get (или методе set), который проверяет запоминающее хранилище или значение параметра.

// Программа C#, которая использует свойство enum.
using System;
 
class Example
{
   DayOfWeek _day;
 
   public DayOfWeek Day
   {
      get
      {
         // В пятницу это делать запрещено.
         if (this._day == DayOfWeek.Friday)
         {
            throw new Exception("Invalid access");
         }
         return this._day;
      }
      set
      {
         this._day = value;
      }
   }
}
 
class Program
{
   static void Main()
   {
      Example example = new Example();
      example.Day = DayOfWeek.Monday;
      Console.WriteLine(example.Day == DayOfWeek.Monday);
   }
}

Вывод:

True

Private. Свойство может быть частным (private). Здесь показан пример свойства IsFound класса Example, которое можно только установить. Мы устанавливаем его в конструкторе Example. Мы можем только получить свойство в Program.Main, используя экземпляр класса Example.

// Программа C#, которая использует частную (private) установку свойства.
using System;
 
class Example
{
   public Example()
   {
      // Установка private-свойства:
      this.IsFound = true;
   }
   bool _found;
   public bool IsFound
   {
      get
      {
         return this._found;
      }
      private set
      {
         // Этот установщик можно вызывать только внутри этого класса.
         this._found = value;
      }
   }
}
 
class Program
{
    static void Main()
    {
        Example example = new Example();
        Console.WriteLine(example.IsFound);
    }
} 

Вывод:

True

Мы также можем сделать свойство полностью приватным. Если сделать так, то его можно использовать только внутри этого класса. Ниже в методе Display показано, как использовать private-свойство. В большинстве программ такой синтаксис не очень полезен. Но он существует, и может помочь в сложном классе.

// Программа C#, использующая private-свойство.
using System;
 
class Example
{
   int _id;
   private int Id
   {
      get
      {
          return this._id;
      }
      set
      {
          this._id = value;
      }
   }
   public void Display()
   {
      // Через этот метод возможен доступ к private-свойству.
      this.Id = 7;
      Console.WriteLine(this.Id);
   }
}
 
class Program
{
   static void Main()
   {
      Example example = new Example();
      example.Display();
   }
} 

Вывод:

7

Static. Свойства также могут быть статическими. Это означает, что они получают связь с типом (классом), но не экземпляром типа (instance). Статические классы могут иметь только статические свойства.

Свойство Count в примере ниже имеет побочный эффект. Это приводит к тому, что поле инкрементируется при каждом доступе. Наличие побочных эффектов говорит о плохом дизайне программы, потому что они делают логику программы трудно отслеживаемой.

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

// Программа C#, которая использует static-свойство.
using System;
 
class Example
{
   static int _count;
   public static int Count
   {
      get
      {
         // Побочный эффект этого свойства:
         _count++;
         return _count;
      }
   }
}
 
class Program
{
   static void Main()
   {
      Console.WriteLine(Example.Count);
      Console.WriteLine(Example.Count);
      Console.WriteLine(Example.Count);
   }
}

Вывод:

1
2
3

Automatic. В следующем примере продемонстрирован синтаксис автоматически реализованного свойства. Скрытое поле генерируется. Таким образом, операторы get и set расширены на использование скрытого поля.

Оператор *= используется для умножения и самого свойства одновременного его присваивания. Это тоже самое, что и "example.Number = example.Number * 4". Это разрешается, поскольку свойства предназначаются быть похожими на поля. Очевидно, что с методами это делать нельзя.

// Программа C#, в которой свойство реализовано автоматически.
using System;
 
class Example
{
   public int Number
   {
      get;
      set;
   }
}
 
class Program
{
   static void Main()
   {
      Example example = new Example();
      example.Number = 8;
      example.Number *= 4;
      Console.WriteLine(example.Number);
   }
} 

Вывод:

32

Automatic, private. Рассмотрим, как сделать get или set автоматического свойства. Для такого типа свойства мы не можем пропустить реализацию get или set. Иначе компилятор C# выдаст ошибку: "Automatically implemented properties must define both get and set accessors".

// Программа C#, которая использует private-установщик
//  для автоматического свойства.
using System;
 
class Example
{
   public Example()
   {
      // Использование private-установщика в конструкторе.
      this.Id = new Random().Next();
   }
   public int Id
   {
      get;
      private set;
   }
}
 
class Program
{
   static void Main()
   {
      Example example = new Example();
      Console.WriteLine(example.Id);
   }
}

Вывод:

2077325073

Automatic, значения по умолчанию. Автоматические свойства поддерживают значения по умолчанию во многом так же, как и поля. В примере ниже свойство Quantity класса Medication назначается значением по умолчанию 30.

// Программа C#, которая использует значение по умолчанию для свойства.
using System;
 
class Medication
{
   // Это свойство имеет значение по умолчанию:
   public int Quantity { get; set; } = 30;
}
 
class Program
{
    static void Main()
    {
        Medication med = new Medication();
        // Свойство Quantity по умолчанию 30:
        Console.WriteLine(med.Quantity);
        // Мы можем поменять свойство Quantity:
        med.Quantity *= 2;
        Console.WriteLine(med.Quantity);
    }
}

Вывод:

30
60

Индексаторы (Indexer). Это также свойства. Такие свойства используют доступ к отдельным элементам свойства (наподобие массива). Используется токен "this" для своего имени, и квадратные скобки с аргументом. Подробнее см. Indexer site:dotnetperls.com.

Interface. Свойство может быть частью интерфейса. Для этого есть специальный синтаксис. С типами, которые реализуют интерфейс, мы должны предоставить реализации для этого свойства. Подробнее см. site:dotnetperls.com.

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

Свойства с телом в виде выражения. Можно использовать лямбда-стиль синтаксиса, чтобы задать свойства. Это так называемые expression-bodied свойства - мы используем "get" и "set", и передаем результат в правую сторону выражения.

// Программа C#, которая использует expression-bodied свойства.
class Program
{
   private static int test;
   public static int Test { get => test; set => test = value; }
 
   static void Main()
   {
      // Использование свойства.
      Program.Test = 200;
      System.Console.WriteLine(Program.Test);
   }
} 

Вывод:

200

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

Ниже дан пример программы с секундомером Stopwatch, где 2 цикла выполняются 10 раз. Каждый внутренний цикл получает 100 миллионов итераций. Результат текста показывает, что нет разницы между использованием свойства и прямым обращением к полю. Скорее всего доступ к свойству реализован с помощью встроенного (inline) кода. Компилятор JIT достаточно умен, чтобы реализовать inline свойства, в которых нет логики. Поэтому они получаются настолько же эффективны, как и поля.

// Программа C#, которая проверяет производительность свойств.
using System;
using System.Diagnostics;
 
class Program
{
   static string _backing;    // Хранилище для свойства.
   // Реализации получателя и установщика:
   static string Property
   {
      get
      {
         return _backing;
      }
      set
      {
         _backing = value;
      }
   }
   static string Field;       // Статическое поле.
 
   static void Main()
   {
      const int m = 100000000;
      for (int x = 0; x < 10; x++)     // Десять проверок.
      {
         Stopwatch s1 = new Stopwatch();
         s1.Start();
         for (int i = 0; i < m; i++)   // Проверка свойства.
         {
            Property = "string";
            if (Property == "cat")
            {
            }
         }
         s1.Stop();
         Stopwatch s2 = new Stopwatch();
         s2.Start();
         for (int i = 0; i < m; i++)   // Проверка поля.
         {
            Field = "string";
            if (Field == "cat")
            {
            }
         }
         s2.Stop();
         Console.WriteLine("{0},{1}",
                           s1.ElapsedMilliseconds,
                           s2.ElapsedMilliseconds);
      }
      Console.Read();
   }
}

Вывод результатов теста:

Property get/set:  604.6 ms
Field read/assign: 603.6 ms

Описание свойств в спецификации C# использует наиболее важную концепцию: свойство это не место хранения чего-либо, как поле. Вместо этого свойство реализовано просто как метод. Оно лишь имеет специальный синтаксис для того, чтобы выглядеть как поле.

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

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

На концептуальном уровне свойства комбинируют поля и методы. Но с точки зрения реализации, свойства это просто методы. Они оптимизированы в JIT-компиляторе точно так же, как методы.

Статическое свойство подобно статическому методу. Оно использует для доступа композитное имя. Static-свойства используют те же самые токены get и set, как и для управления свойствами экземпляра. Статические свойства полезны для создания абстракции глобальных данных в программе.

Рассмотрим пример программы, которая использует статические свойства. Она показывает, как получить и установить статические свойства в статическом классе. Для использования static-свойства с полем для хранилища Вы также должны указать, что хранилище тоже статическое. Программа только демонстрирует синтаксис автоматически реализованного свойства в виде статической двоичной переменной.

// Программа C#, которая использует static-свойства.
using System;
 
static class Settings
{
   public static int DayNumber
   {
      get
      {
         return DateTime.Today.Day;
      }
   }
 
   public static string DayName
   {
      get
      {
         return DateTime.Today.DayOfWeek.ToString();
      }
   }
 
   public static bool Finished
   {
      get;
      set;
   }
}
 
class Program
{
   static void Main()
   {
       // Чтение static-свойств:
       Console.WriteLine(Settings.DayNumber);
       Console.WriteLine(Settings.DayName);
 
       // Изменение значения статического двоичного свойства:
       Settings.Finished = true;
       Console.WriteLine(Settings.Finished);
   }
}

Вывод:

13
Sunday
True

Класс Settings декорирует static-модификатор, и от него не может быть произведен экземпляр. У класса Settings есть 3 статические свойства. Два из них только для чтения имеют только get accessor, в то время как третье свойство можно также и записывать.

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

К статическим свойствам можно получать доступ по составному имени, указанному через точку.

Стоит сравнить концепцию глобальных переменных в обычных языках программирования и на языке C#. Глобальные переменные полезны, и даже функциональные языки используют область глобальной видимости данных для связи переменных и имен функций. Однако глобальные переменные могут увеличить сложность, затрудняя чтение и понимание кода.

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

Совет: одна из стратегий избавиться от проблем с потоками - инициализировать все значения при старте, и затем работать с ними только на чтение.

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

Статические поля. Давайте разберемся, что это такое. У статических методов нет способа получить доступ к полям, когда это поля экземпляра. Поля экземпляра существуют только на экземплярах типа. Однако методы экземпляра могут получить доступ к статическим полям. Использование ключевого слова static ограничивает доступные члены типа.

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

Рассмотрим пример программы, инкрементирующие поля экземпляра и статические поля. Она проверяет, как тип поля влияет на производительность. По результатам теста видно, что поле экземпляра обрабатывается медленнее, потому что должно быть вычислено выражение "this".

// Программа C#, сравнивающая скорость работы static-полей
//  обычными полями (полями экземпляра класса).
using System;
using System.Diagnostics;
 
class Test1
{
   // Поля экземпляра:
   int _a;
   int _b;
   int _c;
   
   public void X()
   {
      // Изменение значений полей экземпляра:
      this._a++;
      this._b++;
      this._c++;
   }
}
 
class Test2
{
   // Статические поля:
   static int _a;
   static int _b;
   static int _c;
 
   public void X()
   {
      // Изменение значения статических полей:
      _a++;
      _b++;
      _c++;
   }
}
 
class Program
{
   const int _max = 200000000;
   static void Main()
   {
      // Инстанциация класса (получение экземпляра класса)
      //  и инстанциация его полей:
      Test1 test1 = new Test1();
      // Инстанциация класса:
      Test2 test2 = new Test2();
      var s1 = Stopwatch.StartNew();
      for (int i = 0; i < _max; i++)
      {
         // Обращение к полям экземпляра:
         test1.X();
         test1.X();
         test1.X();
         test1.X();
         test1.X();
      }
      s1.Stop();
      var s2 = Stopwatch.StartNew();
      for (int i = 0; i < _max; i++)
      {
         // Обращение к статическим полям:
         test2.X();
         test2.X();
         test2.X();
         test2.X();
         test2.X();
      }
      s2.Stop();
      Console.WriteLine(((double)(s1.Elapsed.TotalMilliseconds * 1000 * 1000) /
          _max).ToString("0.00") + " ns");
      Console.WriteLine(((double)(s2.Elapsed.TotalMilliseconds * 1000 * 1000) /
          _max).ToString("0.00") + " ns");
      Console.Read();
   }
}

Вывод:

10.51 ns  (Time for instance field usage)
10.21 ns  (Time for static field usage)

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

При выполнении эта программа меняет значение полей экземпляра в Test1 с помощью метода X. И после этого она меняет значение static-полей, находящихся в декларации класса Test2, через другой метод X.

Результат теста показывает разницу производительности, потому что в Test1 вычисляется выражение для нахождения данных экземпляра, а для static-полей Test2 этого вычисления нет.

Кроме того, статическое поле не должно быть типом значения. Вместо него может быть ссылка. Ссылка автоматически инициализируется в null. Мы можем присвоить её новому экземпляру объекта и использовать всюду в программе. Это демонстрируется в следующей программе - мы используем поле статической ссылки, чтобы указать на новый экземпляр пользовательского класса Test, декларируемого в том же файле.

// Программа C#, которая использует экземпляр static-класса.using System;class Test
{
   public int _value;
}class Program
{
   static Test _field;
   static void Main()
   {
      // Назначение статического поля новому экземпляру объекта.
      Program._field = new Test();
      // Назначение поля экземпляра объекта.
      Program._field._value = 1;
      // Отображение значения.
      Console.WriteLine(Program._field._value);
   }
}

Вывод:

1

IL (intermediate language, промежуточный язык). Поле экземпляра это одно из вещей, которые создаются множество раз в рабочем окружении, в то время как статическое поле создается только один раз, и находится в одном и том же месте. Чтобы вычислить поле экземпляра, должно быть вычислено выражение экземпляра.

На IL выражение экземпляра равно первому аргументу метода экземпляра. Подробнее про IL см. Intermediate Language site:dotnetperls.com.

При доступе к полю экземпляра выражение экземпляра загружается в стек вычисления с "ldarg.0". Для статического поля этот шаг не требуется, так что здесь не потребуется дополнительный уровень косвенной адресации (indirection). Поля экземпляра для этого вставляют дополнительную инструкцию IL для обеспечения доступа, как для операций чтения, так и для операций записи.

То, что производительность static-полей выше, чем полей экземпляра - далеко не повод создавать нетривиальные программы. Если Вам нужны экземпляры типа, то используйте экземпляры полей, поскольку они сделают программу корректнее. Однако иногда имеет смысл перейти к статическим полям для улучшения производительности.

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

Статический класс. Static-класс не может произвести экземпляр. Ключевое слово static для класса принудительно устанавливает, что для него не может быть создан экземпляр вызовом конструктора. Это устраняет ошибочное использование этого класса.

Примечание: static-класс не может иметь не статические члены. У него все методы, поля и свойства также должны быть статическими.

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

// Программа C#, которая демонстрирует статический класс.
using System;
 
class Program
{
   static void Main()
   {
      // Нельзя декларировать переменную типа Perl:
      // Perl perl = new Perl();
 
      // Program это обычный класс, так что Вы можете создать его экземпляр:
      Program program = new Program();
 
      // Вы можете вызвать static-методы внутри статического класса.
      Perl._ok = true;
      Perl.Blend();
   }
}
 
static class Perl
{
    // В static-классе нельзя декларировать члены экземпляра!
    // int _test;
 
    // А такое определение нормальное:
    public static bool _ok;
 
    // В статическом классе могут быть только статические методы:
    public static void Blend()
    {
        Console.WriteLine("Blended");
    }
}

Вывод:

Blended

Концептуально static-класс это форма сокрытия информации. Модификатор static вводит дополнительные ограничения. Конструктор из класса удаляется.

Static-конструктор. Статический конструктор инициализирует статические поля. Он запускается в неопределенное время перед тем, как эти поля будут использованы. Статические конструкторы в типе вводят некоторые дополнительные расходы.

Примечание: static-конструктор иногда называют инициализатором типа. Он инициализирует поля перед доступом к ним. Еще одно замечание: экземпляры не то же самое, что и типы. Экземпляр это определенный, отдельный объект какого-то типа.

Пример: рассмотрим два класса. У первого будет static-конструктор, он инциализирует свое поле в 1. У второго класса не будет статического конструктора. При тестировании класс со статическим конструктором будет медленнее обращаться к полю.

/// < summary>
/// У этого типа есть статический конструктор.
/// < /summary>
static class HasStaticConstructor
{
    /// < summary>
    /// Public-поле.
    /// < /summary>
    public static int _test;
 
    /// < summary>
    /// Статический конструктор, инициализирующий публичное поле.
    /// < /summary>
    static HasStaticConstructor()
    {
        _test = 1;
    }
}
 
/// < summary>
/// У этого типа нет статического конструктора.
/// < /summary>
static class NoStaticConstructor
{
    /// < summary>
    /// Публичное поле инициализируется встроенным (inline) кодом.
    /// < /summary>
    public static int _test = 1;
}
 
class Program
{
    static void Main()
    {
        System.Console.WriteLine(HasStaticConstructor._test);
        System.Console.WriteLine(NoStaticConstructor._test);
    }
}

Вывод:

1
1

При проверке специальной программой (здесь она не показана) классы показали разную скорость работы: HasStaticConstructor выполнялся 2.89 нс, NoStaticConstructor 0.32 нс. При одиночном вызове это никак не скажется на общей скорости работы программы, но при частых вызовах (например, в цикле) снижение быстродействие может быть значительным.

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

Мы можем сделать многих членов тип и сами типы статическими. Это бывает полезно для частого совместного использования члена. Во врезках ниже рассмотрены массивы, Regex и строки.

Массивы тоже могут быть статическими. Они могут сохранять значения, которые не относятся к экземплярам класса. Этот шаблон обычно используется в программах C#, и он часто бывает полезен.

В примере ниже используется статический массив целых чисел. Данные показаны отдельно от состояния объекта. На список начальных чисел состояние никак не влияет. Массив содержит первые 5 чисел Вагстафа.

// Программа C#, которая использует статический массив чисел int.
using System;
using System.Linq;
 
class Program
{
   static void Main()
   {
      // Использование public-функции для проверки static-массива.
      bool isWagstaff = NumberTest.IsWagstaffPrime(43);
      Console.WriteLine(isWagstaff);
 
      // Еще одна проверка static-массива.
      isWagstaff = NumberTest.IsWagstaffPrime(606);
      Console.WriteLine(isWagstaff);
   }
}
 
/// < summary>
/// Класс содержит пример статического массива.
/// < /summary>
public static class NumberTest
{
   /// < summary>
   /// Этот static-массив содержит несколько начальных чисел Вагстафа.
   /// < /summary>
   static int[] _primes = { 3, 11, 43, 683, 2731 };
 
   /// < summary>
   /// Публичный метод для проверки статического private-массива.
   /// < /summary>
   public static bool IsWagstaffPrime(int i)
   {
      return _primes.Contains(i);
   }
}

Первая часть кода показывает метод Main который дважды класс NumberTest. Нижний класс NumberTest содержит массив static int, который присутствует в памяти во все время жизни программы. Этот массив инициализируется пятью числами int, и могут использоваться без какого-либо беспокойства об экземпляре класса.

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

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

// Программа C#, использующая свойство в виде статического массива.
using System;
 
class Program
{
   static void Main()
   {
      foreach (string dog in StringTest.Dogs)
      {
         Console.WriteLine(dog);
      }
   }
}
 
/// < summary>
/// Содержит статический массив строк.
/// < /summary>
public static class StringTest
{
   /// < summary>
   /// Массив пород собак.
   /// < /summary>
   static string[] _dogs = new string[]
   {
      "schnauzer",
      "shih tzu",
      "shar pei",
      "russian spaniel"
   };
 
   /// < summary>
   /// Получение значений из массива через публичное свойство.
   /// < /summary>
   public static string[] Dogs
   {
      get
      {
         return _dogs;
      }
   }
}

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

Пример использования массива List см. в List site:dotnetperls.com.

Были показаны примеры использования массивов целых чисел и строк на языке C#. Эти статические коллекции полезны для доступа к данным, которые не зависят от экземпляра класса.

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

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

// Программа C#, использующая статический словарь Dictionary.
using System;
using System.Collections.Generic;
 
class Program
{
   static void Main()
   {
      // Вывод множественной версии из статического словаря.
      Console.WriteLine(PluralStatic.GetPlural("game"));
      Console.WriteLine(PluralStatic.GetPlural("item"));
      Console.WriteLine(PluralStatic.GetPlural("entry"));
   }
}
 
/// < summary>
/// Содержит множественную логику в статическом классе (отдельный файл)
/// < /summary>
static class PluralStatic
{
   /// < summary>
   /// Пример статического словаря из строк.
   /// < /summary>
   static Dictionary< string, string> _dict = new Dictionary< string, string>
   {
      {"entry", "entries"},
      {"image", "images"},
      {"view", "views"},
      {"file", "files"},
      {"result", "results"},
      {"word", "words"},
      {"definition", "definitions"},
      {"item", "items"},
      {"megabyte", "megabytes"},
      {"game", "games"}
   };
 
   /// < summary>
   /// Доступ к словарю снаружи.
   /// < /summary>
   public static string GetPlural(string word)
   {
      // Попытка получить результат из статического словаря.
      string result;
      if (_dict.TryGetValue(word, out result))
      {
         return result;
      }
      else
      {
         return null;
      }
   }
}

Вывод:

games
items
entries

Статический словарь Dictionary инкапсулирован в статическом классе. Доступ е его ключевым словам и значениям предоставляется через public-метод GetPlural. Этот код вернет значение из статического словаря, найденное по указанному ключу.

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

Статические регулярные выражения (static Regex) помогают повысить производительность. Это везде может использоваться в методах программы на языке C#. Статическое регулярное выражение существует в памяти в одном экземпляре, что повышает быстродействие.

Пример ниже демонстрирует статический объект регулярного выражения. Вы можете использовать инициализатор статической переменной для инстанциации регулярного выражения с помощью оператора new. После этого можно получить доступ к Regex по его идентификатору и вызвать на нем такие методы, как Match, Matches и IsMatch.

// Программа C#, использующая экземпляр статического регулярного выражения.
// Она читает во входной строке первое слово, начинающееся с заглавной R.
using System;
using System.Text.RegularExpressions;
 
class Program
{
   static Regex _rWord = new Regex(@"R\w*");
 
   static void Main()
   {
      // Использование входной строки: попытка найти совпадение первого
      // слова, начинающегося с заглавной R.
      string value = "This is a simple /string/ for Regex.";
      Match match = _rWord.Match(value);
      Console.WriteLine(match.Value);
   }
}

Вывод:

Regex

Большая выгода статического экземпляра Regex а том, что его не нужно создавать более одного раза. Ваше регулярное выражение может быть совместно использовано между многими различными методами в этом типе. Это улучшает производительность. Дополнительно можно улучшить производительность применением специальных схем доступа к регулярному выражению и ограничением выделения регулярных выражений на куче. Подробнее см. Regex performance site:dotnetperls.com.

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

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

// Программа C#, использующая статическую строку.
using System;
 
class Program
{
   static string _example;
 
   static void Main()
   {
      // Проверка начального значения статической строки:
      if (_example == null)
      {
         Console.WriteLine("String is null");
      }
      // Назначение значения статической строке:
      _example = 1000.ToString();
      // Отображение значения строки:
      Console.WriteLine("--- Value, Main method ---");
      Console.WriteLine(_example);
      // Вызов метода:
      Read();
   }
 
   static void Read()
   {
      // Отображение значения строки:
      Console.WriteLine("--- Value, Read method ---");
      Console.WriteLine(_example);
   }
}

Вывод:

String is null
--- Value, Main method ---
1000
--- Value, Read method ---
1000

У статических строк, инициализированных в null, все биты установлены в 0. Мы проверяем значение статической строки перед тем, как назначить ей значение. Это возможно с экземплярами, статическими строками и другими типами

При присваивании значения статической строке оператором присваивания делается побитная копия значения ссылки. Когда Вы назначаете значение ToString(), данные связаны с управляемой кучей (managed heap). Ссылка теперь указывает на данные объекта.

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

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

К const-строкам можно получать доступ с таким же синтаксисом, как и к static-строкам. И наконец, строки readonly могут быть строками экземпляра или статическими строками, и они могут быть назначены только в конструкторе. Подробнее про ключевое слово readonly см. Readonly keyword dotnetperls.com.

Public Static Readonly. Спецификация C# рекомендует использовать переменные "public static readonly" для констант, которые должны быть инициализированы во время выполнения программы (runtime).

Поля public static readonly используются для типов, которые не могут быть представлены как значения const, однако не должны изменяться во время выполнения программы. Такие поля улучшают устойчивость кода по отношению к ошибкам программиста. Подробнее про ключевое слово readonly см. Readonly keyword dotnetperls.com.

Пример ниже использует поля public static readonly. Спецификация языка рекомендует использовать поля public static readonly, когда Вы не можете использовать поле const, или когда поле должно поменяться в будущем.

Поля readonly не ухудшают производительность для обычных полей. Однако поля const работают немного быстрее - они вставляются непосредственно в те места, где используются

// Класс C#, который использует поля public static readonly.
using System.Drawing;
 
static class Points
{
   // Определение на статическом классе публичных статических
   // полей только для чтения:
   public static readonly Point TopLeft = new Point(0, 0);
   public static readonly Point TopRight = new Point(1, 0);
   public static readonly Point BottomRight = new Point(1, 1);
   public static readonly Point BottomLeft = new Point(0, 1);
   // public const Point TopLeft = new Point(0, 0);
}
 
// Program.cs с методом Main:
using System;
 
class Program
{
   static void Main()
   {
      // Использование полей public static readonly:
      Console.WriteLine(Points.TopLeft);
      Console.WriteLine(Points.TopRight);
      Console.WriteLine(Points.BottomRight);
      Console.WriteLine(Points.BottomLeft);
      // Points.TopLeft = new System.Drawing.Point();
   }
}

Вывод:

{X=0,Y=0}
{X=1,Y=0}
{X=1,Y=1}
{X=0,Y=1}

Эта программа определяет статический класс с именем Points, где сохранено 4 ссылки Point, являющиеся полями только для чтения. Они используют модификатор доступности public, так что Вы можете получить к нему доступ из любого места в коде Вашего проекта.

Со статическими полями Вы можете использовать конструктор, и он запустится только 1 раз.

В теле Main загружаются 4 ссылки Point. Поля Point только для чтения всегда вызывают свои конструкторы перед своим использованием.

Могут произойти ошибки при попытке использовать класс const или структуру const, и также при попытке назначить значение readonly-полю вне декларатора переменной или вне конструктора. Это ошибки времени компиляции, и о них сообщит компилятор C#.

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

Пример ошибки 1: "A static readonly field cannot be assigned to" (кроме статического конструктора или инициализатора переменной).

Пример ошибки 2: "The type 'System.Drawing.Point' cannot be declared const".

Спецификация применяет концепцию использования полей public static readonly для симуляции экземпляров класса-константы. Эта техника достаточно важна и эффективна, чтобы быть подчеркнутой в стандарте C#.

Const. Это ключевое слово определяет, что переменная не будет никогда изменяться. Когда мы используем const, то не можем переназначить переменную. Это дает компилятору некоторую свободу при генерации кода, использующего const-переменную.

Ключевое слово const, что понятно уже из названия, показывает константу. Она описывает элемент (переменная, поле), который программа не может изменить во время своего выполнения (runtime). Вместо этого значение элемента должно быть определено во время компиляции (compile-time).

Мы не можем переназначить значение константы. С константами теряется возможность модифицировать переменные runtime, однако такие переменные дают выигрыш в производительности и дают возможность применения дополнительных проверок логики программы во время компиляции. Когда Вы компилируете значения констант, эти значения вставляются непосредственно в части кода программы, где они используются. Это устраняет накладные расходы, связанные с обслуживанием переменных. Кроме того с константами (такими как строки или числа int) мы вытягиваем их в часть программы, где их проще найти и отредактировать. Это улучшает организацию кода.

// Программа C#, использующая const.
using System;
 
class Program
{
   const string _html = ".html";
 
   static void Main()
   {
      // Это приведет к ошибке во время компиляции:
      // _html = "";
      Console.WriteLine(_html);         // Доступ к константе
      Console.WriteLine(Program._html); // Доступ к константе
 
      const string txt = ".txt";
 
      // Это приведет к ошибке во время компиляции:
      // txt = "";
      Console.WriteLine(txt);
   }
}

Вывод:

.html
.html
.txt

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

Примечание: ошибки времени компиляции (compile-time errors) это отдельный класс ошибок, отличающийся от исключений (exceptions): ошибки compile-time срабатывают до запуска программы.

При рефакторинге (пересмотре кода) мы можем попадать в некоторые ошибки, связанные с константами. Чтобы исправить ошибку либо измените константу и сделайте её переменной, либо удалите присваивание.

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

// Программа C#, которая приводит к ошибке константы.class Program
{
   static void Main()
   {
      int size = 5;
      const int value = 10 + size;
   }
}

Результат компиляции: "Error CS0133. The expression being assigned to 'value' must be constant".

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

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

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

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

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

Шаблон синглтон для разработки это интерфейс. Это популярный тип класса для программ, позволяющий принудительно задавать для класса только одно выделение.

// Программа C#, использующая синглтон.
class Program
{
   static void Main()
   {
      SiteStructure s = SiteStructure.Instance;
   }
}
 
public sealed class SiteStructure
{
   static readonly SiteStructure _instance = new SiteStructure();
 
   public static SiteStructure Instance
   {
      get
      {
          return _instance;
      }
   }
   SiteStructure()
   {
      // Initialize.
   }
}

Посмотрите, где инициализируется новый SiteStructure(). Здесь критичны ключевые слова readonly и static. Readonly обеспечивает безопасность для потоков, и это означает, что экземпляр может быть выделен только один раз. И у класса есть публичный статический метод get.  Свойство public Instance используется вызывающим кодом, чтобы получить интерфейс синглтона.

Ключевое слово sealed позволяет компилятору выполнить специальные оптимизации во время JIT-компиляции. Последние методы в этом пример - private instance конструктор, и метод Initialize. Private-конструкторы означают, что класс может выделить только сам себя. Подробнее про ключевое слово sealed см. Sealed site:dotnetperls.com. Также см. Private Constructor site:dotnetperls.com.

Эта реализация быстрая, потому что член класса экземпляра создается непосредственно в своей декларации. FxCop предупреждает, когда Вы инициализируете статический член в статическом конструкторе. Статические конструкторы медленнее, чем большинство конструкторов. Также они создают проблемы - "ленивая" инстанциация. Каждый доступ к классу должен проверять, что статический конструктор был запущен.

Далее рассмотрим пример поля, перепишем свойство как поле. Ранее приведенный код показывал синглтон с использованием свойства для доступа. В среде .NET Framework свойства часто встраиваются в код (inlined) системой CLR (Common Language Runtime). Однако иногда смена свойства на public-поле показывает повышение скорости работы программы.

// Программа C#, которая использует public-поле и синглтон.
using System;
 
class SingletonB
{
   public static readonly SingletonB _instance = new SingletonB();
 
   public void Test()
   {
      Console.WriteLine(true);
   }
 
   SingletonB()
   {
   }
}
 
class Program
{
   public static void Main()
   {
      SingletonB._instance.Test();
   }
}

Вывод:

True

Здесь можно заметить небольшое ускорение работы программы. Если прокрутить тест быстродействия 1 миллиард раз, то время выполнения для свойства составит 1.935 секунд против 1.731 секунд для поля - использование публичного поля работает быстрее. Использование свойств замедляет синглтоны, public-поле будет работать в тех же условиях на 10% быстрее.

Singleton:   Property
Time:        1.935 s 
Singleton:   Public field
Time:        1.731 s

Очевидно, что синглтон здесь имеет смысл оптимизировать, потому что даже небольшие улучшения в плане скорости могут иметь значение. Проведенные исследования такого рода также помогают понять, как работает компилятор C# при генерации кода. Автор [3] рекомендует прочитать статью Implementing the Singleton site:csharpindepth.com.

MSIL (Microsoft Intermediate Language). Как влияет на быстродействие использование синглтона? Исследование показывает, что когда Вы обращаетесь к синглтон в методе C#, статическое поле экземпляра синглтона должно быть загружено в стек вычислений. Таким образом, доступ к синглтону всегда ударяет по производительности, когда метод вызывается первый раз.

MSIL-код для синглтона:

L_0000: ldsfld class Perls.Metadata Perls.Metadata::Instance
L_0005: ldarg.0
L_0006: ldloca.s data
L_0008: callvirt instance bool Perls.Metadata::TryGetFile(string,
                                                          class Perls.FileData&)

MSIL-код для статического класса:

L_0000: ldarg.0
L_0001: ldloca.s data
L_0003: call bool Perls.Metadata::TryGetFile(string, class Perls.FileData&)

MSIL с синглтоном. Этот MSIL при запуске метода использует синглтон. Этот синглтон вызывается Perls.Metadata.Instance, и сохраняется в статическом поле. Вызов ldsfld загружает статический экземпляр Instance в стек.

MSIL с методом статического класса. Здесь мы видим тот же вызов метода, как и выше, но используется статический метод и класс. Сгенерированный MSIL не использует код операции ldsfld, это означает, что с стек вычислений будет сохранено на одну переменную меньше. Также callvirt заменен кодом операции call, что тоже улучшает производительность.

MSDN предоставляет статью, которая показывает цену в наносекундах для статических вызовов, вызовов экземпляра и виртуальных вызовов. Статические вызовы обычно быстрее, чем другие типы вызова метода. См. Writing Faster Managed Code: Know What Things Cost site:msdn.microsoft.com.

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

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

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

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

// Реализация синглтона на C#.
 
/// < summary>
/// Пример объекта синглтон.
/// < /summary>
public sealed class SiteStructure
{
   /// < summary>
   /// Это затратный ресурс.
   /// Нам нужно сохранить его в одном месте.
   /// < /summary>
   object[] _data = new object[10];
 
   /// < summary>
   /// Выделение самого себя.
   /// У нас private-конструктор, никто его больше вызвать не может.
   /// < /summary>
   static readonly SiteStructure _instance = new SiteStructure();
 
   /// < summary>
   /// Доступ к SiteStructure.Instance для получения объекта синглтон.
   /// Затем вызов методов на этом экземпляре.
   /// < /summary>
   public static SiteStructure Instance
   {
      get { return _instance; }
   }
 
   /// < summary>
   /// Это private-конструктор, что означает, что к нему нельзя получить
   /// доступ снаружи.
   /// < /summary>
   private SiteStructure()
   {
      // В этом месте инициализируйте члены класса.
   }
}

В вышеприведенном примере член класса с именем _instance это статический экземпляр класса SiteStructure. Это означает, что он может быть создан только один раз во время выполнения кода (runtime). Статические члены класса существуют только в одном месте.

Мы использовали статическое обращение к реальному, обычному объекту. Статическое свойство Instance позволяет упростить доступ к синглтону SiteStructure. Это публичное свойство, и оно также называется getter.

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

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

// Пример статического класса на C#.
 
/// < summary>
/// Пример статического класса. Обратите внимание на использование
/// ключевого слова static.
/// < /summary>
static public class SiteStatic
{
   /// < summary>
   /// Данные в этом примере должны храниться в статическом члене класса.
   /// < /summary>
   static object[] _data = new object[10];
 
   /// < summary>
   /// C# не определяет, когда запустится этот конструктор, однако это
   /// скорее всего произойдет непосредственно перед использованием класса.
   /// < /summary>
   static SiteStatic()
   {
      // В этом месте инициализируйте все Ваши статические члены класса.
   }
}

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

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

// Использование синглтона в качестве параметра на C#.
// Мы хотим вызвать функцию с этой структурой как объектом.
// Получение ссылки из свойства Instance синглтона.
SiteStructure site = SiteStructure.Instance;
OtherFunction(site); // Использование синглтон в качестве параметра.

Интерфейсы. Вы можете использовать синглтоны с интерфейсами точно так же, как и любые другие классы. На языке C# interface это контракт, и объекты, которые имеют интерфейс, должны удовлетворять всем требованиям к этому интерфейсу.

Повторное использование синглтона. Здесь мы можем использовать синглтон в любом методе, который принимает интерфейс. Нам не надо будет переписывать ничего снова и снова. Это показывает лучшие практики объектно-ориентированного программирования. Подробнее про тип интерфейса языка C# см. Interface site:dotnetperls.com.

// Синглтоны с интерфейсом на C#.
 
/// < summary>
/// Сохраняет сигнатуры различных важных методов.
/// < /summary>
public interface ISiteInterface
{
};
 
/// < summary>
/// Скелет синглтона, который наследует интерфейс.
/// < /summary>
class SiteStructure : ISiteInterface
{
   // Реализует все методы ISiteInterface.
   // Здесь это пропущено.
}
 
/// < summary>
/// Здесь показан пример, где мы используем синглтон с интерфейсом.
/// < /summary>
class TestClass
{
   /// < summary>
   /// Пример.
   /// < /summary>
   public TestClass()
   {
      // Отправка объекта синглтон в любую функцию, которая может
      // принять его интерфейс.
      SiteStructure site = SiteStructure.Instance;
      CustomMethod((ISiteInterface)site);
   }
 
   /// < summary>
   /// Принимает синглтон, который придерживается интерфейса ISiteInterface.
   /// < /summary>
   private void CustomMethod(ISiteInterface interfaceObject)
   {
      // Мы используем синглтон через его интерфейс.
   }
}

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

Инструкции. Лучшее понимание статических полей дает рассмотрение инструкций IL. Это также дает нам хорошую идею о том, как модификаторы static влияют на производительность (часто "static" улучшает производительность).

IL это сокращение от Intermediate language (промежуточный язык). Код существует на многих уровнях, абстракция реализована в виде лестницы. На самых высоких уровнях абстракции у нас есть код C#. На уровне ниже работает IL.

Инструкции. На IL есть инструкции (коды операций), которые манипулируют стеком. Эти инструкции, называемые годами операций промежуточного языка (IL opcode), находятся в реляционной базе данных, называемой метаданными (metadata).

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

Анализ: исходный код обрабатывается, и создается таблица символов. Анализ работает перед синтезом.

Синтез: генерируется объектный код, и записывается в исполняемый файл. Анализ и синтез приводят к промежуточной форме программы.

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

// Пример программы C#.
using System;
 
class Program
{
   static void Main()
   {
      int i = 0;
      while (i < 10)
      {
         Console.WriteLine(i);
         i++;
      }
   }
}

Этот код C# скомпилируется в следующий промежуточный код:

// IL: промежуточный язык для метода Main.
.method private hidebysig static void Main() cil managed
{
   .entrypoint
   .maxstack 2
   .locals init (      [0] int32 num)
   L_0000: ldc.i4.0
   L_0001: stloc.0
   L_0002: br.s L_000e
   L_0004: ldloc.0
   L_0005: call void [mscorlib]System.Console::WriteLine(int32)
   L_000a: ldloc.0
   L_000b: ldc.i4.1
   L_000c: add
   L_000d: stloc.0
   L_000e: ldloc.0
   L_000f: ldc.i4.s 10
   L_0011: blt.s L_0004
   L_0013: ret
}

Ldc. По метке L_0000 видно, что число нулевой константы проталкивается в стек вычислений с помощью инструкции ldc.

Br. Инструкция br показывает ветвление (branch). Это инструкция, проверяющая условие, и от него зависит, будет ли выполняться следующая инструкция, или произойдет переход по метке.

На языке C# такие операторы, как goto, while, for, break и return могут быть реализованы вариантами инструкции ветвления.

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

Add. На IL инкремент реализован путем проталкивания константы 1 в стек вычислений, и затем для инкремента используется арифметическая инструкция add.

Строка L_0011 реализует другую инструкцию, которая возвращает управление в начала цикла, если условие цикла соблюдается. Это еще одно ветвление.

Примечание: инструкции ветвления используют метки на IL. На низком уровне ветвления по сути это операторы goto.

Дизассемблер IL. Чтобы понимать язык C#, рекомендуется изучать критические куски кода, который Вы пишете, с помощью дизассемблирования промежуточного языка. IL Disassembler предоставляется средой программирования Visual Studio. Подробнее см. IL Disassembler site:dotnetperls.com.

Bne. Давайте еще рассмотрим примеры кода на промежуточном языке. Операторы if транслируются в инструкции ветвления. BNE одна из таких инструкций IL, её имя расшифровывается branch if not equal (перейти, если не равно).

// Программа C#, которая использует на IL инструкцию bne.
using System;
 
class Program
{
   static void Main()
   {
      A(0);
      A(1);
      A(2);
   }
 
   static void A(int value)
   {
      if (value == 1)
      {
         Console.WriteLine("Hi");
      }
      else
      {
         Console.WriteLine("Bye");
      }
   }
}

Вывод:

Bye
Hi
Bye

Здесь для нас важен вызов метода A(). Если его аргумент равен 1, то он что-то записывает в консоль. Рассмотрим листинг дизассемблера этого метода. Константа 1 проталкивается в стек инструкцией ldc, и используется ветвление с помощью инструкции bne. Если два значения на вершине стека (константа 1 и аргумент int) не равны, то происходит переход вперед по метке IL_000f.

.method private hidebysig static void  A(int32 'value') cil managed
{
   // Code size       26 (0x1a)
   .maxstack  8
   IL_0000:  ldarg.0
   IL_0001:  ldc.i4.1
   IL_0002:  bne.un.s   IL_000f
   IL_0004:  ldstr      "Hi"
   IL_0009:  call       void [mscorlib]System.Console::WriteLine(string)
   IL_000e:  ret
   IL_000f:  ldstr      "Bye"
   IL_0014:  call       void [mscorlib]System.Console::WriteLine(string)
   IL_0019:  ret
} // end of method Program::A

Call. Инструкция call запускает статический (или другими словами общий) метод.

Еще пример: в методе Main мы создаем экземпляр класса Program и вызываем метод A экземпляра. В конце мы вызываем статический метод B.

// Программа C#, которая использует методы экземпляра и static-методы.
class Program
{
   void A()
   {
   }
 
   static void B()
   {
   }
 
   static void Main()
   {
      Program p = new Program();
      p.A();
      Program.B();
   }
}

Вот что получилось на IL:

.method private hidebysig static void  Main() cil managed
{
   .entrypoint
   // Code size       18 (0x12)
   .maxstack  1
   .locals init ([0] class Program p)
   IL_0000:  newobj     instance void Program::.ctor()
   IL_0005:  stloc.0
   IL_0006:  ldloc.0
   IL_0007:  callvirt   instance void Program::A()
   IL_000c:  call       void Program::B()
   IL_0011:  ret
} // end of method Program::Main

Callvirt. Callvirt это инструкция IL, она использует методы экземпляра. Между тем на статических вызовах используется инструкция call.

Код на C#, который использует метод экземпляра:

class Test
{
   public int M()
   {
      return 0;
   }
}
 
class Program
{
   static void Main()
   {
      Test t = new Test();
      int i = t.M();
   }
}

Результат дизассемблирования IL метода Main:

.method private hidebysig static void Main() cil managed
{
   .entrypoint
   .maxstack 1
   .locals init (       [0] class Test t)
   L_0000: newobj instance void Test::.ctor()
   L_0005: stloc.0
   L_0006: ldloc.0
   L_0007: callvirt instance int32 Test::M()
   L_000c: pop
   L_000d: ret
}

Код C#, который использует static-метод:

class Test
{
   static public int M()
   {
      return 0;
   }
}
 
class Program
{
   static void Main()
   {
      int i = Test.M();
   }
}

Результат дизассемблирования IL метода Main:

.method private hidebysig static void Main() cil managed
{
   .entrypoint
   .maxstack 8
   L_0000: call int32 Test::M()
   L_0005: pop
   L_0006: ret
}

Очевидно, что короче код, вызывающий статический метод, и он должен работать быстрее вызова метода экземпляра (callvirt). Результаты тестирования показывают, что вызов метода экземпляра примерно на 1.23% медленнее вызова статического метода.

Ldarg. Инструкция ldarg помещает аргумент в стек вычислений. Есть несколько общих вариантов ldarg в .NET Framework.

Рассмотрим пример, где будут использоваться инструкции ldarg.0 и ldarg.1. Здесь числа 0 и 1 относятся к позиции аргумента в списке аргументов (формальный список параметров). Это означает, что в нашем примере ldarg.0 проталкивает в стек вычислений int32, и ldarg.1 проталкивает строку.

Метод на C#, у которого 2 параметра: 

static int Something(int a, string b)
{
   a += 1;
   int y = b.Length + a;
   return y;
}

Код IL:

.method private hidebysig static int32  Something(int32 a,
                                                  string b) cil managed
{
   // Code size       16 (0x10)
   .maxstack  2
   .locals init ([0] int32 y)
   IL_0000:  ldarg.0
   IL_0001:  ldc.i4.1
   IL_0002:  add
   IL_0003:  starg.s    a
   IL_0005:  ldarg.1
   IL_0006:  callvirt   instance int32 [mscorlib]System.String::get_Length()
   IL_000b:  ldarg.0
   IL_000c:  add
   IL_000d:  stloc.0
   IL_000e:  ldloc.0
   IL_000f:  ret
} // end of method Program::Something

Ldc. С помощью инструкции ldc среда .NET загружает значения констант (таких как целые числа) в стек вычислений, что делает возможным многие конструкции программы. Пример ниже использует 6 значений констант, каждая из них передается в Console.WriteLine. Обратите внимание, что для значений 0 и 8, используются инструкции ldc.i4.0 и ldc.i4.8. Эти инструкции загружают 4-байтные значения 0 и 8 на вершину стека.

// Программа C#, которая использует инструкции ldc на IL.
using System;
 
class Program
{
   static void Main()
   {
      Console.WriteLine(0);
      Console.WriteLine(8);
      Console.WriteLine(9);
      Console.WriteLine(-1);
      Console.WriteLine('a');
      Console.WriteLine(1.23);
   }
}

Соответствующий код на IL:

.method private hidebysig static void  Main() cil managed
{
   .entrypoint
   // Code size       47 (0x2f)
   .maxstack  8
   IL_0000:  ldc.i4.0
   IL_0001:  call       void [mscorlib]System.Console::WriteLine(int32)
   IL_0006:  ldc.i4.8
   IL_0007:  call       void [mscorlib]System.Console::WriteLine(int32)
   IL_000c:  ldc.i4.s   9
   IL_000e:  call       void [mscorlib]System.Console::WriteLine(int32)
   IL_0013:  ldc.i4.m1
   IL_0014:  call       void [mscorlib]System.Console::WriteLine(int32)
   IL_0019:  ldc.i4.s   97
   IL_001b:  call       void [mscorlib]System.Console::WriteLine(char)
   IL_0020:  ldc.r8     1.23
   IL_0029:  call       void [mscorlib]System.Console::WriteLine(float64)
   IL_002e:  ret
} // end of method Program::Main

Как на IL используются локальные переменные? Они выделяются отдельно и к ним осуществляется доступ по индексам с помощью инструкции ldloc. В нашем примере ниже 4 локальные переменные индексируются числами 0, 1, 2 и 3. Эти индексы будут впоследствии использоваться в IL.

Примечание: на промежуточном языке секция ".locals" показывает, что 4 локальные переменные выделяются каждый раз при вызове метода.

Метод C#, который вводит локальные переменные:

static int Example()
{
   int a = 1;
   string y = "cat";
   bool b = false;
   int result = a + y.Length + (b ? 1 : 0);
   return result;
}

Соответствующий код IL:

.method private hidebysig static int32  Example() cil managed
{
   // Code size       29 (0x1d)
   .maxstack  3
   .locals init ([0] int32 a,
                 [1] string y,
                 [2] bool b,
                 [3] int32 result)
   IL_0000:  ldc.i4.1
   IL_0001:  stloc.0
   IL_0002:  ldstr      "cat"
   IL_0007:  stloc.1
   IL_0008:  ldc.i4.0
   IL_0009:  stloc.2
   IL_000a:  ldloc.0
   IL_000b:  ldloc.1
   IL_000c:  callvirt   instance int32 [mscorlib]System.String::get_Length()
   IL_0011:  add
   IL_0012:  ldloc.2
   IL_0013:  brtrue.s   IL_0018
   IL_0015:  ldc.i4.0
   IL_0016:  br.s       IL_0019
   IL_0018:  ldc.i4.1
   IL_0019:  add
   IL_001a:  stloc.3
   IL_001b:  ldloc.3
   IL_001c:  ret
} // end of method Program::Example

Ldsfld, stsfld. В дополнение к сохранению значений в статических полях на IL имеется инструкция, загружающая значения оттуда. Ниже показан пример программы, которая назначает значение статическому полю _a значение 3. Это осуществляется инструкцией stsfld. Инструкция ldsfld проталкивает значение, сохраненное в _a, на вершину стека вычислений. Константа 4 загружается на вершину стека вычислений инструкцией ldc.i4.4.

// Программа C#, использующая инструкцию ldsfld.
using System;
 
class Program
{
   static int _a;
   static void Main()
   {
      Program._a = 3;
      if (Program._a == 4)
      {
         Console.WriteLine(true);
      }
   }
}

Соответствующий код IL:

.method private hidebysig static void  Main() cil managed
{
   .entrypoint
   // Code size       21 (0x15)
   .maxstack  8
   IL_0000:  ldc.i4.3
   IL_0001:  stsfld     int32 Program::_a
   IL_0006:  ldsfld     int32 Program::_a
   IL_000b:  ldc.i4.4
   IL_000c:  bne.un.s   IL_0014
   IL_000e:  ldc.i4.1
   IL_000f:  call       void [mscorlib]System.Console::WriteLine(bool)
   IL_0014:  ret
} // end of method Program::Main

Newarr. Инструкция newarr создает вектор, это одномерный массив. Векторы имеют специальную поддержку со стороны .NET Framework. Одномерные массивы не использует принцип "завершения нулем".

Newobj. Многомерные массивы создаются инструкцией newobj вместо newarr.

Следующий пример программы создает массив int из 10 элементов. Первый элемент инициализируется компилятором так, что никакой код не удаляется.

// Программа C#, создающая новый массив.
using System;
 
class Program
{
   static void Main()
   {
      int[] array = new int[10];
      array[0] = 1;
   }
}

Соответствующее представление этого примера на IL:

.method private hidebysig static void  Main() cil managed
{
   .entrypoint
   // Code size       13 (0xd)
   .maxstack  3
   .locals init ([0] int32[] 'array')
   IL_0000:  ldc.i4.s   10
   IL_0002:  newarr     [mscorlib]System.Int32
   IL_0007:  stloc.0
   IL_0008:  ldloc.0
   IL_0009:  ldc.i4.0
   IL_000a:  ldc.i4.1
   IL_000b:  stelem.i4
   IL_000c:  ret
} // end of method Program::Main

Константа 10 загружается в стек перед инструкцией newarr. Это приведет к тому, что в массиве будет десять элементов.

Ret. Каждый выполняемый метод имеет инструкцию ret. Она возвращает управление из этого метода в вызвавший метод. Эта инструкция иногда возвращает значение.

Давайте рассмотрим простой метод C#, который возвращает целочисленное значение. Этот метод добавляет 2 к своему аргументу.

static int Method(int value)
{
   return 2 + value;
}

Соответствующий код IL:

.method private hidebysig static int32  Method(int32 'value') cil managed
{
   // Code size       4 (0x4)
   .maxstack  8
   IL_0000:  ldc.i4.2
   IL_0001:  ldarg.0
   IL_0002:  add
   IL_0003:  ret
} // end of method Program::Method

Как Вы можете видеть, к аргументу осуществляется доступ с помощью инструкции ldarg.0. В конце метода стоит инструкция ret. Стек вычислений в момент выполнения ret сохраняет результат сложения с помощью инструкции add, это значение и возвращается.

Stelem. Эта инструкция сохраняет значение в элементе массива. Её скорость работы зависит от типа элемента в массиве. Давайте проверим скорость работы stelem в нескольких программах C#.

Когда Вы назначаете элементы в массиве сам массив проталкивается в стек (newarr). Индекс, который Вы используете в массиве, проталкивается в стек (ldc), и проталкивается в стек значение, которое Вы хотите поместить в массив (ldloc).

Пример 1:

class Program
{
   static void Main()
   {
      char[] c = new char[100];
      c[0] = 'f';
   }
}

Пример 1 на IL:

.method private hidebysig static void Main() cil managed
{
   .entrypoint
   .maxstack 3
   .locals init (       [0] char[] c)
   L_0000: ldc.i4.s 100
   L_0002: newarr char
   L_0007: stloc.0
   L_0008: ldloc.0
   L_0009: ldc.i4.0
   L_000a: ldc.i4.s 0x66
   L_000c: stelem.i2
   L_000d: ret
}

Пример 2:

class Program
{
   static void Main()
   {
      int[] i = new int[100];
      i[0] = 5;
   }
}

Пример 2 на IL:

.method private hidebysig static void Main() cil managed
{
   .entrypoint
   .maxstack 3
   .locals init (        [0] int32[] i)
   L_0000: ldc.i4.s 100
   L_0002: newarr int32
   L_0007: stloc.0
   L_0008: ldloc.0
   L_0009: ldc.i4.0
   L_000a: ldc.i4.5
   L_000b: stelem.i4
   L_000c: ret
}

Пример 3:

class Program
{
   static void Main()
   {
      uint[] u = new uint[100];
      u[0] = 2;
   }
}

Пример 3 на IL:

.method private hidebysig static void Main() cil managed
{
   .entrypoint
   .maxstack 3
   .locals init (        [0] uint32[] u)
   L_0000: ldc.i4.s 100
   L_0002: newarr uint32
   L_0007: stloc.0
   L_0008: ldloc.0
   L_0009: ldc.i4.0
   L_000a: ldc.i4.s 0x66
   L_000c: stelem.i4
   L_000d: ret
}

Может ли определенный тип элементов работать быстрее, чем другие? Тест быстродействия stelem показывает, что тип данных unsigned int (т. е. Uint32) наиболее эффективный при установке значений в массиве. Тип char из трех тестов был самый медленный. Скорость тестировалась на 100 миллионах итераций, повторенных 100 раз.

  char array: 5208 ms
 int32 array: 4321 ms
uint32 array: 4125 ms

Stsfld. Эта инструкция сохраняет значение в статическом поле на вершине стека. В примере ниже будет показано, что код IL этого примера загружает значение константы 3 (ldc.i4.3) на вершину стека. Затем stsfld будет использоваться с адресом _a. Таким образом, значение на вершине стека сохраняется в ячейку, указанную _a.

// Программа C#, в которой используется инструкция stsfld.
using System;
 
class Program
{
   static int _a;
 
   static void Main()
   {
      Program._a = 3;
      if (Program._a == 4)
      {
         Console.WriteLine(true);
      }
   }
}

Соответствующий код IL:

.method private hidebysig static void  Main() cil managed
{
   .entrypoint
   // Code size       21 (0x15)
   .maxstack  8
   IL_0000:  ldc.i4.3
   IL_0001:  stsfld     int32 Program::_a
   IL_0006:  ldsfld     int32 Program::_a
   IL_000b:  ldc.i4.4
   IL_000c:  bne.un.s   IL_0014
   IL_000e:  ldc.i4.1
   IL_000f:  call       void [mscorlib]System.Console::WriteLine(bool)
   IL_0014:  ret
} // end of method Program::Main

Switch. Инструкция switch реализована с помощью таблицы переходов. Ключевое слово switch на языке C# обычно компилируется в инструкцию switch на языке IL. В примере ниже мы увидим, что инструкция switch использует таблицу меток IL_0020, IL_0027, IL_002e.

// Программа C#, использующая оператор switch.using System;class Program
{
   static void Main()
   {
      int i = int.Parse("1");
      switch (i)
      {
      case 0:
         Console.WriteLine(0);
         break;
      case 1:
         Console.WriteLine(1);
         break;
      case 2:
         Console.WriteLine(2);
         break;
      }
   }
}

Соответствующий код IL:

.method private hidebysig static void  Main() cil managed
{
   .entrypoint
   // Code size       53 (0x35)
   .maxstack  1
   .locals init ([0] int32 i,           [1] int32 CS$0$0000)
   IL_0000:  ldstr      "1"
   IL_0005:  call       int32 [mscorlib]System.Int32::Parse(string)
   IL_000a:  stloc.0
   IL_000b:  ldloc.0
   IL_000c:  stloc.1
   IL_000d:  ldloc.1
   IL_000e:  switch     (IL_0020,
                         IL_0027,
                         IL_002e)
   IL_001f:  ret
   IL_0020:  ldc.i4.0
   IL_0021:  call       void [mscorlib]System.Console::WriteLine(int32)
   IL_0026:  ret
   IL_0027:  ldc.i4.1
   IL_0028:  call       void [mscorlib]System.Console::WriteLine(int32)
   IL_002d:  ret
   IL_002e:  ldc.i4.2
   IL_002f:  call       void [mscorlib]System.Console::WriteLine(int32)
   IL_0034:  ret
} // end of method Program::Main

[Общие выводы]

Нет смысла каждый день исследовать промежуточный язык (IL). Он обычно делает все что нужно, без каких-либо замечаний. Однако рассмотрение IL открывает новые пути к пониманию, что делает код C#.

Использование static. Мы можем использовать static-класс в программе для упрощения нашего синтаксиса. В примере ниже показано использование статических классов System.Math и System.Console. Мы можем запускать вызов Math.Abs просто по имени функции "Abs", так как эту возможность предоставляет статический класс System.Math. Мы избегаем использование Console в вызове WriteLine. Для этого нужна директива "using static System.Console".

// Программа C#, показывающая использование синтаксиса static.
using static System.Math;
using static System.Console;
 
class Program
{
    static void Main()
    {
        int number = -100;
 
        // Использование функции Abs из статического класса System.Math:
        int result = Abs(number);
        // Использование WriteLine из статического класса System.Console:
        WriteLine(result);
    }
}

Вывод:

100

Концепция "статики" входит во многие языки. На C# мы не можем использовать static для описания локальных переменных. Static модифицирует место хранения поля - поле static идентифицирует исключительно одну ячейку, которая может совместно использоваться всеми экземплярами имеющегося закрытого типа класса.

На языках C и C++ существуют статические локальные переменные, представляя место хранения данных с постоянным временем жизни. Эта концепция не существует в языке C#. Однако const может использоваться в методе.

Подпрограммы доступа. Книжка "Code Complete" (автор Steve McConnell) показывает стратегии использования глобальных переменных в программах, не создающие проблем с поддержкой кода.

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

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

В примере используется 2 файла: один GlobalVar.cs содержит глобальные переменные в статическом классе, и другой Program.cs, который использует глобальный класс.

Класс для глобальных переменных, GlobalVar.cs:

/// < summary>
/// Содержит глобальные переменные для проекта.
/// < /summary>
public static class GlobalVar
{
   /// < summary>
   /// Глобальная переменная - константа.
   /// < /summary>
   public const string GlobalString = "Important Text";
 
   /// < summary>
   /// Статическое приватное значение, защищенное подпрограммой доступа.
   /// < /summary>
   static int _globalValue;
 
   /// < summary>
   /// Подпрограмма доступа для глобальной переменной.
   /// < /summary>
   public static int GlobalValue
   {
      get
      {
         return _globalValue;
      }
      set
      {
         _globalValue = value;
      }
   }
 
   /// < summary>
   /// Глобальное статическое поле.
   /// < /summary>
   public static bool GlobalBoolean;
}

C# program that uses global variables, Program.cs

using System;
 
class Program
{
   static void Main()
   {
      // Вывод глобальной строки-константы:
      Console.WriteLine(GlobalVar.GlobalString);
      // Установка глобальной целочисленной переменной:
      GlobalVar.GlobalValue = 400;
      // Установка глобального двоичного значения:
      GlobalVar.GlobalBoolean = true;
      // Вывод двух предыдущих значений переменных:
      Console.WriteLine(GlobalVar.GlobalValue);
      Console.WriteLine(GlobalVar.GlobalBoolean);
   }
}

Вывод:

Important Text
400
True

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

Переменная GlobalValue это свойство, у которого есть методы доступа get и set (accessors). Это подпрограммы доступа к хранилищу данных свойства. Хранилище это поле со ссылкой типа int. Поле _globalValue модифицируется аксессором set. Доступ к этому полю осуществляется аксессором get.

Класс глобальной переменной содержит глобальное поле типа Boolean. На языке C# тип bool является псевдонимом типа System.Boolean, который наследуется от типа System.ValueType.

Предупреждение: поле bool может вызвать проблемы в программе, если у него нет подпрограмм для доступа.

Подпрограммы доступа. Глобальные переменные используются более эффективно, когда к ним осуществляется доступ с помощью специальных подпрограмм (access routines). Вы можете использовать подпрограммы доступа в почти любом языке программирования. Методы доступа предоставляют еще один уровень абстракции о том, как Вы используете глобальные переменные.

В приведенном примере аксессор к свойству (get) это и есть подпрограмма доступа

Потоки. Статические поля могут привести к проблемам в многопоточном окружении. Подпрограмма доступа должна предоставить механизм блокировки с помощью оператора lock. Подробнее см. Global Variables ASP.NET site:dotnetperls.com, Lock site:dotnetperls.com.

Подобная проблема встречается во многих приложениях Windows Forms, которые используют потоки BackgroundWorker (см. BackgroundWorker site:dotnetperls.com).

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

Заключение: мы используем static-методы для доступа к данным, базируясь на типе (не на экземпляре типа!). С static-классами мы обрабатываем глобальные переменные и поля, существующие в единственном экземпляре. Static-конструкторы инициализируют данные, но могут приводить к генерации медленного кода. Static-определения могут привести к усложнению кода. Может произойти, что значения static-полей станет недопустимым. В таких ситуациях лучше использовать экземпляры класса.

[Ссылки]

1. Статические классы и члены статических классов (Руководство по программированию в C#) site:microsoft.com.
2. Статика в C# site:habrahabr.ru.
3. C# Static Method, Class and Constructor site:dotnetperls.com.

 

Добавить комментарий


Защитный код
Обновить

Top of Page