Форум программистов, компьютерный форум, киберфорум
Наши страницы
netBool
Войти
Регистрация
Восстановить пароль
Оценить эту запись

Заметки про C#. А знали ли вы?

Запись от netBool размещена 08.05.2018 в 12:29

Погружаясь в леса и дебри Reflection и IL, постепенно узнаю много новых вещей о .NET, о которых раньше даже не задумывался. И хотел бы поделиться некоторыми из них (буду дописывать по мере наличия свободного времени):

1. Тип void
Знали ли вы, что в C# void - не просто ключевое слово для обозначения функции, не возвращающей значение, а полноценных тип данных?

Да, да. Несколько фактов о нем:
  • В С# нельзя просто так объявить экземпляр типа void. Сам тип можно получить через typeof(void)
  • Тип void подобен структуре: этот тип не является примитивом, но в то же время считается значимым типом
  • В C#, как в С++, нельзя разыменовать void:*void, несмотря на то, что он является значимым типом. По сути void* - не что иное, как неуправляемый аналог IntPtr

2. Делегаты

На уровне IL все - классы, интерфейсы, структуры и даже делегаты - все является классами. Но делегаты выделяются из всей этой кучи. Delegate - это особый класс в .NET Framework. Он не объявлен как sealed, но мы не можем явно наследовать от него свои классы (так же, как и от MulticastDelegate). Но когда мы пишем
C#
1
delegate void EmptyDelegate();
Мы на самом деле объявляем класс, унаследованный от Delegate и MulticastDelegate.

3. Исходники

Майкрософт выложила исходный код .NET Framework, а так же компилятора Roslyn, изъяв ото всюду старый нативный компилятор. И у многих программистов сложилось впечатление, что весь .NET написан на C#. Но мало кто заглядывал под капот: сам jit-компилятор, "виртуальная машина" и многое другое написаны на С++ и assembler, в том числе assembler ARM/ARMx64

4. Хранение больших объектов в памяти

Принято считать, что CLR для ссылочных типов хранит только ссылку на объект, а сам под сам объект выделяется память где-то в куче. Но мало кто догадывается, поскольку об этом не пишут в учебниках, что
Цитата:
CLR работает с объектами, размер которых больше или равен 85000 байтам, иначе, чем с объектами меньшего размера. Большие объекты создаются в Large Object Heap (LOH), а объекты меньшего размера — в обычной куче, GC Heap, что позволяет оптимизировать выделение памяти под объекты и сбор мусора. LOH не уплотняется, а GC Heap уплотняется при каждом сборе мусора. Кроме того, сбор мусора в LOH осуществляется только при полном сборе мусора.
Вероятно, это и есть причина тому, что NET-программы, работающие с большими объемами данных в памяти, не успевают очищать ее. И этот тот самый момент, на который стоит уделить особое внимание разработчиков таких программ

5. IL-код
Как мы знаем код C#, а так же VB.NET, и других .NET языков компилируется не в машинные инструкции, а в так называемый промежуточный байт-код, описываемый на языке MSIL. Подробное описание инструкций IL-ассемблера можно нати на msdn в описании класса System.Reflection.Emit.Opcodes. Но следует знать, что там описаны только часть инструкций. Например, там нет описания инструкций no и break.

Получить .*il файл можно через командную строку разработчика, предварительно перейдя cd в директорию в исполняемым файлом, и набрав
Код:
ildasm file.exe /out=file.il
Подробнее об опциях запуска ildasm можно почитать здесь

Существуют так же способы отладки непосредственно msil-кода

6. Инициализаторы модулей
В .NET помимо статических конструкторов класса, можно так же задавать и статические конструкторы модулей. К сожалению, в C# такой возможности нет, но к C#-проекту можно прикрутить статический конструктор модуля через ilasm.
Его сигнатура будет выглядеть так:
C#
1
2
3
4
.method assembly specialname rtspecialname static void  .cctor() cil managed
{
 
}
Для чего это можно использовать, пока с трудом себе представляю. Но нюанс любопытный

7. Деструкторы
В C#, как и в C++, есть деструкторы. Но играют совсем иную роль: разумеется, их нельзя вызвать принудительно, т.к. сборкой мусора в .NET занимается сборщик мусора. Но тем не менее, их можно использовать как событие, оповещающее о том, что объект удален GC, вместо проверки слабых ссылок

8. try/catch/finally
Еще одной интересной особенностью C# и .NET в частности, является то, что для блок try/catch не имеет опкодов для реализации. Они задаются не через инструкции il, а в специальных блоках Exception внутри тела метода. Эти блоки не влияют на CodeSize метода! И тем удивительнее, что finally не указываются в этих блоках: для него есть специальный опкод endfinally, по достижении которого передается управление обработчику исключений для прыжка на следующую после обработки блока инструкцию.

Не по теме:

Зная особенности такой конструкции, можно выполнять запутывание кода, на мой взгляд, похлеще, чем на чистом asm (но, конечно, местами в ущерб производительности). Тем более, что нормальных отладчиков msil-кода не существует



9. Удивительная компиляция foreach
При пересборке своего проекта столкнулся с удивительной ситуацией:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
            this.DeserilizeFromCass();
 
 
            //создание категорий товаров в списке перемещения товара
            if (folds.Count > 0)
            {
                #region Создание категорий в cbxFolders
                foreach (DFolder fold in folds)
                {
                    cbxFolders.Items.Add(@fold.Name.ToString());
                }
                cbxFolders.SelectedIndex = 0;
                #endregion                
            }
 
            this.ReadDemands();
Этот код компилятор преобразовал в следующие il-код:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
564      L_07ab:    ldarg.0 
565      L_07ac:    call    System.Void ForReady.frmMain::DeserilizeFromCass()
566      L_07b1:    ldarg.0 
567      L_07b2:    ldfld   ForReady.DFolders ForReady.frmMain::folds
568      L_07b7:    callvirt    System.Int32 System.Collections.Generic.List`1<ForReady.DFolder>::get_Count()
569      L_07bc:    ldc.i4.0    
570      L_07bd:    ble.s   599 -> ldarg.0 
571      L_07bf:    ldarg.0 
572      L_07c0:    ldfld   ForReady.DFolders ForReady.frmMain::folds
573      L_07c5:    callvirt    System.Collections.Generic.List`1/Enumerator<!0> System.Collections.Generic.List`1<ForReady.DFolder>::GetEnumerator()
574      L_07ca:    stloc.s V_30
575      L_07cc:    br.s    587 -> ldloca.s V_30
576      L_07ce:    ldloca.s    V_30
577      L_07d0:    call    !0 System.Collections.Generic.List`1/Enumerator<ForReady.DFolder>::get_Current()
578      L_07d5:    stloc.s V_5
579      L_07d7:    ldarg.0 
580      L_07d8:    ldfld   System.Windows.Forms.ComboBox ForReady.frmMain::cbxFolders
581      L_07dd:    callvirt    System.Windows.Forms.ComboBox/ObjectCollection System.Windows.Forms.ComboBox::get_Items()
582      L_07e2:    ldloc.s V_5
583      L_07e4:    callvirt    System.String ForReady.DFolder::get_Name()
584      L_07e9:    callvirt    System.String System.Object::ToString()
585      L_07ee:    callvirt    System.Int32 System.Windows.Forms.ComboBox/ObjectCollection::Add(System.Object)
586      L_07f3:    pop 
587      L_07f4:    ldloca.s    V_30
588      L_07f6:    call    System.Boolean System.Collections.Generic.List`1/Enumerator<ForReady.DFolder>::MoveNext()
589      L_07fb:    brtrue.s    576 -> ldloca.s V_30
590      L_07fd:    leave.s 595 -> ldarg.0 
591      L_07ff:    ldloca.s    V_30
592      L_0801:    constrained.    System.Collections.Generic.List`1/Enumerator<ForReady.DFolder>
593      L_0807:    callvirt    System.Void System.IDisposable::Dispose()
594      L_080c:    endfinally  
595      L_080d:    ldarg.0 
596      L_080e:    ldfld   System.Windows.Forms.ComboBox ForReady.frmMain::cbxFolders
597      L_0813:    ldc.i4.0    
598      L_0814:    callvirt    System.Void System.Windows.Forms.ListControl::set_SelectedIndex(System.Int32)
599      L_0819:    ldarg.0 
600      L_081a:    call    System.Void ForReady.frmMain::ReadDemands()
Разберем код поподробнее. Строки
Assembler
1
2
3
4
5
6
564      L_07ab:    ldarg.0 
565      L_07ac:    call    System.Void ForReady.frmMain::DeserilizeFromCass()
566      L_07b1:    ldarg.0 
567      L_07b2:    ldfld   ForReady.DFolders ForReady.frmMain::folds
568      L_07b7:    callvirt    System.Int32 System.Collections.Generic.List`1<ForReady.DFolder>::get_Count()
569      L_07bc:    ldc.i4.0
ясны: get_Count() - получаем folds.Count. ldc.i4.0 - загружаем в стек ноль. Далее
Assembler
1
570      L_07bd:    ble.s   599 -> ldarg.0
это сравнение (условие if(folds.Count > 0), то выполняем код дальше. Иначе переход на инструкцию L_0819).
Далее применяем GetEnumerator к folds:
Assembler
1
2
3
571      L_07bf:    ldarg.0 
572      L_07c0:    ldfld   ForReady.DFolders ForReady.frmMain::folds
573      L_07c5:    callvirt    System.Collections.Generic.List`1/Enumerator<!0> System.Collections.Generic.List`1<ForReady.DFolder>::GetEnumerator()
Сохраняем Enumerator в локальную переменную с условным именем V_30. И далее снова идет инструкция br.s:
Assembler
1
575      L_07cc:    br.s    587 -> ldloca.s V_30
Согласно мсдн она выполняет переход к конечной инструкции с указанным смещением. Вот в этой строке и начинается цикл. Переброс происходит на инструкцию 587 (L_07f4). Эта инструкция загружает Enumerator, чтобы применить к нему MoveNext(). Следующая за тем инструкция brtrue.s:
Цитата:
Передает управление конечной инструкции (короткая форма), если value является true, не равно null или ненулевое значение.
То есть если MoveNext вернет true, то переход на инструкцию 576. В самом цикле не происходит ничего особенного:
Assembler
1
2
3
4
5
6
7
8
9
10
11
576      L_07ce:    ldloca.s    V_30
577      L_07d0:    call    !0 System.Collections.Generic.List`1/Enumerator<ForReady.DFolder>::get_Current()
578      L_07d5:    stloc.s V_5
579      L_07d7:    ldarg.0 
580      L_07d8:    ldfld   System.Windows.Forms.ComboBox ForReady.frmMain::cbxFolders
581      L_07dd:    callvirt    System.Windows.Forms.ComboBox/ObjectCollection System.Windows.Forms.ComboBox::get_Items()
582      L_07e2:    ldloc.s V_5
583      L_07e4:    callvirt    System.String ForReady.DFolder::get_Name()
584      L_07e9:    callvirt    System.String System.Object::ToString()
585      L_07ee:    callvirt    System.Int32 System.Windows.Forms.ComboBox/ObjectCollection::Add(System.Object)
586      L_07f3:    pop
Никаких скачков, переходов. Исключительно работа со стеком и вызовы лок функций
Но вот если MoveNext вернет 0, то управление перейдет к инструкции leave.s, бросающей нас на № 595

И если у вас уже терпение на исходе и вы не понимаете, что особенного в этом разборе, то хочу вас обрадовать. Мы дошли до той самой точки, которой я хотел поделиться. Это строки:
Assembler
1
2
3
4
591      L_07ff:    ldloca.s    V_30
592      L_0801:    constrained.    System.Collections.Generic.List`1/Enumerator<ForReady.DFolder>
593      L_0807:    callvirt    System.Void System.IDisposable::Dispose()
594      L_080c:    endfinally
На них не происходит переход. Они пропускаются в любом случае: и при выполнении цикла и при его завершении

Что же в них происходит?

Метод constrained ограничивает тип, для которого был вызван виртуальный метод, а Dispose() освобождает ресурсы. Опкод endfinally - это End finally clause of an exception block. Что за ерунда? Не ерунда

Оказывается, к каждому блоку foreach cil-компилятор добавляет автоматически try-блок. В частности для приведенного выше отрывка к методу был сформирован .try1:
Assembler
1
         .try1  575 to 591  Finally handler  591 to 595
Для циклов for такого нет. Любопытно, да?

10. Получение свойств родительских (реализованных) интерфейсов

Об этом уже много написано и кому надо, тот знает. Но на всякий случай напишу

Мы все знаем метод GetProperties(). У класса он позволяет получить все public свойства static и instance характера (соответствует BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public параметрам). При чем у класса вернет все свойства, в т.ч. и наследуемые. Чтобы это не произошло, надо вызвать с параметром DeclaredOnly. У интерфейсов получить свойства родительского интерфейса нельзя. Например:
C#
1
2
3
4
5
6
7
8
9
    interface A : B
    {
        int d { get; set; }
    }
 
    interface B
    {
        int c { get; }
    }
Здесь typeof(A).GetInterfaces() вернет только d
Для того, чтобы получить все свойства, используют такой подход:
C#
1
2
3
4
5
6
7
8
9
10
11
        public static PropertyInfo[] GetNonPublicProperties(Type type)
        {
            var types = type.GetInterfaces();
            List<PropertyInfo> pr = new List<PropertyInfo>();
            foreach (var typ in types)
            {
                pr.AddRange(typ.GetProperties());
            }
            pr.AddRange(type.GetProperties());
            return pr.ToArray();
        }
Так же стоит отметить, что, если интерфейс является оболочкой для COM-объекта, то получить свойства, которые не использованы в проекте, не выйдет, поскольку проект не содержит ссылок на них
Размещено в C#/WinForms
Просмотров 576 Комментарии 5
Всего комментариев 5
Комментарии
  1. Старый комментарий
    Аватар для Avazart
    Цитата:
    В отличие от C++, где указатель на void является своеобразным универсальным указателем, в C# нельзя разыменовать void: *void, несмотря на то, что он является значимым типом
    Во первых в С# не юзают как правило указатели.
    Во вторых в С++ void* тоже не удастся разыменовать, можно привести его как другому типу указателя и потом разыменовать.
    Запись от Avazart размещена 08.05.2018 в 12:57 Avazart на форуме
  2. Старый комментарий
    Аватар для netBool
    Цитата:
    Во первых в С# не юзают как правило указатели.
    Юзают в unsafe режиме
    Цитата:
    Во вторых в С++ void* тоже не удастся разыменовать, можно привести его как другому типу указателя и потом разыменовать.
    Да, точно)) Спасибо за замечание. Сейчас исправлю))
    Запись от netBool размещена 08.05.2018 в 17:34 netBool вне форума
  3. Старый комментарий
    Аватар для Avazart
    Цитата:
    Юзают в unsafe режиме
    А много людей пишут в unsafe режиме ?
    Запись от Avazart размещена 08.05.2018 в 18:02 Avazart на форуме
  4. Старый комментарий
    Аватар для Rius
    Это вовсе не круто.
    Те, кому это надо, используют сразу C/C++, а не эти костыли в виде unsafe.
    Запись от Rius размещена 08.05.2018 в 19:10 Rius вне форума
  5. Старый комментарий
    Аватар для netBool
    Цитата:
    Это вовсе не круто.
    Может быть, но мне чертовски приятно, что на шарпе это тоже можно делать

    Цитата:
    Те, кому это надо, используют сразу C/C++, а не эти костыли в виде unsafe.
    В C/C++ в целом по работе с указателями, разумеется, больше возможностей и работать удобнее. Но иногда какую-нибудь мелочь проще приятней реализовать на шарпе, чем постоянно дергать DllImport

    PS: Я на самом деле нисколько не фанат unsafe-C# в рабочих проектах. Скорее интересны эти возможности для личного велосипедирования и спортивного интереса
    Запись от netBool размещена 09.05.2018 в 11:44 netBool вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2018, vBulletin Solutions, Inc.
Рейтинг@Mail.ru