Изучая язык программирования C#, я сталкивался с особенностями как самого языка, так и его средой исполнения, *некоторые из которых, с позволения сказать, «широко известны в узких кругах». Собирая таковые день за днем в своей копилке, что бы когда-нибудь повторить, чего честно сказать еще ни разу не делал до этого момента, пришла идея поделиться ими.
Эти заметки не сделают ваш код красивее, быстрее и надежнее, для этого есть Стив Макконнелл. Но они определенно внесут свой вклад в ваш образ мышления и понимание происходящего.
Что-то из приведенного ниже покажется слишком простым, другое, наоборот, сложным и не нужным, но куда без этого.
Итак, начинаем:
1) Расположение объектов и экземпляров в динамической памяти
Объекты содержат в себе статические поля и все методы. Экземпляры содержат только не статические поля. Это значит, что методы не дублируются в каждом экземпляре, и здесь применяется паттерн Flyweight.
2) Передача параметров в методы
Структура передает свою копию в метод, Класс передает копию своей ссылки. А вот когда мы используем ключевое слово REF — структура передает указатель на себя, а класс передает свою исходную ссылку.
Пример REF для ссылочного типа.
1) Работает без ошибок, мы зануляем копию переданной ссылки.
static void Main( string[] args )
{
StringBuilder sb = new StringBuilder();
sb.Append("Hello ");
AppendHello(sb);
Console.WriteLine(sb.ToString());
}
private static void AppendHello(StringBuilder sb)
{
sb.Append(" World!");
sb = null;
}
2) Возникает исключение System.NullReferenceException при попытке обратиться к методу в переменной значение которой null.
static void Main( string[] args )
{
StringBuilder sb = new StringBuilder();
sb.Append("Hello ");
AppendHello(ref sb);
Console.WriteLine(sb.ToString());
}
private static void AppendHello(ref StringBuilder sb)
{
sb.Append(" World!");
sb = null;
}
3) Подготовить код до выполнения
В CLR есть блок CER, который говорит JIT — «подготовь код до выполнения, так что когда в нем возникнет необходимость, все будет под рукой». Для этого подключаем пространства имен System.Runtime.CompilerServices и RuntimeHelpers.PrepareConstrainedRegions.
4) Регулярные выражения
Regex можно создать с опцией Compiled — это генерация выражения в IL-код. Он значительно быстрее обычного, но первый запуск будет медленным.
5) Массивы
Одномерные массивы в IL представлены вектором, они работают быстрее многомерных. Массивы одномерных массивов используют векторы.
6) Коллекции
Пользовательские коллекции лучше наследовать от ICollection, реализация IEnumerable получается бесплатно. Но нет индекса (очень индивидуально).
7) Расширяющие методы
Если имя расширяющего метода вступает в конфликт с именем метода типа, то можно использовать полное имя расширяющего метода, и тип передать аргументом.
StaticClass.ExtesionMethod( type );
8) LINQ
LINQ lazy loading («ленивая» загрузка) — select, where, take, skip etc.
LINQ eager loading (запросы выполняются сразу) — count, average, min, max, ToList etc. (Но если коллекция бесконечна, то запрос ни когда не завершится.)
9) Блок синхронизации
У структурных типов и примитивных (byte,int,long...) нет блока синхронизации, который присутствует у объектов в управляемой куче на ряду с ссылкой. Поэтому не будет работать конструкция Monitor.() или Lock().
10) Интерфейсы
Если в C# перед именем метода указано имя интерфейса, в котором определен этот метод (IDisposable.Dispose), то вы создаете явную реализацию интерфейсного метода (Explicit Interface Method Implementation, EIMI). При явной реализации интерфейсного метода в C# нельзя указывать уровень доступа (открытый или закрытый). Однако когда компилятор создает метаданные для метода, он назначает ему закрытый уровень доступа (private), что запрещает любому коду использовать экземпляр класса простым вызовом интерфейсного метода. Единственный способ вызвать интерфейсный метод — это обратиться через переменную этого интерфейсного типа.
Без EIMI не обойтись (например, при реализации двух интерфейсных методов с одинаковыми именами и сигнатурами).
11) Нет в C#, но поддерживается IL
Статические поля в интерфейсах, методы отличающиеся только возвращаемым значением и многое другое.
12) Сериализация
При сериализации графа объектов некоторые типы могут оказаться сериализуемыми, а некоторые — нет. По причинам, связанным с производительностью, модуль форматирования перед сериализацией не проверяет возможность этой операции для всех объектов. А значит, может возникнуть ситуация, когда некоторые объекты окажутся сериализованными в поток до появления исключения SerializationException. В результате в потоке ввода-вывода оказываются поврежденные данные. Этого можно избежать, например, сериализуя объекты сначала в MemoryStream.
В C# внутри типов, помеченных атрибутом [Serializable], не стоит определять автоматически реализуемые свойства. Дело в том, что имена полей, генерируемые компилятором, могут меняться после каждой следующей компиляции, что сделает невозможной десериализацию экземпляров типа.
13) Константы
Константы помещаются в метаданные сборки, поэтому если были изменения, нужно перекомпилировать все использующие ее сборки. Т.к. DLL с константой может даже не загружаться.
Лучше использовать static readonly задавая значения в конструкторе, она постоянно загружается в использующих ее сборках, и выдает актуальное значение.
14) Делегаты
GetInvocationList — возвращает цепочку делегатов, можно вызывать любые, отлавливать исключения, получать все возвращаемые значения, а не только последнее.
15) Сравнение строк
В Microsoft Windows сравнение строк в верхнем регистре оптимизировано. *StringComparison.OrdinalIgnoreCase, на самом деле, переводит Char в верхний регистр. ToUpperInvariant. Используем string.compare(). Windows по умолчанию использует UTF-16 кодировку.
16) Оптимизация для множества строк
Если в приложении строки сравниваются часто методом порядкового сравнения с учетом регистра или если в приложении ожидается появление множества одинаковых строковых объектов, то для повышения производительности надо применить поддерживаемый CLR механизм интернирования строк (string interning).
При инициализации CLR создает внутреннюю хеш-таблицу, в которой ключами являются строки, а значениями — ссылки на строковые объекты в управляемой куче.
17) Безопасные строки
При создании объекта SecureString его код выделяет блок неуправляемой памяти, которая содержит массив символов. Уборщику мусора об этой неуправляемой памяти ничего не известно. Очищать эту память нужно вручную.
18) Безопасность
Управляемые сборки всегда используют DEP и ASLR.
19) Проектирование методов
Объявляя тип параметров метода, нужно по возможности указывать «минимальные» типы, предпочитая интерфейсы базовым классам. Например, при написании метода, работающего с набором элементов, лучше всего объявить параметр метода, используя интерфейс IEnumerable.
public void ManipulateItems<T>(IEnumerable<T> collection) { ... }
В то же время, объявляя тип возвращаемого методом объекта, желательно выбирать самый сильный из доступных вариантов (пытаясь не ограничиваться конкретным типом). Например, лучше объявлять метод, возвращающий объект FileStream, а не Stream.
20) Еще раз про автосвойства
Автоматически реализуемые свойства AIP лучше не использовать (мнение автора, угадай какого).
а) Значение по умолчанию можно задать только в конструкторе. (Изменено в Roslyn C# 6);
б) Проблема при сериализации (пункт 12);
в) Нельзя поставить точку останова.
21) Конфигурационный файл
а) Любому двоичному коду .NET может быть сопоставлен внешний конфигурационный файл XML. Этот файл располагается в том же каталоге, и имеет такое же имя с добавленным в конце словом .CONFIG;
б) Если вы предоставляете решение только в двоичной форме, документирующие комментарии могут быть собраны в XML файл при компиляции, поэтому и в такой ситуации вы можете предоставить пользователям отличный набор подсказок. Для этого нужно только разместить итоговый XML файл в том же каталоге, что и двоичный файл, и Visual Studio .NET будет автоматически отображать комментарии в подсказках IntelliSense.
22) Исключения
CLR обнуляет начальную точку исключения:
try {} catch (Exception e) { throw e; }
CLR не меняет информацию о начальной точке исключения:
try {} catch (Exception e) { throw; }
Можно создать событие FirstChanceException класса AppDomain и получать информацию об исключениях еще до того, как CLR начнет искать их обработчики.
Исключения медленно работают, т.к. происходит переход в режим ядра.
23) IS и AS
IS — В этом коде CLR проверяет объект дважды:
if ( obj is Person ) { Person p = (Person) obj; }
AS — В этом случае CLR проверяет совместимость obj с типом Person только один раз:
Person p1 = obj as Person; if ( p1 != null ) { ... }
24) Проверяем хватит ли памяти перед выполнением
Создание экземпляра класса MemoryFailPoint проверяет, достаточно ли памяти перед началом действия или вбрасывает исключение. Однако, учтите, что физически память еще не выделялась, и этот класс не может гарантировать, что алгоритм получит необходимую память. Но его использование определенно поможет сделать приложение более надежным.
25) Немного про Null
Чтобы использовать null совместимый Int32 можно написать:
Nullable<Int32> x = null; или Int32? x = null;
Оператор объединения null совместимых значений — ?? (если левый операнд равен null, оператор переходит к следующему), рассмотрим два эквивалентных выражения:
1)
string temp = GetFileName();
string fileName = ( temp != null ) ? temp : "Untitled";
2)
string fileName = GetFileName() ?? "Untitled";
26) Таймеры
Библиотека FCL содержит различные таймеры:
1) Timer в System.Threading — подходит для выполнения фоновых заданий в потоке пула;
2) Timer в System.Windows.Forms — таймер связан с вызывающим потоком, это предотвращает параллельный вызов;
3) DispatcherTimer в System.Windows.Threading. — эквивалентен второму, но для приложений Silverlight и WPF;
4) Timer в System.Timers. — по сути является оболочкой первого, Джеффри Рихтер не советует его использовать.
27) Type и typeof
Чтобы получить экземпляр Type для типов, вместо метода Type.GetType применяется операция typeof. Если тип известен во время компиляции, то операция typeof сразу осуществляет поиск по методанным вместо того, чтобы делать это во время выполнения.
28) Фишка using
Что бы уменьшить количество кода и сделать его понятнее? можно использовать директиву using следующим образом:
using DateTimeList = System.Collections.Generic.List<System.DateTime>;
29) Директивы препроцессора
а) #IF DEBUG и #ENDIF — используются для указания блоков кода которые будут компилироваться только в DEBUG режиме.
б) #DEFINE XXX; #IF (DEBUG && XXX) — можно добавить в условие номер сборки «XXX».
в) !DEBUG == RELEASE (на всякий случай).
г) #LINE 111 — в окне ошибок покажет строку 111.
д) #LINE HIDDEN — скрывает строчку от отладчика.
е) #WARNING XXX; #ERROR YYY — означают XXX — предупреждение, YYY — ошибку.
30) Всякая *антность
Ковариантность — преобразование в прямом порядке, ключевое слово OUT.
string[] strings = new string[3];
object[] objects = strings;
interface IMyEnumerator<out T>
{
T GetItem( int index );
}
T --> R
IOperation<T> --> IOperation<R>
Контравариантность — преобразование в обратном порядке, ключевое слово IN.
interface IMyCollection<in T>
{
void AddItem( T item );
}
R --> T
IOperation<T> --> IOperation<R>
Инвариантность — не разрешено неявное преобразование типов.
По умолчанию обобщенные типы инвариантны. Еще обобщенные классы называют открытыми, в рантайме они закрываются конкретными типами «int», «string», например. И это разные типы, статические поля в них будут тоже разными.
31) Методы расширения
public static class StringBuilderExtensions {
public static Int32 IndexOf ( this StringBuilder sb, Char char) { ... }
}
Позволяют вам определить статический метод, который вызывается по средством синтаксиса экземплярного метода. Например, можно определить собственный метод IndexOf для StringBuilder. Сначала компилятор проверит класс StringBuilder или все его базовые классы на наличие метода IndexOf с нужными параметрами, если он не найдет такого, то будет искать любой статический класс с определенным методом IndexOf, у которого первый параметр соответствует типу выражения используемого при вызове метода.
А это реализация паттерна Visitor в .Net.
32) Контексты исполнения
С каждым потоком связан определенный контекст исполнения. Он включает в себя параметры безопасности, параметры хоста и контекстные данные логического вызова. По умолчанию CLR автоматически его копирует, с самого первого потока до всех вспомогательных. Это гарантирует одинаковые параметры безопасности, но в ущерб производительности. Чтобы управлять этим процессом, используйте класс ExecutionContext.
33) Volatile
JIT — компилятор гарантирует что доступ к полям помеченным данным ключевым словом будет происходить в режиме волатильного чтения или записи. Оно запрещает компилятору C# и JIT-компилятору кэшировать содержимое поля в регистры процессора, что гарантирует при всех операциях чтения и записи манипуляции будут производиться непосредственно с памятью.
34) Классы коллекций для параллельной обработки потоков
ConcurrentQueue — обработка элементов по алгоритму FIFO;
ConcurrentStack — обработка элементов по алгоритму LIFO;
ConcurrentBag — несортированный набор элементов, допускающий дублирование;
ConcurrentDictionary<TKey, TValue> — несортированный набор пар ключ-значение.
35) Потоки
Конструкции пользовательского режима:
а) Волатильные конструкции — атомарная операция чтения или записи.
VolatileWrite, VolatileRead, MemoryBarrier.
б) Взаимозапирающие конструкции — атомарная операция чтения или записи.
System.Threading.Interlocked (взаимозапирание) и System.Threading.SpinLock (запирание с зацикливанием).
Обе конструкции требуют передачи ссылки (адрес в памяти) на переменную (вспоминаем о структурах).
Конструкции режима ядра:
Примерно в 80 раз медленнее конструкций пользовательского режима, но зато имеют ряд преимуществ описанных в MSDN (много текста).
Иерархия классов:
WaitHandle
EventWaitHandle
AutoResetEvent
ManualResetEvent
Semaphore
Mutex
36) Поля класса
Не инициализируйте поля явно, делайте это в конструкторе по умолчанию, а уже его с помощью ключевого слова this() используйте для конструкторов принимающих аргументы. Это позволит компилятору генерировать меньше IL кода, т.к. инициализация происходит 1 раз в любом из конструкторов (а не во всех сразу одинаковые значения, копии).
37) Запуск только одной копии программы
public static void Main ()
{
bool IsExist;
using ( new Semaphore ( 0, 1, "MyAppUniqueString", out IsExist ) ) {
if ( IsExist ) { /* Этот поток создает ядро, другие копии программы не смогут запуститься. */ }
else { /* Этот поток открывает существующее ядро с тем же именем, ничего не делаем, ждем возвращения управления от метода Main, что бы завершить вторую копию приложения. */ }
}}
38) Сборка мусора
В CLR реализовано два режима:
1) Рабочая станция — сборщик предполагает что остальные приложения не используют ресурсы процессора. Режимы — с параллельной сборкой и без нее.
2) Сервер — сборщик предполагает что на машине не запущено никаких сторонних приложений, все ресурсы CPU на сборку! Управляемая куча разбирается на несколько разделов — по одному на процессор (со всеми вытекающими, т.е. один поток на одну кучу).
39) Финализатор
Выполняет функцию последнего желания объекта перед удалением, не может длиться дольше 40 секунд, не стоит с этим играться. Переводит объект минимум в 1 поколение, т.к. удаляется не сразу.
40) Мониторинг и управление сборщиком мусора на объекте
Вызываем статический метод Alloc объекта GCHandle, передаем ссылку на объект и тип GCHandleType в котором:
1) Weak — мониториг, обнаруживаем что объект более не доступен, финализатор мог выполниться.
2) WeakTrackResurrection — мониторинг, обнаруживаем что объект более не доступен, финализатор точно был выполнен (при его наличии).
3) Normal — контроль, заставляет оставить объект в памяти, память занятая этим объектом может быть сжата.
4) Pinned — контроль, заставляет оставить объект в памяти, память занятая этим объектом не может быть сжата (т.е. перемещена).
41) CLR
CLR по сути является процессором для команд MSIL. В то время как традиционные процессоры для выполнения всех команд используют регистры и стеки, CLR использует только стек.
42) Рекурсия
Если вы когда нибудь видели приложение, которое приостанавливается на секунду другую, после чего полностью исчезает безо всякого сообщения об ошибке, почти наверняка это было вызвано бесконечной рекурсией. Переполнение стека, оно как известно, не может быть перехвачено и обработано. Почему? Читаем в книгах или блогах.
43) Windbg и SOS (Son of Strike)
Сколько доменов присутствуют в процессе сразу?
— 3. System Domain, Shared Domain и Domain 1 (домен с кодом текущего приложения).
Сколько куч (поколений) на самом деле?
— 0, 1, 2 и Large Object Heap.
Large Object Heap — для очень больших объектов, не сжимается по умолчанию, только через настройку в файле XML конфигурации.
Еще отличие в клиентском и серверном режиме сборки мусора (в книгах не все так подробно, возможно неточность перевода).
— для каждого ядра создается свой HEAP, в каждом из которых свои 0, 1, 2 поколения и Large Object Heap.
Создание массива размером больше чем 2 Гб на 64 разрядных платформах.
— gcAllowVeryLargeObjects enabled=«true|false»
Что делать, когда свободная память есть, а выделить большой непрерывный ее участок для нового объекта нельзя?
— разрешить режим компакт для Large Object Heap. GCSettings.LargeObjectHeapCompactionMode;
Не рекомендуется использовать, очень затратно перемещать большие объекты в памяти.
Как быстро в рантайме найти петли потоков (dead-locks)?
— !dlk
Источники (не реклама):
1) Джеффри Рихтер, «CLR via C#» 3-е/4-е издание.
2) Трей Нэш, «C# 2010 Ускоренный курс для профессионалов».
3) Джон Роббинс, «Отладка приложений для Microsoft .NET и Microsoft Windows».
4) Александр Шевчук (MCTS, MCPD, MCT) и Олег Кулыгин (MCTS, MCPD, MCT) ресурс ITVDN (https://www.youtube.com/user/CBSystematicsTV/videos?shelf_id=4&view=0&sort=dd).
5) Сергей Пугачев. Инженер Microsoft (https://www.youtube.com/watch?v=XN8V9GURs6o)
Надеюсь, данный перечень пришелся по вкусу как начинающим, так и бывалым программистам на C#.
Спасибо за внимание!
*Обновил, исправил ошибки, некоторые моменты дополнил примерами.
Если вы нашли ошибку, прошу сообщить об этом в личном сообщении.