Visual Basic, .NET, ASP, VBScript
 

   
 
Описание для автора не найдено
 
     
   
 

Для поддержания широкого диапазона функциональных возможностей управляемая среда .NET Framework предоставляет разработчикам возможность совершенствовать модель программирования. Задачей рекомендаций по разработке библиотек для .NET Framework является поддержание совместимости и предсказуемости открытых членов API вместе с обеспечением возможности Web и межъязыковой интеграции. Настойчиво рекомендуется следовать этим рекомендациям при разработке классов и компонентов, расширяющих возможности .NET Framework. Несовместимая разработка неблагоприятно влияет на производительность разработчика. Инструментальные средства разработки и дополнения могут превратить некоторые из этих рекомендаций в предписывающие правила де факто и уменьшить количество несогласующихся компонентов. Несогласующиеся компоненты будут функционировать, но не на полную их мощность.

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

Взаимосвязь между Общей системой типов и Общеязыковой спецификацией

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

Общеязыковая спецификация (CLS) определяет ряд программно контролируемых правил, которые регулируют взаимодействие типов, созданных на разных языках программирования. Нацеливание на CLS - прекрасный способ обеспечения возможности межъязыкового взаимодействия. Разработчики управляемой библиотеки классов могут использовать CLS, чтобы гарантировать возможность работы их API с различными языками программирования. Обратите внимание, что хотя CLS и способствует разработке хорошей библиотеки, она не гарантирует ее. Более подробно смотрите в разделе MSDN Writing CLS-Compliant Code.

При выборе того, какие свойства включать в библиотеку классов, вы должны следовать двум основополагающим принципам по отношению к CLS:

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

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

  2. Определите вызывает ли раскрытие возможности сложности у компилятора

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

Рекомендации по присваиванию имен

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

Применение заглавных букв

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

Pascal нотация

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


BackColor

Camel нотация

Первая буква идентификатора в нижнем регистре, а первая буква каждого последующего соединенного слова заглавная. Например:


backColor

Врехний регистр

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


System.IO
System.Web.UI

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

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

Идентификатор Регистр Пример
Class Pascal AppDomain
Enum type Pascal ErrorLevel
Enum values Pascal FatalError
Event Pascal ValueChange
Exception class Pascal WebException
Примечание: всегда оканчивается суффиксом Exception.
Read-only Static field Pascal RedValue
Interface Pascal IDisposable
Примечание: всегда начинается с префикса I.
Method Pascal ToString
Namespace Pascal System.Drawing
Parameter Camel typeName
Property Pascal BackColor
Protected instance field Camel redValue
Примечание: Редко используется. В свойстве предпочтительнее применять protected instance field.
Public instance field Pascal RedValue
Примечание: Редко используется. В свойстве предпочтительнее применять public instance field.

Чувствительность к регистру

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

  • Не используйте имен, требующих чувствительности к регистру. Компоненты должны быть годными к использованию, как в чувствительных к регистру, так и в нечувствительных к регистру языках. Нечувствительные к регистру языки не могут различить имен, различающихся только регистром. Следовательно, вы должны предупредить возникновение такой ситуации в создаваемых компонентах или классах.
  • Не создавайте два пространства имен с именами, различающимися только регистром. Например, нечувствительный к регистру язык не сможет различить пространства имен, объявленные следующим образом:
    
    namespace ee.cummings;
    namespace Ee.Cummings;
    
  • Не создавайте функции, имена параметров которых различаются только регистром. Вот пример неверного задания функции:
    
    void MyFunction(string a, string A)
    
  • Не создавайте пространства имен, имена типов которых различаются только регистром. В следующем примере имена типов Point p и POINT p являются неправильными, потому что они различаются только регистром:
    
    System.Windows.Forms.Point p
    System.Windows.Forms.POINT p
    
  • Не создавайте типы, имена свойств которых различаются только регистром. В следующем примере имена int Color и int COLOR являются неправильными, потому что они различаются только регистром.
    
    int Color {get, set}
    int COLOR {get, set}
    
  • Не создавайте типы, названия методов которых различаются только регистром. В следующем примере названия методов calculate и Calculate являются неправильными, потому что они различаются только регистром:
    
    void calculate()
    void Calculate()
    

Аббревиатуры

Чтобы избежать неразберихи и гарантировать возможность межъязыкового взаимодействия, при использовании аббревиатур следуйте правилам:

  • Не используйте аббревиатуры или сокращения в качестве частей имен идентификаторов. Например, используйте GetWindow вместо of GetWin.
  • Используйте только общепринятые в компьютерной области акронимы.
  • По возможности используйте общеизвестные акронимы для замены длинных имен. Например, используйте UI для User Interface и OLAP для On-line Analytical Processing.
  • Используйте Pascal нотацию или camel нотацию для акронимов, чья длина более двух символов. Например, используйте HtmlButton или HTMLButton. Однако необходимо капитализировать акронимы, состоящие только из двух символов, например, System.IO, а не System.Io.
  • Не используйте аббревиатуры в именах идентификаторов или параметров. Для аббревиатур, состоящих более, чем из двух символов, даже если это не соответствует стандартной аббревиатуре слова, используйте camel нотацию.

Выбор слова

Избегайте использования имен классов, которые дублируют общепринятые пространства имен .NET Framework. Например, не используйте в качестве имен классов следующие имена: System, Collections, Forms или UI. Список имен, используемых в .NET Framework, приведен в разделе Class Library.

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

AddHandler AddressOf Alias And Ansi
As Assembly Auto Base Boolean
ByRef Byte ByVal Call Case
Catch CBool CByte CChar Cdate
CDec CDbl Char CInt Class
CLng CObj Const CShort CSng
CStr CType Date Decimal Declare
Default Delegate Dim Do Double
Each Else ElseIf End Enum
Erase Error Event Exit ExternalSource
False Finalize Finally Float For
Friend Function Get GetType Goto
Handles If Implements Imports In
Inherits Integer Interface Is Let
Lib Like Long Loop Me
Mod Module MustInherit MustOverride MyBase
MyClass Namespace New Next Not
Nothing NotInheritable NotOverridable Object On
Option Optional Or Overloads Overridable
Overrides ParamArray Preserve Private Property
Protected Public RaiseEvent ReadOnly ReDim
Region REM RemoveHandler Resume Return
Select Set Shadows Shared Short
Single Static Step Stop String
Structure Sub SyncLock Then Throw
To True Try TypeOf Unicode
Until volatile When While With
WithEvents WriteOnly Xor eval extends
instanceof package Var    

Предупреждение неразберихи с именами типов

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

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


void Write(double value);
void Write(float value);
void Write(long value);
void Write(int value);
void Write(short value);

Не создавайте язык-специфические имена методов, как в следующем примере.


void Write(double doubleValue);
void Write(float floatValue);
void Write(long longValue);
void Write(int intValue);
void Write(short shortValue);

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

Имя типа C# Имя типа Visual Basic Имя типа JScript Имя типа Visual C++ Ilasm.exe представление Универсальное имя типа
sbyte SByte sByte char int8 SByte
byte Byte byte unsigned char unsigned int8 Byte
short Short short short int16 Int16
ushort UInt16 ushort unsigned short unsigned int16 UInt16
int Integer int int int32 Int32
uint UInt32 uint unsigned int unsigned int32 UInt32
long Long long __int64 int64 Int64
ulong UInt64 ulong unsigned __int64 unsigned int64 UInt64
float Single float float float32 Single
double Double double double float64 Double
bool Boolean boolean bool bool Boolean
char Char char wchar_t Char Char
string String string String string String
object Object object Object object Object

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


double ReadDouble();
float ReadSingle();
long ReadInt64();
int ReadInt32();
short ReadInt16();

Использование имен из предыдущего примера более предпочтительно, чем использование следующих язык-специфических имен:


double ReadDouble();
float ReadFloat();
long ReadLong();
int ReadInt();
short ReadShort();

Рекомендации по присваиванию имени пространству имен

Общим правилом при присваивании имен пространствам имен является использование сочетания имени компании, названия технологии, feature и design (необязательно).


CompanyName.TechnologyName[.Feature][.Design]

Например:


Microsoft.Media
Microsoft.Media.Design

Использование имени компании или другой твердо установившейся марки в качестве префикса в имени пространства имен предохраняет от возможности появления двух пространств имен с одинаковыми именами. Например, Microsoft.Office - префикс, соответствующий классам Office Automation, поставляемым компанией Microsoft.

Используйте неизменное, узнаваемое название технологии на втором уровне иерархического имени. Используйте иерархию организации, как основу для иерархии пространства имен. Добавьте к имени пространства имен, содержащего типы, которые обеспечивают функциональность времени разработки для основного пространства имен, суффикс .Design. Например, пространство имен System.Windows.Forms.Design содержит дизайнеры и классы, используемые для разработки приложений, основанных на System.Windows.Forms.

Вложенные пространства имен должны иметь зависимость от типов содержащего их пространства имен. Например, классы в System.Web.UI.Design зависят от классов в System.Web.UI. Однако классы в System.Web.UI не зависят от классов в System.UI.Design.

Вы должны использовать Pascal нотацию для пространств имен и отдельные логические компоненты с точками, например, как Microsoft.Office.PowerPoint. Если ваша торговая марка использует нетрадиционную нотацию, используйте эту нотацию, даже если она отклоняется от предписанного к использованию Pascal нотации. Например, пространства имен NeXT.WebObjects и ee.cummings иллюстрируют соответствующие отклонения от правил Pascal нотации.

Используйте имена пространства имен в множественном числе, если это семантически подходит. Например, лучше использовать имя System.Collections, чем System.Collection. Исключением из этого правила являются имена торговых марок и аббревиатуры. Например, лучше использовать System.IO, а не System.IOs.

Не используйте одинаковые имена для класса и пространства имен. Например, не создавайте и пространство имен Debug, и класс Debug.

И в заключение, обратите внимание, что имя пространства имен не обязательно должно дублировать имя сборки. Например, если вы называете сборку MyCompany.MyTechnology.dll, это не значит что она должна содержать пространство имен MyCompany.MyTechnology.

Рекомендации по присваиванию имен классам

Вот основные рекомендации по присваиванию имен классам:

  • В качестве имени класса используйте существительное или именную группу.
  • Используйте Pascal нотацию.
  • Редко используйте аббревиатуры.
  • Не используйте префикс типа, такой как в C/С++, в имени класса. Например, лучше используйте имя класса FileStream, чем CFileStream.
  • Не используйте символ подчеркивания (_).
  • Иногда необходимо обеспечить имя класса, начинающееся с буквы I, даже если класс не является интерфейсом. Это допустимо только в случае, если слово, являющееся частью имени класса, начинается с буквы I. Например, имя класса IdentityStore.
  • Где возможно, используйте составное слово в качестве имени наследуемого класса. Вторая часть имени наследуемого класса должна быть именем основного класса. Например, ApplicationException - имя, соответствующее для класса, наследуемого от класса Exception, потому что ApplicationException является разновидностью Exception. Применяйте это правило разумно. Например, Button - подходящее имя для класса, наследуемого от Control. Хотя кнопка является разновидностью элемента управления, добавление слова Control в имя класса просто излишне удлинит это имя.

Далее приведены примеры правильно названных классов.


public class FileStream
public class Button
public class String

Рекомендации по присваиванию имен интерфейсам

Здесь приведены основные рекомендации по присваиванию имен интерфейсам:

  • В качестве имен интерфейсов используйте существительные или именные группы, или прилагательные, описывающие поведение. Например, в имени интерфейса IComponent используется описательное существительное; в имени ICustomAttributeProvider используется именная группа; в имени IPersistable используется прилагательное.
  • Используйте Pascal нотацию.
  • Редко используйте аббревиатуры.
  • Имя интерфейса начинайте с буквы I, чтобы обозначить то, что это интерфейс.
  • Используйте подобные имена при определении пары класс/интерфейс, где класс является стандартной реализацией интерфейса. Имена должны различаться только префиксом I в имени интерфейса.
  • Не используйте символ подчеркивания (_).

Далее приведены примеры корректных имен интерфейсов.


public interface IServiceProvider
public interface IFormatable

Следующий пример кода демонстрирует, как определить интерфейс IComponent и его стандартную реализацию, класс Component.


public interface IComponent 
{
...
}
public class Component: IComponent 
{
...
}

Рекомендации по присваиванию имен атрибутам

Вы всегда должны добавлять суффикс Attribute к именам специальных классов атрибутов. Ниже приведен пример корректного имени класса атрибута.


public class ObsoleteAttribute{}

Рекомендации по присваиванию имен перечисляемым типам

Перечисляемый тип наследуется от класса Enum. Вот основные рекомендации по присваиванию имен перечисляемым типам:

  • Используйте Pascal нотацию для типов Enum и имен значений.
  • Редко используйте аббревиатуры.
  • Не используйте суффикс Enum для имен типа Enum.
  • Используйте слова в единственном числе в качестве имен для большинства типов Enum, но используйте множественное число для имен типов Enum, которые являются битовыми полями.
  • Всегда добавляйте FlagsAttribute в битовое поле типа Enum.

Рекомендации по присваиванию имен статическим полям

При присваивании имен статическим полям руководствуйтесь следующими рекомендациями:

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

Рекомендации по присваиванию имен параметрам

Далее приведены основные рекомендации по присваиванию имен параметрам:

  • Используйте описательные имена параметров. Имена параметров должны быть достаточно описательными для того, чтобы по имени параметра и его типа можно было бы определить их значения в большинстве сценариев.
  • Используйте Сamel нотацию для имен параметров.
  • Используйте имена, описывающие значение параметра, а не его тип. Инструментальные средства разработки должны обеспечивать ясную информацию о типе параметра. Поэтому лучше использовать имя параметра, как описание. Старайтесь редко использовать имена параметров, основанные на их типе, и только в тех случаях, где это действительно необходимо.
  • Не используйте зарезервированные параметры. Они являются private параметрами, которые могут быть раскрыты в будущих версиях, если понадобиться. Вместо этого, если в последующих версиях вашей библиотеки классов требуется большее количество данных, добавьте новое переопределение для метода.
  • Не используйте в качестве префикса для имени параметра венгерскую нотацию.

Далее приведены примеры корректных имен параметров.


Type GetType(string typeName)
string Format(string format, args() As object)

Рекомендации по присваиванию имен методам

Основные рекомендации по присваиванию имен методам следующие:

  • Используйте глаголы или глагольные группы в качестве имен методов.
  • Используйте Pascal нотацию.

Далее приведены примеры корректных имен методов.


RemoveAll()
GetCharArray()
Invoke()

Рекомендации по присваиванию имен свойствам

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

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

В следующем примере кода продемонстрировано корректное присваивание имен свойствам.


public class SampleClass
{
   public Color BackColor 
   {
      // Code for Get and Set accessors goes here.
   }
}

В следующем примере кода приведено свойство, имеющее такое же имя, как и имя его типа.


public enum Color 
{
   // Insert code for Enum here.
}
public class Control 
{
   public Color Color 
   { 
      get {// Insert code here.} 
      set {// Insert code here.} 
   }
}

Далее следует пример неправильного присваивания имени , потому что свойству типа Integer присвоено имя Color.


public enum Color {// Insert code for Enum here.}
public class Control 
{
   public int Color 
   { 
      get {// Insert code here.} 
      set {// Insert code here.}  
   }
}

В последнем примере невозможно обратиться к членам перечисления Color. Color.Xxx будет интерпретировано, как обращение к члену, который сначала получает значение свойства Color (типа Integer в Visual Basic или типа int в C#), а затем обращается к члену, имеющему такое значением (который должен быть отдельным членом System.Int32).

Рекомендации по присваиванию имен событиям

Ниже приведены основные рекомендации по присваиванию имен событиям:

  • Используйте суффикс EventHandler в именах обработчиков событий.
  • Определите два параметра с именами sender и e. Параметр sender представляет объект, который провоцирует событие. Тип параметра sender всегда object, даже если есть возможность использовать более специфический тип. Состояние, ассоциированное с событием, инкапсулировано в экземпляр класса события, названного e. В качестве типа параметра e используйте соответствующий и специфический класс события.
  • В имени класса аргумента события используйте суффикс EventArgs.
  • Ассоциируйте название события с глаголом.
  • Используйте герундий ("ing" форму глагола), чтобы создать имя события, выражающее событие до, и используйте прошедшую форму глагола для представления события после. Например, событие Close, которое может быть отменено, должно иметь событие Closing и событие Closed. Не используйте шаблоны имен BeforeXxx/AfterXxx.
  • Не используйте префикс или суффикс при декларации события в типе. Например, используйте Close вместо OnClose.
  • Вы должны обеспечить protected метод с именем OnXxx в типах с событиями, которые могут быть переопределены в наследуемых классах. Этот метод должен иметь только параметр события e, потому что отправитель всегда является экземпляром типа.

Следующий пример иллюстрирует обработчик события с соответствующими именем и параметрами.


public delegate void MouseEventHandler(object sender, MouseEventArgs e);

Следующий пример иллюстрирует класс аргумента события с корректным именем.


public class MouseEventArgs : EventArgs 
{
   int x;
   int y;
   public MouseEventArgs(int x, int y) 
      { this.x = x; this.y = y; }
   public int X { get { return x; } } 
   public int Y { get { return y; } } 
}

Рекомендации по использованию членов класса

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

Рекомендации по использованию свойств

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

Выберите имя для свойства на основе рекомендаций, приведенных в разделе Принципы присваивания имен свойствам. Не создавайте свойство с таким же именем, как и у существующего типа, это обусловливает неоднозначности в некоторых языках программирования. Например, System.Windows.Forms.Control имеет свойство цвета. Т.к. также существует и структура Color, свойство цвета в System.Windows.Forms.Control названо BackColor. Это имя более выразительное и не конфликтует с именем структуры Color.

Возможны ситуации, в которых вам придется нарушить это правило. Например, класс System.Windows.Forms.Form содержит свойство Icon несмотря на то, что в .NET Framework также существует класс Icon. Это имя использовано потому, что Form.Icon более открытое и понятное имя для свойства, чем Form.FormIcon или Form.DisplayIcon.

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

Вопросы состояния свойства

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

Например, элемент управления TextBox может иметь два взаимосвязанных свойства: DataSource и DataField. DataSource определяет имя таблицы, а DataField - имя столбца. Как только оба свойства определены, элемент управления может автоматически связывать данные таблицы со свойством Text. Следующий пример кода иллюстрирует свойства, которые могут задаваться в любом порядке:


TextBox t = new TextBox();
t.DataSource = "Publishers";
t.DataField = "AuthorID";
// The data-binding feature is now active.

Вы можете определить свойства DataSource и DataField в любом порядке. Таким образом, следующий код эквивалентен предыдущему:


TextBox t = new TextBox();
t.DataField = "AuthorID";
t.DataSource = "Publishers";
// The data-binding feature is now active.

Можно также присвоить свойству значение null (Nothing в Visual Basic), чтобы обозначить, что значение не определено.


TextBox t = new TextBox();
t.DataField = "AuthorID";
t.DataSource = "Publishers";
// The data-binding feature is now active.
t.DataSource = null;
// The data-binding feature is now inactive.

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


public class TextBox
{
   string dataSource;
   string dataField;
   bool active;

   public string DataSource
   {   
      get
      {
         return dataSource;
      }
      set
      {
         if (value != dataSource)
         {
            // Set the property value first, in case activate fails.
            dataSource = value;                
            // Update active state.
            SetActive(dataSource != null && dataField != null);
         }
      }
      }
      
   public string DataField
   {
      get
      {
         return dataField;
      }
      set
      {
         if (value != dataField)
         {
            // Set the property value first, in case activate fails.
            dataField = value;              
            // Update active state.
            SetActive(dataSource != null && dataField != null);
         }
      }
   }   
   void SetActive(Boolean value)
   {
      if (value != active)
      {
         if (value)
         {
            Activate();
            Text = dataBase.Value(dataField);
         }
         else
         {
            Deactivate();
            Text = "";
         }
         // Set active only if successful.
         active = value; 
      }
   }
   void Activate()
   {
      // Open database.
   }
      
   void Deactivate()
   {
      // Close database.
   }
}

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


dataSource != null && dataField != null

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


void UpdateActive()
{
   SetActive(dataSource != null && dataField != null);
}

Если у вас есть взаимосвязанные свойства, такие как DataSource и DataMember, вы должны реализовать интерфейс ISupportInitialize. Это позволит разработчику (или пользователю) вызывать методы ISupportInitialize.BeginInit и ISupportInitialize.EndInit при определении множества свойств, чтобы дать возможность компоненту предоставлять оптимизации. В примере, приведенном выше, ISupportInitialize может предотвратить ненужные попытки доступа к базе данных до тех пор пока установка не завершена корректно.

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

Вызов событий изменения свойства

События изменения свойств должны вызываться в компонентах, если надо уведомлять потребителей о программном изменении свойства компонента. По соглашению о присваивании имен имена событий изменения свойства формируются из имени свойства и суффикса Changed, как TextChanged. Например, элемент управления вызывает событие TextChanged, когда изменяется его свойство text. Чтобы вызвать это событие, можно использовать protected вспомогательную процедуру Raise<Property>Changed. Однако, возможно, не стоит вызывать событие изменения свойства для добавления элемента хэш-таблицы, т.к. это влечет за собой лишние затраты производительности. Следующий пример иллюстрирует реализацию вспомогательной процедуры для события произошедшего изменения свойства.


class Control: Component
{
   string text;
   public string Text
   { 
      get
      { 
         return text; 
      }
      set
      {
         if (!text.Equals(value))
         {
            text = value;
            RaiseTextChangedEvent();
         }
      }
   }
}

При связывании данных этот шаблон используется для того, чтобы обеспечить возможность двухстороннего связывания свойства. Без событий <Property>Changed и Raise<Property>Changed связывание данных работает в одном направлении: если изменяется база данных, свойство обновляется. Каждое свойство, вызывающее событие <Property>Changed, должно предоставлять метаданные для подтверждения того, что свойство поддерживает связывание данных.

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

Хорошим примером в данном случае является свойство Text элемента управления Edit. По мере того как пользователь записывает информацию в элемент управления, значение свойства автоматически изменяется. Событие вызывается до того, как значение свойства изменилось. Оно не передает старое или новое значение, и разработчик может отменить событие, сформировав исключительную ситуацию. Имя события состоит из имени свойства и суффикса Changing. Далее приведен пример события происходящего изменения.


class Edit : Control 
{
   public string Text 
   { 
      get 
      { 
         return text; 
      }
      set 
      {
         if (text != value) 
         {
            OnTextChanging(Event.Empty);
            text = value;
         }
      }
   }
}

Событие также вызывается после того, как значение свойства уже изменилось. Это событие не может быть отменено. Имя события формируется из имени свойства и суффикса Changed. Также должно вызываться характерное событие PropertyChanged. Шаблоном для вызова этих событий является вызов особого события из метода OnPropertyChanged. Следующий пример иллюстрирует использование метода OnPropertyChanged.


class Edit : Control 
{
   public string Text 
   {
      get 
      { 
         return text; 
      }
      set 
      {
         if (text != value) 
         {
            OnTextChanging(Event.Empty);
            text = value;
            RaisePropertyChangedEvent(Edit.ClassInfo.text);
         }
      }
   }

   protected void OnPropertyChanged(PropertyChangedEvent e) 
   {
      if (e.PropertyChanged.Equals(Edit.ClassInfo.text))
         OnTextChanged(Event.Empty);
      if (onPropertyChangedHandler != null)
         onPropertyChangedHandler(this, e);
   }
}

Возможны ситуации, когда базовое значение свойства не сохраняется как поле, что осложняет отслеживание изменений значения. При вызове события происходящего изменения найдите все места, на которые окажет влияние изменение значения события, и обеспечьте возможность отменить событие. Например, предыдущий пример элемента управления Edit не вполне правильный, потому что значение Text в действительности сохраняется в описателе окна (HWND). Для вызова события TextChanging вы должны проверить сообщения Windows, чтобы определить, когда может измениться текст, и предусмотреть появление исключительной ситуации в OnTextChanging, чтобы отменить событие. Если обеспечить событие происходящего изменения слишком сложно, разумно будет поддерживать только событие произошедшего изменения.

Свойства и методы

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

  • Используйте свойство, когда член класса является логическим членом данных. В следующем примере Name - это свойство, потому что это - логический член класса.
    
    public string Name
       get 
       {
          return Name;
       }
       set 
       {
          Name = value;
       }
    
  • Используйте метод, когда:
    • операция является преобразованием, как Object.ToString;
    • операция требует довольно больших затрат, и вы хотите сообщить пользователю о том, что они должны рассмотреть возможность кеширования результата;
    • получение значения свойства, используя аксессор get, будет иметь ощутимый побочный эффект;
    • вызов члена два раза подряд выдает разные результаты;
    • важен порядок выполнения. Обратите внимание, что свойства типа должны устанавливаться и возвращать значение в любом порядке;
    • член класса статический, но возвращает значение, которое может быть изменено;
    • член класса возвращает массив. Свойства, возвращающие массивы, могут быть очень обманчивыми. Обычно важно возвращать копию внутреннего массива, чтобы пользователь не мог изменить внутреннее состояние. Это в совокупности с тем фактом, что пользователь запросто может принять его, как индексированное свойство, приводит к созданию неэффективного кода. В следующем примере каждый вызов свойства Methods создает копию массива. В результате в цикле создается 2n+1 копия массива.

Type type = // Get a type.
for (int i = 0; i < type.Methods.Length; i++)
{
   if (type.Methods[i].Name.Equals ("text"))
   {
      // Perform some operation.
   }
}

Следующий пример иллюстрирует правильное применение свойств и методов.


class Connection
{
   // The following three members should be properties
   // because they can be set in any order.
   string DNSName {get{};set{};}
   string UserName {get{};set{};}
   string Password {get{};set{};}

   // The following member should be a method
   // because the order of execution is important.
   // This method cannot be executed until after the 
   // properties have been set.
   bool Execute ();
}

Свойства только для чтения и только для записи

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

Использование индексированных свойств

Следующие правила описывают основные принципы использования индексированных свойств:

  • используйте только одно индексированное свойство в классе и делайте его индексированным свойством по умолчанию для этого класса.
  • Не используйте индексированных свойств не по умолчанию.
  • Присваивайте индексированному свойству имя Item. Например, свойство DataGrid.Item. Всегда следуйте этому правилу за исключением случаев, когда существует более подходящее имя, например, такое как свойство Chars в классе String.
  • Используйте индексированное свойство, когда логические члены данных свойства являются массивом.
  • Не поставляйте индексированное свойство и метод, семантически эквивалентные двум или более переопределяемым методам. В следующем примере свойство Method надо заменить методом GetMethod (строка).

// Change the MethodInfo Type.Method property to a method.
MethodInfo Type.Method[string name]
MethodInfo Type.GetMethod (string name, Boolean ignoreCase)
// The MethodInfo Type.Method property is changed to
// the MethodInfo Type.GetMethod method.
MethodInfo Type.GetMethod(string name)
MethodInfo Type.GetMethod (string name, Boolean ignoreCase)

Рекомендации по использованию событий

Здесь приведены основные рекомендации по использованию событий:

  • Выберите имя для вашего события, руководствуясь рекомендациями, приведенными в разделе Принципы присваивания имен событиям.
  • Не используйте венгерскую нотацию.
  • При обращении к событиям в документации вместо выражений "событие было запущено" или "событие было инициировано" используйте фразу "событие было вызвано".
  • В языках, поддерживающих ключевое слово void, используйте void тип возвращаемого результата для обработчиков событий, как показано в следующем примере кода на C#.
    
    public delegate void MouseEventHandler(object sender, MouseEventArgs e);
    
  • Классы событий должны наследоваться от класс System.EventArgs, как показано в следующем примере.
    
    public class MouseEvent: EventArgs {}
    
  • Реализовывайте обработчик события, используя синтаксис public EventHandler Click. Для добавления и удаления обработчиков событий используйте аксессоры add и remove. Если используемый вами язык программирования не поддерживает этого синтаксиса, назовите методы add_Click и remove_Click.
  • Если класс вызывает множество событий, компилятор генерирует одно поле для каждого экземпляра делегата события. Если событий много, сохранение отдельного поля для каждого делегата неприемлемо. Для таких случаев .NET Framework предоставляет конструкцию, называемую свойством событий, которую можно использоваться вместе с другой структурой данных (по вашему усмотрению) для сохранения делегатов события. Далее приведен пример того, как класс Component реализовывает эту пространство сберегающую технологию для сохранения обработчиков.
    
    public class MyComponent : Component 
    {
       static readonly object EventClick = new object();
       static readonly object EventMouseDown = new object();
       static readonly object EventMouseUp = new object();
       static readonly object EventMouseMove = new object();
       
       public event MouseEventHandler Click 
       {
          add
          {
             Events.AddHandler(EventClick, value);
          }
         remove
         {
             Events.RemoveHandler(EventClick, value);
          }
       }
       // Code for the EventMouseDown, EventMouseUp, and 
       // EventMouseMove events goes here.
    
       // Define a private data structure to store the event delegates
    }
    
  • Чтобы вызывать любое событие, используйте protected (Protected в Visual Basic) virtual метод. Эта технология не подходит для sealed классов, потому что от них не возможно наследоваться. Методы используются для того, чтобы обеспечить наследуемому классу способ обработки события, используя переопределение. Это более естественно, чем использование делегатов в тех случаях, когда разработчик создает наследуемый класс. Имя метода принимает форму OnEventName, где EventName - имя вызванного события. Например:
    
    public class Button 
    {
       ButtonClickHandler onClickHandler;
          
       protected virtual void OnClick(ClickEvent e) 
       {
          // Call the delegate if non-null.
          if (onClickHandler != null)
                onClickHandler(this, e);      
       }
    }
    

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

  • Вы должны предполагать, что обработчик событий может содержать любой код. Классы должны быть готовы к тому, что обработчик событий может осуществлять практически любую операцию, а объект во всех случаях после вызова события должен оставаться в соответствующем состоянии. Используйте блок try/finally в той точке кода, где вызывается событие. Т.к. разработчик может выполнять функцию обратного вызова в объекте для того, чтобы осуществить другие действия, ничего не предполагайте по поводу состояния объекта, когда элемент управления возвращается в точку, в которой возникло событие. Например:
    
    public class Button
    {
       ButtonClickHandler onClickHandler;
    
       protected void DoClick()
       {
          // Paint button in indented state.
          PaintDown();
             try
          {
             // Call event handler.
             OnClick();         
          }
          finally
          {
             // Window might be deleted in event handler.
             if (windowHandle != null)
                // Paint button in normal state.
                PaintUp();            
          }
       }
    
       protected virtual void OnClick(ClickEvent e)
       {
          if (onClickHandler != null)
             onClickHandler(this, e);
       }
    }
    
  • Используйте или расширьте класс System.ComponentModel.CancelEventArgs, чтобы обеспечить возможность разработчику контролировать поведение объекта, свойственное ему по умолчанию. Например, элемент управления TreeView вызывает CancelEvent тогда, когда пользователь хочет изменить метку узла. В следующем примере проиллюстрировано, как разработчик может использовать это событие для того, чтобы предотвратить редактирование узла.
    
    public class Form1: Form 
    {
       TreeView treeView1 = new TreeView();
    
       void treeView1_BeforeLabelEdit(object source, 
          NodeLabelEditEvent e) 
       {
          e.cancel = true;
       }
    }
    

    Обратите внимание, что в данном случае не возникает сообщения об ошибке. Метка является свойством только для чтения.

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

    
    public class Form1: Form 
    {
       Edit edit1 = new Edit();
    
       void edit1_TextChanging(object source, Event e) 
       {
          throw new RuntimeException("Invalid edit");
       }
    

Рекомендации по использованию методов

Здесь приведены основные рекомендации по использованию методов:

  • Выберите имя для события на основе рекомендаций, приведенных в разделе Рекомендации по присваиванию имен методам.
  • Не используйте венгерскую нотацию.
  • По умолчанию методы не виртуальные. Сохраняйте это в тех ситуациях, когда нет необходимости предоставлять виртуальные методы. Более подробная информация о реализации наследования приведена в разделе Рекомендации по использованию базовых классов.

Рекомендации по перезагрузке методов

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

  • Используйте перезагрузку метода, чтобы обеспечить различные методы, которые делают семантически одинаковые вещи.
  • Используйте перезагрузку метода вместо аргументов, используемых по умолчанию. Эти аргументы плохо поддерживают версии и поэтому не допускаются в Общеязыковой спецификации (CLS). Следующий пример кода иллюстрирует перезагруженный метод String.IndexOf.
    
    int String.IndexOf (String name);
    int String.IndexOf (String name, int startIndex);
    
  • Правильно используйте значения по умолчанию. В семействе перезагруженных методов комплексный метод должен использовать такие имена параметров, которые отражают отличие от состояния по умолчанию, принятого в простом методе. Например:
    
    // Method #1: ignoreCase = false.
    MethodInfo Type.GetMethod(String name);
    // Method #2: Indicates how the default behavior of method #1 is being // changed.
     MethodInfo Type.GetMethod (String name, Boolean ignoreCase);
    
  • Используйте последовательное расположение и шаблоны присваивания имен для параметров методов. Обычно ряд перезагруженных методов обеспечивается увеличивающимся количеством параметров для того, чтобы предоставить возможность разработчику определить желаемый уровень информации. Чем больше параметров вы определите, тем больше деталей может определить разработчик. В следующем примере перезагруженный метод Execute имеет последовательный порядок параметров и различные варианты шаблонов присваивания имен. Каждый из вариантов метода Execute использует одну и ту же семантику для совместно используемого набора параметров.
    
    public class SampleClass
    {
       readonly string defaultForA = "default value for a";
       readonly string defaultForB = "default value for b";
       readonly string defaultForC = "default value for c";
       
       public void Execute()
       {
          Execute(defaultForA, defaultForB, defaultForC);
       }
                
       public void Execute (string a)
       {
          Execute(a, defaultForB, defaultForC);
       }
             
       public void Execute (string a, string b)
       {
          Execute (a, b, defaultForC);     
       }
       
       public virtual void Execute (string a, string b, string c)
       {
          Console.WriteLine(a);
          Console.WriteLine(b);
          Console.WriteLine(c);
          Console.WriteLine();
       } 
    }
    

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

  • Используйте перезагрузку метода для переменного числа параметров. Используйте соглашение по объявлению n методов с увеличивающимся количеством параметров, когда необходимо определить переменное число параметров в методе. Для числа методов больше n, предоставьте метод, который принимает массив значений. Например, n=3 или n=4 подходит в большинстве случаев. Следующий пример иллюстрирует эту схему.
    
    public class SampleClass
    {
       public void Execute(string a)
       {
          Execute(new string[] {a});
       }
       
       public void Execute(string a, string b)
       {
          Execute(new string[] {a, b});
       }
       
       public void Execute(string a, string b, string c)
       {
          Execute(new string[] {a, b, c});
       }
    
       public virtual void Execute(string[] args)
       {
          foreach (string s in args)
          {
             Console.WriteLine(s);
          }
       } 
    }
    
  • Если вы должны обеспечить возможность переопределения метода, делайте только наиболее полную перезагрузку и определите другие операции на ее основе. Следующий пример иллюстрирует эту схему.
    
    public class SampleClass
    {
       private string myString;
    
       public MyClass(string str)
       {
          this.myString = str;
       }
       
       public int IndexOf(string s) 
       {
          return IndexOf (s, 0);
       }
    
       public int IndexOf(string s, int startIndex) 
       {
          return IndexOf(s, startIndex, myString.Length - startIndex );
       }
    
       public virtual int IndexOf(string s, int startIndex, int count) 
       {
          return myString.IndexOf(s, startIndex, count);
       }
    }
    

Методы с переменным количеством аргументов

Вы, возможно, захотите создать метод, имеющий переменное количество аргументов. Классическим примером этого является метод printf в языке программирования С. В управляемых библиотеках классов для этого структурного компонента используйте ключевое слово params (ParamArray в Visual Basic). Например, вместо нескольких перезагружаемых методов используйте следующий код:


void Format(string formatString, params object [] args)

Рекомендации по использованию конструктора

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

  • Если в классе присутствуют только статические методы и свойства, обеспечьте private конструктор, применяемый по умолчанию. В следующем примере private конструктор препятствует созданию класса.
    
    public sealed class Environment
    {
       // Private constructor prevents the class from being created.
       private Environment()
       {
          // Code for the constructor goes here.
       }
    }
    
  • Сведите к минимуму работу, выполняемую в конструкторе. Конструкторы должны только фиксировать параметр или параметры конструктора. Это откладывает затраты на выполнение следующих операций до тех пор, пока пользователь использует специальные возможности экземпляра.
  • Предоставьте protected (Protected в Visual Basic) конструктор, который может использоваться типами в наследуемых классах.
  • Рекомендуется не поставлять пустой конструктор для структурных типов. Если конструктор не поставляется, среда выполнения устанавливает все поля struct в ноль. Это увеличивает скорость создания массива и статического поля.
  • Параметры в конструкторе используйте, как быстрый способ для задания свойств. Нет разницы в семантике между использованием пустого конструктора с аксессорами свойства set, следующими за ним, и использованием конструктора с множеством аргументов. Следующие три примера кода эквивалентны:
    
    // Example #1.
    Class SampleClass = new Class();
    SampleClass.A = "a";
    SampleClass.B = "b";
    
    // Example #2.
    Class SampleClass = new Class("a");
    SampleClass.B = "b";
    
    // Example #3.
    Class SampleClass = new Class ("a", "b");
    
  • Для параметров конструктора используйте последовательное расположение и шаблоны присваивания имен. Общепринятым является предоставлять большое количество параметров для того, чтобы дать возможность разработчику определить необходимый уровень информации. Чем больше параметров вы установите, тем больше деталей сможет определить разработчик. В следующем примере кода последовательное расположение и присваивание имен параметров применяется для всех конструкторов SampleClass.
    
    public class SampleClass 
    {
       private const string defaultForA = "default value for a";
       private const string defaultForB = "default value for b";
       private const string defaultForC = "default value for c";
       
       private string a;
       private string b;
       private string c;
          
       public MyClass():this(defaultForA, defaultForB, defaultForC) {}
       public MyClass (string a) : this(a, defaultForB, defaultForC) {}
       public MyClass (string a, string b) : this(a, b, defaultForC) {}
       public MyClass (string a, string b, string c)
       { 
          this.a = a;
          this.b = b;
          this.c = c;
       }
    }
    

Рекомендации по использованию полей

Вот основные рекомендации по использованию полей:

  • Не используйте public или protected (Public или Protected в Visual Basic) поля экземпляра. Если отменить раскрытие полей непосредственно разработчиком, поддержание версий классами может быть упрощено потому, что поле не сможет преобразовываться в свойство пока поддерживает двоичную совместимость. Рассмотрите возможность применения к полям аксессоров свойств get и set вместо того, чтобы делать их public. Наличие выполняемого кода в аксессорах свойств get и set делает возможными последующие усовершенствования, такие как создание объекта по запросу при использовании свойства или при нотификации изменения свойства. Следующий пример кода иллюстрирует правильное использование private полей экземпляра с аксессорами свойств get и set.
    
    public struct Point
    {
       private int xValue;
       private int yValue;
       
       public Point(int x, int y)
       {
          this.xValue = x;
          this.yValue = y;
       }
    
       public int X
       {
          get
          {
             return xValue; 
          }
          set
          { 
             xValue = value; 
          }
       }
       public int Y
       {
          get
          { 
             return yValue; 
          }
          set
          { 
             yValue = value; 
          }
       }
    }
    
  • Откройте поле для наследуемого класса, используя protected свойство, которое возвращает значение поля.
    
    public class Control: Component
    {
       private int handle;
       protected int Handle
       {
          get
          { 
             return handle; 
          }
       }
    }
    
  • Рекомендуется использовать статические поля только для чтения вместо свойств, в которых значение является глобальной константой.
    
    public struct Int32
    {
       public static readonly int MaxValue = 2147483647;
       public static readonly int MinValue = -2147483648;
       // Insert other members here.
    }
    
  • Не сокращайте слова, входящие в название поля. Используйте только общепринятые аббревиатуры. Не используйте заглавные буквы в названиях полей. Ниже приведен пример правильного присваивания имен полям.
    
    class SampleClass 
    {
       string url;
       string destinationUrl;
    }
    
  • Не используйте венгерскую нотацию для имен полей. Хорошие имена описывают семантику, а не тип.
  • Не применяйте префикс в именах полей или статических полей. А именно, не применяйте префикс в имени поля для того, чтобы различить статическое и нестатическое поле. Например, применение префиксов g_ или s_ неверно.
  • Используйте public статические поля только для чтения для предопределенных экземпляров объекта. Если есть предопределенные экземпляры объекта, объявите их, как public статические поля только для чтения самого объекта. Используйте Pascal нотацию, потому что поля public. Следующий пример кода иллюстрирует правильное использование public статических полей только для чтения.
    
    public struct Color
    {
       public static readonly Color Red = new Color(0x0000FF);
       public static readonly Color Green = new Color(0x00FF00);
       public static readonly Color Blue = new Color(0xFF0000);
       public static readonly Color Black = new Color(0x000000);
       public static readonly Color White = new Color(0xFFFFFF);
    
       public Color(int rgb)
       { // Insert code here.}
       public Color(byte r, byte g, byte b)
       { // Insert code here.}
    
       public byte RedValue 
       { 
          get
          {
             return Color;
          }
       }
       public byte GreenValue 
       { 
          get
          {
             return Color;
          }
       }
       public byte BlueValue
       { 
          get
          {
             return Color;
          }
       }
    }
    

Рекомендации по использованию параметров

Далее приведены основные рекомендации по использованию параметров:

  • Найдите действительные аргументы параметра. Осуществите проверку правильности аргумента для каждого аксессора set public или protected метода и свойства. Сформируйте значимые исключительные ситуации для разработчика в случае появления неправильных аргументов параметров. Используйте класс System.ArgumentException или класс, наследуемый от System.ArgumentException. Далее приведен пример проверки действительных аргументов параметра и формирования значимых исключительных ситуаций.
    
    class SampleClass
    {
       public int Count
       {
          get
          {
             return count;
          }
          set
          {
                // Check for valid parameter.
                if (count < 0 || count >= MaxValue)
                   throw newArgumentOutOfRangeException(
                      Sys.GetString(
                         "InvalidArgument","value",count.ToString()));
          }
       }
    
       public void Select(int start, int end)
       {
          // Check for valid parameter.
          if (start < 0)
             throw new ArgumentException(
                   Sys.GetString("InvalidArgument","start",start.ToString()));
          // Check for valid parameter.
          if (end < 0)
             throw new ArgumentException(
                   Sys.GetString("InvalidArgument","end",end.ToString()));
       }
    }
    

    Обратите внимание, что действительная проверка не обязательно должна пройти в самом public или protected методе. Она может иметь место на более низком уровне в private методах.

Рекомендации по использованию типов

Типы - это элементы инкапсуляции в общеязыковой среде выполнения. Детальное описание полного списка типов данных, поддерживаемых средой выполнения, приведено в разделе MSDN Common Type System. В этом разделе представлены рекомендации по использованию основных типов.

Рекомендации по использованию основных классов

Класс - это наиболее общий вид типа. Класс может быть абстрактным или sealed. Чтобы предоставить реализацию, абстрактному классу нужен наследуемый класс. От sealed класса наследоваться невозможно. Рекомендуется использовать классы через другие типы.

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

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

Сравнение базовых классов и интерфейсов

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

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

Интерфейсы подходят для следующих ситуаций:

  • Несколько неродственных классов хотят поддерживать протокол.
  • Эти классы уже имеют установленные базовые классы (например, некоторые являются элементами управления пользовательского интерфейса (UI), а некоторые - XML Web сервисами).
  • Агрегирование не подходит или не целесообразно.

Во всех других ситуациях наилучшей моделью является наследование классов.

Protected методы и конструкторы

Обеспечивают создание произвольных классов через protected методы. Public интерфейс базового класса должен предоставлять потребителю класса богатый набор функциональных возможностей. Однако для обеспечения этого богатого набора функциональных возможностей пользователи класса часто стремятся реализовать как можно меньшее число методов. Для достижения этой цели, предоставьте ряд невиртуальных или final public методов, которые вызывают единственный protected метод, обеспечивающий реализации методов. Этот метод должен быть отмечен суффиксом Impl. Этот процесс продемонстрирован в следующем примере кода.


public class MyClass
{
  private int x;
  private int y;
  private int width;
  private int height;
  BoundsSpecified specified;

  public void SetBounds(int x, int y, int width, int height)
   {
      SetBoundsCore(x, y, width, height, this.specified);
   }

   public void SetBounds(int x, int y, int width, int height,
      BoundsSpecified specified)
   {    
      SetBoundsCore(x, y, width, height, specified);
   }

   protected virtual void SetBoundsCore(int x, int y, int width, int 
      height, BoundsSpecified specified)
   {
         // Add code to perform meaningful opertions here.
         this.x = x;
         this.y = y;
         this.width = width;
         this.height = height;
         this.specified = specified;
   }
}

Многие компиляторы, если вы этого не сделаете, вставят конструкторы public или protected. Поэтому для улучшения документирования и читабельности исходного кода, необходимо явно определить protected конструктор во всех абстрактных классах.

Рекомендации по использованию sealed классов

Далее приведены основные рекомендации по использованию sealed классов:

  • Используйте sealed классы, если нет необходимости создавать дочерние классы. От sealed класса невозможно наследоваться.
  • Используйте sealed классы, если в классе есть только статические методы и свойства. Далее приведен пример корректно определенного sealed класса.
    
    public sealed class Runtime
    {
       // Private constructor prevents the class from being created.
       private Runtime();
       
       // Static method.
       public static string GetCommandLine() 
       {
          // Implementation code goes here.
       }
    }
    

Рекомендации по использованию типов значений

Тип значения описывает значение, которое представлено как последовательность битов, сохраненных в стеке. Описание всех встроенных типов данных .NET Framework приведено в разделе MSDN Value Types. В этом разделе представлены рекомендации по использованию типов значений структуры (struct) и перечисления (enum).

Рекомендации по использованию структур

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

  • Работает, как примитивные типы.
  • Размер экземпляра менее 16 байт.
  • Неизменны.
  • Желательно наличие семантики значения.

В следующем примере показана корректно определенная структура.


public struct Int32: IComparable, IFormattable
{ 
   public const int MinValue = -2147483648;
   public const int MaxValue = 2147483647;
   
   public static string ToString(int i) 
   {
      // Insert code here.
   }

   public string ToString(string format, IFormatProvider formatProvider) 
   {
      // Insert code here.
   }

   public override string ToString() 
   {
      // Insert code here.
   }

   public static int Parse(string s)
   {
      // Insert code here.
      return 0;
   }

   public override int GetHashCode()
   {
      // Insert code here.
      return 0;
   }

   public override bool Equals(object obj)
   {
      // Insert code here.
      return false;
   }

   public int CompareTo(object obj)
   {
      // Insert code here.
      return 0;
   }

}

При использовании struct не предоставляйте конструктор по умолчанию. Среда выполнения добавит конструктор, который устанавливает все значения в нулевое состояние. Это дает возможность более эффективно создавать массивы структур. Вы также должны разрешить состояние, в котором данные экземпляра устанавливаются в нуль (zero), false или null (соответственно) без вызова конструктора.

Рекомендации по применению Enum

Далее приведены рекомендации по применению перечислений:

  • Применяйте enum к параметрам, свойствам и возвращаемым типам со строгим типом. Всегда определяйте пронумерованные значения, используя enum, если они используются в параметре или свойстве. Это обеспечивает возможность инструментальным средствам разработки знать возможные значения свойства или параметра. В следующем примере показано, как определить enum тип.
    
    public enum FileMode
    {
       Append,
       Create,
       CreateNew,
       Open,
       OpenOrCreate,
       Truncate
    }
    
  • Далее приведен конструктор для объекта FileStream, который использует FileMode enum.
    
    public FileStream(string path, FileMode mode);
    
  • Используйте класс System.FlagsAttribute для создания специального атрибута enum, если необходимо осуществить побитовую операцию OR для числовых значений. Этот атрибут применяется в следующем примере кода.
    
    [Flags]
    public enum Bindings
    {
       IgnoreCase = 0x01,
       NonPublic = 0x02,
       Static = 0x04,
       InvokeMethod = 0x0100,
       CreateInstance = 0x0200,
       GetField = 0x0400,
       SetField = 0x0800,
       GetProperty = 0x1000,
       SetProperty = 0x2000,
       DefaultBinding = 0x010000,
       DefaultChangeType = 0x020000,
       Default = DefaultBinding | DefaultChangeType,
       ExactBinding = 0x040000,
       ExactChangeType = 0x080000,
       BinderBinding = 0x100000,
       BinderChangeType = 0x200000
    }
    

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

  • Используйте enum с атрибутом flags, только если значение может быть полностью выражено, как набор битовых флагов. Не используйте enum для открытых наборов (таких как версия операционной системы).
  • Не допускайте, чтобы аргументы enum были в определенном диапазоне. Осуществляйте проверку правильности аргумента, как показано в следующем примере кода.
    
    public  void SetColor (Color color)
    {
       if (!Enum.IsDefined (typeof(Color), color) 
          throw new ArgumentOutOfRangeException();
    }
    
  • Используйте enum вместо статических констант.
  • Используйте тип Int32, как базовый тип enum, за исключением тех случаев, когда:
    • enum представляет флаги и в текущий момент существует более 32 флагов, или enum должна образовать слишком много флагов в будущем.
    • тип должен быть отличным от типа int для обеспечения обратной совместимости.
  • Не используйте нецелочисленный тип enum. Используйте только Byte, Int16, Int32 или Int64.
  • Не определяйте методы, свойства или события в enum.
  • Не используйте суффикс Enum в типах enum.

Рекомендации по использованию делегатов

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

Нотификации событий

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

Функции обратного вызова

Функции обратного вызова передаются методу таким образом, что код пользователя может многократно вызываться во время выполнения, чтобы обеспечить гибкость. Передача функции обратного вызова Compare в процедуру сортировки является классическим примером использования функций обратного вызова. Эти методы должны использовать соглашение функций обратного вызова, описанные в разделе MSDN Использование функций обратного вызова (Callback Function Usage).

В имени функции обратного вызова используйте суффикс Callback.

Рекомендации по использованию атрибутов

.NET Framework дает возможность разработчикам создавать новые виды декларативной информации, определять декларативную информацию для различных сущностей программы и извлекать информацию атрибута в среде времени выполнения. Например, оболочка может определить атрибут HelpAttribute, который можно поместить в элементы программы, такие как классы и методы, чтобы обеспечить преобразование от элементов программы к их документации. Новые виды декларативной информации определяются через декларацию классов атрибутов, которые могут иметь позиционные и поименованные параметры. Более подробная информация об атрибутах приведена в разделе MSDN Создание специальных атрибутов (Writing Custom Attributes).

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

  • Добавьте суффикс Attribute к специальным классам атрибутов, как показано в следующем примере.
    
    public class ObsoleteAttribute{}
    
  • Определите AttributeUsage в ваших атрибутах, чтобы точно определить их использование, как показано в следующем примере.
    
    [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
    public class ObsoleteAttribute: Attribute {}
    
  • Изолируйте классы атрибутов где только возможно, чтобы от них нельзя было наследоваться.
  • Используйте позиционные аргументы для обязательных параметров.
  • Используйте поименованные аргументы для необязательных параметров.
  • Не описывайте параметр сразу и поименованным, и позиционным аргументами.
  • Обеспечьте свойства только для чтения с одним и тем же именем, но с различным стилем, чтобы иметь возможность различать их, для каждого позиционного аргумента.
  • Обеспечьте свойства для чтения/записи с одним и тем же именем, но с различным стилем, чтобы иметь возможность различать их, для каждого названного аргумента.
    
    public class NameAttribute: Attribute 
    {
       public NameAttribute (string username) 
       { 
           // Implement code here.
       }
       public string UserName 
       { 
          get 
          {
             return UserName; 
          }
       }
       public int Age 
       { 
          get 
          {
             return Age;
          }
          set 
          {
             Age = value;
          }
       } 
       // Positional argument.
    }
    

Рекомендации по использованию вложенных типов

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

Public вложенные типы должны использоваться редко. Используйте их только в таких ситуациях, когда истинны оба из нижеприведенных утверждений:

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

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


// With nested types.
ListBox.SelectedObjectCollection 
// Without nested types.
ListBoxSelectedObjectCollection  

// With nested types.
RichTextBox.ScrollBars 
// Without nested types.
RichTextBoxScrollBars

Не используйте вложенные типы, если истинны следующие утверждения:

  • Тип используется во многих различных методах в разных классах. Примером такого типа является перечень FileMode.
  • Тип обычно используется в различных API. Примером такого типа является класс StringCollection.

Рекомендации по раскрытию функциональных возможностей для COM

Общеязыковая среда выполнения предоставляет богатую поддержку для взаимодействия с компонентами COM. Компонент СОМ может использоваться из управляемого типа, и управляемый экземпляр может использоваться компонентом СОМ. Эта поддержка является ключом для преобразования неуправляемого кода в управляемый; однако это порождает некоторые проблемы для разработчиков библиотек классов. Для того, чтобы полностью раскрыть управляемый тип клиенту СОМ, тип должен раскрыть функциональность способом, поддерживаемым СОМ и контрактом поддержания версий СОМ.

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

Типы и интерфейсы, которые должны использоваться клиентами СОМ прямо, например, расположение в неуправляемом контейнере, должны быть отмечены атрибутом ComVisible (true). Транзитивное замыкание всех типов, к которым обращаются раскрытые типы, должно быть явно отмечено, как ComVisible (true); в противном случае, они будут раскрыты, как IUnknown.

Примечание: члены типа также могут быть отмечены, как ComVisible (false); это сокращает воздействие на СОМ и, следовательно, снижает ограничения использования управляемого типа.

Типы, отмеченные атрибутом ComVisible (true), могут раскрывать функциональность исключительно тем способом, который поддерживается в СОМ. В частности, СОМ не поддерживает статические методы или параметризованные конструкторы. Для подтверждения корректности поведения, протестируйте функциональность типа из СОМ клиентов. Убедитесь, что вы понимаете влияние registry на то, чтобы сделать все типы cocreateable.

Передача по ссылке

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

  • Типам, чьи экземпляры копируются тогда, когда они передаются через границу AppDomain (на том же или на другом компьютере). Эти типы должны быть отмечены атрибутом Serializable.
  • Типам, для которых при их передаче через границу AppDomain (на том же или на другом компьютере) среда выполнения создает прозрачный proxy объект. Эти типы конечно же должны наследоваться от класса System.MarshalByRefObject.
  • Типам, которые вообще не передаются через AppDomains. Этот вид типа используется по умолчанию.

При использовании передачи по ссылке следуйте этим рекомендациям:

  • По умолчанию экземпляры должны быть объектами, передаваемыми по значению. Это означает, что их типы должны быть обозначены, как Serializable.
  • Типы компонентов должны быть объектами, передаваемыми по ссылке. Такими должны быть большинство компонентов, потому что общий базовый класс, System.Component, является классом, передаваемым по ссылке.
  • Если тип инкапсулирует ресурс операционной системы, он должен быть объектом, передаваемым по ссылке. Если тип реализует интерфейс IDisposable, он весьма вероятно должен быть передаваемым по ссылке. System.IO.Stream наследуется от MarshalByRefObject. Большинство потоков, таких как FileStreams и NetworkStreams, инкапсулируют внешние ресурсы, таким образом, они должны быть объектами, передаваемыми по ссылке.
  • Экземпляры, которые просто поддерживают состояние, должны быть объектами, передаваемыми по значению (как DataSet).
  • Специальные типы, которые не могут быть вызваны через AppDomain (такие, как владелец статических утилитных методов), должны быть отмечены, как Serializable.

Рекомендации по генерированию и обработке ошибок

Далее приведены основные рекомендации по формированию и обработке ошибок:

  • Все куски кода, которые приводят к возникновению исключительной ситуации, должны предоставлять метод для проверки успешного завершения операции без возникновения исключительной ситуации. Например, чтобы предотвратить FileNotFoundException, вы можете вызвать File.Exists. Это не всегда может быть возможным, но целью является то, чтобы при нормальном выполнении не возникало исключительных ситуаций.
  • Заканчивайте имена класса Exception суффиксом Exception, как это сделано в следующем примере:
    
    public class FileNotFoundException : Exception 
    {
       // Implementation code goes here.
    }
    
  • Используйте три обычных конструктора, показанных в следующем примере, при создании классов исключительных ситуаций в C# и Управляемых расширениях для С++ (Managed Extensions for C++).
    
    public class XxxException : ApplicationException 
    {
       XxxException() {... }
       XxxException(string message) {... }
       XxxException(string message, Exception inner) {... }
    }
    
  • В большинстве случаев используйте предопределенные типы исключительных ситуаций. Новые типы исключительных ситуаций определяйте только в тех программных сценариях, в которых предполагаете, что пользователи вашей библиотеки классов будут обрабатывать исключительные ситуации этого нового типа и осуществлять программные действия, основанные на самом типе исключительной ситуации. Это проводится вместо синтаксического анализа строки исключительной ситуации, что негативно повлияет на производительность и эксплуатацию.

    Например, имеет смысл определить FileNotFoundException, потому что разработчик может захотеть создать отсутствующий файл. Однако FileIOException не является тем, что обычно должно обрабатываться именно в коде.

  • Не наследуйте новые исключительные ситуации прямо от базового класса Exception.
  • Группируйте новые исключительные ситуации, унаследованные от базового класса Exception, по пространству имен. Например, классы будут наследоваться от XML, IO, Collections и т.д. Каждая из этих областей, соответственно, будет иметь унаследованный класс исключительных ситуаций. При добавлении разработчиками другой библиотеки или приложения любой исключительной ситуации будет наращиваться непосредственно класс Exception. Вы должны создать единое имя для всех подобных исключительных ситуаций и расширять все исключительные ситуации, связанные с этим приложением или библиотекой, из этой группы.
  • В каждой исключительной ситуации используйте локализованную строку описания. Сообщение об ошибке, которое видит пользователь, будет наследоваться от строки описания возникшей исключительной ситуации и никогда от класса исключительной ситуации.
  • Создайте сообщения об ошибках, соблюдая правила грамматики и пунктуации. Каждое предложение в строке описания исключительной ситуации должно оканчиваться точкой. Код, который обычно отображает сообщение об исключительной ситуации пользователю, не должен обрабатывать случаи, когда разработчик забыл поставить заключительную точку.
  • Обеспечьте свойства исключительных ситуаций для программного доступа. Дополнительную информацию (не строку описания) включайте в исключительную ситуацию только, если реализуется программный сценарий, в котором эта дополнительная информация полезна. Такие случаи довольно редки.
  • Не используйте исключительные ситуации для нормальных или ожидаемых ошибок, или для нормального потока исполнения.
  • Вы должны возвращать null для очень общих случаев ошибок. Например, команда File.Open возвращает нулевую ссылку в том случае, если файл не найден, но формирует исключительную ситуацию, если файл блокирован.
  • Разрабатывайте классы так, чтобы при нормальном использовании исключительные ситуации никогда не возникали. В следующем примере класс FileStream раскрывает другой способ определения, достигнут ли конец файла, для того чтобы предотвратить возникновение исключительной ситуации в случае, если пользователь читает после окончания файла.
    
    class FileRead 
    {
       void Open() 
       {
          FileStream stream = File.Open("myfile.txt", FileMode.Open);
          byte b;
    
          // ReadByte returns -1 at end of file.
          while ((b = stream.ReadByte()) != true) 
          {
             // Do something.
          }
       }
    }
    
  • Формируйте исключительную ситуацию InvalidOperationException, если обращение к аксессору set свойства или метода не соответствует текущему состоянию объекта.
  • Формируйте ArgumentException или создайте исключительную ситуацию, наследуемую от этого класса, если переданы или выявлены неверные параметры.
  • Помните, что отслеживание стека начинается в той точке, где возникла исключительная ситуация, а не там, где она создается оператором new. Учитывайте это при принятии решения о том, где формировать исключительную ситуацию.
  • Используйте методы построителя исключительных ситуаций. Обычно из разных мест в реализации класса вызываются одни и те же исключительные ситуации. Чтобы предотвратить повторяемость кода, используйте вспомогательные методы, которые создают исключительную ситуацию, используя оператор new и возвращая его. В следующем примере продемонстрировано, как реализовать вспомогательный метод.
    
    class File 
    {
       string fileName;  
       public byte[] Read(int bytes) 
       {
          if (!ReadFile(handle, bytes))
                throw NewFileIOException();
       }
       
       FileException NewFileIOException() 
       {
          string description = 
             // Build localized string, include fileName.
          return new FileException(description);
       }
    }
    
  • Вместо возвращения кода ошибки или HRESULT формируйте исключительную ситуацию.
  • Создавайте как можно более специфические исключительные ситуации.
  • Текст сообщения об исключительной ситуации, формируемого для разработчика, должен быть значащим.
  • Используйте внутренние исключительные ситуации (исключительные ситуации, образующие цепь). Однако не до тех пор, пока вводите дополнительную информацию и/или изменяете тип исключительной ситуации, не фиксируйте и не формируйте повторно исключительные ситуации.
  • Не создавайте методы, которые формируют NullReferenceException или IndexOutOfRangeException.
  • Осуществляйте проверку аргумента в protected (Семья) и внутренних (Сборка) членах. Если protected метод не делает проверку аргумента, четко обозначьте это в документации. Если не установлено иначе, считайте, что проверка аргумента осуществлена. Однако отказ от проведения проверки аргумента, возможно, приведет к повышению производительности.
  • Очистите все побочные эффекты, когда формируете исключительную ситуацию. Вызывающие программы должны иметь возможность допустить, что при формировании исключительной ситуации из функции нет никаких побочных эффектов. Например, если метод Hashtable.Insert формирует исключительную ситуацию, вызывающая программа может допускать, что определенный элемент не был добавлен в Hashtable.

Стандартные типы исключительных ситуаций

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

Тип исключительной ситуации Базовый тип Описание Пример
Exception Object Базовый класс для всех исключительных ситуаций.  
SystemException Exception Базовый класс для всех ошибок, генерируемых во время выполнения.  
IndexOutOfRangeException SystemException Формируется средой выполнения только в случае, если массив неправильно проиндексирован. Indexing an array outside of its valid range:
arr[arr.Length+1]
NullReferenceException SystemException Формируется средой выполнения только в случае, если идет обращение к несуществующему объекту.

object o = null; 
o.ToString();
InvalidOperationException SystemException Формируется методами в неправильном состоянии. Вызов Enumerator.GetNext() после перемещения Item из основной коллекции.
ArgumentException SystemException Базовый класс для всех исключительных ситуаций аргументов.  
ArgumentNullException ArgumentException Формируется методами, которые не допускают чтобы значение аргумента было null.

String s = null; 
"Calculate".IndexOf (s);
ArgumentOutOfRangeException ArgumentException Формируется методами, которые проверяют, находятся ли аргументы в данном диапазоне.

String s = "string"; 
s.Chars[9];
ExternalException SystemException Базовый класс для исключительных ситуаций, которые случаются или нацелены на среды вне среды выполнения.  
COMException ExternalException Исключительная ситуация, инкапсулирующая информацию COM Hresult. Используется в COM взаимодействии.
SEHException ExternalException Исключительная ситуация, инкапсулирующая Win32 структурированную информацию обработки исключительной ситуации. Используется во взаимодействии неуправляемого кода.

Обернутые исключительные ситуации

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


public class TextReader
{
   public string ReadLine()
   {
      try
      {
         // Read a line from the stream.
      } 
      catch (Exception e)
      {
         throw new IOException ("Could not read from stream", e);
      }
   }
}

Рекомендации по использованию массивов

Общее описание массивов и использования массивов приведено в разделе MSDN Arrays и System.Array Class.

Сравнение массивов и коллекций

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

  • Когда поддерживаются Add, Remove или другие методы манипулирования коллекциями.
  • Чтобы добавить обертки только для чтения для внутренних массивов.

Более подробно об использовании коллекций смотри в разделе MSDN Группирование данных в коллекции (Grouping Data in Collections).

Использование индексированных свойств в коллекциях

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

Array Valued свойства

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


for (int i = 0; i < obj.myObj.Count; i++)
      DoSomething(obj.myObj[i]);

Более подробная информация приведена в разделе Свойства и методы.

Возвращение пустых массивов

Свойства String и Array никогда не должны возвращать нулевую ссылку. В этом контексте может быть трудно осмыслить нуль (Null). Например, пользователь может предположить, что следующий код будет работать:


public void DoSomething()
{
   string s = SomeOtherFunc();
   if (s.Length > 0)
   {
      // Do something else.
   }
}

Общим правилом является то, что нуль (null), пустая строка ("") и пустые (0 элементов) массивы должны интерпретироваться одинаково. Возвращайте вместо нулевой ссылки пустой массив.

Рекомендации по использованию перезагрузки операторов

Далее приведены основные рекомендации по перезагрузке операторов:

  • Определите операторы для типов значений, которые являются логическими встроенными типами языка программирования, например, как структура System.Decimal.
  • Обеспечьте методы перезагрузки операторов только в тех классах, в которых определены методы.
  • Используйте имена и условные обозначения сигнатур, описанные в Общеязыковой спецификации (CLS).
  • Используйте перезагрузку операторов в случаях, когда сразу очевидно, что будет результат операции. Например, есть смысл иметь возможность вычитать одно значение времени Time из другого и получать промежуток времени TimeSpan. Однако неуместно использовать оператор or, чтобы создать объединение двух запросов базы данных, или использовать shift, чтобы писать в поток.
  • Перезагружайте операторы симметрично. Например, если вы перезагружаете оператор равенства (==), вы также должны перезагрузить оператор неравенства (!=).
  • Предоставляйте альтернативные сигнатуры. Большинство языков программирования не поддерживают перезагрузку операторов. Поэтому всегда включайте дополнительный метод с соответствующим домен-специфическим именем, который имеет эквивалентную функциональность. Это является требованием Общеязыковой спецификации (CLS). Следующий пример соответствует требованиям CLS.
    
    class Time 
    {
       TimeSpan operator -(Time t1, Time t2) { }
       TimeSpan Difference(Time t1, Time t2) { }
    }
    

В данной таблице приведен список символов операторов и соответствующих имен альтернативных методов и операторов.

Символ оператора C++ Имя альтернативного метода Имя оператора
Не определен ToXxx или FromXxx op_Implicit
Не определен ToXxx или FromXxx op_Explicit
+ (binary) Add op_Addition
- (binary) Subtract op_Subtraction
* (binary) Multiply op_Multiply
/ Divide op_Division
% Mod op_Modulus
^ Xor op_ExclusiveOr
& (binary) BitwiseAnd op_BitwiseAnd
| BitwiseOr op_BitwiseOr
&& And op_LogicalAnd
|| Or op_LogicalOr
= Assign op_Assign
<< LeftShift op_LeftShift
>> RightShift op_RightShift
Не определен LeftShift op_SignedRightShift
Не определен RightShift op_UnsignedRightShift
== Equals op_Equality
> Compare op_GreaterThan
< Compare op_LessThan
!= Compare op_Inequality
>= Compare op_GreaterThanOrEqual
<= Compare op_LessThanOrEqual
*= Multiply op_MultiplicationAssignment
-= Subtract op_SubtractionAssignment
^= Xor op_ExclusiveOrAssignment
<<= LeftShift op_LeftShiftAssignment
%= Mod op_ModulusAssignment
+= Add op_AdditionAssignment
&= BitwiseAnd op_BitwiseAndAssignment
|= BitwiseOr op_BitwiseOrAssignment
, Не присвоено op_Comma
/= Divide op_DivisionAssignment
-- Decrement op_Decrement
++ Increment op_Increment
- (одинарный) Negate op_UnaryNegation
+ (одинарный) Plus op_UnaryPlus
~ OnesComplement op_OnesComplement

Рекомендации по применению метода Equals и оператора равенства (==)

Далее приведены основные рекомендации по применению метода Equals и оператора равенства (==):

  • Реализуйте метод GetHashCode всегда, когда реализовываете метод Equals. Это сохраняет синхронность Equals и GetHashCode.
  • Всегда переопределяйте метод Equals, когда применяете ==, и пусть они выполняют одно и то же. Это позволяет коду инфраструктуры, такому как Hashtable и ArrayList, который использует метод Equals, вести себя так же, как и код пользователя, написанный с использованием ==.
  • Всегда при реализации интерфейса IComparable переопределяйте метод Equals.
  • Вы должны продумать реализацию перезагрузки оператора для операторов равенства (==), неравенства (!=), меньше (<) и больше (>), когда реализовываете IComparable.
  • Не формируйте исключительные ситуации из методов Equals или GetHashCode или оператора равенства (==).

Информация, касающаяся реализации метода Equals, представлена в разделе MSDN Реализация метода Equals (Implementing the Equals Method).

Реализация оператора равенства (==) в типах значений

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

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

Реализовывайте == всегда, когда переопределяете метод Equals.

Реализация оператора равенства (==) в ссылочных типах

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

Переопределите ==, если ваш тип является базовым типом, таким как Point, String, BigNumber и т.д. Всегда, когда рассматриваете переопределение операторов сложения (+) или вычитания (-), вы должны также рассмотреть и переопределение ==.

Рекомендации по приведению типов

Далее приведены основные рекомендации по применению приведений типов:

  • Не используйте неявное преобразование, когда оно может привести к потере точности. Например, не должно использоваться неявное преобразование Double в Int32, но допускается преобразование Int32 в Int64.
  • Не формируйте исключительные ситуации из неявных преобразований, потому что разработчику очень сложно понять, что происходит.
  • Поставляйте преобразования, которые действуют во всем объекте. Значение, которое преобразовывается, должно представлять весь объект, а не отдельного члена объекта.
  • Не генерируйте семантически различные значения. Например, преобразование типа Time или TimeSpan в Int32 верно. Int32 отображает время или продолжительность. Однако нет смысла преобразовывать строку имени файла, например, c:\mybitmap.gif, в объект Bitmap.
  • Не преобразовывайте значения из разных доменов. Преобразование действует в пределах отдельного домена значений. Например, числа и строки являются различными доменами. Имеет смысл то, что Int32 можно преобразовать в Double. Однако не имеет смысла преобразовывать Int32 в String, потому что они относятся к разным доменам.

Общие шаблоны разработки

В этом разделе приведены рекомендации по реализации общих шаблонов разработки в библиотеках классов.

Применение методов Finalize и Dispose для очистки неуправляемых ресурсов

Экземпляры классов часто инкапсулируют элемент управления поверх ресурсов, таких как описатель окна (HWND), соединения с базами данных и т.д., которые не управляются средой выполнения. Поэтому вы должны обеспечить как явный, так и неявный способ освобождения этих ресурсов. Неявный контроль обеспечьте реализацией в объекте protected метода Finalize (синтаксис деструктора в C# и управляемые расширения (Managed Extensions) для С++). Сборщик мусора вызывает этот метод в определенный момент после того, как больше не существует ни одной действительной ссылки на объект.

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

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

Более подробно об использовании методов Finalize и Dispose для очистки неуправляемых ресурсов смотри в разделе MSDN Programming for Garbage Collection. Следующий пример иллюстрирует основную схему разработки для реализации метода Dispose.


// Design pattern for a base class.
public class Base: IDisposable
{
   //Implement IDisposable.
   public void Dispose() 
   {
     Dispose(true);
      GC.SuppressFinalize(this); 
   }

   protected virtual void Dispose(bool disposing) 
   {
      if (disposing) 
      {
         // Free other state (managed objects).
      }
      // Free your own state (unmanaged objects).
      // Set large fields to null.
   }

   // Use C# destructor syntax for finalization code.
   ~Base()
   {
      // Simply call Dispose(false).
      Dispose (false);
   }
   
// Design pattern for a derived class.
public class Derived: Base
{   
   protected override void Dispose(bool disposing) 
   {
      if (disposing) 
      {
         // Release managed resources.
      }
      // Release unmanaged resources.
      // Set large fields to null.
      // Call Dispose on your base class.
      base.Dispose(disposing);
   }
   // The derived class does not have a Finalize method
   // or a Dispose method with parameters because it inherits
   // them from the base class.
}

Более подробный пример, иллюстрирующий схему разработки для реализации методов Finalize и Dispose, смотри в разделе MSDN Implementing a Dispose Method.

Изменение имени метода Dispose

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


// Do not make this method virtual.
// A derived class should not be allowed
// to override this method.
public void Close()
{
   // Call the Dispose method with no parameters.
   Dispose();
}

Finalize

Далее приведены основные рекомендации по применению метода Finalize:

  • Применяйте метод Finalize только в тех объектах, которые требуют финализации. При использовании этого метода возникают некоторые потери производительности.
  • Если вам необходим метод Finalize, вы должны рассмотреть реализацию IDisposable, чтобы дать возможность пользователям вашего класса предотвратить затраты на активизацию метода Finalize.
  • Не делайте метод Finalize более видимым. Он должен быть protected, а не public.
  • Метод Finalize объекта должен освобождать любые внешние ресурсы, принадлежащие объекту. Более того, метод Finalize должен освобождать только те ресурсы, которые удерживаются объектом. Метод Finalize не должен обращаться к другим объектам.
  • Не делайте прямых вызовов метода Finalize в объектах, кроме объектов базового класса.
  • Вызывайте метод base.Finalize из метода Finalize объекта.

    Примечание: Метод Finalize базового класса вызывается автоматически с синтаксисом деструктора C# и Управляемых расширений (Managed Extensions) для C++.

Dispose

Далее приведены основные рекомендации по применению метода Dispose:

  • Применяйте схему разработки ликвидации в типе, инкапсулирующем ресурсы, которые нуждаются в явном освобождении. Пользователи могут освобождать внешние ресурсы, вызывая public метод Dispose.
  • Применяйте схему разработки ликвидации в базовом типе, обычно имеющем наследуемые типы, которые удерживают ресурсы, даже если базовый класс не делает этого. Наличие в базовом типе закрытого метода обычно свидетельствует о необходимости реализации Dispose. В таких случаях не реализуйте метод Finalize в базовом типе. Finalize должен быть реализован в любых унаследованных типах, представляющих ресурсы, которые нуждаются в очистке.
  • Освобождайте все имеющиеся в распоряжении типа ресурсы в методе Dispose.
  • После того, как метод Dispose был вызван в экземпляре, вызовите метод GC.SuppressFinalize, чтобы не допустить запуск метода Finalize. Исключением из этого правила является редкая ситуация, когда работа должна быть сделана в методе Finalize, который не покрывается методом Dispose.
  • Вызывайте метод Dispose базового класса, если он реализует IDisposable.
  • Не предполагайте, что метод Dispose будет вызван. Неуправляемые ресурсы, принадлежащие типу, также должны быть освобождены в методе Finalize на случай, если метод Dispose не вызывается.
  • Формируйте ObjectDisposedException, когда ресурсы уже освобождены. Если вы решили перераспределить ресурсы после того, как объект освобожден, убедитесь что вы вызываете метод GC.ReRegisterForFinalize.
  • Передавайте вызовы Dispose через иерархию базовых типов. Метод Dispose должен освободить все ресурсы, удерживаемые данным объектом и любым другим объектом, принадлежащим данному. Например, вы можете создать такой объект, как TextReader, который поддерживает объекты Stream и Encoding. Оба эти объекта создаются TextReader без ведома пользователя. Более того, и Stream, и Encoding могут запрашивать внешние ресурсы. Когда вы вызываете метод Dispose в TextReader, Dispose вызывается и в Stream и Encoding, заставляя их освободить внешние ресурсы.
  • Объект не может использоваться после того, как был вызван его метод Dispose. Повторное создание объекта, который уже был ликвидирован, сложно реализовать.
  • Предоставьте возможность многократного вызова метода Dispose без формирования исключительной ситуации. После первого вызова метод не должен ничего делать.

Реализация метода Equals

Информация, касающаяся реализации оператора равенства (==), представлена в разделе Рекомендации по применению метода Equals и оператора равенства (==).

  • Переопределите метод GetHashCode, чтобы предоставить возможность типу корректно работать в хэш-таблице.
  • Не формируйте исключительную ситуацию в реализации метода Equals. Вместо этого возвращайте false для null аргумента.
  • Следуйте контракту, определенному в методе Object.Equals, как показано ниже:
    • x.Equals(x) возвращает true.
    • x.Equals(y) возвращает то же значение, что и y.Equals(x).
    • (x.Equals(y) && y.Equals(z)) возвращает true, если и только если x.Equals(z) возвращает true.
    • Успешные вызовы x.Equals(y) возвращают одно и то же значение до тех пор, пока не модифицированы объекты, на которые ссылаются x и y.
    • x.Equals(null) возвращает false.
  • Для некоторых видов объектов желательно применять критерий Equals для обозначения равенства значений вместо ссылочного равенства. Такая реализация Equals возвращает true, если два объекта имеют одно и то же значение, даже если они не являются одним и тем же экземпляром. Определение того, что составляет значение объекта, является задачей разработчика типа, но обычно некоторые или все данные сохраняются в переменных экземпляра объекта. Например, значение строки основывается на ее символах; метод Equals класса String возвращает true для любых двух экземпляров строки, которые содержат именно эти символы в том же порядке.
  • Когда метод Equals базового класса предоставляет равенство значений, переопределение Equals в наследуемом классе должно вызывать унаследованную реализацию Equals.
  • Если вы используете язык программирования, который поддерживает перезагрузку операторов, и решаете перезагрузить оператор равенства (==) для определенного типа, этот тип должен переопределить метод Equals. При такой реализации метод Equals должен возвращать такие же результаты, что и оператор равенства. Выполнение данных рекомендации поможет обеспечить совместимость кода библиотеки классов, использующего Equals (например, ArrayList и Hashtable), с кодом приложения, который использует оператор равенства.
  • При реализации типа значения вы должны рассмотреть переопределение метода Equals, для того чтобы добиться увеличения производительности через реализацию метода Equals, используемую по умолчанию, в System.ValueType. Если вы переопределяете Equals, и язык программирования поддерживает перезагрузку операторов, вы должны перезагрузить оператор равенства для своего типа значения.
  • При реализации ссылочных типов вы должны рассмотреть переопределение метода Equals в ссылочном типе, если ваш тип похож на базовые типы, такие как Point, String, BigNumber и т.д. В большинстве ссылочных типов не надо переопределять оператор равенства, даже если переопределяется Equals. Однако, если реализуется ссылочный тип, который будет иметь симантики значения, например, тип комплексного числа, оператор равенства должен быть переопределен.
  • При реализации интерфейса IComparable в данном типе его метод Equals а должен быть переопределен.

Примеры

Реализация метода Equals

В следующем примере содержатся два вызова реализации метода Equals, применяемой по умолчанию:


using System;
class SampleClass 
{
   public static void Main() 
   {
      Object obj1 = new Object();
      Object obj2 = new Object();
      Console.WriteLine(obj1.Equals(obj2));
      obj1 = obj2; 
      Console.WriteLine(obj1.Equals(obj2)); 
   }
}

В результате получим следующее:


False
True

Переопределение метода Equals

В следующем примере представлены класс Point, который переопределяет метод Equals, чтобы обеспечить равенство значений, и класс Point3D, унаследованный от Point. Поскольку переопределение метода Equals в классе Point является первым в цепи наследования, метод Equals базового класса (который наследуется от Object и проверяет ссылочное равенство) не вызывается. Однако Point3D.Equals вызывает Point.Equals, потому что класс Point реализовывает метод Equals так, что обеспечивается равенство значений.


using System;
class Point: object 
{
   int x, y;
   public override bool Equals(Object obj) 
   {
      // Check for null values and compare run-time types.
      if (obj == null || GetType() != obj.GetType()) 
         return false;
      Point p = (Point)obj;
      return (x == p.x) && (y == p.y);
   }
   public override int GetHashCode() 
   {
      return x ^ y;
   }
}

class Point3D: Point 
{
   int z;
   public override bool Equals(Object obj) 
   {
      return base.Equals(obj) && z == ((Point3D)obj).z;
   }
   public override int GetHashCode() 
   {
      return base.GetHashCode() ^ z;
   }
}

Метод Point.Equals проверяет наличие аргумента obj и то, что он ссылается на экземпляр того же типа, что и данный объект. Если одна из этих проверок не удается, метод возвращает значение false. Метод Equals использует метод Object.GetType для того, чтобы определить идентичность типов времени выполнения этих двух объектов. Обратите внимание, что typeof (TypeOf в Visual Basic) здесь не используется, потому что он возвращает статический тип. Если вместо этого метод использовал проверку в форме obj is Point, в результате проверки будет возвращено значение true в случаях, когда obj является экземпляром класса, наследуемого от Point, даже если obj и текущий экземпляр не одного типа времени выполнения. Установив, что эти два объекта имеют один и тот же тип, метод приводит obj к типу Point и возвращает результат сравнения переменных экземпляра двух объектов.

В Point3D.Equals унаследованный метод Equals вызывается первым. Метод Equals проверяет, существует ли obj, является ли obj экземпляром того же класса, что и данный объект, и совпадают ли переменные экземпляра. Только когда унаследованный Equals возвращает значение true, метод сравнивает переменные экземпляра, представленные в дочернем классе. В частности, приведение к Point3D не производится до тех пор, пока не будет определено, что obj имеет тип Point3D или является классом, унаследованным от Point3D.

Использование метода Equals для сравнения переменных экземпляра

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


using System;
class Rectangle 
{
   Point a, b;
   public override bool Equals(Object obj) 
   {
      if (obj == null || GetType() != obj.GetType()) return false;
      Rectangle r = (Rectangle)obj;
      // Use Equals to compare instance variables.
      return a.Equals(r.a) && b.Equals(r.b);
   }
   public override int GetHashCode() 
   {
      return a.GetHashCode() ^ b.GetHashCode();
   }
}

Перезагрузка оператора равенства (==) и метода Equals

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


public struct Complex 
{
   double re, im;
   public override bool Equals(Object obj) 
   {
      return obj is Complex && this == (Complex)obj;
   }
   public override int GetHashCode() 
   {
      return re.GetHashCode() ^ im.GetHashCode();
   }
   public static bool operator ==(Complex x, Complex y) 
   {
      return x.re == y.re && x.im == y.im;
   }
   public static bool operator !=(Complex x, Complex y) 
   {
      return !(x == y);
   }
}

Т.к. Complex является структурой, известно, что ни один класс не будет наследоваться от Complex. Поэтому методу Equals не надо сравнивать результаты GetType для каждого объекта. Вместо этого он использует оператор is, чтобы проверить тип параметра obj.

Использование функции обратного вызова

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

События

Используйте события, если истинны следующие утверждения:

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

Делегаты

Используйте делегаты, если истинно следующее:

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

Интерфейсы

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

Использование ожиданий (Time-Out)

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

Ожидания должны принять форму параметра вызова метода, как показано ниже.


server.PerformOperation(timeout);

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


server.Timeout = timeout;
server.PerformOperation();

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

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

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


public class Server
{
   void PerformOperation(TimeSpan timeout)
   {
      // Insert code for the method here.
   } 
}

public class TestClass
{
   public Server server = new Server();
   server.PerformOperation(new TimeSpan(0,15,0));
}

Когда задано TimeSpan(0), если операция не завершается немедленно, метод формирует исключительную ситуацию. Если ожидание задано TimeSpan.MaxValue, операция будет ожидать постоянно, как будто ожидание не задано. Не требуется, чтобы серверный класс поддерживал какое-то из этих значений, но он должен формировать исключительную ситуацию InvalidArgumentException в случае, если ожиданию задается значение, не поддерживаемое классом.

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

Если используется ожидание, применяемое по умолчанию, серверный класс должен включать статическое свойство defaultTimeout, которое используется, если пользователь не задал ожидания. В следующем примере в код включено статическое свойство OperationTimeout типа TimeSpan, которое возвращает defaultTimeout.


class Server
{
   TimeSpan defaultTimeout = new TimeSpan(1000); 

   void PerformOperation()
   {
      this.PerformOperation(OperationTimeout);
   }

   void PerformOperation(TimeSpan timeout)
   {
      // Insert code here.
   }

   TimeSpan OperationTimeout
   {
      get
      {
         return defaultTimeout;
      }
   }
}

Типы, которые не могут обеспечить разрешения, установленного в TimeSpan, должны округлять значение ожидания до ближайшего промежутка, который может быть обеспечен. Например, тип, который может обеспечить разрешение в одну секунду, должен округлять к ближайшей секунде. Исключением из этого правила является округление в нуль. В таком случае ожидание должно округляться до минимально возможного значения. Предотвращение округления в нуль предупреждает возникновение циклов активного ожидания, когда нулевое ожидание приводит к 100% использованию мощности процессора

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

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


void OnReceiveCompleted(Object sender, ReceiveAsyncEventArgs asyncResult)
{
   MessageQueue queue = (MessageQueue) sender;
   // The following code will throw an exception
   // if BeginReceive has timed out.
   Message message = queue.EndReceive(asyncResult.AsyncResult);
   Console.WriteLine("Message: " + (string)message.Body);
queue.BeginReceive(new TimeSpan(1,0,0));
}

Дополнительная информация представлена в разделе Рекомендации по асинхронному программированию.

Безопасность библиотек классов

Разработчики библиотеки классов должны понимать, как обеспечить защиту доступа к коду для того, чтобы писать защищенные библиотеки классов. При написании библиотеки классов учитывайте два принципа безопасности: защищайте объекты, применяя права доступа, и пишите полностью надежный код. Степень применения этих принципов будет зависеть от того, какой класс вы создаете. Некоторые классы, такие как класс System.IO.FileStream, представляют объекты, которые необходимо защищать путем применения прав доступа. Реализация этих классов проверяет права доступа пользователей и только зарегистрированным пользователям позволяется осуществлять те операции, на которые они имеют разрешение. Пространство имен System.Security содержит классы, которые могут помочь вам осуществить эти проверки в создаваемых библиотеках классов. Код библиотеки классов часто является полностью надежным или, по крайней мере, высоконадежным. Т.к. код библиотеки классов часто работает с защищенными ресурсами и неуправляемым кодом, любые дефекты кода представляют серьезную угрозу целостности внутренней системы безопасности. Чтобы минимизировать эту угрозу, при написании кода библиотеки классов следуйте рекомендациям, приведенным в данном разделе. Более подробная информация представлена в разделе MSDN Writing Secure Class Libraries.

Применение прав доступа для защиты объектов

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

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

Полностью надежный код библиотеки классов

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

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

  1. Пользователь запрашивает удаление файла c:\test.txt вызывая метод File.Delete.
  2. Метод Delete создает разрешающий объект, представляющий разрешение на удаление c:\test.txt.
  3. Код класса File проверяет всех пользователей в стеке на наличие у них необходимого разрешения; если такого разрешения нет, возникает исключительная ситуация безопасности.
  4. Класс File объявляет FullTrust, для того чтобы вызвать внутренний код, потому что его пользователи могут не иметь разрешения на это.
  5. Чтобы осуществить операцию удаления файла, класс File использует внутренний API.
  6. Класс File возвращается его пользователям, и запрос на удаление файла завершается успешно.

Меры предосторожности для высоконадежного кода

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

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

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

Производительность

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

Заключение

  • Любая библиотека классов, которая использует защищенные ресурсы, должна гарантировать, что это происходит только при наличии разрешений у всех пользователей.
  • Утверждение разрешений должно проводиться только по необходимости, и ему должны предшествовать необходимые проверки разрешений.
  • Чтобы повысить производительность, сгруппируйте операции, которые вызовут проверки безопасности, и продумайте использование подтверждения, чтобы ограничить пробег по стеку без снижения безопасности.
  • Отдавайте себе отчет в том, как может злонамеренный пользователь использовать класс, чтобы обойти систему безопасности.
  • Не принимайте, что код будет вызываться только пользователями с определенными правами.
  • Не определяйте интерфейсы, не обеспечивающие безопасность типов, они могут быть использованы для обхода системы безопасности.
  • Не открывайте функциональные возможности в классе, это позволит злонамеренному пользователю воспользоваться преимуществами высокой надежности класса.

Рекомендации по организации поточной обработки

Далее приведены основные рекомендации по организации поточной обработки:

  • Избегайте использования статических методов, которые изменяют статическое состояние. В обычных серверных сценариях статическое состояние используется совместно через запросы, это означает, что этот код одновременно могут выполнять множество потоков. Это делает возможными сбои в организации поточной обработки. Рассмотрите использование схемы разработки, при которой данные инкапсулируются в экземпляры, которые не используются совместно через запросы.
  • Статическое состояние должно быть потокобезопасным.
  • Состояние экземпляра не нуждается в потокобезопасности. По умолчанию библиотека не является потокобезопасной. Добавление блокировок для создания потокобезопасного кода снижает производительность, увеличивает конфликты между блокировками и создает условия для возникновения зависаний. В обычных моделях приложений только один поток выполняет пользовательский код в единицу времени. Это минимизирует потребность в потокобезопасности. Поэтому .NET Framework непотокобезопасна по умолчанию. Если вы хотите обеспечить потокобезопасную версию, используйте метод GetSynchronized, чтобы вернуть потокобезопасный экземпляр типа. В качестве примера рассмотрите пространство имен System.Collections.
  • Разрабатывайте библиотеку классов с учетом пиковых нагрузок в серверном сценарии. По возможности избегайте применения блокировок.
  • Осмыслите, как осуществляются вызовы методов в блокированных секциях. Когда статический метод в классе А вызывает статический метод класса В и наоборот, могут возникать зависания. Если классы А и В синхронизируют свои статические методы, это приведет к зависанию. Вы сможете обнаружить это зависание только во время пиковых нагрузок.

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

    Имейте ввиду проблемы с оператором lock (SyncLock в Visual Basic). Довольно заманчиво использовать оператор lock для решения всех проблем с организацией поточной обработки. При проверке кода вы должны остерегаться таких экземпляров, как тот, который показан в следующем примере.

    
    lock(this) 
    {
       myField++;
    }
    

    Для увеличения производительности, необходимо заменить этот код System.Threading.Interlocked.Increment(myField);

    То же косается и следующего примера. Вместо этого кода:

    
    if (x == null) 
    {
       lock (this) 
       {
          if (x == null) 
          {
             x = y;
          }
       }
    }
    
  • Следует использовать такую запись:
    
    System.Threading.Interlocked.CompareExchange(ref x, y, null);
    
  • Избегайте по возможности необходимости синхронизации.

Рекомендации по асинхронному программированию

Асинхронное программирование поддерживается многими областями общеязыковой среды выполнения, такими как Remoting, ASP.NET и Windows Forms. Асинхронное программирование является центральной идеей .NET Framework. В этом разделе приведены схемы разработки для асинхронного программирования.

Следует придерживаться следующих рекомендаций:

  • Клиент должен решить, должен ли отдельный вызов быть асинхронным.
  • Необязательно дополнительно программировать сервер, чтобы поддерживать асинхронное поведение его клиента. Среда выполнения сможет различить представления клиента и сервера. В результате исключена ситуация, в которой сервер должен реализовывать IDispatch и делать большую работу для поддержания динамических вызовов клиента.
  • Сервер может выбрать явную поддержку асинхронного поведения по двум причинам: или потому, что он может реализовать асинхронное поведение более эффективно, чем вся архитектура, или потому, что он захочет поддерживать только асинхронное поведение своими клиентами. Таким серверам для раскрытия асинхронных операций рекомендуется следовать схеме разработки, приведенной в этом документе.
  • Безопасность типов обязательна.
  • Среда выполнения предоставляет необходимые сервисы для поддержки модели асинхронного программирования. Эти сервисы включают следующее:
    • Базовые элементы синхронизации, такие как критические секции и экземпляры ReaderWriterLock.
    • Структурные компоненты синхронизации, такие как контейнеры, которые поддерживают метод WaitForMultipleObjects.
    • Пулы потоков.
    • Открытость базовой инфраструктуре, например, объектам Message и ThreadPool.

Схемы разработки для асинхронного программирования

Следующий пример демонстрирует серверный класс:


public class PrimeFactorizer
{
   public bool Factorize(long factorizableNum,  
            ref long primefactor1,
            ref long primefactor2)
   {
      primefactor1 = 1;
      primefactor2 = factorizableNum;

      // Factorize using a low-tech approach.
      for (int i=2;i<factorizableNum;i++)
      {
         if (0 == (factorizableNum % i))
         {
            primefactor1 = i;
            primefactor2 = factorizableNum / i;
            break;
         }
      }
      if (1 == primefactor1 )
         return false;
      else
         return true   ;
   }
}

В следующем примере показан клиент, определяющий шаблон для асинхронного вызова метода Factorize из класса PrimeFactorizer предыдущего примера.


// Define the delegate.
public delegate bool FactorizingCallback(long factorizableNum,  
      ref long primefactor1,
      ref long primefactor2);

// Create an instance of the Factorizer.
PrimeFactorizer pf = new PrimeFactorizer();

// Create a delegate on the Factorize method on the Factorizer.
FactorizingDelegate fd = new FactorizingDelegate(pf.Factorize);

Компилятор создаст следующий класс FactorizingCallback после синтаксического разбора его определения в первой строке предыдущего примера. Будут сгенерированы методы BeginInvoke и EndInvoke.


public class FactorizingCallback : Delegate
{
   public bool Invoke(ulong factorizableNum,  
         ref ulong primefactor1, ref ulong primefactor2);

   // Supplied by the compiler.   
   public IAsyncResult BeginInvoke(ulong factorizableNum,  
            ref unsigned long primefactor1,
            ref unsigned long primefactor2, AsyncCallback cb, 
            Object AsyncState);

   // Supplied by the compiler.   
   public bool EndInvoke(ref ulong primefactor1,
               ref ulong primefactor2, IAsyncResult ar);
}

Интерфейс, используемый как параметр делегата в следующем примере, определен в библиотеке классов .NET Framework.. Подробнее смотри в разделе MSDN IAsyncResult Interface.


public delegate AsyncCallback (IAsyncResult ar);

public interface IAsyncResult
{
   // Returns true if the asynchronous operation has completed.
   bool IsCompleted { get; }

   // Caller can use this to wait until operation is complete.
   WaitHandle AsyncWaitHandle { get; }

   // The delegate object for which the async call was invoked.
   Object AsyncObject { get; }

   // The state object passed in through BeginInvoke.
   Object AsyncState { get; }

   // Returns true if the call completed synchronously.
   bool CompletedSynchronously { get; }
}

Обратите внимание, что объект, реализующий интерфейс IAsyncResult, должен быть объектом ожидания (waitable) и его базовые элементы синхронизации должны быть уведомлен после отмены или завершения вызова. Это дает возможность клиенту ожидать окончания вызова, вместо проведения опроса. Среда выполнения поставляет большое количество объектов ожидания, которые зеркально отображают базовые элементы синхронизации Win32, такие как ManualResetEvent, AutoResetEvent и Mutex.

Метод Cancel - это запрос на отмену обработки метода после того, как закончился программный период ожидания. Обратите внимание, что это только запрос клиента, и серверу рекомендуется принимать его на обработку. Далее клиент не должен принимать того, что сервер полностью остановил обработку запроса после получения нотификации об отмене метода. Другими словами, клиенту рекомендуется не уничтожать такие ресурсы, как объекты файла, потому что сервер в данный момент может активно их использовать. Свойству IsCanceled будет присвоено значение true, если вызов был отменен, и свойству IsCompleted будет присвоено значение true после того, как сервер закончит обработку вызова. После того, как сервер присвоит свойству IsCompleted значение true, сервер не сможет использовать никакие предоставленные клиентом ресурсы вне обусловленной семантики совместного использования. Таким образом, клиент может безопасно уничтожать ресурсы после того, как свойство IsCompleted возвращает true.

Свойство Server возвращает серверный объект, который предоставил IAsyncResult.

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


public class ProcessFactorizeNumber
{
   private long _ulNumber;

   public ProcessFactorizeNumber(long number)
   {
      _ulNumber = number;
   }

   [OneWayAttribute()]
   public void FactorizedResults(IAsyncResult ar)
   {
      long factor1=0, factor2=0; 
      
      // Extract the delegate from the AsynchResult.
      FactorizingCallback fd = 
         (FactorizingCallback) ((AsyncResult)ar).AsyncDelegate;
      // Obtain the result.
      fd.EndInvoke(ref factor1, ref factor2, ar);

      // Output the results.
      Console.Writeline("On CallBack: Factors of {0} : {1} {2}",
                        _ulNumber, factor1, factor2);
   }
}

// Async Variation 1.
// The ProcessFactorizeNumber.FactorizedResults callback function
// is called when the call completes.
public void FactorizeNumber1()
{
   // Client code.
   PrimeFactorizer pf = new PrimeFactorizer();
   FactorizingCallback fd = new FactorizingCallback(pf.Factorize);

   long factorizableNum = 1000589023, temp=0; 

   // Create an instance of the class that
   // will be called when the call completes.
   ProcessFactorizedNumber fc = 
      new ProcessFactorizedNumber(factorizableNum);
   // Define the AsyncCallback delegate.   
   AsyncCallbackDelegate cb = new AsyncCallback(fc.FactorizedResults);
   // Any object can be the state object.
   Object state = new Object();

   // Asynchronously invoke the Factorize method on pf.
   // Note: If you have pure out parameters, you do not need the 
   // temp variable.
   IAsyncResult ar = fd.BeginInvoke(factorizableNum, ref temp, ref temp,  
                        cb, state); 

// Proceed to do other useful work.

// Async Variation 2.
// Waits for the result.
// Asynchronously invoke the Factorize method on pf.
// Note: If you have pure out parameters, you do not need 
// the temp variable.
public void FactorizeNumber2()
{
   // Client code.
   PrimeFactorizer pf = new PrimeFactorizer();
   FactorizingCallback fd = new FactorizingCallback(pf.Factorize);

   long factorizableNum = 1000589023, temp=0;
   // Create an instance of the class 
   // to be called when the call completes.
   ProcessFactorizedNumber fc = 
            new ProcessFactorizedNumber(factorizableNum);

   // Define the AsyncCallback delegate.
   AsyncCallback cb = new AsyncCallback(fc.FactorizedResults);

   // Any object can be the state object.
   Object state = new Object();

   // Asynchronously invoke the Factorize method on pf.
   IAsyncResult ar = fd.BeginInvoke(factorizableNum, ref temp, ref temp, 
                        null, null); 

   ar.AsyncWaitHandle.WaitOne(10000, false);

   if(ar.IsCompleted)
   {
      int factor1=0, factor2=0;

      // Obtain the result.
      fd.EndInvoke(ref factor1, ref factor2, ar);

      // Output the results.
      Console.Writeline("Sequential : Factors of {0} : {1} {2}", 
                     factorizableNum, factor1, factor2);
   }
}

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

Вызов EndInvoke до завершения асинхронной операции также заблокирует вызывающую функцию. Повторный его вызов с тем же AsyncResult не определен.

Заключение

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

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

Клиенту доступны следующие варианты завершения асинхронных операций:

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

Один сценарий, в котором желательны и синхронные, и асинхронные методы чтения и записи, - это использование ввода/вывода (input/output) файла. Следующий пример иллюстрирует схему разработки, показывая как объект File реализует операции чтения и записи.


public class File
{
   // Other methods for this class go here.

   // Synchronous read method.
   long Read(Byte[] buffer, long NumToRead);

   // Asynchronous read method.
   IAsyncResult BeginRead(Byte[] buffer, long NumToRead, 
            AsyncCallbackDelegate cb);
   long EndRead(IAsyncResult ar);

   // Synchronous write method.
   long Write(Byte[] buffer, long NumToWrite);

   // Asynchrnous write method.
   IAsyncResult BeginWrite(Byte[] buffer, long NumToWrite, 
            AsyncCallbackDelegate cb);

   long EndWrite(IAsyncResult ar);
}

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

Приложение А. Методы повышения производительности .NET приложений

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

Обзор

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

Этот раздел состоит из нескольких частей, в которых подсказки организованы в соответствии с типами проектов и категориями разработчиков. Первый набор подсказок относится к любому языку программирования и содержит совет, который поможет вам в любом языке программирования Общеязыковой среды выполнения (CLR). Далее следуют подсказки для ASP.NET.

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

Рекомендации по повышению производительности для всех приложений

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

Формируйте меньшее количество исключительных ситуаций

Формирование исключительной ситуации может требовать очень больших затрат производительности, так что убедитесь, что вы не формируете их слишком много. Чтобы увидеть, сколько исключительных ситуаций формирует ваше приложение, используйте Perfmon. Вы будете удивлены обнаружив, что определенные области вашего приложения формирует больше исключительных ситуаций, чем вы ожидали. Для большей конкретизации вы также можете проверить их количество программно, применяя Performance Counters (Счетчики производительности).

Обнаружение и удаление кода, формирующего большое количество исключительных ситуаций, может привести к большому выигрышу в производительности. Помните, что не надо ничего делать с блоками try/catch: вы несете потери только при формировании фактической исключительной ситуации. Вы можете использовать столько блоков try/catch, сколько хотите. Использование исключительных ситуаций неоправданно там, где вы теряете производительность. Например, вы должны избегать использования исключительных ситуаций для формирования управляющей логики.

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


public static void Main(string[] args){
  int j = 0;
  for(int i = 0; i < 10000; i++){
    try{   
      j = i;
      throw new System.Exception();
    } catch {}
  }
  System.Console.Write(j);
  return;   
}
  • Будьте внимательны! Среда выполнения может самостоятельно формировать исключительные ситуации! Например, Response.Redirect() формирует исключительную ситуацию ThreadAbort. Даже если вы явно не формируете исключительную ситуацию, вы, возможно, используете функции, которые делают это. Используйте Perfmon, чтобы получить представление о реальной ситуации, и отладчик, чтобы найти источник.
  • Для разработчиков Visual Basic: Visual Basic включает проверку int по умолчанию, чтобы гарантировать формирование исключительных ситуаций в случае возникновения таких ситуаций, как переполнение и деление на нуль. Возможно, вы захотите отключить эту возможность, чтобы выиграть в производительности.
  • Используя СОМ, вы должны помнить, что HRESULTS могут возвратиться как исключительные ситуации. Убедитесь, что вы внимательно отслеживаете это.

Делайте емкие вызовы

Емкий вызов - это вызов функции, который выполняет несколько задач, так же как метод, инициализирующий несколько полей объекта. Его надо рассматривать в противопоставлении неёмкому вызову, который выполняет очень простые задачи и использует для этого множество вызовов (например, установка каждого поля объекта отдельным вызовом). В методах, в которых непроизводительные расходы выше, чем для простых вызовов методов внутри AppDomain, важно делать емкие вызовы. Вызовы P/Invoke, interop и remoting приводят к непроизводительным расходам и вы хотите пореже их использовать. В каждом из этих случаев надо попытаться спроектировать приложение так, чтобы оно не использовало маленькие, частые вызовы, которые приводят к таким большим непроизводительным расходам.

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

  • Осуществить маршаллинг данных
  • Настроить соглашение о вызовах
  • Защитить регистры, сохраняемые вызываемым объектом
  • Переключить режим потока таким образом, чтобы GC не блокировал неуправляемые потоки
  • Установить оболочку, обрабатывающую исключительные ситуации, на вызовы в управляемом коде
  • Заняться управлением потоков (необязательно)

Чтобы уменьшить время перехода через границы, старайтесь по возможности пользоваться P/Invoke. Непроизводительные расходы соответствуют 31 команде плюс затраты на маршаллинг, если нужен маршаллинг данных, и только 8 в противоположном случае. Взаимодействие СОМ намного более дорого, более 65 команд.

Маршаллинг данных не всегда требует больших затрат. Примитивным типам вообще не нужен маршаллинг, также дешевы классы с четкой компоновкой. Реальное снижение производительности происходит во время преобразования данных, например, конвертации текста из ASCI в Unicode. Убедитесь, что данные, передаваемые через границы управляемого кода преобразуются только в том случае, если это необходимо: может оказаться, что просто соглашаясь на определенный тип данных или формат в вашей программе, вы можете избежать больших затрат на маршаллинг.

Следующие типы называют блитируемыми, имея ввиду, что их можно копировать прямо через управляемую/неуправляемую границу вообще без маршаллинга: sbyte, byte, short, ushort, int, uint, long, ulong, float и double. Вы можете свободно передавать эти типы без всяких потерь производительности, так же как типы значений и одномерные массивы, содержащие блитируемые типы. Мельчайшие детали маршаллинга можно изучить дополнительно в библиотеке MSDN.

Разработка с использованием типов значений

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


using System;
namespace ConsoleApplication{ 
  public struct foo{
    public foo(double arg){ this.y = arg; }
    public double y;
  }
  public class bar{
    public bar(double arg){ this.y = arg; }
    public double y;
  }
  class Class1{
    static void Main(string[] args){
      System.Console.WriteLine("starting struct loop...");
      for(int i = 0; i < 50000000; i++)
      {foo test = new foo(3.14);}
      System.Console.WriteLine("struct loop complete. 
                                starting object loop...");
      for(int i = 0; i < 50000000; i++)
      {bar test2 = new bar(3.14); }
      System.Console.WriteLine("All done");
    }
  }
}

Когда запустите этот пример, вы увидите, что цикл с использованием структуры выполняется на порядок быстрее. Однако важно быть осторожным при использовании типов значений в качестве объектов. Это создает дополнительные затраты на упаковку и распаковку, которые могут в конце концов превысить затраты, которые могли бы возникнуть, если бы вы использовали объекты! Чтобы увидеть это на деле, измените код и используйте массив структур foo и объектов bar. Вы обнаружите, что производительность приблизительно та же.

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

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

Можно проследить за тем, как интенсивно вы используете упаковку и распаковку, просмотрев распределения и сборки мусора GC. Это можно сделать прямо с помощью Perfmon или через Performance Counters в коде.

Полную информацию по типам значений смотрите в Приложении Б.

Используйте AddRange для добавления групп элементов

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

  • StringCollection, TraceCollection и т.д.
  • HttpWebRequest
  • UserControl
  • ColumnHeader

Ограничьте ваш Working Set

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

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

  • Чтобы отслеживать Working Set используйте vadump.exe.
  • Просмотрите Perfmon или Performance Counters. Они могут предоставить вам детальную информацию о количестве загруженных классов или количестве методов, которые подвергались JIT компиляции. Вы можете узнать, сколько времени потрачено на загрузку или какой процент времени выполнения потрачен на подкачку файлов.

Используйте циклы For для перебора символов в строке

В C# ключевое слово foreach позволяет пройти по элементам списка, строки и т.д. и осуществить операции над каждым из них. Это очень мощный инструмент, т.к. он работает как универсальный нумератор для многих типов. Альтернативой этого обобщения является скорость, и если вы действительно часто используете итерацию строк, лучше используйте цикл For. Поскольку строки - это простые массивы символов, их можно пройти с намного меньшими потерями, чем другие структуры. JIT компиляция достаточно интеллектуальна (в большинстве случаев), чтобы оптимизировать граничные проверки и другие вещи внутри цикла For, но при обходах foreach этого сделать нельзя. В результате цикл For для строк работает более, чем в пять раз быстрее, чем цикл foreach.

Здесь приведен простой тестовый метод для демонстрации разницы скоростей. Попытайтесь запустить его, затем удалите цикл For и раскомментируйте выражение foreach.


public static void Main(string[] args) {
  string s = "monkeys!";
  int dummy = 0;

  System.Text.StringBuilder sb = new System.Text.StringBuilder(s);
  for(int i = 0; i < 1000000; i++)
    sb.Append(s);
  s = sb.ToString();
  //foreach (char c in s) dummy++;
  for (int i = 0; i < 1000000; i++)
    dummy++;
  return;   
  }
}

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

Используйте StringBuilder для работы со строками

После изменения строки среда выполнения создаст новую строку и вернет ее, а оригинальная строка будет собрана сборщиком мусора. В большинстве случаев это быстрый и простой процесс, но частые изменения строки приводят к снижению производительности: все эти размещения в конечном концов приводят к непроизводительным расходам. Здесь приведены два примера. Первый - это простая программа, в которой происходит дополнение строки 50 000 раз; во втором примере для модификации строки используется объект StringBuilder. Код, использующий StringBuilder, выполняется намного быстрее, если вы запустите оба этих примера, это сразу станет очевидным.


namespace ConsoleApplication1.Feedback{
  using System;
  
  public class Feedback{
    public Feedback(){
      text = "You have ordered: \n";
    }

    public string text;

    public static int 
	Main(string[] args) {
      Feedback test = new Feedback();
      String str = test.text;
      for(int i=0;i<50000;i++){
        str = str + "blue_toothbrush";
      }
      System.Console.Out.WriteLine(
		"done");
      return 0;
    }
  }
}

namespace ConsoleApplication1.Feedback{
  using System;

  public class Feedback{
    public Feedback(){
      text = "You have ordered: \n";
    }

    public string text;

    public static int Main(string[] 
		args) {
      Feedback test = new Feedback();
      System.Text.StringBuilder SB = 
        new System.Text.StringBuilder(
			test.text);
      for(int i=0;i<50000;i++){
        SB.Append("blue_toothbrush");
      }
      System.Console.Out.WriteLine(
		"done");
      return 0;
    }
  }
}

Посмотрите в Perfmon сколько времени экономится, когда не надо распределять тысячи строк. Посмотрите на счетчик "% времени в GC" под списком .NET CLR Memory. Здесь вы также можете увидеть число сохраненных распределений, а так же статистику сборки мусора.

Примечание: с созданием объекта StringBuilder связаны некоторые непроизводительные затраты времени и памяти. На машинах с быстрой памятью имеет смысл применять StringBuilder, если вы делаете около пяти операций. Как правило, 10 и более строковых операций являются оправданием для непроизводительных расходов на любой машине, даже самой медленной.

Прекомпилируйте приложения Windows Forms

Методы подвергаются JIT компилированию при первом использовании. Это означает, что, чем больше методов вызывает ваше приложение при запуске, тем дольше будет идти загрузка программы. Windows Forms используют большое количество совместно используемых библиотек в ОС, и непроизводительные расходы при их запуске могут быть намного большими, чем для любых других приложений. Хотя это и не всегда так, но прекомпилирование приложений Windows Forms обычно приводит к выигрышу в производительности.

Microsoft обеспечивает возможность прекомпилировать приложения, вызывая ngen.exe. Вы можете запустить ngen.exe во время инсталляции или перед поставкой приложения. Определенно больший смысл имеет запускать ngen.exe во время инсталляции, т.к. в этом случае вы сможете удостовериться, что приложение оптимизировано для машины, на которой оно было инсталлировано. Если вы запускаете ngen.exe до поставки программы, вы ограничиваете оптимизацию тем, что доступно на вашей машине. В таблице приведены объективные времена запуска для ShowFormComplex, приложений winforms с примерно сотней элементов управления.

Состояние кода Время
Framework JITed
ShowFormComplex JITed
3.4 c
Framework Precompiled, ShowFormComplex JITed 2.5 c
Framework Precompiled, ShowFormComplex Precompiled 2.1c

Каждый эксперимент проводился после перезагрузки. Как вы видите, приложения Windows Forms сразу используют много методов, поэтому прекомпилирование приводит к существенным выигрышам в производительности.

Используйте Jagged массивы

JIT компиляция оптимизирует jagged массивы (´массивы массивов´) более эффективно, чем прямоугольные массивы, и разница весьма заметна. Данная таблица демонстрирует выигрыш в производительности при использовании jagged массивов вместо прямоугольных массивов в C# и Visual Basic (большие числа лучше):

  C# Visual Basic 7
Assignment (jagged)

Assignment (rectangular)
14.16

8.37
12.24

8.62
Neural Net (jagged)

Neural net (rectangular)
4.48

3.00
4.58

3.13
Numeric Sort (jagged)

Numeric Sort (rectangular)
4.88

2.05
5.07

2.06

Как видите, применение jagged массивов может привести к поразительному повышению производительности.

Поддерживайте размер буфера ввода/вывода (IO) в пределах 4KB - 8KB

Практически для всех приложений размер буфера в пределах 4KB - 8KB обеспечит максимальную производительность. В особых случаях вы, возможно, сможете достичь лучших результатов с большим буфером (загрузка больших графических объектов предсказуемого размера, например), но в 99.99% случаев это только приведет к ненужной растрате памяти. Во всех буферах, унаследованных от BufferedStream, предоставлена возможность установить любой желаемый размер, но в большинстве случаев 4 и 8 обеспечат наилучшую производительность.

Будьте внимательны с асинхронным вводом/выводом (IO)

В редких случаях вы сможете получить выгоду от асинхронного ввода/вывода (IO). Одним из примеров может быть загрузка и декомпрессия ряда файлов: вы можете считать биты из одного потока, декодировать их и записать в другой поток. Чтобы эффективно использовать асинхронный ввод/вывод, необходимы большие усилия, и если что-то сделано неправильно, это приведет только к потерям производительности. Преимущество асинхронного ввода/вывода в том, что при правильном его использовании есть возможность достичь в десять раз большей производительности.

Рекомендации по доступу к базам данных

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

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

Используйте оптимальный управляемый провайдер

Вместо того, чтобы полагаться на универсальный провайдер, сделайте правильный выбор управляемого провайдера. Есть управляемые провайдеры, написанные специально для различных баз данных, например, SQL (System.Data.SqlClient). При использовании универсального интерфейса, например, System.Data.Odbc, в то время как есть возможность использования специализированного компонента, вы потеряете в производительности занимаясь дополнительными уровнями преобразований. Использование оптимального провайдера даст возможность использовать различные языки: Управляемый SQL клиент обращается к SQL базе данных на TDS, что намного лучше, чем универсальный OleDbprotocol.

Используйте Data Reader вместо Data Set когда это возможно

Используйте data reader как можно чаще. Это обеспечивает быстрое считывание данных, которые могут быть кэшированы по желанию пользователя. Data reader - это просто поток, который обеспечивает возможность считывать данные по мере их поступления и затем удаляет их без сохранения в dataset для дальнейшей навигации. Потоковый подход более быстрый и вызывает меньшие непроизводительные затраты, поскольку вы можете сразу использовать данные. Чтобы решить, имеет ли смысл кэширование для навигации, вы должны определить, как часто вам нужны одни и те же данные. Данная таблица приведена, чтобы продемонстрировать разницу между DataReader и DataSet в ODBC и SQL провайдерах при извлечении данных с сервера (большие числа лучше):

  ADO SQL
DataSet 801 2507
DataReader 1083 4585

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

Используйте Mscorsvr.dll для многопроцессорных машин

Для автономных серверных приложений и приложений среднего уровня (middle-tier) убедитесь, что используете mscorsvr для многопроцессорных машин. Mscorwks не оптимизированы по масштабированию и пропускной способности, а серверные версии имеют несколько оптимизаций, которые обеспечивают им хорошую масштабируемость, когда доступно более одного процессора.

По возможности используйте хранимые процедуры

Хранимые процедуры являются высоко оптимизированными инструментальными средствами, которые при эффективном использовании обеспечивают великолепную производительность. Используйте хранимые процедуры для обработки вставок, обновлений и удалений с data adapter. Хранящиеся процедуры не надо интерпретировать, компилировать или даже передавать от клиента, что уменьшает непроизводительные расходы как сетевого трафика, так и сервера. Убедитесь, что используете CommandType.StoredProcedure вместоCommandType.Text.

Будьте осторожны с использованием строк динамического соединения

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

Отключите возможности, которые не используете

Отключите автоматическое управление пранзакциями, если не используете его. Для SQL управляемого провайдера это делается посредством строки соединения:


SqlConnection conn = new SqlConnection(
"Server=mysrv01;
= Integrated Security true;
Enlist=false");

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


public DataSet SelectSqlSrvRows(DataSet dataset,string connection,string query){
    SqlConnection conn = new SqlConnection(connection);
    SqlDataAdapter adapter = new SqlDataAdapter();
    adapter.SelectCommand = new SqlCommand(query, conn);
    adapter.MissingSchemaAction = MissingSchemaAction.AddWithKey;
    adapter.Fill(dataset);
    return dataset;
}

Избегайте команд, генерируемых автоматически

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

Правильно используйте ADO

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

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

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

  • убедившись, что первичный ключ присутствует в таблице
  • изменяя команду select адаптера данных по обстановке, и
  • вызывая Fill

Поддерживайте небольшие размеры своих наборов данных

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

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

Используйте CommandBehavior.SequentialAccess с data reader. Это важно при работе с blob типами данных, т.к. дает возможность считывать данные маленькими порциями. Поскольку вы можете в данный момент времени работать только с одной частью данных, исчезает задержка на загрузку большого количества данных. Если нет необходимости работать сразу со всем объектом, использование последовательного доступа обеспечит намного лучшую производительность.

Рекомендации по повышению производительности для ASP.NET приложений

Активно используйте кэширование

При разработке приложений, используя ASP.NET, будьте внимательны с кэшированием. В серверных версиях ОС у вас есть множество средств для тонкой настройки использования кэшей со стороны сервера и клиента. В ASP.NET для повышения производительности есть несколько возможностей и инструментальных средств.

Выходное кэширование - сохраняет статический результат ASP.NET запроса. Определяется с помощью директивы <@% OutputCache %>:

  • Duration - время существования элемента в кэше
  • VaryByParam - различие содержимого кэша по Get/Post параметрам
  • VaryByHeader - различие содержимого кэша по Http заголовкам
  • VaryByCustom - различие содержимого кэша по типу браузера
  • Переопределите, чтобы различать по тому параметру, какому хотите:
    • Фрагментарное кэширование - когда нет возможности сохранить всю страницу (конфиденциальность, персонализация, динамическое содержимое), при сохранении ее частей для более быстрого восстановления позже можно использовать фрагментарное кэширование.
      1. VaryByControl - проверяет кэшированные элементы по значениям элемента управления
    • Cache API - обеспечивает очень хорошую детализацию для кэширования, сохраняя хэш-таблицу кэшированных объектов в памяти (System.web.UI.caching). Также:
      1. Включает зависимости (ключ, файл, время)
      2. Автоматически уничтожает неиспользуемые элементы
      3. Поддерживает обратные вызовы

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

Используйте Session State только по необходимости

Одной чрезвычайно мощной особенностью ASP.NET является возможность сохранения session state для пользователей, например, корзину покупок на сайте электронной коммерции или историю браузера. Т.к. это свойство используется по умолчанию, даже если вы не пользуетесь им, расходуется память. Если вы не используете session state, выключите его и предохраните себя от непроизводительных расходов добавлением <@% EnabledSessionState = false %> в свое приложение.

Для страниц, только считывающих session state, можно выбрать EnabledSessionState=readonly. В этом случае затраты будут меньшими, чем на полное считывание/запись состояния соединения, и этот вариант более приемлем, если необходима только часть функциональных возможностей.

Используйте View State только по необходимости

Примером View State может быть длинная форма, которую пользователи должны заполнить: если они нажимают кнопку Back в своем браузере и затем возвращаются, форма останется заполненной. Когда данные функциональные возможности не используются, это состояние расходует память и производительность. Возможно, наибольшая потеря производительности в этом случае происходит из-за того, что возвращаемые данные должны посылаться по сети каждый раз при загрузке страницы, чтобы обновить и проверить кэш. Т.к. это происходит по умолчанию, необходимо обозначить, что вы не хотите использовать View State, следующим образом: <@% EnabledViewState = false %>.

Избегайте STA COM

Апартаменты СОМ созданы для работы с потоковой и неуправляемой средой. Существует два вида апартаментов СОМ: однопоточный и многопоточный. Многопоточный (MTA) СОМ разработан для обработки многопоточности, в то время как однопоточный (STA) СОМ основывается на системе обмена сообщениями, чтобы сериализовывать запросы потока. Управляемый мир потоконезависим, и использование STA COM требует, чтобы все неуправляемые потоки совместно использовали один поток для взаимодействия. В результате - огромное снижение производительности, т.е. этого надо избегать. Если вы не можете перенести апартамент COM объект в управляемый мир, используйте <@%AspCompat = "true" %> для страниц, которые его используют.

Удалите ненужные Http модули

В зависимости от используемых возможностей удалите неиспользуемые или ненужные http модули из конвейера (pipeline).

Избегайте использования свойства Autoeventwireup

Вместо того, чтобы полагаться на autoeventwireup, переопределите события Page. Например, вместо записи метода Page_Load() попытайтесь перезагрузить public метод void OnLoad(). Это освобождает среду выполнения от необходимости выполнять CreateDelegate() для каждой страницы.

Кодируйте, используя ASCII, когда нет необходимости использовать UTF

По умолчанию, ASP.NET настроена так, чтобы кодировать запросы и ответы, как UTF-8. Если ASCII - это все, в чем нуждается ваше приложение, устранение затрат на UTF может повысить производительность. Обратите внимание, что это можно делать только для каждого отдельного приложения.

Используйте оптимальную процедуру аутентификации

Существует несколько способов аутентификации пользователя и несколько более дорогих, чем другие (в порядке возрастания затрат: None, Windows, Forms, Passport). Убедитесь, что используете самую дешевую из подходящих для вашего случая как классы.

Приложение Б. Вопросы производительности .NET Framework

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

Обзор

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

Сборка мусора

Основные сведения

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


Foo a = new Foo();      // Allocate memory for the object and Initialize

…a…                     // Use the object 
  
delete a;               // Tear down the state of the object, clean up
                        // and free the memory for that object

В неуправляемом коде вам надо проделывать все эти операции самостоятельно. Упущение этапов распределения или очистки памяти, может привести к совершенно непредсказуемому поведению, что будет очень трудно отладить, а если вы забудете освободить объекты, могут возникнуть утечки памяти. Последовательность распределения памяти в Общеязыковой среде выполнения (CLR) очень похожа на только что рассмотренную. Если мы добавим GC-специфичную информацию, мы получим нечто очень похожее:


Foo a = new Foo();   // Allocate memory for the object and Initialize

…a…  
                     // Use the object (it is strongly reachable)
a = null;            // A becomes unreachable (out of scope, nulled, etc)
                     // Eventually a collection occurs, and a´s resources
                     // are torn down and the memory is freed

До тех пор, пока объект может быть освобожден, в обоих мирах предпринимаются одни и те же шаги. В неуправляемом коде вы должны помнить о необходимости освобождения объекта по окончании работы с ним. В управляемом коде, как только объект больше не используется, GC может удалить его. Конечно же, если ваш ресурс требует особого внимания для освобождения (скажем, закрытие соединения), GC может понадобиться помощь, для того чтобы закрыть его правильно. До сих пор применяется такой же код, как вы писали ранее для очистки ресурса перед освобождением, в форме методов Dispose() и Finalize().

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

Сборщик мусора CLR - это относящийся к определенному поколению, сборщик. Он следует некоторым принципам, которые позволяют достигать превосходной производительности. Во-первых, известно, что объекты с коротким временем жизни обычно небольшие, к ним часто обращаются. GC разделяет таблицу распределения на несколько подтаблиц, называемых поколениями, что позволяет максимально сократить время сборки мусора. Поколение 0 включает молодые, часто используемые объекты. Это поколение самое маленькое, для сборки мусора в нем требуется около 10 миллисекунд. GC может игнорировать другие поколения во время этой сборки мусора, таким образом обеспечивается намного большая производительность. Поколения 1 и 2 предназначены для больших и более старых объектов, и сборка мусора в них происходит не так часто. Когда происходит сборка мусора в поколении 1, также просматривается и поколение 0. Сборка мусора в поколении 2 - это полная сборка мусора, и только здесь GC проходит всю таблицу. Это также приводит к разумному использованию кэшей CPU, который может настроить подсистему памяти под конкретный процессор, на котором выполняется GC.

Когда происходит сборка мусора?

Когда сделано распределение времени, GC проверяет необходимость сборки мусора. Учитывается размер мусора, размер оставшейся памяти, размеры каждого поколения, а затем для принятия решения используется эвристическое правило. Во время сборки мусора скорость распределения объекта обычно такая же (или больше), чем в С или С++.

Что происходит, когда идет сборка мусора?

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

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

    Рисунок 1. Перед сборкой мусора: обратите внимание, что не все блоки доступны!

  4. Удаление объектов из списка доступных делает большинство объектов подлежащими уничтожению. Однако некоторые ресурсы должны быть обработаны специально. При определении объекта у вас есть выбор использования метода Dispose() или метода Finalize() (или и того, и другого).
  5. Заключительным этапом в сборке мусора является фаза уплотнения. Все используемые объекты перемещаются в непрерывный блок, все указатели и ссылки обновляются.
  6. Уплотняя живущие объекты и обновляя начальный адрес свободного пространства, GC поддерживает непрерывность всего свободного пространства. Если для размещения объекта достаточно места, GC возвращает элемент управления программе. В противном случае он вызывает OutOfMemoryException.

    Рисунок 2. После сборки мусора: блоки, к которым происходят обращения, уплотнены. Больше свободного пространства!

Удаление объекта

Некоторые объекты нуждаются в специальной обработке до того, как ресурс сможет быть возвращен. Примерами таких ресурсов являются файлы, сетевые соединения или соединения с базами данных. Если вы хотите закрыть эти ресурсы изящно, простого освобождения памяти в куче будет недостаточно. Чтобы осуществить очистку объекта, вы можете использовать или метод Dispose(), или метод Finalize(), или сразу оба этих метода.

Метод Finalize():

  • Вызывается сборщиком мусора
  • Не гарантируется, что будет вызван в каком-либо порядке или в предсказуемое время
  • После вызова освобождает память после следующей сборки мусора
  • Сохраняет все дочерние объекты до следующей сборки мусора

Метод Dispose():

  • Вызывается программистом
  • Размещается и планируется программистом
  • Возвращает ресурсы по завершению метода

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

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

  • В сценарии, в котором задействованы ресурсы, которые должны быть освобождены в определенное время, в случае использования финализаторов вы теряете контроль. Скажем, у вас есть открытый файл и из соображений безопасности его надо закрыть. Даже после того как вы обнулите объект и вызовите GC, файл останется открытым до тех пор, пока не будет вызван его метод Finalize(), и вы не имеете никакого представления, когда это может быть сделано.
  • N объектов, которые должны быть освобождены в определенном порядке, могут быть обработаны неправильно.
  • Огромный объект с дочерними объектами может занимать очень много памяти, требуя дополнительных сборок мусора и понижая производительность. Такие объекты могут оставаться в памяти длительное время.
  • Маленький объект, который должен быть финализирован, может иметь указатели на большие ресурсы, которые могут быть освобождены в любое время. Эти объекты не будут освобождены до тех пор, пока на них ссылается объект, который должен быть финализирован, создавая ненужное сжатие памяти и вызывая частые сборки мусора.

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

Рисунок 3. Пути освобождения и финализации, которые может пройти объект

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

Как выбрать, какой сборщик мусора использовать?

В CLR есть два разных GC: Рабочая станция (mscorwks.dll) и Сервер (mscorsvr.dll). При работе в режиме Рабочей станции больший интерес, чем пространство и эффективность, представляет задержка . Сервер с множеством процессоров и клиентов, соединенных через сеть, может позволить некоторую задержку, но основной является производительность. Microsoft включила два сборщика мусора, каждый из которых приспособлен к определенной ситуации.

GC Сервер:

  • Много процессорный масштабируемый
  • Один поток GC на CPU
  • Программа останавливается во время сборки

GC Рабочая станция:

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

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

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

Миф: сборка мусора с помощью GC всегда медленнее, чем сборка мусора вручную

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

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

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

Настройка для повышения скорости

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

Наиболее важным правилом для поддержания производительности и также самое легкое правило для программистов, которые пишут неуправляемый код: отслеживайте выделения памяти, которые необходимо сделать и освобождайте их, когда они больше не нужны. GC никак не может знать, что вы не собираетесь использовать созданную вами строку размером 20KB, если она является частью объекта, который остается в рабочем состоянии. Предположим, вы где-то сохраняете этот объект и никогда не собираетесь снова использовать эту строку. Обнуление поля даст возможность GC позже собрать эти 20KB, даже если этот объект до сих пор нужен вам для других целей. Если объект больше не нужен, убедитесь, что вы не сохраняете ссылки на него. Для более маленьких объектов это является меньшей проблемой.

Следующий важный аспект, влияющий на производительность, касается деталей очистки объекта. Как упоминалось ранее, финализация имеет огромное влияние на производительность. Наиболее общим примером является применение управляемого обработчика к неуправляемому ресурсу: вам надо применить некоторые методы очистки, и здесь производительность становится проблемой. Если вы зависите от финализации, вы открываетесь проблемам производительности, о которых говорилось ранее. Надо помнить еще о том, что GC практически совершенно не осведомлен о сжатии памяти в мире неуправляемого кода, таким образом вы можете использовать огромное количество ресурсов только сохраняя указатель в управляемой куче. Отдельный указатель не занимает много памяти, т.е. он может существовать некоторое время до того, как понадобится сборка мусора. Чтобы обойти эти проблемы производительности, вы должны выбрать схему разработки для всех объектов, требующих специальной очистки.

У программиста есть три варианта при работе с очисткой объекта:

  1. Реализуйте оба варианта

    Рекомендуемая схема очистки объекта. Это объект с некоторой смесью неуправляемых и управляемых ресурсов. Примером будет System.Windows.Forms.Control. У него есть неуправляемый ресурс (HWND) и потенциально управляемый ресурс (DataConnection, и т.д.). Если вы не уверены в том, когда используются неуправляемые ресурсы, можно открыть манифест вашей программы в ILDASM и проверить наличие ссылок на неуправляемые библиотеки. Другой вариант - это воспользоваться vadump.exe, чтобы посмотреть, какие ресурсы загружаются вместе с вашей программой. С помощью обоих этих способов вы сможете понять, какие ресурсы используете.

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

    Пример:

    
    public class MyClass : IDisposable {
      public void Dispose() {
        Dispose(true);
        GC.SuppressFinalizer(this);
      }
      protected virtual void Dispose(bool disposing) {
        if (disposing) {
          …
        }
          …
      }
      ~MyClass() {
        Dispose(false);
      }
    }
    
  2. Реализуйте только Dispose()

    Делайте это в случае, когда объект имеет только управляемые ресурсы, и вы хотите гарантировать, что его очистка детерминирована. Примером такого объекта является System.Web.UI.Control.

    Пример:

    
    public class MyClass : IDisposable {
      public virtual void Dispose() {
        …
      }
    
  3. Реализуйте только Finalize()

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

    Пример:

    
    public class MyClass {
      …
      ~MyClass() {
        …
      }
    
  4. Ничего не реализуйте

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

Рекомендации

Рекомендации для работы с управлением памятью хорошо знакомы: освобождайте объекты после окончания работы с ними и не допускайте, чтобы оставались указатели на объекты. Когда доходите до очистки объекта с неуправляемыми ресурсами, реализовывайте оба метода: и Finalize(), и Dispose(). Это предотвратит непредсказуемость поведения в будущем и обеспечит хорошие навыки программирования.

Метод Dispose() должен поддерживаться объектами, которые используют неуправляемые ресурсы; однако метод Finalize() должен быть помещен только в те объекты, которые прямо используют эти ресурсы, например, OS Handle или распределение неуправляемой памяти. Рекомендуется создавать маленькие управляемые объекты, такие как "упаковщики", для реализации Finalize() в дополнение к поддержанию метода Dispose(), которые будут вызываться методом Dispose() родительского объекта. Т.к. родительские объекты не имеют финализатора, все дерево объектов не переживет сборку мусора невзирая на то, был вызван метод Dispose() или нет.

Хорошим правилом при работе с финализаторами является использование их только в наиболее примитивных объектах, которым необходима финализация. Помните: используйте метод Finalize() только там и тогда, когда вы должны.

JIT

Основные сведения

Как и любая VM, CLR нуждается в методе компилирования промежуточного языка в машинный код. Когда вы компилируете программу, чтобы запустить в CLR, компилятор преобразовывает ваш источник из языка высокого уровня в комбинацию метаданных MSIL (Microsoft Intermediate Language - промежуточный язык Microsoft). Все это собирается в РЕ файл, который затем может выполняться на любой имеющей CLR машине. При запуске этого исполняемого файла JIT начинает компилирование IL в машинный код и выполнение этого кода на реальной машине. Это делается на основе пометодной компиляции, т.е. при JIT компилировании длительность задержки зависит от размера кода, который вы хотите запустить.

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

  • Свертывание констант - подсчитывает значения констант во время компиляции.

    До После
    x = 5 + 7 x = 12

  • Передача констант и копий - проводит обратное замещение, чтобы раньше освободить переменные.

    До После
    x = a x = a
    y = x y = a
    z = 3 + y z = 3 + a

  • Замещение вызовов методов - Замещает args значениями, передаваемыми во время вызова, и устраняет вызов. Чтобы отсечь невыполняемый участок программы могут быть сделаны другие оптимизации. Из соображений скорости JIT имеет несколько ограничений, по которым он может замещать вызовы методов. Например, только маленькие методы могут быть замещены (размер IL менее 32), анализ управления потоками довольно примитивен.

    До После
    
    … 
    x=foo(4, true);
    …
    }
    foo(int a, bool b){
    if(b){
    return a + 5;
    } else {
    return 2a + bar();
    }
    
    
    … 
    x = 9
    …
    } 
    foo(int a, bool b){
    if(b){
    return a + 5;
    } else {
    return 2a + bar();
    }
    

  • Code Hoisting и Dominators- Удаляет код из внутренних циклов, если он дублируется во внешних. Пример цикла ´before´, приведенный ниже, демонстрирует то, что действительно генерируется на уровне IL, т.к. все индексы массива должны быть проверены.

    До После
    
    for(i=0; i< a.length;i++){ 
    if(i < a.length()){
    a[i] = null
    } else {
    raise IndexOutOfBounds;
    }
    }
    
    
    for(int i=0; i<a.length; i++){ a[i] = null;}
    

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

    До После
    
    for(i=0; i< 3; i++){ 
    print("flaming monkeys!");
    }
    
    
    print("flaming monkeys!"); 
    print("flaming monkeys!");
    print("flaming monkeys!");
    

  • Общее удаление подвыражения - Если активная переменная до сих пор содержит информацию, которая должна пересчитываться, замените ее.

    До После
    x = 4 + y
    z = 4 + y
    x = 4 + y
    z = x


Когда код должен быть JIT компилирован?

Здесь приведены этапы, которые проходит ваш код во время выполнения:

  1. Программа загружается, и инициализируется таблица функций с указателями, ссылающимися на IL.
  2. Главный (Main) метод путем JIT преобразуется в машинный код, который затем запускается. Вызовы функций компилируются в непрямые вызовы функций через таблицу.
  3. Когда вызывается другой метод, среда выполнения просматривает таблицу, чтобы проверить, нет ли указателей на код, обработанный JIT:
    1. Если указатель есть (возможно, он вызывается из другого вызова или был прекомпилирован), выполнение управляющей логики продолжается.
    2. Если нет, метод подвергается JIT компиляции и таблица обновляется.
  4. По мере вызова все больше и больше методов компилируются в машинный код, и больше элементов таблицы указывают на растущий пул процессорных инструкций.
  5. По мере выполнения программы JIT вызывается все реже и реже до тех пор, пока все не будет перекомпилировано.
  6. Метод не подвергается JIT до тех пор, пока он не вызван, и потом в течение выполнения программы он никогда не подвергается JIT повторно. Вы платите только за то, что используете.

Миф: программы, использующие JIT выполняются медленнее прекомпилированных программ

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

Более важным является то, что JIT может выполнять некоторые оптимизации, которые недоступны обычным компиляторам, такие как оптимизации, характерные для CPU, и настройка кэша.

Оптимизации, присущие только JIT

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

  • Процессор-специфические оптимизации - Во время выполнения JIT знает о том, может ли он использовать инструкции SSE или 3DNow. Ваш выполняемый файл будет скомпилирован специально для P4, Athlon или любого другого семейства процессоров. Будучи однажды созданным, тот же код будет совершенствоваться вместе с JIT и машиной пользователя.
  • Удаление уровней преобразования логических адресов в физические, т.к. расположение функции и объекта доступны во время выполнения.
  • JIT может осуществлять оптимизации через сборки, обеспечивая множество преимуществ, получаемых вами при компилировании программы со статическими библиотеками, но сохраняя гибкость и небольшие последствия использования динамических библиотек.
  • Активные inline функции, вызываются чаще, т.к. во время выполнения учитывается управляющая логика. Оптимизации могут обеспечить существенное повышение скорости, и остается еще большое поле для улучшений в следующих версиях.

Перекомпилирование кода (использование ngen.exe)

Для производителей приложения привлекательна возможность прекомпиляции кода во время инсталляции. Компания Microsoft предоставляет эту возможность в форме ngen.exe, который позволит единожды запустить нормальный JIT компилятор по всей вашей программе и сохранит результат. Поскольку оптимизация только времени выполнения не может быть осуществлена во время прекомпиляции, генерируемый код не всегда так же хорош, как при нормальной JIT компиляции. Но из-за того, что не надо на лету обрабатывать JIT методы, затраты на запуск намного меньшие, и некоторые программы будут запускаться заметно быстрее. В будущем ngen.exe сможет делать больше, чем просто запускать ту же JIT компиляцию времени выполнения: более активные оптимизации, чем время выполнения, раскрытие оптимизации порядка загрузки разработчикам (оптимизация способа упаковки кода в VM страницы) и более сложные, требующие затрат времени оптимизации, которые могут воспользоваться преимуществом времени во время прекомпиляции.

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

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

Повышение производительности

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


… 
for(int i = 0; i < myArray.length; i++){
…
}
…

int l = myArray.length;
for(int i = 0; i < l; i++){
…
}
…

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

Истина в том, что такие оптимизации были полезны примерно в течение 10 лет: современные компиляторы способны осуществлять эти оптимизации. Кстати, иногда такие вещи могут действительно навредить производительности. В примере, приведенном выше, компилятор, вероятно, будет проверять то, что длина myArray постоянна, и вставит постоянную в сравнение цикла for. Но код справа может заставить компилятор думать, что это значение должно быть сохранено в регистре, т.к. l активна на всем протяжении цикла. Вывод таков: пишите наиболее читабельный и осмысленный код. Нет смысла стараться передумать компьютер, а иногда это может даже навредить.

AppDomains

Основные сведения

Межпроцессное взаимодействие становится все более и более распространенным. Из соображений стабильности и безопасности OS содержат приложения в разных адресных пространствах. Простым примером является способ, которым в NT выполняется 16-битное приложение: при запуске в отдельном процессе одно приложение не может пересекаться с выполнением другого. Проблемой здесь являются затраты на контекстные переключения и открытие связи между процессами. Эти операции имеют огромное негативное влияние на производительность. В серверных приложениях, которые обычно выполняют несколько web приложений, это основной непроизводительный расход как в производительности, так и в масштабируемости.

CLR представляет концепцию AppDomain, который похож на процесс, в котором для приложения есть модульное пространство. Однако AppDomains не ограничен одним процессом. Благодаря безопасности типов, предоставляемой управляемым кодом, есть возможность запуска двух совершенно независимых AppDomains в одном процессе. Для ситуаций, в которых вы обычно тратили большое количество времени выполнения на межпроцессное взаимодействие, выигрыш в производительности огромен: IPC между сборками в пять раз быстрее, чем между процессами в NT. Значительно уменьшая эти затраты, вы получаете и выигрыш в скорости, и новую возможность во время разработки программы: теперь имеет смысл использовать разделенные процессы там, где раньше это могло быть слишком дорого. Возможность запуска множества программ в одном процессе с той же системой безопасности, как раньше, имеет громадные последствия для масштабируемости и безопасности.

В OS нет поддержки для AppDomains. AppDomains обрабатывается хостом CLR, таким как представлен в ASP.NET, выполняемый оболочкой, или Microsoft Internet Explorer. Вы также можете написать собственный хост. Каждый хост определяет домен, применяемый по умолчанию, который загружается при первой загрузке приложения и закрывается только после завершения процесса. Когда вы загружаете другие сборки в процесс, вы можете определить, что они будут загружаться в определенный AppDomain, и установить различные политики безопасности для каждой из них. Это детально описано в документации Microsoft .NET Framework SDK.

Повышение производительности

Чтобы эффективно использовать AppDomains, вы должны подумать о том, какое приложение вы пишите, и какую работу оно должно выполнять. Применение AppDomains наиболее эффективно, если ваше приложение соответствует некоторым из следующих характеристик:

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

Пример ситуации, в которой полезны AppDomains, можно найти в сложном приложении ASP.NET. Предположим, что вы хотите усилить изоляцию двух различных vRoots: в собственном пространстве вам надо поместить каждый vRoot в отдельный процесс. Это, а также переключение контекста между ними, требует довольно больших затрат. В управляемом мире каждый vRoot может быть отдельным AppDomain. Это сохраняет требуемую изоляцию и полностью исключает непроизводительные издержки.

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

Сборки, которые совместно используются множеством AppDomains, должны быть JIT скомпилированы для каждого домена, для того чтобы сохранить изолированность доменов. В результате создается много дубликатов кода и идет пустое растрачивание памяти. Рассмотрите случай приложения, которое отвечает на запросы с помощью определенного XML сервиса. Если определенные запросы должны быть изолированы друг от друга, вы должны направлять их к разным AppDomains. В данном случае проблема в том, что каждому AppDomain теперь нужны одни и те же XML библиотеки, и одна и та же сборка будет загружаться много раз.

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

Безопасность

Основные сведения

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

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

Повышение производительности

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

  • Декларативная безопасность является самым быстрым способом для Assert, Deny и PermitOnly.
  • При осуществлении взаимодействия в неуправляемом коде, вы можете отменить проверки безопасности во время выполнения, используя атрибут SuppressUnmanagedCodeSecurity. Это перемещает проверку на время компоновки, что намного быстрее. Убедитесь, что код не вызывает ослабление безопасности в другом коде, который может использовать перемещенную проверку в небезопасном коде.
  • Проверки на идентичность требуют больших затрат, чем проверки кода. Вы можете использовать LinkDemand, чтобы делать эти проверки во время компоновки.

Есть два способа оптимизировать систему безопасности:

  • Осуществляйте проверки во время компоновки, а не во время выполнения.
  • Делайте проверки безопасности декларативными, а не обязательными.

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

Remoting

Основные сведения

Remoting технология в .NET распространяет богатую систему типов и функциональных возможностей CLR по всей сети. Используя XML, SOAP и HTTP вы можете вызывать процедуры и передавать объекты удаленно так, как будто они размещены на одном и том же компьютере. Вы можете рассматривать это, как .NET версию DCOM или CORBA, в которой реализован расширенный набор их функциональных возможностей.

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

Повышение производительности

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

  • Вместо большого количества небольших вызовов делайте единичные и емкие - если вы можете сократить количество вызовов, вы должны это сделать. Например, вы устанавливаете некоторые свойства для удаленного объекта с помощью get() и set() методов. Повторное создание объекта удаленно с установкой этих свойств при создании сохранит ваше время. Т.к. это может быть сделано с помощью одного удаленного вызова, вы сохраните время, потраченное на сетевой трафик. Иногда имеет смысл переместить объект на локальную машину, здесь установить свойства и затем скопировать объект обратно. Решение необходимо принимать в зависимости от пропускной способности и задержек.
  • Сбалансируйте загруженность CPU и сети - иногда имеет смысл работать через сеть, а иногда лучше все делать самостоятельно. Если у вас уходит много времени на сетевые вызовы, будет страдать производительность. Если вы слишком загружаете CPU, вы не сможете отвечать на другие запросы. Сбалансированность между использованием CPU и сети является жизненно важной для масштабируемости вашего приложения.
  • Используйте асинхронные вызовы - когда вы делаете вызов через сеть, убедитесь что он асинхронный, за исключением случаев, когда вам действительно нужны синхронные вызовы. В противном случае ваше приложение будет в состоянии ожидания до тех пор, пока не получит ответ, что может быть неприемлемым в пользовательском интерфейсе. Хороший пример приведен в Framework SDK, поставляемом с .NET, Samples\technologies\remoting\advanced\asyncdelegate.
  • Оптимально используйте объекты - вы можете определить, чтобы новый объект создавался для каждого запроса (SingleCall) или чтобы один и тот же объект использовался для всех запросов (Singleton). Использование одного объекта для всех запросов конечно же менее ресурсоемко, но много усилий потребуется для синхронизации и конфигурации объекта от запроса к запросу.
  • Используйте сменные каналы и средства форматирования - мощной возможностью удаления является возможность включения любого канала или средства форматирования в ваше приложение. Например, если вам не надо проходить через брандмауэр, нет причины использовать HTTP канал. TCP канал обеспечит вам гораздо лучшую производительность. Убедитесь, что вы выбрали наиболее подходящий канал или средство форматирования.

Типы значений

Основные сведения

Гибкость, доступная объекту, обеспечивается за счет очень небольших затрат производительности. Объекты, размещаемые в куче, требуют больше времени для распределения, доступа и обновления, чем объекты размещаемые в стеке. Это происходит потому, что, например, структура в С++ намного более эффективна, чем объект. Конечно, объекты могут делать то, что не под силу структуре, и намного более универсальны.

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

Повышение производительности

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

Далее приведен пример простой проверки, проведенной для сравнения времени, необходимого для создания большого числа объектов и типов значений:


using System;
using System.Collections;

namespace ConsoleApplication{
  public struct foo{
    public foo(double arg){ this.y = arg; }
    public double y;
  }
  public class bar{
    public bar(double arg){ this.y = arg; }
    public double y;
  }
class Class1{
  static void Main(string[] args){
    Console.WriteLine("starting struct loop....");
    int t1 = Environment.TickCount;
    for (int i = 0; i < 25000000; i++) {
      foo test1 = new foo(3.14);
      foo test2 = new foo(3.15);
       if (test1.y == test2.y) break; // prevent code from being 
       eliminated JIT
    }
    int t2 = Environment.TickCount;
    Console.WriteLine("struct loop: (" + (t2-t1) + "). starting object 
       loop....");
    t1 = Environment.TickCount;
    for (int i = 0; i < 25000000; i++) {
      bar test1 = new bar(3.14);
      bar test2 = new bar(3.15);
      if (test1.y == test2.y) break; // prevent code from being 
      eliminated JIT
    }
    t2 = Environment.TickCount;
    Console.WriteLine("object loop: (" + (t2-t1) + ")");
    }

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


using System;
using System.Collections;

namespace ConsoleApplication{
  public struct foo{
    public foo(double arg){ this.y = arg; }
    public double y;
  }
  public class bar{
    public bar(double arg){ this.y = arg; }
    public double y;
  }
  class Class1{
    static void Main(string[] args){
      Hashtable boxed_table = new Hashtable(2);
      Hashtable object_table = new Hashtable(2);
      System.Console.WriteLine("starting struct loop...");
      for(int i = 0; i < 10000000; i++){
        boxed_table.Add(1, new foo(3.14)); 
        boxed_table.Add(2, new foo(3.15));
        boxed_table.Remove(1);
      }
      System.Console.WriteLine("struct loop complete. 
                                starting object loop...");
      for(int i = 0; i < 10000000; i++){
        object_table.Add(1, new bar(3.14)); 
        object_table.Add(2, new bar(3.15));
        object_table.Remove(1);
      }
      System.Console.WriteLine("All done");
    }
  }
}

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

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

 
     

   
   
     
  VBNet рекомендует