Изучая язык программирования 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)
Sane
28.04.2015 13:36+58 пункт — неправда. Запрос выполняется, если он возвращает скаляр(Count, Sum, First...). Даже если на нем вызвать foreach (то есть, выполнить GetEnumerator()) он будет выполнен так лениво, как только можно. То есть, такое выражение не выполнится сразу:
var s = from f in Foo() group f by f%2;
lair
28.04.2015 14:08Если быть точным, то выполнено оно будет лениво, однако для получения второй группы нужно будет проитерировать всю исходную последовательность.
T-D-K
28.04.2015 13:55+5Многое в статье — просто цитаты из Рихтера.
И про пункт 20 — можно поставить бряк на автосвойство. В 2015 студии это можно даже сделать мышкой.Dywar Автор
28.04.2015 22:02Именно, рад что заметил это. Название намекает «Интересные заметки по C# и CLR». Сколько именно из Джеффри Рихтера не считал.
withoutuniverse
28.04.2015 15:33Мне статья понравилась, но есть замечание по терминологии в 1 пункте
Объекты содержат в себе статические поля и все методы. Экземпляры содержат только не статические поля. Это значит, что методы не дублируются в каждом экземпляре, и здесь применяется паттерн Flyweight.
Есть понятие («класс») и есть понятие («экземпляр класса», что по сути == «объект»). Используя такие термины, никогда не возникнет путаницы при изучении нового языка.
Также мне интересно, есть ли у разработчиков понимание, почему один и тот же метод у разных экземпляров класса в разных потоках не пересекается с другими и не меняет их значения, ведь по сути инструкции метода не дублируются?tabushi
29.04.2015 10:30Также мне интересно, есть ли у разработчиков понимание, почему один и тот же метод у разных экземпляров класса в разных потоках не пересекается с другими и не меняет их значения, ведь по сути инструкции метода не дублируются?
Я думаю не у всех =) Это уже относится к более фундаментальным знаниям (многопоточность в ОС, стек, регистры процессора, контекст потока и т.д.), а не напрямую к C#.
А в MSIL у методов экземляра есть неявный параметр this, что и позволяет работать с конкретным объектом.
Когда this == null: невыдуманная история из мира CLR
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).aspxDywar Автор
28.04.2015 21:40Не использовал переводчик при прочтении книги написанной на Русском языке.
п. 30 дополнил примерами, он правильный.mayorovp
28.04.2015 22:16По умолчанию обобщенные типы инвариантны. Еще обобщенные классы называют открытыми, в рантайме они закрываются конкретными типами «int», «string», например. И это разные типы, статические поля в них будут тоже разными.
Какое отношение выделенное предложение имеет к ковариантности, контрвариантности или инвариантности?Dywar Автор
28.04.2015 22:32К перечисленным вами слов — никакого, именно поэтому использовано слово «еще», это как дополнение к «обобщенные типы».
a_mastrakov
29.04.2015 06:03+1все еще не правильный, возможность использования наследника вместо указанного типа не есть «преобразование в одну сторону». Преобразование типов возможно вообще без наследования например.
Возможно у вас проблемы с терминологией, но в нашем деле это очень важный аспект и нужно очень тщательно подбирать слова для описания процессов, особенно на русском.Dywar Автор
29.04.2015 07:01-1За что купил за то и продал, это цитаты.
Да, есть явные и неявные числовые преобразования с потерей точности, но имелось ввиду другое.
mayorovp
28.04.2015 16:26+3Местами бред полный.
У структурных типов и примитивных (byte,int,long...) нет блока синхронизации
Исключения медленно работают, т.к. происходит переход в режим ядра.
в) !DEBUG == RELEASE (на всякий случай).
31) Методы расширения
…
А это реализация паттерна Visitor в .Net.T-D-K
28.04.2015 16:46Местами бред полный.
У структурных типов и примитивных (byte,int,long...) нет блока синхронизации
Я конечно не специалист, но разве у неупакованных ссылочных типов есть индекс блока синхронизации? По-моему как раз у них его и нет.
Safe Thread Synchronization
Правда тут ссылка на того же Рихтера, из чьей книги автор и взял половину «заметок».a_mastrakov
28.04.2015 18:58Я конечно не специалист, но разве у неупакованных ссылочных типов есть индекс блока синхронизации? По-моему как раз у них его и нет.
Вы скорее всего опечатались, механизм упаковки/распаковки существует только для value type.
У структурных типов и примитивных (byte,int,long...) нет блока синхронизации
У value type нет слова заголовка объекта, у reference type (в том числе упакованных value type) он есть — это основа основ, которую, я думал, знают все.
Исключения медленно работают, т.к. происходит переход в режим ядра.
Формально да, это так. Хотя определение «медленно» для разных задач разное, так что нужно просто сказать что происходит переключение контекста потока.mayorovp
28.04.2015 19:29+3
При обработке исключения происходит серия переходов managed-unmanaged. Но сами такие переходы не страшны. К примеру, по два таких перехода происходит на каждый вызов делегата, если верить отладчику студии. Означает ли это, что делегатов надо избегать?Исключения медленно работают, т.к. происходит переход в режим ядра.
Формально да, это так. Хотя определение «медленно» для разных задач разное, так что нужно просто сказать что происходит переключение контекста потока.
Исключения работают медленно, главным образом, из-за сбора StackTrace. Чтобы не потерять информацию, стек надо собрать еще перед переходом в блок catch — то есть в тот момент, когда не ясно, понадобится ли он вообще.
Но при чем тут вообще режим ядра?a_mastrakov
28.04.2015 20:29Но при чем тут вообще режим ядра?
На сколько я знаю механизм обработки исключений в windows, требует перевода процессора в режим ядра (kernel mode), т.к. диспетчер исключений обращается системной памяти.
А по поводу делегатов я к сожалению не могу ничего сказать, я не знаю как они работаю внутри, но на сколько я понимаю делегаты не требуют обращения за пределы виртуальной памяти процесса, поэтому для их работы перевод процессора в режим ядра врятли происходит.mayorovp
28.04.2015 22:06Э… зачем? Какая секретная информация хранится в системной памяти?
a_mastrakov
29.04.2015 05:54Некоторые исключения система обрабатывает сама, собственно обработчик хранится в системной памяти.
Например используемый при отладке breakpoint вызывает исключение, которое ОС обрабатывает вызывая отладчик.mayorovp
29.04.2015 05:59Вы сейчас путаете аппаратные исключения, то есть прерывания, сгенерированные самим процессором — и исключения языков программирования, которые обрабатываются целиком в пользовательском режиме.
a_mastrakov
29.04.2015 09:15Я их не путаю, у аппаратных исключений в отличие от программных просто другой источник, но механизм обработки в общем-то такой же.
Я не большой специалист в этих вопросах и видимо моих знаний недостаточно чтобы объяснить этот механизм. За более подробной информацией могу посоветовать посмотреть книгу Windows Internals глава Диспечеризация исключений.mayorovp
29.04.2015 12:43Не знаю, что в этой книге понимается под программными исключениями — но в этой главе пишется про исключительные ситуации, генерируемые железом. Про исключения, генерируемые вот так:
throw new Exception()
— там нет ни слова.
GrigoryPerepechko
30.04.2015 09:50>>Я не большой специалист в этих вопросах
Это видно. Эксепшны в дотнете сугубо user-mode. И да, в процессоре нет kernel mode. Там ring0.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 перечитаю эти заметки, спасибо!mayorovp
30.04.2015 22:40Нашел эту статью в открытом доступе (hint: надо стереть тройку в конце ссылки). Сделал поиск по ключевому слову «kernel». Не нашел утверждений о том, что любое исключение SEH безусловно проходит через ядро.
Вот самое близкое из найденного:
Probably take a trip through the OS kernel. Often take a hardware exception.
Но тут говорится о возможном проходе через ядро — в случае использования отладчиков, профайлеров и прочих инструментов, которые при нормальной работе приложения отключены.
Откуда вообще пошло гулять по инету утверждение о том, что любое исключение SEH проходит через ядро ОС?Dywar Автор
30.04.2015 22:46В книге могла быть ошибка перевода. Спорные моменты требуют нескольких независимых источников.
Блог надо прочитать целиком, не Испанский ведь.mayorovp
30.04.2015 22:57Лично мне достаточно других источников. Не помню название, но была какая-то переведенная книга именно про SEH. Стоит сейчас где-то в шкафу у бабушки.
Если кто-нибудь найдет в первоисточниках информацию, опровергающую мои утверждения — буду благодарен. Пока что доказательств я не вижу (приведенная в пункте 2 цитата — не первоисточник, потому что сама ссылается на другой источник).Dywar Автор
30.04.2015 23:19Самое смешное в этом диалоге то что мы в поисках ответа не подумали о том что бы запустить отладчик поддерживающий ring 0 и проверить это собственными руками, а не ссылаться вслепую на авторитетов.
mayorovp
30.04.2015 23:24Достаточно отладчика, поддерживающего перехват системных вызовов: не требуется отлаживать ядро, достаточно зафиксировать факт обращения к нему (ну и, конечно же, надо потом как-то проверить, что это обращение не было вызвано самим фактом отладки).
К сожалению, я не знаю ни одного отладчика, кроме отладчика Студии. И даже в отладчике Студии не знаю кнопки, позволяющей хотя бы переключиться в режим дизассемблера (обычно у меня стоит обратная задача — выключить этот чертов режим, так так, чтобы он не включался автоматически). Но это не означает, что я не подумал о проверке! Просто результатом этих дум был вывод о том, что для меня такой повод сейчас непосилен :)
gusev_p
04.05.2015 12:22Вот еще ссылка в тему blog.codinghorror.com/understanding-user-and-kernel-mode.
написал простой тест — в бесконечном цикле создаем исключение/пробрасываем/ловим — в ProcessExplorer-е
Kernel Time стабильно растет. Если в тесте оставить только создание класса исключения — Kernel Time не изменяется (win7, x64).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.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».
Очень интересно, правда забыл уже почти все.
mayorovp
28.04.2015 19:23+1Кто такие «неупакованные ссылочные типы»? :) Наверное, речь шла о неупакованных значимых типах. Разумеется, у них такого индекса нет. Но такой индекс есть, к примеру, у упакованных значимых типов!
Но вообще-то, когда я писал свой комментарий, я думал совсем о другом. Дело в том, чтоstring
— это тоже примитивный тип! Но будет ли кто-то спорить с тем, что у него есть все необходимое для синхронизации?T-D-K
28.04.2015 19:26mayorovp, a_mastrakov
Да. Я очепятался. Правда очень грубый косяк. К вечеру на работе уже голова болит. Я и тот комментарий не с первого раза смог сформулировать. Несколько раз переписывал.
zebraxxl
28.04.2015 22:22Дело в том, что string — это тоже примитивный тип!
Я может быть чего-то не знаю — но всегда считал string — указательный неизменяемый тип. Так по крайней мере говорится в MSDN: https://msdn.microsoft.com/en-us/library/system.type.isprimitive(v=vs.100).aspx.mayorovp
28.04.2015 22:29Примитивный тип — это тип, который не может быть описан через другие типы. У типа string существует одна возможность, которая не может быть реализована иначе как средствами CLR — это получение строкового объекта исходя из строкового литерала.
zebraxxl
29.04.2015 00:16А почему string нельзя описать через другие типы? Один int для длинны и массив char. Собственно в CLR тип string так и реализован.
mayorovp
29.04.2015 05:49+2Представим, что строкового типа у нас нет. Что нужно сделать, чтобы следующая строка кода заработала?
MyString foo = "Hello, world!";
Dywar Автор
28.04.2015 21:571) У структурных типов и примитивных (byte,int,long...) нет блока синхронизации, который присутствует у объектов в управляемой куче на ряду с ссылкой.
Если не читать то что после запятой, то да, такое допущение будет ошибочным.
2) Методы расширения — прочитайте паттерн Visitor в бесплатной книге itvdn.com/ru/patterns (которая идет как дополнение к первоисточнику GOF).
Я не сам это придумал, это ведь заметки а не мои домыслы.mayorovp
28.04.2015 22:12+11) У структурных типов и примитивных (byte,int,long...) нет блока синхронизации, который присутствует у объектов в управляемой куче на ряду с ссылкой.
Не вижу разницы, если честно.
Если не читать то что после запятой, то да, такое допущение будет ошибочным.
2) Методы расширения — прочитайте паттерн Visitor в бесплатной книге itvdn.com/ru/patterns (которая идет как дополнение к первоисточнику GOF).
Спасибо, я знаю что такое паттерн Visitor. Не вижу, как в его реализации могут помочь методы расширения, которые являются всего лишь обычными статическими методами.
Я не сам это придумал, это ведь заметки а не мои домыслы.Dywar Автор
28.04.2015 22:461) Enum структурный тип, но не примитивный. Согласен что предложение не самое удачное (спасибо что указали), но это не делает его не правильным.
2) Цитата из бесплатной книги (ссылка на нее выше):
«Паттерн Visitor – позволяет единообразно обойти набор элементов с разнородными интерфейсами (т.е. набор объектов разных классов не приводя их к общему базовому типу), а также позволяет добавить новый метод (функцию) в класс объекта, при этом не изменяя сам класс этого объекта.»
Я не имею такой же квалификации как автор этих слов, но согласен с ним.mayorovp
28.04.2015 22:51Тот факт, что метод расширения иногда решает ту же самую задачу, которую можно было бы решить паттерном Visitor, не делает из него реализацию этого паттерна.
Dywar Автор
28.04.2015 23:02Цитата из той же книги:
«Известные применения паттерна в .Net
Паттерн Visitor, выражен в платформе .Net в виде идеи использования расширяющих методов.»
По моему скромному мнению нет смысла спорить о теплом и твердом. Но раз вы считаете иначе, возможно так и есть.
Qbit
29.04.2015 13:23> Паттерн Visitor… позволяет добавить новый метод (функцию) в класс объекта, при этом не изменяя сам класс этого объекта.
Добавить новый _полимофный_ метод в класс объекта. Extension-методы — это просто синтаксический сахар (языковой, не CLR) для _вызова_ функций. Языки, где нет этого сахара, просто используют другой синтаксис вызова. Паттерн Visitor к синтаксису того или иного языка не имеет никакого отношения. К объекту какого класса будет применён extension-метод — это определяется во время компиляции. К объекту какого класса будет применятся навешенный с помощью Visitor'а «метод» — это определяется во время исполнения, так как в compile time этой информации в общем случае нет.
Viacheslav01
29.04.2015 13:36+1Структура передает свою копию в метод, Класс передает копию своей ссылки. А вот когда мы используем ключевое слово REF — структура передает указатель на себя, а класс передает свою исходную ссылку.
А я всегда считал что ref для ссылочных типов передает указатель на ссылку на объект.mayorovp
29.04.2015 15:57А я вообще считал, что параметры передает в метод вызывающий метод, а не они сами…
Qbit
29.04.2015 13:46+2> В то же время, объявляя тип возвращаемого методом объекта, желательно выбирать самый сильный из доступных вариантов (пытаясь не ограничиваться конкретным типом). Например, лучше объявлять метод, возвращающий объект FileStream, а не Stream.
Да, с точки зрения сигнатуры функции, входные и выходные значения имеют разное направление вариантности. Но с точки зрения API библиотеки — это просто торчащие наружу типы. Если вы выберете конкретный тип для результата метода, то не сможете в новой версии библиотеки перейти при необходимости к более общему, это может сломать неподконтрольный клиентский код. Изменение в другую сторону от общего к частному не является ломающим изменением.
Например, пользователь вызывает ваш код:
`FileStream hisObject = MyApi.GetStream();`
Вы решили, что ваша реализация GetStream может возвращать не только FileStream, но и MemoryStream, так что вы изменили возвращаемое значение на Stream — и у пользователя сломается код. Этого не было бы проблемой, если бы вы изначально не обещали большего, чем Stream.
Наоборот, если вы начинали с сигнатуры `MyApi.GetStream(): Stream`, а потом решили специфицировать более конкретным типом (и заморозить его в API) и дать больший контроль пользователю, то изменение сигнатуры не сломает уже существующий клиентский код.
dobriykot
02.05.2015 00:24В C# внутри типов, помеченных атрибутом [Serializable], не стоит определять автоматически реализуемые свойства. Дело в том, что имена полей, генерируемые компилятором, могут меняться после каждой следующей компиляции, что сделает невозможной десериализацию экземпляров типа.
Нашел эту цитату у Рихтера. Но не пойму, насколько и в каких случаях это может быть проблемой? За сколько-то лет работы ни разу с такой ошибкой не сталкивался. Куча сериализуемых классов.
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 :)mayorovp
02.05.2015 22:52Да это-то понятно. Один и тот же компилятор генерирует одни и те же имена… Интереснее было бы сравнить разные компиляторы.
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, пусть выложат свои результаты по автосвойствам.
lair
Что вы хотели этим сказать?
ParaPilot
Я думаю, автор имел в виду app.config и подобные файлы конфигурации.
lair
Я тоже так думаю, но они так не работают.
GrigoryPerepechko
В контексте .net, перевести assembly как двоичный код — это сильно.