Изучая язык программирования 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#.

Спасибо за внимание!

*Обновил, исправил ошибки, некоторые моменты дополнил примерами.
Если вы нашли ошибку, прошу сообщить об этом в личном сообщении.

Комментарии (62)


  1. lair
    28.04.2015 11:51
    +1

    Любому двоичному коду .NET может быть сопоставлен внешний конфигурационный файл XML. Этот файл располагается в том же каталоге, и имеет такое же имя с добавленным в конце словом .CONFIG;

    Что вы хотели этим сказать?


    1. ParaPilot
      28.04.2015 12:13

      Я думаю, автор имел в виду app.config и подобные файлы конфигурации.


      1. lair
        28.04.2015 12:15
        +3

        Я тоже так думаю, но они так не работают.


    1. GrigoryPerepechko
      29.04.2015 23:48
      +1

      В контексте .net, перевести assembly как двоичный код — это сильно.


  1. Sane
    28.04.2015 13:36
    +5

    8 пункт — неправда. Запрос выполняется, если он возвращает скаляр(Count, Sum, First...). Даже если на нем вызвать foreach (то есть, выполнить GetEnumerator()) он будет выполнен так лениво, как только можно. То есть, такое выражение не выполнится сразу:

    	var s =  from f in Foo()
              	group f by f%2;
    



    1. lair
      28.04.2015 14:08

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


    1. timramone
      28.04.2015 21:19

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


      1. mayorovp
        28.04.2015 22:13

        Если мы не получаем никакого скалярного значения после упорядочивания и не производим итерацию, как в примере сверху — еще как поможет :)


  1. T-D-K
    28.04.2015 13:55
    +5

    Многое в статье — просто цитаты из Рихтера.
    И про пункт 20 — можно поставить бряк на автосвойство. В 2015 студии это можно даже сделать мышкой.


    1. Dywar Автор
      28.04.2015 22:02

      Именно, рад что заметил это. Название намекает «Интересные заметки по C# и CLR». Сколько именно из Джеффри Рихтера не считал.


  1. hmspns
    28.04.2015 15:09
    +1

    Как быстро в рантайме найти петли потоков (dead-locks)?
    — !dlk

    Не очень понятно, что вы имели в виду.


    1. m08pvv
      28.04.2015 15:30

      Видимо, это относится к отладке в Windbg с подгруженной библиотекой SOS (Son of Strike).


  1. withoutuniverse
    28.04.2015 15:33

    Мне статья понравилась, но есть замечание по терминологии в 1 пункте

    Объекты содержат в себе статические поля и все методы. Экземпляры содержат только не статические поля. Это значит, что методы не дублируются в каждом экземпляре, и здесь применяется паттерн Flyweight.
    Есть понятие («класс») и есть понятие («экземпляр класса», что по сути == «объект»). Используя такие термины, никогда не возникнет путаницы при изучении нового языка.

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


    1. tabushi
      29.04.2015 10:30

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

      Я думаю не у всех =) Это уже относится к более фундаментальным знаниям (многопоточность в ОС, стек, регистры процессора, контекст потока и т.д.), а не напрямую к C#.
      А в MSIL у методов экземляра есть неявный параметр this, что и позволяет работать с конкретным объектом.
      Когда this == null: невыдуманная история из мира CLR


  1. a_mastrakov
    28.04.2015 15:57
    +8

    В целом статься получилась довольно сумбурной и содержит довольно много ошибок. Возможно автор злоупотреблял переводчиком или недостаточно понимает описываемые механизмы, но в любом случае статью нужно серьезно переработать.
    Из ошибок которые сразу бросились в глаза это п.35 — VolatileWrite, VolatileRead — это не атомарные операции, по пункту Б — я даже не знаю как это написать, но в целом весь пункт сплошная ошибка. В англоязычной литературе взаимным запиранием называется мьютекс (Mutual Exclusion). Interlocked — класс предоставляет набор атомарных операций и не имеет ничего общего с мьютексом.
    п.30 — написано не имеет отношения к ковариации и контравариации msdn.microsoft.com/ru-ru/library/dd799517(v=vs.110).aspx


    1. Dywar Автор
      28.04.2015 21:40

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


      1. mayorovp
        28.04.2015 22:16

        По умолчанию обобщенные типы инвариантны. Еще обобщенные классы называют открытыми, в рантайме они закрываются конкретными типами «int», «string», например. И это разные типы, статические поля в них будут тоже разными.
        Какое отношение выделенное предложение имеет к ковариантности, контрвариантности или инвариантности?


        1. Dywar Автор
          28.04.2015 22:32

          К перечисленным вами слов — никакого, именно поэтому использовано слово «еще», это как дополнение к «обобщенные типы».


      1. a_mastrakov
        29.04.2015 06:03
        +1

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


        1. Dywar Автор
          29.04.2015 07:01
          -1

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


  1. mayorovp
    28.04.2015 16:26
    +3

    Местами бред полный.

    У структурных типов и примитивных (byte,int,long...) нет блока синхронизации

    Исключения медленно работают, т.к. происходит переход в режим ядра.

    в) !DEBUG == RELEASE (на всякий случай).

    31) Методы расширения

    А это реализация паттерна Visitor в .Net.


    1. T-D-K
      28.04.2015 16:46

      Местами бред полный.

      У структурных типов и примитивных (byte,int,long...) нет блока синхронизации

      Я конечно не специалист, но разве у неупакованных ссылочных типов есть индекс блока синхронизации? По-моему как раз у них его и нет.
      Safe Thread Synchronization
      Правда тут ссылка на того же Рихтера, из чьей книги автор и взял половину «заметок».


      1. a_mastrakov
        28.04.2015 18:58

        Я конечно не специалист, но разве у неупакованных ссылочных типов есть индекс блока синхронизации? По-моему как раз у них его и нет.

        Вы скорее всего опечатались, механизм упаковки/распаковки существует только для value type.
        У структурных типов и примитивных (byte,int,long...) нет блока синхронизации

        У value type нет слова заголовка объекта, у reference type (в том числе упакованных value type) он есть — это основа основ, которую, я думал, знают все.
        Исключения медленно работают, т.к. происходит переход в режим ядра.

        Формально да, это так. Хотя определение «медленно» для разных задач разное, так что нужно просто сказать что происходит переключение контекста потока.


        1. mayorovp
          28.04.2015 19:29
          +3

          Исключения медленно работают, т.к. происходит переход в режим ядра.

          Формально да, это так. Хотя определение «медленно» для разных задач разное, так что нужно просто сказать что происходит переключение контекста потока.
          При обработке исключения происходит серия переходов managed-unmanaged. Но сами такие переходы не страшны. К примеру, по два таких перехода происходит на каждый вызов делегата, если верить отладчику студии. Означает ли это, что делегатов надо избегать?

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

          Но при чем тут вообще режим ядра?


          1. a_mastrakov
            28.04.2015 20:29

            Но при чем тут вообще режим ядра?

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


            1. mayorovp
              28.04.2015 22:06

              Э… зачем? Какая секретная информация хранится в системной памяти?


              1. a_mastrakov
                29.04.2015 05:54

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


                1. mayorovp
                  29.04.2015 05:59

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


                  1. a_mastrakov
                    29.04.2015 09:15

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


                    1. mayorovp
                      29.04.2015 12:43

                      Не знаю, что в этой книге понимается под программными исключениями — но в этой главе пишется про исключительные ситуации, генерируемые железом. Про исключения, генерируемые вот так: throw new Exception() — там нет ни слова.


                    1. GrigoryPerepechko
                      30.04.2015 09:50

                      >>Я не большой специалист в этих вопросах
                      Это видно. Эксепшны в дотнете сугубо user-mode. И да, в процессоре нет kernel mode. Там ring0.


                      1. Dywar Автор
                        30.04.2015 21:50

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

                        1) "Я говорил об ущербе производительности, потому что, несмотря на свободу исключений в .NET, внутри они реализуются через SEH. Если хотите это проверить, отладьте приложение .NET, используя отладку в неуправляемом режиме (native mode–only debugging), — вы увидите те же первые случаи исключения при инициации вашего исключения. Это подразумевает переход в режим ядра при каждом исключении. Идея, повторяю, в том, чтобы инициировать исключения при ошибках, а не в нормальном ходе выполнения программы."

                        2) «Actually, .NET exceptions are implemented in terms of SEH exceptions, which means that they do go on a trip through kernel mode. See blogs.msdn.com/cbrumme/archive/2003/10/01/51524.aspx3 for Chris Brumme's explanation.»

                        С такими отзывами, я раз 10 перечитаю эти заметки, спасибо!


                        1. mayorovp
                          30.04.2015 22:40

                          Нашел эту статью в открытом доступе (hint: надо стереть тройку в конце ссылки). Сделал поиск по ключевому слову «kernel». Не нашел утверждений о том, что любое исключение SEH безусловно проходит через ядро.

                          Вот самое близкое из найденного:

                          Probably take a trip through the OS kernel. Often take a hardware exception.
                          Но тут говорится о возможном проходе через ядро — в случае использования отладчиков, профайлеров и прочих инструментов, которые при нормальной работе приложения отключены.

                          Откуда вообще пошло гулять по инету утверждение о том, что любое исключение SEH проходит через ядро ОС?


                          1. Dywar Автор
                            30.04.2015 22:46

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


                            1. mayorovp
                              30.04.2015 22:57

                              Лично мне достаточно других источников. Не помню название, но была какая-то переведенная книга именно про SEH. Стоит сейчас где-то в шкафу у бабушки.

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


                              1. Dywar Автор
                                30.04.2015 23:19

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


                                1. mayorovp
                                  30.04.2015 23:24

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

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


                                  1. Dywar Автор
                                    01.05.2015 21:42

                                    WinDbg, IDA.


                          1. gusev_p
                            04.05.2015 12:22

                            Вот еще ссылка в тему blog.codinghorror.com/understanding-user-and-kernel-mode.

                            написал простой тест — в бесконечном цикле создаем исключение/пробрасываем/ловим — в ProcessExplorer-е
                            Kernel Time стабильно растет. Если в тесте оставить только создание класса исключения — Kernel Time не изменяется (win7, x64).


                            1. Dywar Автор
                              04.05.2015 13:43

                              Здорово.


                            1. gusev_p
                              04.05.2015 15:52

                              еще немного поиcследовал вопрос:
                              1. в предыдущем эксперименте у меня стояла галочка Prefer32Bit, т.е. процесс из под wow64 был запущен. Если ее убрать, то время в ядре становится меньше гораздо, но все равно с работой программы продолжает расти
                              2. по стекам ProcessExplorer-а видно, что throw замыкается на RaiseException — стандартную
                              API функцию для генерации SEH-вских эксепшнов.
                              3. обработка SEH-вских фреймов (как и описано в вышеприведенных статьях) происходит в два
                              этапа — вначале RtlDispatchException ищет обработчик выше по стеку, затем этот обработчик вызывает RtlUnwind для снятия со стека всего лишнего (EXCEPTION_REGISTRATION-ов/активаций функций).
                              4. судя по частично доступным в сети сорцам винды 2000, единственное место, которое обращается
                              в ядро в этой схеме — это переход из RtlUnwind на найденный фрейм через системный сервис ZwContinue.


                              1. Dywar Автор
                                04.05.2015 16:40

                                Есть WRK v1.2 (официально доступный исходник)
                                The Windows Research Kernel v1.2 contains the sources for the core of
                                the Windows (NTOS) kernel and a build environment for a kernel that will run on
                                x86 (Windows Server 2003 Service Pack 1) and
                                amd64 (Windows XP x64 Professional)
                                A future version may also support booting WRK kernels on Windows XP x86 systems,
                                but the current kernels will fail to boot due to differences in some shared
                                structures.

                                The NTOS kernel implements the basic OS functions
                                for processes, threads, virtual memory and cache managers, I/O management,
                                the registry, executive functions such as the kernel heap and synchronization,
                                the object manager, the local procedure call mechanism, the security reference
                                monitor, low-level CPU management (thread scheduling, Asynchronous and Deferred
                                Procedure calls, interrupt/trap handling, exceptions), etc.

                                Смотрел по курсу Intuit.ru «Введение во внутреннее устройство Windows».
                                Очень интересно, правда забыл уже почти все.


      1. mayorovp
        28.04.2015 19:23
        +1

        Кто такие «неупакованные ссылочные типы»? :) Наверное, речь шла о неупакованных значимых типах. Разумеется, у них такого индекса нет. Но такой индекс есть, к примеру, у упакованных значимых типов!

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


        1. T-D-K
          28.04.2015 19:26

          mayorovp, a_mastrakov
          Да. Я очепятался. Правда очень грубый косяк. К вечеру на работе уже голова болит. Я и тот комментарий не с первого раза смог сформулировать. Несколько раз переписывал.


        1. zebraxxl
          28.04.2015 22:22

          Дело в том, что string — это тоже примитивный тип!

          Я может быть чего-то не знаю — но всегда считал string — указательный неизменяемый тип. Так по крайней мере говорится в MSDN: https://msdn.microsoft.com/en-us/library/system.type.isprimitive(v=vs.100).aspx.


          1. mayorovp
            28.04.2015 22:29

            Примитивный тип — это тип, который не может быть описан через другие типы. У типа string существует одна возможность, которая не может быть реализована иначе как средствами CLR — это получение строкового объекта исходя из строкового литерала.


            1. zebraxxl
              29.04.2015 00:16

              А почему string нельзя описать через другие типы? Один int для длинны и массив char. Собственно в CLR тип string так и реализован.


              1. mayorovp
                29.04.2015 05:49
                +2

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

                MyString foo = "Hello, world!";
                


    1. Dywar Автор
      28.04.2015 21:57

      1) У структурных типов и примитивных (byte,int,long...) нет блока синхронизации, который присутствует у объектов в управляемой куче на ряду с ссылкой.
      Если не читать то что после запятой, то да, такое допущение будет ошибочным.

      2) Методы расширения — прочитайте паттерн Visitor в бесплатной книге itvdn.com/ru/patterns (которая идет как дополнение к первоисточнику GOF).
      Я не сам это придумал, это ведь заметки а не мои домыслы.


      1. mayorovp
        28.04.2015 22:12
        +1

        1) У структурных типов и примитивных (byte,int,long...) нет блока синхронизации, который присутствует у объектов в управляемой куче на ряду с ссылкой.
        Если не читать то что после запятой, то да, такое допущение будет ошибочным.
        Не вижу разницы, если честно.

        2) Методы расширения — прочитайте паттерн Visitor в бесплатной книге itvdn.com/ru/patterns (которая идет как дополнение к первоисточнику GOF).
        Я не сам это придумал, это ведь заметки а не мои домыслы.
        Спасибо, я знаю что такое паттерн Visitor. Не вижу, как в его реализации могут помочь методы расширения, которые являются всего лишь обычными статическими методами.


        1. Dywar Автор
          28.04.2015 22:46

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

          2) Цитата из бесплатной книги (ссылка на нее выше):
          «Паттерн Visitor – позволяет единообразно обойти набор элементов с разнородными интерфейсами (т.е. набор объектов разных классов не приводя их к общему базовому типу), а также позволяет добавить новый метод (функцию) в класс объекта, при этом не изменяя сам класс этого объекта.»
          Я не имею такой же квалификации как автор этих слов, но согласен с ним.


          1. mayorovp
            28.04.2015 22:51

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


            1. Dywar Автор
              28.04.2015 23:02

              Цитата из той же книги:
              «Известные применения паттерна в .Net

              Паттерн Visitor, выражен в платформе .Net в виде идеи использования расширяющих методов.»

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


              1. mayorovp
                28.04.2015 23:16
                +4

                Сожгите эту книгу!


          1. Qbit
            29.04.2015 13:23

            > Паттерн Visitor… позволяет добавить новый метод (функцию) в класс объекта, при этом не изменяя сам класс этого объекта.

            Добавить новый _полимофный_ метод в класс объекта. Extension-методы — это просто синтаксический сахар (языковой, не CLR) для _вызова_ функций. Языки, где нет этого сахара, просто используют другой синтаксис вызова. Паттерн Visitor к синтаксису того или иного языка не имеет никакого отношения. К объекту какого класса будет применён extension-метод — это определяется во время компиляции. К объекту какого класса будет применятся навешенный с помощью Visitor'а «метод» — это определяется во время исполнения, так как в compile time этой информации в общем случае нет.


  1. Viacheslav01
    29.04.2015 13:36
    +1

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

    А я всегда считал что ref для ссылочных типов передает указатель на ссылку на объект.


    1. mayorovp
      29.04.2015 15:57

      А я вообще считал, что параметры передает в метод вызывающий метод, а не они сами…


  1. Qbit
    29.04.2015 13:46
    +2

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

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

    Например, пользователь вызывает ваш код:
    `FileStream hisObject = MyApi.GetStream();`
    Вы решили, что ваша реализация GetStream может возвращать не только FileStream, но и MemoryStream, так что вы изменили возвращаемое значение на Stream — и у пользователя сломается код. Этого не было бы проблемой, если бы вы изначально не обещали большего, чем Stream.

    Наоборот, если вы начинали с сигнатуры `MyApi.GetStream(): Stream`, а потом решили специфицировать более конкретным типом (и заморозить его в API) и дать больший контроль пользователю, то изменение сигнатуры не сломает уже существующий клиентский код.


  1. dobriykot
    02.05.2015 00:24

    В C# внутри типов, помеченных атрибутом [Serializable], не стоит определять автоматически реализуемые свойства. Дело в том, что имена полей, генерируемые компилятором, могут меняться после каждой следующей компиляции, что сделает невозможной десериализацию экземпляров типа.
    Нашел эту цитату у Рихтера. Но не пойму, насколько и в каких случаях это может быть проблемой? За сколько-то лет работы ни разу с такой ошибкой не сталкивался. Куча сериализуемых классов.


  1. Dywar Автор
    02.05.2015 10:58

    Результат тестов.
    Класс:

    [Serializable]
    class Person
    {
         string FirstName { get; set; }
         string SecondName { get; set; }
         int Age { get; set; }
    }
    
    ILDASM.exe
    
    Debug (first compile):
    .field private int32 '<Age>k__BackingField'
    .field private string '<FirstName>k__BackingField'
    .field private string '<SecondName>k__BackingField'
    
    Debug (second compile):
    .field private int32 '<Age>k__BackingField'
    .field private string '<FirstName>k__BackingField'
    .field private string '<SecondName>k__BackingField'
    
    Release (third compile):
    .field private int32 '<Age>k__BackingField'
    .field private string '<FirstName>k__BackingField'
    .field private string '<SecondName>k__BackingField'
    


    Имена остались прежними, но возможно Джеффри Рихтер встречал другие ситуации (иначе зачем он обратил на это внимание) или в компиляторе были изменения. У меня VS 2013, летом на 2015 :)


    1. mayorovp
      02.05.2015 22:52

      Да это-то понятно. Один и тот же компилятор генерирует одни и те же имена… Интереснее было бы сравнить разные компиляторы.


      1. Dywar Автор
        03.05.2015 09:23

        Проверил пункт «36) Поля класса».

        1) Неправильно.

        class Car
            {
                private string _name = "car";
                private int _speed = 15;
                private int _sits = 5;
        
                public Car()
                {
        
                }
        
                public Car(string name)
                {
                    this._name = name;
                }
        
                public Car(int speed)
                {
                    this._speed = speed;
                }
        
                public Car(int speed ,int sits)
                {
                    this._speed = speed;
                    this._sits = sits;
                }
            }
        
          // Code size       43 (0x2b)
          .maxstack  8
          IL_0000:  ldarg.0
          IL_0001:  ldstr      "car"
          IL_0006:  stfld      string ConsoleApplication1.Car::_name
          IL_000b:  ldarg.0
          IL_000c:  ldc.i4.s   15
          IL_000e:  stfld      int32 ConsoleApplication1.Car::_speed
          IL_0013:  ldarg.0
          IL_0014:  ldc.i4.5
          IL_0015:  stfld      int32 ConsoleApplication1.Car::_sits
          IL_001a:  ldarg.0
          IL_001b:  call       instance void [mscorlib]System.Object::.ctor()
          IL_0020:  nop
          IL_0021:  nop
          IL_0022:  ldarg.0
          IL_0023:  ldarg.1
          IL_0024:  stfld      string ConsoleApplication1.Car::_name
          IL_0029:  nop
          IL_002a:  ret
        } // end of method Car::.ctor
        
          // Code size       43 (0x2b)
          .maxstack  8
          IL_0000:  ldarg.0
          IL_0001:  ldstr      "car"
          IL_0006:  stfld      string ConsoleApplication1.Car::_name
          IL_000b:  ldarg.0
          IL_000c:  ldc.i4.s   15
          IL_000e:  stfld      int32 ConsoleApplication1.Car::_speed
          IL_0013:  ldarg.0
          IL_0014:  ldc.i4.5
          IL_0015:  stfld      int32 ConsoleApplication1.Car::_sits
          IL_001a:  ldarg.0
          IL_001b:  call       instance void [mscorlib]System.Object::.ctor()
          IL_0020:  nop
          IL_0021:  nop
          IL_0022:  ldarg.0
          IL_0023:  ldarg.1
          IL_0024:  stfld      int32 ConsoleApplication1.Car::_speed
          IL_0029:  nop
          IL_002a:  ret
        } // end of method Car::.ctor
        
          // Code size       50 (0x32)
          .maxstack  8
          IL_0000:  ldarg.0
          IL_0001:  ldstr      "car"
          IL_0006:  stfld      string ConsoleApplication1.Car::_name
          IL_000b:  ldarg.0
          IL_000c:  ldc.i4.s   15
          IL_000e:  stfld      int32 ConsoleApplication1.Car::_speed
          IL_0013:  ldarg.0
          IL_0014:  ldc.i4.5
          IL_0015:  stfld      int32 ConsoleApplication1.Car::_sits
          IL_001a:  ldarg.0
          IL_001b:  call       instance void [mscorlib]System.Object::.ctor()
          IL_0020:  nop
          IL_0021:  nop
          IL_0022:  ldarg.0
          IL_0023:  ldarg.1
          IL_0024:  stfld      int32 ConsoleApplication1.Car::_speed
          IL_0029:  ldarg.0
          IL_002a:  ldarg.2
          IL_002b:  stfld      int32 ConsoleApplication1.Car::_sits
          IL_0030:  nop
          IL_0031:  ret
        } // end of method Car::.ctor
        


        2) Правильно.
         class Car
            {
                private string _name;
                private int _speed;
                private int _sits;
        
                public Car()
                {
                    this._name = "car";
                    this._speed = 15;
                    this._sits = 5;
                }
        
                public Car(string name) : this()
                {
                    this._name = name;
                }
        
                public Car(int speed) : this()
                {
                    this._speed = speed;
                }
        
                public Car(int speed ,int sits) : this()
                {
                    this._speed = speed;
                    this._sits = sits;
                }
            }
        
          // Code size       17 (0x11)
          .maxstack  8
          IL_0000:  ldarg.0
          IL_0001:  call       instance void ConsoleApplication1.Car::.ctor()
          IL_0006:  nop
          IL_0007:  nop
          IL_0008:  ldarg.0
          IL_0009:  ldarg.1
          IL_000a:  stfld      string ConsoleApplication1.Car::_name
          IL_000f:  nop
          IL_0010:  ret
        } // end of method Car::.ctor
        
          // Code size       17 (0x11)
          .maxstack  8
          IL_0000:  ldarg.0
          IL_0001:  call       instance void ConsoleApplication1.Car::.ctor()
          IL_0006:  nop
          IL_0007:  nop
          IL_0008:  ldarg.0
          IL_0009:  ldarg.1
          IL_000a:  stfld      int32 ConsoleApplication1.Car::_speed
          IL_000f:  nop
          IL_0010:  ret
        } // end of method Car::.ctor
        
          // Code size       24 (0x18)
          .maxstack  8
          IL_0000:  ldarg.0
          IL_0001:  call       instance void ConsoleApplication1.Car::.ctor()
          IL_0006:  nop
          IL_0007:  nop
          IL_0008:  ldarg.0
          IL_0009:  ldarg.1
          IL_000a:  stfld      int32 ConsoleApplication1.Car::_speed
          IL_000f:  ldarg.0
          IL_0010:  ldarg.2
          IL_0011:  stfld      int32 ConsoleApplication1.Car::_sits
          IL_0016:  nop
          IL_0017:  ret
        } // end of method Car::.ctor
        


        А так если у кого есть желание и VS не 2013, пусть выложат свои результаты по автосвойствам.