Страницы

Поиск по вопросам

Показаны сообщения с ярлыком clr. Показать все сообщения
Показаны сообщения с ярлыком clr. Показать все сообщения

понедельник, 30 марта 2020 г.

sealed, virtual, невиртуальные методы в C# и производительность

#c_sharp #net #ооп #производительность #clr


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

class X
{
    public int NonVirtual() => DateTime.Now.Millisecond;
    public virtual int Virtual() => DateTime.Now.Minute;
}

class Y : X
{
    public override int Virtual() => DateTime.Now.Millisecond;
}

class Program
{

    private static volatile int TST = 0;

    static int Slow(Y x) => x.Virtual();

    static int Fast(Y y) => y.NonVirtual();

    static void Main(string[] args)
    {

        int i = 0;
        var stopwatch1 = new Stopwatch();
        var stopwatch2 = new Stopwatch();

        stopwatch1.Start();
        var y = new Y();
        for (i = 0; i < 100000000; i++)
        {
            TST = Fast(y);
        }
        stopwatch1.Stop();
        Console.WriteLine("Time elapsed: {0}", stopwatch1.Elapsed);

        stopwatch2.Start();
        for (i = 0; i < 100000000; i++)
        {
            TST = Slow(y);
        }
        stopwatch2.Stop();
        Console.WriteLine("Time elapsed: {0}", stopwatch2.Elapsed);

        Console.ReadKey();
    }
}


Поле TST объявлено как volatile, чтобы компилятор не оптимизировал вызовы.

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

Тогда я полез в IL:

IL_0001: callvirt  instance int32 PerformanceTests.X::Virtual()


Это вызов виртуального метода. Вроде всё нормально. Второй вызов:

IL_0001: callvirt  instance int32 PerformanceTests.X::NonVirtual()


Что, простите? callvirt? Разве тут не должно быть call? sealed так-же никак не влияет
на вызов виртуального метода.

Хотел бы выяснить у более опытных коллег, всё-таки, есть ли смысл в запечатывании
классов в плане производительности? А так-же, почему вызов невиртуальной функции в
IL такой же как и виртуальной?

Update:
В комментариях @Grundy написал, что callvirt - из-за того, что метод объявлен в базовом
классе. Переписал код так, что теперь используется базовый класс (т.е. Y - не используется).
callvirt так и вызывается.
    


Ответы

Ответ 1



Как уже сказали в комментариях, компилятор использует callvirt что бы генерировать NullReferenceException. Что бы получить чистую call инструкцию компилятор должен быть уверен, что экземпляр класса не может быть null. Пример: class Test { public void Method() => Console.WriteLine(123); } static void Main(string[] args) { new Test().Method(); } IL-код: IL_0001: newobj instance void ConsoleApp1.Program/Test::.ctor() IL_0006: call instance void ConsoleApp1.Program/Test::Method() Если код немного изменить: static Test GetTest() => new Test(); static void Main(string[] args) { GetTest().Method(); } То уже получаем callvirt, так как компилятор предполает, что GetTest может вернуть null: IL_0001: call class ConsoleApp1.Program/Test ConsoleApp1.Program::GetTest() IL_0006: callvirt instance void ConsoleApp1.Program/Test::Method() sealed ни на что не влияет в рантайме. Это просто маркер для разработчиков, который сообщает, что можно делать в высокоуровневом коде, а что нельзя. Для каждого callvirt невиртуального метода JIT вставляет одну дополнительную инструкцию перед каждым call: cmp dword ptr [/*здесь регистр с адресом экземляра*/],ecx call 00007FF9CD540098 // метод Эффект от одной cmp инструкции очень незначителен.

Делать ли каскад методов асинхронными, если они могут возвращать просто Task<T>

#c_sharp #net #async_await #async #clr


Терзает вопрос, найти внятный ответ не могу. Гуглится не то.

Вот у меня есть действительно асинхронный метод, в котором несколько await'ов. Этот
метод вызывается на более верхнем уровне:

public async Task UpperLevelMethod()
{
    return await _component.RealyAsyncMethod();
}


Ну и так далее, по всем слоям приложения. 

Но ведь по идее, можно написать так:

public Task UpperLevelMethod()
{
    return _component.RealyAsyncMethod();
}


И вызов await UpperLevelMethod() будет корректно отрабатывать. И, на сколько я понимаю,
этот метод даже лучше, т.к. не будет создаваться дополнительные экземпляры IAsyncStateMachine,
поправьте, если я не прав.

Интересно, что авторы статей и книг в красках описывают последствия упаковки/распаковки,
но этот момент как-то упускается в разделе про асинхронность.

Есть ли какие-то подводные камни, у такого вызова? Либо, в случаях, когда не нужно
делать асинхронное продолжение, можно (а возможно даже лучше) делать метод не асинхронным?

P.S. Тот же Скит описывает, что для проверки аргументов лучше делать так:

public Task UpperLevelMethod(string arg)
{
    /*
    Проверка аргумента и выброс исключения в случае ошибки
    */
    return _component.RealyAsyncMethod();
}


Но он говорит именно про проверку аргументов, чтобы она проходила в синхронном режиме.
Об оптимизации - ничего.

Update: 
Консольное приложение, собраное в релизе, с таким кодом:

static void Main(string[] args)
{
    Task.Run(Foo5);
}

static async Task Foo1() => await Task.FromResult(5);

static Task Foo2() => Foo1();

static Task Foo3() => Foo2();

static Task Foo4() => Foo3();

static Task Foo5() => Foo4();


весит 8 192 байт, при добавлении await'ов - 10 240 байт.
    


Ответы

Ответ 1



Совершенно верно, лучше не писать лишний раз async если код работает и без него. Нет никаких причин писать лишние слова async и await.

пятница, 20 марта 2020 г.

Что входит в состав .NET?

#net #clr


Можно ли сказать,что в состав .NET входят CLR и классы(FCL).Или там есть что-то ещё?
    


Ответы

Ответ 1



Нет, в состав .NET на самом деле входит больше, чем только CLR и FCL. .NET как технология включает: Спецификацию Common Language Infrastructure (CLI) ECMA-334 (и аналогичную ISO), которая описывает принципы работы CLR, систему управляемых типов, набор инструкций байткода, формат файлов сборок, структуру стандартных библиотек и др. Спецификации языков C# (ECMA-335) и C++/CLI (ECMA-372) Реализации этих спецификаций в виде платформ (.NET Framework, .NET Core) и компиляторов. Прочее ПО, не имеющее прямого отношения к CLI, но все равно являющееся естественной частью разработки под .NET, например .NET Framework SDK для Visual Studio. .NET Framework как программный продукт включает: Исполняющую среду CLR Библиотеку классов Компиляторы C# и VB.NET Систему сборки MSBuild Набор специфических утилит командной строки, вроде aspnet_regiis.exe Прочие служебные компоненты, например Счетчики производительности Правда, MSBuild и компиляторы языков в состав .NET Framework включены устаревшие, на уровне C# 5.0, и в .NET Core от их включения уже отказались - вместо этого компиляторы Roslyn поставляются с Visual Studio и в составе отдельных NuGet-пакетов. Но все равно, и архитектурно и структурно .NET - больше, чем только CLR и FCL.

воскресенье, 15 марта 2020 г.

Как SomeInstance.ToString() всегда возвращает правильное название класса?

#c_sharp #net #clr


Собственно вопрос: как и почему автоматически перегружается виртуальный метод базового
класса System.Object ToString(), если мы его явно ни как не трогаем?

В данной технике используется АОП?

Пример кода:

using System;

class A
{
    class B
    {
        public void D()
        {
             Console.WriteLine(base.ToString());
        }
    }
    static void Main()
    {
        B b = new B();
        Console.WriteLine(b.ToString());
    }
}

    


Ответы

Ответ 1



Проще всего посмотреть имплементацию через Рефлектор или ILSpy. Или на reference source. class object { // ... public virtual string ToString() { return this.GetType().ToString(); } // ... } Компилятор мог бы применить свою компиляторную магию, но тут она не понадобилась. Метод GetType() возвращает настоящий, runtime-тип объекта, а уж тип знает, как он называется.

среда, 26 февраля 2020 г.

c# и debug режим

#c_sharp #cpp #clr


Можно ли в с# CLR код, запустить в "трассировочном" режиме? Можно ли  как-то перехватить
вызов Invoke? Как можно написать дебаггер под с# код? Единственное что пока-что приходит
в голову - емулировать выполнение CLR-кода, но тут нужно тонна кода на емулятор, а
потом... перехватывать код готовых библиотек, ведь есть мостики CLR-Native-CLR, поэтому
с емулятором не всё так просто. На с++-cli это чудо думаю было бы реально реализовать...
но... Я думаю есть какой-то особый режим, который легко включается. Видел уже есть
"кривые" аналоги VS... Как эта проблема решена в других проэктах? 
    


Ответы

Ответ 1



Как написать дебагер, а точнее трейсер. С чего начать Создание ICorDebug и ICorProcess Создание обработчика Апгрейт обработчика Статья предполагает что базовые знания с++ есть. Дебагер можно писать на с++, можно даже на с#, но я предпочитаю с++. Покажу базу - как написать трейсер. С чего начать. Давайте создадим простенький с# using System; public class Demo { public static void Main(string[] args) { Console.WriteLine("Hello world"); } } И соберём его c:\windows\Microsoft.NET\Framework\v2.0.50727\csc.exe /platform:x86 /target:1.exe 1.cs /pdb:1.pdb /debug У студии есть папка SDK в которой можно найти нужные хедеры. У меня тут SDK\v2.0\include. Понадобятся следующие библиотеки #include #include "COR\CorHdr.h" #include "COR\cor.h" #include "COR\cordebug.h" ICorDebug* dbg; // Библиотека дебаггера ICorProcess* process;//Процесс для отладки. Будет для одного процесса Поначалу я думал - создам CoCreateIntance, там есть ф-ция CreateProcess - и будет всё ок, но нет. Создание ICorDebug и ICorProcess. Второй создаётся легко, если создался первый. Есть несколько способов его создать, покажу один из. Если mscoree.dll не получается прилинковать - подключайте ёё через LoadLibrary и GetProcAddress. Есть две ф-ции, одна проверяет версию, другая - создает ICorDebug. void main(){ wchar_t* module = "c:\\yourdebug\\1.exe"; wchar_t ver[20]; GetRequestedRuntimeVersion(module,ver,sizeof(ver),&dw);//mscoree.dll hr=CreateDebugginInterfaceFromVersion(CorDebugVersion_2_0,ver,&dbg); // hr=CreateDebugginInterfaceFromVersion(CorDebugVersion_2_0+1,ver,&dbg); // для .NET 4 // Если не подходит версия - пробуйте менять первый аргумент //TODO: проверка hr hr = dbg->Initialize(); STARTUPINFOW si = {sizeof(STARTUPINFOW),0,}; PROCESS_INFORMATION pi = {0,}; // dbg->SetUnmanagedHandler dbg->SetManagedHandler(MgrHandler); // ниже будет // TODO: параметры запуска можно будет доделать hr = dbg->CreateProcess(module,module,0,0,true, CREATE_NEW_CONSOLE, L"\0\0\0\0", L".", &si, &pi,0,&process); // TODO: цикл ожидания конца работы дебагера for (int i=0;i<100;i++) WaitForSingleObject(pi.hProcess,1000); } Ну... почти готово. Или почти не считается. Долго мучался с SetManagedHandler, если ф-ция не работает, значит не все хандлеры вы описали. Теперь перейдем к... Создание обработчика. В версии FrameWork 2.0 оказывается нужно поддерживать два каллбека. Ну и... коечего повписывать в обработчик. Нужно везде где можно написать pAppDomain->Continue(); вписать его в каждый обработчик. Весь код приводить не буду, приведу главное. Дальше речь идет только об этом обработчике class MGRHandler:public ICorDebugManagedCallback,ICorDebugManagedCallback2{ // 1 Рассказать какие у нас калбеки HRESULT __stdcall QueryInterface(REFIID riid,void**ppvObject){ if (riid==IID_ICorDebugManagedCallback){ *ppvObject=(ICorDebugManagedCallback*)this;//Первый return 0; } if (riid==IID_ICorDebugManagedCallback2){ *ppvObject=(ICorDebugManagedCallback2*)this;//Второй return 0; } return (HRESULT)-1; } // 2 Вначале нас бросит сюда HRESULT __stdcall CreateProcess(ICorDebugProcess* pProcess){ pProcess->Continue(0); } // 3 Потом будет создан "домен" HRESULT __stdcall CreateAppDomain(ICorDebugProcess *pProcess, ICorDebugAppDomain *pAppDomain) { pAppDomain->Attach(); // Переводим домен в дебаг режим pProcess->Continue(0); return 0;}; // 4 Ставим Continue тут обязательно, желательно везде HRESULT __stdcall LoadAssembly(ICorDebugAppDomain *pAppDomain, ICorDebugAssembly *pAssembly) { pAppDomain->Continue(0);return 0; } HRESULT __stdcall CreateThread(ICorDebugAppDomain *pAppDomain, ICorDebugThread *thread) { pAppDomain->Continue(0);return 0; }; Аналогично ф-ции Breakpoint,NameChange и другие. Перехват функций нужно делать в модуле, например так: HRESULT __stdcall LoadModule(ICorDebugAppDomain *pAppDomain, ICorDebugModule *pModule) { ICorDebugFunction* fn=0;//ф-ция номер 1 имеет такой код 6000001 pModule->GetFunctionFromToken(0x6000001,&fn); if (fn!=0) fn->CreateBreakpoint(&bp1); // TODO: очистить bp1 pAppDomain->Continue(0); return 0; } И добавим обработчик BreakPoint HRESULT __stdcall Breakpoint(ICorDebugAppDomain *pAppDomain, ICorDebugThread *pThread, ICorDebugBreakpoint *pBreakpoint) { ICorDebugFrame * f = 0; pThread->GetActiveFrame(&f); ICorDebugStepper * step = 0; f->CreateStepper(&step); step->Step(0); // Заставляем работать в шаговом режиме // TODO: step освободить когда не нужен будет f->Release(); pAppDomain->Continue(0); return 0; } Да, добавьте MGRHandler MgrHandler; под обработчиком, когда обработчик доконца допишите. Если компилятор ругается пишет слова abstract и error значит не все функции реализованы. Нужно все добавить (современные компиляторы умеют реализовать абстрактные классы сами, если дополнительно сделать два три клика в нужных местах). Теперь дебагер может кое-как отлаживать программу. Столкнулся с тем... Ой, а где же мой ip.... Апгрейт обработчика. Есть интерфейс ICorDebugILFrame - в нем куча полезностей, локальные переменные, аргументы, и ip. Последний "штрих" программы, правим в обработчике ф-цию: HRESULT __stdcall StepComplete( ICorDebugAppDomain *pAppDomain, ICorDebugThread *pThread, ICorDebugStepper *pStepper, CorDebugStepReason reason) { ICorDebugFrame * f = 0; ICorDebugILFrame * ff = 0; unsigned ip,tok; pThread->GetActiveFrame(&f); f->QueryInterface(IID_ICorDebugILFrame , &ff); ff->GetIP(&ip); f->GetFunctionToken(&tok); char buf[30]; wsprintfA("tok:%x ip:%x\r\n",tok,buf); // TODO: вывод на екран pStepper->Step(0); // пусть ещё шагает pAppDomain->Continue(0); return 0; } Теперь получился "трейсер", который позволяет по-шагам выполнить ф-цию. Я не упомянул IMetaDataImport - через него можно получить имена всех ф-ций и параметров, остальную нужную информацию найти относительно легко. Я постарался показать 4-ре шага, которые мне было не очень просто найти. Показан чисто "скелет". P.S. Тесты. Модуль собран в borland c++ x86. win2003 x86 - модуль работает (студия 2005) win7 x64 - модуль работает, но нужно создать 32-битную сборку отлаживаемой программы, т.е. добавить в csc.exe параметр /platform:x86 (иначе ловим ошибку DebuggerError 0x80131C30 ) Полезные ссылки MS debug api http://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/debugging/ Создание ICorDebug http://lowleveldesign.org/2010/10/11/writing-a-net-debugger-part-1-starting-the-debugging-session/ Часть 2 Часть 3 Часть 4 Коды ошибoк HRESULT http://github.com/mrfearless/UASM-with-RadASM/blob/master/UASM64/ErrorCodes.dat Файл с кодами ошибок есть corerror.h но... он не удобный.

понедельник, 24 февраля 2020 г.

Аргументы методов в C# по ссылке или по значению?

#c_sharp #clr #теория


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

public static string GetRequest(string Url, string UserAgent = "", CookieDictionary
Cookies = null, ProxyType ProxyProto = ProxyType.None, string ProxyString = "")
{
    ...

    try
    {
        HttpResponse response = request.Get(Url);
        Cookies = response.Cookies;
        response_html = response.ToString();
    } 

}


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

CookieDictionary cookies = new CookieDictionary();
string url = "http://mail.ru";
string resp = Web.GetRequest(url, "", cookies, Web.ProxyType.HTTP, "127.0.0.1:8888");
resp = Web.GetRequest(url, "", cookies, Web.ProxyType.HTTP, "127.0.0.1:8888");


При втором запросе передаются куки, которые получены при первом. Кроме того в методе
я пробовал добавлять свои куки через Cookies.Add() и они присутствовали в следующих
вопросах. Почему это работает?
    


Ответы

Ответ 1



В C# действительно аргументы передаются по значению. Вопрос только, что это за аргументы. Если это аргументы — числа или похожие объекты (они называются типами-значениями), то они передаются таки как есть — передаётся их копия. А если аргументы — объекты классов (они называются ссылочными типами), то по значению передаётся ссылка (то есть как бы указатель, если вы знакомы с C) на объект, а не копия самого объекта. По копии ссылки объект доступен точно так же, как и по оригиналу, и это тот же самый объект. Почему это сделано так? Дело в том, что не все объекты можно просто склонировать. И даже если это можно, что делать со ссылками на другие объекты, которые содержит данный объект? Клонировать все внутренние объекты? Это слишком долго и слишком сложно (например, что делать, если ссылки выстраиваются в цикл?). А если внутренние объекты не клонировать, возникает та же проблема: сам объект копируется, а внутренние подобъекты всё равно передадутся по ссылке.

Ответ 2



В отличие от некоторых языков программирования, C # имеет две разновидности типов данных: для значения и для ссылки. Если производительность приложения имеет существенное значение или есть заинтересованность в том, как C# управляет данными и памятью, важно знать различия между этими типами. Если в объявлении переменной используется один из основных встроенных типов данных или определенная пользователем структура данных, значит мы имеем дело с типом значения. Исключение составляет тип данных string, который является ссылочным типом. Структуры и перечисления в C# так же являются значимыми типами(value types). По этому не рекомендуется использовать в качестве аргументов слишком большие структуры, так как это может сказаться на производительности. На счет строк рекомендую прочитать например эту статейку, т.к. со строками не все так очевидно, как могло бы показаться.

Скорость работы dynamic в C#

#c_sharp #оптимизация #clr


Смотрю курсы по C# proffesional от ITVDN. Там пример, первый раз обращаются к полю
dynamic, которое раннее приравнено к ссылке на объект, и показывают, что первое обращение
к такому полю занимает 2 миллиона тиков. Я повторяю это на своем компьютере, но у меня
всего 2 тысячи тиков. Примерно в 10 раз больше, чем следующие обращения к dynamic.
Соль то в том, что dynamic запоминает объект, но в первый раз он ничего о нем не знает
и поэтому занимает больше времени. Но 2 тысячи и 2 миллиона для одинаковых функций
- очень большая разница. Может курс устарел и разработчики C# оптимизировали dynamic?
Код:

MyClass c = new MyClass();
dynamic d = c;
long start, end;

while (true)
{
    QueryPerformanceCounter(out start);
    d.m();
    QueryPerformanceCounter(out end);
    Console.WriteLine((end - start) + '\n');
} 

    


Ответы

Ответ 1



На вопрос о разнице в количестве тиков отвечать не буду, потому как неизвестны ни способ измерения, ни конфигурации вашего и автора курса компьютеров, ни версии .NET. Вместо этого отвечу, почему первый вызов метода работает медленнее. Авось кому пригодится. Дополнения к посту приветствуются. Для каждого выражения (операции), использующего объект типа dynamic, компилятор генерирует специальный объект под названием dynamic call site, который представляет это выражение (операцию). После компиляции приведенного в вопросе кода получается примерно следующее (код с измерением опустил): static DynamicCallSite dCallSite; ... MyClass c = new MyClass(); dynamic d = c; while (true) { if (dCallSite == null) { dCallSite = new DynamicCallSite(); } dCallSite.DoInvocation("m", d); } При этом объект dynamic call site генерируется только один раз для каждого выражения. Это первая монетка в копилку "почему первый вызов работает медленнее". Дальше DLR проверяет типа объекта d, обнаруживает, что это C# объект, и вызывает С# компилятор, который, используя метаданные, генерирует expression tree для данного выражения. Expression tree возвращается обратно DLR, компилируется, получившийся делегат вызывается и кэшируется. Т.е. если DLR в дальнейшем встретит такое же выражения для объекта такого же типа, будет вызван закэшированный делегат. Это вторая монетка в копилку "почему первый вызов работает медленнее". Во время второго вызова у нас уже есть dynamic call site, так что DLR просто проверяет тип объекта, достает из кэша скомпилированный делегат и выполняет его.

пятница, 14 февраля 2020 г.

Собрать универсальное C# приложение

#c_sharp #net #clr


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

Сам я java-ист. С детства думал, что не важно, для чего собирать, важно, какой разрядности
jvm. Но тут, видимо, все иначе...

Прошу помочь с такой проблемой:

Нужно собрать приложение, которое использует стороннюю c++ библиотеку. Для библиотеки
доступны 32 и 64-битные .dll-ки. От приложения не требуется сверхпроизводительности,
так что и 32-битная версия меня устроит.

Могу ли я собрать свой проект как 32-битный с использованием лишь 32-битной .dll
и рассчитывать на то, что все это дело будет работать как в 32-битной, так и в 64-битной
системе? И если нет, то как лучше сделать?

И пара второстепенных вопросов, которые помогут мне понять, что я делаю:

Собранное для 64-битной системы .NET приложение ведь будет невозможно запустить в
32-битной системе, или же это работает слегка иначе, чем с нативными приложениями?

Верно ли я понимаю, что приложения, собранные для систем разной разрядности, запускаются
на разных виртуальных машинах, и, как следствие, на 64-битной версии windows установлены
виртуальные машины CLR двух разрядностей?
    


Ответы

Ответ 1



Все именно так как и кажется: 32х-разрядная версия будет работать почти' где угодно, 64х-разрядная потребует 64х-разрядную систему. Однако, если у вас есть обе версии чужой библиотеки - то лучше оставить свою сборку не привязанной к разрядности (AnyCPU), а при загрузке библиотеки загружать ту, которая соответствует разрядности текущего процесса. Подробности - вот тут: Подгрузка разных dll в зависимости от разрядности системы ' 32х-разрядная версия не будет работать на 64х-разрядной системе без поддержки 32х-разрядных приложений, например такое возможно на Windows Server Core (спасибо PetSerAl за уточнение)

воскресенье, 9 февраля 2020 г.

Boxing при интерполяции строк в c#

#c_sharp #net #clr


Есть код 1

int i = 123;
string s = $"{i}";


И есть код 2

int i = 123;
string s = $"{i.ToString()}";


В первом варианте будет происходить боксинг или с интерполяцией не происходит такого
и компилятор понимает что надо вызвать ToString?
    


Ответы

Ответ 1



1) string s = $"{i}"; превращается в string.format, который принимает object'ы, боксинг присутствует 2) string s = $"{i.ToString()}"; передается строка, боксить нечего

Ответ 2



В первом варианте будет происходить боксинг Да. с интерполяцией не происходит такого и компилятор понимает что надо вызвать ToString? Ваше утверждение "надо вызвать ToString" не является корректным, так как "надо" вызвать вовсе не тот ToString, который вызываете Вы. using System; class Test { struct S: IFormattable { public override string ToString() => "Object.ToString"; public string ToString(string format, IFormatProvider formatProvider) => "IFormattable.ToString"; } public static void Main() { S s = new S(); Console.WriteLine($"{s}"); Console.WriteLine($"{s.ToString()}"); } } https://ideone.com/gRz74U Так как поведение String.Format не является частью стандарта языка C#, то компилятор не имеет права выполнять, предложенную Вами, оптимизацию, так как он не может знать какой именно ToString нужно вызвать и с какими параметрами. Более того, если определение типа S находится в другой сборке, то на этапе компиляции компилятор даже не будет иметь достаточного количества информации, чтобы определить, какой метод вызвать.

среда, 5 февраля 2020 г.

Семантика работы\хранения UpCast“инга \ DownCast”инга в CLR

#c_sharp #память #clr #типы


Начнем с теории.
Допустим,имеется следующие классы:  

class A{}
class B : A{}
class C : B{}


Далее,мы делаем UpCast :  

A a1 = new C();  


Будет ли следующее утверждение верным : объект a1 является объектом типа C,и базовым
классом для него является тип А (то бишь вверх по иерархии) !?  

Далеко не уходя от кассы, представьте что добавили в код следующее: 

class A
{
    public virtual void Method()
    {
        Console.WriteLine("Method A invoked");
    }
}
class B : A
{
    public new virtual void Method()
    {
        Console.WriteLine("Method B invoked");
    }
}
class C : B
{
    public override void Method()
    {
        Console.WriteLine("Method C invoked");
    }
}


Что будет выведено на экран ?
В первую очередь покажется,что тут все очевидно,и вывод выходе получим :  

Method A invoked
Method A invoked
Method С invoked
Method C invoked   



   Но на самом то деле,мы получим : Method A invoked Method A invoked Method A invoked
Method C invoked


Исходя из этой логики,выходит что мое предыдущее высказывание не верное,и это значит,что
все таки базовым классом для C,является B ?  



Теперь перейдем к другой части вопроса.
К примеру имеем код:  

  class Program
    {
        static void Main(string[] args)
        {
            //объект типа класса А
            A a = new A();
            //объект типа класса B
            B b = new B();
            //UpCast, который равен объекту "b"
            A a1 = b;
            //UpCast как отдельный объект
            A a2 = new B();
            //DownCast, который равен объекту "а1"
            B b1 = (B)a1;

            B b2 = a as B; // вернет Null, т.к. DownCast
            //без предварительного UpCast не возможен

            // B b2 = new A(); - невозможно из за безопасности типов

            //сравниваем b с а1,видим что типы идентичны.
            Console.WriteLine(b.GetType() == a1.GetType());
            //сравниваем а2 с а1,видим что типы идентичны.
            Console.WriteLine(a2.GetType() == a1.GetType());
            //сравниваем b1 и а1,видим что типы идентичны
            Console.WriteLine(b1.GetType() == a1.GetType());

            //Проверяем сами обьекты,вернет True
            Console.WriteLine(a1.Equals(b));
            //вернет False,но реализация этих объектов идентична
            Console.WriteLine(a2.Equals(a1));
            //Вернет True
            Console.WriteLine(b1.Equals(a1));

            Console.ReadKey();
        }
    }
    class A
    {
    }
    class B : A
    {
    }  




Так все же,что происходит за кулисами?
Как при UpCast"е \ DownCast"е ,два одинаковых объекта(точнее две ссылки,указывающие
на один и тот же объект),имеют различную реализацию(да,да - это полиморфизм). За счет
чего это достигается(то бишь,как CLR реализует эту модель поведения) и как примерно
выглядит все это чудо-юдо в самой среде CLR ? Как выглядит "наследование" внутри CLR
между типами?
    


Ответы

Ответ 1



По порядку: Будет ли следующее утверждение верным : объект a1 является объектом типа C,и базовым классом для него является тип А (то бишь вверх по иерархии) !? Нет. Корректным утверждением будет следующее: объект a1 является объектом типа C,и базовыми классами для него являются типы B и А. Разница большая, поскольку каждый тип в иерархии наследования может привносить новые аспекты поведения. Теперь дальше: class A { public virtual void Method() { Console.WriteLine("Method A invoked"); } } class B : A { public new virtual void Method() { Console.WriteLine("Method B invoked"); } } class C : B { public override void Method() { Console.WriteLine("Method C invoked"); } } А данном примере сложно сказать, что именно хотел сказать автор этих строк с точки зрения бизнес-логики, но звучит это примерно так: класс B добавляет новый метод Method, но, к сожалению, он использует метод, имя которого уже есть в базовом классе. Но класс B хочет не "подменить" поведение метода из базового класса, а создать свой собстенный метод, который ничего не имеет общего с методом базового класса, кроме имени. Подобная практика приводит к неоднозначному поведению, поскольку теперь выбор метода определяется не только динамическим типом объекта (типом времени исполнения), но и типом переменной (типом, известным компилятору): если используется переменная типа A, то будет вызван метод из класса A. Если же тип переменной - это B или C, то будет использоваться другая ветка методов (ниже будет объяснение, почему это так). Другими словами, с точки зрения метода Method существует две ветки: одна начинается типом А и им же и ограничивается, и есть другая полиморфная ветка, которая начинается в типе B и продолжается в наследнике - типе С. Теперь немного о том, как это устроено в CLR. Для каждого типа CLR хранит табличку с методами (Method Table), где каждая запись описывает отельный метод - его сигнатуру и признак того, переопределяет ли данный слот метод из базового класса. Когда вы объявили метод с приставкой new в классе B, CLR добавила "новый" метод в табличку, при этом пометила этот метод, как новый, не связанный с методом из базового класса. В случае же класса С, метод Method в табличке методово типа C помечен, как полиморфный, т.е. переопределяющий поведение непосредственного базового класса. Теперь стоит сказать, как происходит разрешение метода во время исполнения: в случае вызова a.Method будет вначале определен статический тип переменной a, после чего в таблице методов будет найден метод Method. Если статический тип переменной - это A, то вначале будет просмотрена таблица методов типа A. И в этом случае CLR увидет, что этот метод виртуальный. После чего будет определен реальный тип объекта (например, тип C) и CLR посмотрит, а есть ли у этого типа переопределение метода, объявленного в типе A. CLR получит отрицательный ответ, поскольку тип C не переопределяет метод Method, объявленный в типе A (ведь этот тип переопределяет метод Method типа B). Вот и получается, что результат разрешения имени метода у нас теперь зависит не только от типа времени исполнения, но и от типа переменной времени компиляции.

среда, 29 января 2020 г.

Поиск сборки (алгоритм)

#c_sharp #net #clr


Подскажите оптимальный способ поиска сборки. Например в папке лежит 1k сборок (*.dll)
и нужно выбрать например две специальные сборки. Как нужно пометить (атрибуты? вроде
медленно будет искать рефлекшеном)  специфические сборки, для быстрого поиска в другой
программе? 

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


Ответы

Ответ 1



Вы можете воспользоваться библиотекой Mono.Cecil (доступна так же через NuGet) и использовать ее для анализа сборок. Для теста я взял почти все сборки из GAC, скопировал их, чтобы получилось ~1 тысяча сборок, добавил туда несколько экземпляров моих библиотек с искомым атрибутом и выполнил такой код: var assemblys = Directory.GetFiles("C:\\dlls\\") .Select(AssemblyDefinition.ReadAssembly) .Where(assembly => assembly.CustomAttributes.Any(ca=> ca.AttributeType.Name == "MyCustomAttribute")) .ToList(); Результат он выдал через ~600 миллисекунд (мерил с помощью Stopwatch). Мне кажется, что этого может быть для Вас достаточно.

Ответ 2



Основная потеря скорости будет при загрузке сборки в домен. Дальнейший анализ сборки, по сравнению с ее загрузкой, дешев, так что там хоть атрибут вешайте, хоть что. Если пойдете по этому пути, не забудьте делать это в отдельном аппдомене, чтобы потом выгрузить ненужные сборки. Что можно придумать без загрузки всех сборок в домен: Самым быстрым вариантом будет поиск сборки просто по имени. Подписать сборки определенным ключом. При поиске считывать ключ и сравнивать с нужным. Обозначить сборки фиктивной версией (например, 127.0.0.1). При поиске считывать версию. Попробовать дописать несколько байт в dll, закодировав в них свой маркер, не повредив при этом сборки. Дальше открывать сборки как байт стримы и считывать маркеры.

Ответ 3



Я сталкивался с подобной проблемой, и решил её для себя при помощи статического определения нужных библиотек. То есть при компиляции/установке программы специальная утилита определяет, какие из сборок нужно загрузить, и складывает эти данные в специальное место (например, файл). Теперь основная программа должна просто прочитать этот файл и загрузить нужные сборки, без необходимости рантайм-тестирования. При добавлении сборок в каталог (или удалении из него) должна автоматически запускаться та же утилита, которая будет обновлять список. Разрешать пользователю добавлять/убирать сборки руками не нужно. С другой стороны, если вы пишете плагинную систему, возможно, вам нужно не изобретать велосипед, и воспользоваться встроенным фреймворком System.AddIn.

Ответ 4



Я не знаю, подойдет ли данное решение для вашей задачи. Но смысл примерно в следующем: Вам понадобится интерфейс, который описывает функциональность классов, которые будут загружаться из сборок: public interface ISomeTypeInterface { string Name { get; } } После этого в сборках вы объявляете типы следующим образом: [Export(typeof(ISomeTypeInterface))] public class SomeType1 : ISomeTypeInterface { public string Name { get { return "Hello, world!"; } } } Далее, в месте, где нужно это все собрать пишете: class SomeClass { [ImportMany] private ISomeTypeInterface[] SomeTypes { get; set; } } void ComposeParts() { var someClass = new SomeClass(); // Каталог, в котором нужно искать типы можно настраивать более тонко, используя других наследников ComposablePartCatalog или создавая своих using (var catalog = new DirectoryCatalog(".")) using (var container = new CompositionContainer(catalog)) { container.ComposeParts(someClass); } foreach (var t in someClass.SomeTypes) { Console.WriteLine(t.Name); } } Если вам подойдет такой алгоритм работы с типами, то я бы советовал использовать MEF. Не могу сказать о скорости работы. У нас не возникало необходимости ускорить этот механизм, хоть он и используется часто и густо.

среда, 22 января 2020 г.

Передача аргумента в функцию по дескриптору C++/CLR

#net #visual_cpp #cpp_cli #clr


Есть 2 программы на C++/CLR:

1)

#include "stdafx.h"

using namespace System;

String^ InsertSpacesBeforBigLetters(String ^str)
{
    for (int i = 1; i < str->Length; i++)
    {
        if (str[i] >= 'A' && str[i] <= 'Z')
        {
            str = str->Insert(i, " ");
            ++i;
        }
    }

    return str;
}

int main()
{
    Console::WriteLine("Enter string:");
    String ^input_str = Console::ReadLine();

    //input_str = InsertSpacesBeforBigLetters(input_str);

    InsertSpacesBeforBigLetters(input_str);

    Console::WriteLine(input_str);

    Console::ReadKey();
    return 0;
}


Программа, которая вставляем пробелы перед каждым большим символом строки, начиная
с 1 (используется .NET класс String). В этом примере при передаче аргумента по дескриптору,
его значение после выполнения функции остаётся таким же, как и при вхождении в функцию
- то есть при вызове функции и передаче ей строки по дескриптору в качестве аргумента
она не меняется.

2)

#include "stdafx.h"
#include 

using namespace std;
using namespace System;

ref class SomeObj
{
    public:
        int num;

        SomeObj()
        {
            num = 0;
        }

        SomeObj(int Num)
        {
            num = Num;
        }

        void Meth()
        {
            cout << "I'm an object with number " << num;
        }
};

void Upgrade(SomeObj ^smo)
{
    smo->num = smo->num + 1;
}

int main()
{
    SomeObj ^so = gcnew SomeObj(1);

    so->Meth();

    Upgrade(so);

    cout << "\n";
    so->Meth();

    Console::ReadKey();
    return 0;
}


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

Вопрос: почему так происходит? По идеи, в обоих случаях состояние объекта должно
меняться, ведь в официальной документации сказанно, что дескриптор ведёт себя как указатель
на объект. Или я что-то не так понял?
    


Ответы

Ответ 1



Вообщем я, походу, понял в чём проблема. При вызове функции создаётся новый указатель, которому присваивается значение, находящееся в указателе, который мы передаём этой функции. Тамким образом во 2 примере мы изменяем значение num объекта, на который указывает so. В первом же примере значение не меняется по той причине, что мы меняем знаение не объекта, по адресу который мы передали в качестве аргумента в функцию, а мы меняем значение указателя, который располагается в функции. Решение этой проблемы - передача объекта в функцию по ссылке, например: String ^%str.

пятница, 10 января 2020 г.

Когда уничтожается ValueType и семантика работы GC с ValueType

#net #память #сборщик_мусора #clr #stack


Ходят мифы и легенды,мол ValueType удаляется посредством GC(то бишь GC деаллоцирует
как ReferenceType,так и ValueType).
Но на самом то деле это не так. К примеру у нас есть код:  

class A {}
class B
{
    void TestMethod()
    {
        A a = new A();
        int x = 100;
    }
}  


в контексте(Scope) метода TestMethod(), создается объект(ReferenceType) типа А,а
так же переменная X(ValueType).   

По завершению работы метода, переменная X уничтожается,а объект типа А теряет ссылку
на объект,и становится претендентом для удаления от GC.  

Иными словами,ValueTypе существует в контексте до тех пор,пока выполняется,и соответственно
Stack, по типу метода Pop() сам удалит эти данные из памяти, и никакого участия в этом
не принимает GC, поэтому ValueType и работает быстрее (хотя все зависит от задачи).   

И сам вопрос,всегда ли это так работает? (читал разные статьи,иногда пишут,что это
происходит только тогда,когда стек забивается, т.е. доходит до заполнения)   

Что делает CLR,когда стек уже почти переполнен,а все данные в нем к примеру являются
ссылками на объекты в куче?
Как и когда удаляются пользовательские структуры? Как именно CLR решает удалять ли
данные из стека или оставить их еще существовать N-ое кол-во времени!??
    


Ответы

Ответ 1



Локальные переменные типов ValueType [на которых нет замыканий из анонимных методов и лябмд] лежат прямо в стеке или в регистрах процессора (как захочется оптимизатору). Вы можете прямо посмотреть, как выполняется ваш код, нажав правой кнопкой по нему в отладке, и выбрав Go To Disassembly, может быть это прояснит картину. Вот как это выглядит в отладочном режиме (что выключает оптимизации). Я добавил комментарии в важных местах: { 025B2E48 push ebp // это так называемый пролог функции 025B2E49 mov ebp,esp // https://en.wikipedia.org/wiki/Function_prologue 025B2E4B push edi // суть его - сохранить текущее положение 025B2E4C push esi // стека в "базовый указатель" - [e]bp 025B2E4D push ebx в стек запихнули значения 3-х регистров, так что его указатель теперь отличается уже на 12 от того, который был в начале функции 025B2E4E sub esp,3Ch esp - это указатель на начало стека. Уменьшить его на 3Ch - это выделить в стеке 3Сh (60) байт под локальные переменные (или другие накладные расходы) к этому моменту он уже отличался на 12 от значения, которое лежит в ebp, так что локальные переменные находятся в диапазоне по адресам от [ebp-13] до [ebp-72]. Он же [ebp-0Dh] до [ebp-48h]. Потом делаем кучу проверок и долго и мучительно создаем объект (это все из-за отладочного режима). Я пропущу большую часть кода, она не имеет отношения к вопросу: 025B2E51 mov esi,ecx 025B2E7C nop A a = new A(); 025B2E7D mov ecx,700F98h 025B2E82 call 024130F4 025B2E87 mov dword ptr [ebp-48h],eax 025B2E8A mov ecx,dword ptr [ebp-48h] 025B2E8D call 025B0D18 025B2E92 mov eax,dword ptr [ebp-48h] и вот наконец ложим указатель на созданный объект в стек (в ebp лежит положение стека на момент начала вызова функции) 025B2E95 mov dword ptr [ebp-40h],eax С целым числом попроще - просто запихиваем нужное значение в относительно epb - т.е. относительно начала стека на момент функции. int x = 100; 025B2E98 mov dword ptr [ebp-44h],64h } 025B2E9F nop А вот теперь фокус. Берем и загружаем в указатель стека значение, которое в нем было сразу после 025B2E4D push ebx. По сути это esp = ebp-0Ch 025B2EA0 lea esp,[ebp-0Ch] 025B2EA3 pop ebx 025B2EA4 pop esi 025B2EA5 pop edi и после следующей строчки получаем значение esp равное тому, которое было в начале функции. 025B2EA6 pop ebp 025B2EA7 ret За счет чего при этом выделалась и освобождалась память в стеке? Выделалась за счет уменьшения указателя стека на нужное значение. Освобождалась - за счет восстановления старого значения указателя. Расходов на разрушение или "сброрку мусора" локальных переменных при этом не было. Это стандартный механизм на x86, так что можно считать что так происходит почти всегда. По возврату из функции значение Stack Pointer восстанавливается в то, что было до ее вызова.

суббота, 4 января 2020 г.

Инициализация значимых типов в C#

#c_sharp #net #clr


Читаю книгу, написанную Дж. Рихтером, CLR via C#. К сожалению, в самом начале у меня
возникают вопросы. Надеюсь, вы поможете мне разобраться.

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

Дж. Рихтер приводит пример:

struct SomeVal { public Int32 x; } // здесь он объявляет структуру
SomeVal v1 = new SomeVal();


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

SomeVal v1;


Дословно:


  Здесь тоже создается IL-код, который помещает экземпляр SomeVal в стек
  потока и обнуляет все его поля.


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


Ответы

Ответ 1



Это правда. Вся выделенная память просто инициализируется нулями. Ведь мы же не можем обратиться к какому-нибудь полю структуры до тех пор, пока не выполним его инициализацию. Это проверка C# компилятора, CLR же не накладывает таких ограничений. Книга Рихтера -- она же в первую очередь о CLR, поэтому он не врет.

Как логировать работу программы с помощью ETW?

#c_sharp #net #clr #логирование


Информация о работе .NET выводится в ETW (Event Tracing for Windows) и ее можно получить
с помощью программы PerfView.
Как из приложения выводить свою информацию в ETW?
И возможно ли выводить 100 тыс. сообщений в секунду? 
    


Ответы

Ответ 1



ETW позволяет выводить до 500 тыс. сообщений в секунду с минимальными затратами. Например, надо в ETW выводить уведомления о ходе выполнения, а также информацию о начале и завершении какой-то активности. Для этого надо определить класс, производный от EventSource. using System.Diagnostics.Tracing; [EventSource(Name = "MyApp")] class MyAppEvent : EventSource { public static MyAppEvent Log = new MyAppEvent(); [Event(1)] // уведомление о ходе выполнения public void Progress(int v, string msg) { WriteEvent(1, v, msg); } [Event(2, Opcode = EventOpcode.Start)] // начало активности public void StartAction(int id) { WriteEvent(2, id); } [Event(3, Opcode = EventOpcode.Stop)] // завершение активности public void StopAction(int id) { WriteEvent(3, id); } } для вывода в ETW пишем partial class MainWindow : Window { public MainWindow() { var log = MyAppEvent.Log; ... log.Start(1); log.Progress(1, "ok"); log.Stop(1); Компилируем и из командной строки запускаем PerView > PerfView /OnlyProviders=*MyApp run WpfApplication.exe В результате создается PerfViewData.etl.zip, в котором находится файл PerfViewData.etl - его можно открыть в PerfView и посмотреть информацию, например, по вызовам Start.

вторник, 31 декабря 2019 г.

Освобождение памяти в Stack'e

#c_sharp #память #clr


Всем известно,что с Stack это некий участок памяти,который аллоцируется на каждый
поток в виде размера 1МБ , в нем хранятся ссылки(ObjRef) на ReferenceType,пользовательские
структуры,примитивные данные,ну и локальные переменные метода.  

А теперь вопрос: что делает CLR, когда Stack полностью заполниться, и соответственно
удалять по сути нечего.То бишь Stack переполнен?   

Может ли CLR расширить его границы с 1МБ до 2МБ?(или это не возможно,в связи с чем,мы
просто получим Exception, который оповестит нас о переполнении стека).  

Другой вопрос: в контексте unsafe кода,unamanged участку памяти выделяется так же
1МБ или аллоцировать можно кастомный размер?  
    


Ответы

Ответ 1



Для начала стоит отметить что стек в момент выполнения кода - это не какой-то абстрактный safe-механизм. .NET использует JIT-компиляцию, так что в реальности выполняется код, привязанный к конкретной платформе, с использованием механизма стека этой платформы. В случае x86 - сегмент стека + пара регистров SS/ESP и операциями push/pop. Никакого отдельного стека для unsafe не создается. Что происходит при заполнении стека и можно ли его увеличить по достижению лимита? Нет, в общем случае - нельзя. Дело в том, что стек, по крайней мере в x86/64 - это структура данных, заполняемая с конца. Т.е. каждое помещение чего-то в стек сдвигает его указатель ближе к началу. Это идет из древних (еще до-.net) времен, когда памяти было мало, и стандартное разделение памяти выглядело так: [код][данные-куча --> ....... пустое место....... <-- данные-стек] Физическая память делилась между кучей и стеком, и у программиста всегда был выбор - выделить побольше памяти под что-то в куче, или положить побольше объектов в стеке. К тому же такая раскладка эффективно избавляла от необходимости контролировать размер отдельно кучи, и отдельно - стека. Т.к. если в ней куча и стек встретились - то памяти не осталось совсем. С тех пор многое поменялось (хотя раскладка выше все еще актуальна на некоторых микроконтроллерах). В x86 Для стека теперь обычно выделен отдельный сегмент. Но он, по традиции, заполняется с конца. Т.е. каждая операция push уменьшает значение SP на размер положенного в стек. По достижению ESP значения 0 - процессор выбрасывает ошибку. Механизма "довыделения памяти" в стеке в x86 нет - просто потому, что нельзя "довыделить" память в начало сегмента - а выделить память в конце и переложить все данные в стеке процессор не осмеливается - для него это слишком сложная операция. Это могла бы сделать операционная система, но по крайней мере Windows на x86 так не поступает. На выходе по достижению 0 указателем стека вы получаете StackOverflowException в вашем коде .NET. Это индикатор достижения лимита "железного" стека, а не какого-то хитрого стека CLR. Стоит отметить, что к StackOverflowException на x86 может привести также бесконечная рекурсия, причем даже в случае если вы не объявляете локальные переменные. Дело в том, что механизм вызовов методов на x86 (call) сохраняет в стеке адрес возврата из вызываемого метода. А оператор возврата (ret) достает адрес из стека. Поэтому слишком глубокая цепочка вызовов забивает стек. Собственно, именно поэтому окно просмотра вызовов и называется Call Stack - информация цепочке возврата хранится только в стеке. Причем на x86 - в том же физическом стеке что и параметры фунций и локальные переменные.

четверг, 26 декабря 2019 г.

App.config и приложение на C#

#c_sharp #net #config #clr


У меня есть небольшая программа на 2 версии .NET, но новых версиях Windows  она не
запускается, но если создать файл настроек, то она запустится.




  
  
  




Есть какие-нибудь варианты, что можно сделать, чтоб было не 2 файла, а 1? Т.е внедрить
этот файл настроек в exe или ещё как-нибудь.
    


Ответы

Ответ 1



Встроить файл конфигурации в EXE-файл нельзя (так как весь смысл конфигурации - это возможность редактирования параметров без перекомпиляции программы, такой возможности не предусмотрели). Влиять на параметр supportedRuntime из кода на C# также нельзя, так этот параметр используется неуправляемым кодом загрузчика до того, как в процесс загружена CLR, и в этот момент управляемый код еще не может выполняться. Если нужно управлять выбором версии CLR без файла конфигурации, единственный способ - написать свой собственный загрузчик на С++, пользуясь Unmanaged .NET API. Например, создадим такую программу на C#: using System; namespace ConsoleApplication1 { class Program { static int Run(string arg) { Console.WriteLine("Hello from .NET " + Environment.Version.ToString()); Console.ReadKey(); return 0; } static void Main(string[] args) { Run(""); } } } Скомпилируем ее, получаем файл Program.exe. Создадим проект С++, добавим в него файл Program.exe и создадим файл ресурсов resource.rc следующего содержания: #define IDR_RCDATA1 101 IDR_RCDATA1 RCDATA "Program.exe" Напишем на С++ код загрузчика, который находит первую установленную версию CLR, загружает ее, извлекает из ресурсов программу на C# во временную папку и запускает ее: #include #include #include #include #pragma comment(lib, "mscoree.lib") #define IDR_RCDATA1 101 int wmain(int argc, wchar_t* argv[]) { LPCWSTR prog_name = L"Program.exe"; //имя программы на C# //построим путь к временному файлу WCHAR temppath[300] = L"c:\\temp\\"; GetTempPath(300,temppath); wcscat(temppath,prog_name); //извлечем программу из ресурсов HRSRC myResource = ::FindResource(NULL, MAKEINTRESOURCE(IDR_RCDATA1), RT_RCDATA); UINT Size = ::SizeofResource(NULL, myResource); HGLOBAL myResourceData = ::LoadResource(NULL, myResource); void* pMyBinaryData = ::LockResource(myResourceData); FILE* f = _wfopen(temppath,L"wb"); fwrite(pMyBinaryData,Size,1,f); fclose(f); //инициализация CLR... HRESULT hr; ICLRMetaHost *pMetaHost = NULL; ICLRRuntimeInfo *pRuntimeInfo = NULL; ICLRRuntimeHost *pClrRuntimeHost = NULL; IEnumUnknown* pEnum= NULL; ICLRRuntimeInfo* pInfo= NULL; IUnknown* pUnk = NULL; hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost)); if(FAILED(hr)){printf("CLRCreateInstance failed\n");goto End;} //поиск установленных версий CLR... pMetaHost->EnumerateInstalledRuntimes(&pEnum); if(FAILED(hr)){printf("EnumerateInstalledRuntimes failed\n");goto End;} ULONG c= 0; WCHAR buffer[250]; DWORD cch = 250; while(1){ if(pInfo!=NULL){pInfo->Release();pInfo = NULL;} if(pUnk!=NULL){pUnk->Release();pUnk = NULL;} if(pRuntimeInfo!=NULL){pRuntimeInfo->Release();pRuntimeInfo = NULL;} hr = pEnum->Next(1,&pUnk,&c); if(hr != S_OK)break; pUnk->QueryInterface(IID_ICLRRuntimeInfo, (void**)&pInfo); if(FAILED(hr)){printf("QueryInterface failed\n");continue;} pInfo->GetVersionString(buffer,&cch); if(FAILED(hr)){printf("GetVersionString failed\n");continue;} hr = pMetaHost->GetRuntime(buffer, IID_PPV_ARGS(&pRuntimeInfo)); if(hr == S_OK){break;} else {wprintf(L".NET %s: GetRuntime HRESULT 0x%x\n",buffer,(UINT)hr);} } if(pRuntimeInfo == NULL){printf("Failed to initialize CLR\n");goto End;} /* Можно также указать версию явно, например: pMetaHost->GetRuntime(L"v2.0.50727", IID_PPV_ARGS(&pRuntimeInfo)); pMetaHost->GetRuntime(L"v4.0.30319", IID_PPV_ARGS(&pRuntimeInfo)); и т.п. */ hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&pClrRuntimeHost)); if(FAILED(hr)){printf("GetInterface failed\n");goto End;} //запуск CLR hr = pClrRuntimeHost->Start(); if(FAILED(hr)){printf("Start failed\n");goto End;} //Запуск программы на C# DWORD pReturnValue; hr = pClrRuntimeHost->ExecuteInDefaultAppDomain( temppath, L"ConsoleApplication1.Program", //класс L"Run", //метод L"", //параметр &pReturnValue); if(FAILED(hr)){printf("ExecuteInDefaultAppDomain failed 0x%x\n",(UINT)hr);goto End;} End: //Освобождение ресурсов if(pMetaHost != NULL) pMetaHost->Release(); if(pRuntimeInfo != NULL) pRuntimeInfo->Release(); if(pClrRuntimeHost != NULL) pClrRuntimeHost->Release(); if(pEnum != NULL) pEnum->Release(); if(pInfo != NULL) pInfo->Release(); if(pUnk != NULL) pUnk->Release(); return 0; } В результате программа, собранная под .NET 2.0, при его отсутствии будет запускаться на имеющейся версии .NET, как и при использовании параметра supportedRuntime. Источники: Embedding supportedRuntime into exe file - ответ Ondrej Svejdar How to load a custom binary resource in a VC++ static library as part of a dll? - ответ LihO

суббота, 21 декабря 2019 г.

Семантика работы/хранения статики в CLR

#c_sharp #clr #static


Известно, что статика не привязана к объекту (экземпляру), а хранится в типе объекта
(!), и соответственна эта статика (в виде полей/методов и т.д.), будет существовать
в едином экземпляре для всех созданных объектов типа.  

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

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

Стоит ли избегать статические коллекции/массивы, которые работают с большим кол-вом
данных (или же стоит, но нужно будет их "чистить вручную")?

Что является дурным тонном по использованию статики?

Или же можно использовать статику в таком же количестве, как и экземплярные варианты?
    


Ответы

Ответ 1



Я думаю, что стоит руководствоваться не «физическими» особенностями хранения, а семантикой, смыслом ваших данных. Если у вас какой-либо метод или данные относится ко всем экземплярам класса, то этот метод/данные следует объявлять статическими. Если же у вас, например, экземпляр существует в единственном числе в системе, то следует объявлять его синглтоном. Это должно быть главным соображением, влияющим на то, как именно вы объявляете ваши данные. Если в вашей программе по её смыслу данные являются статическими, объявляйте их статическими. Если они по своей сути являются экземпляром, объявляйте их данными экземпляра. Пример: цвет автомашины, выпускаемой концерном Генри Форда — чёрный. Значит, это статические данные: class FordCar { public static readonly Color = Colors.Black; } В моей компании есть на текущий момент только одна машина. Но это всё равно конкретный экземпляр. Значит, это синглтон: class OurCompanyCar { private OurCompanyCar() { } public static OurCompanyCar() Instance { get { return lazy.Value; } } private static readonly Lazy lazy = new Lazy(() => new OurCompanyCar()); }

Интерфейс, тип object, упаковка интерфейса, перечисление

#c_sharp #net #clr


Всем привет. Решил прочесть Рихтера (после Шилдта) и попал в путаницу. Насколько
я раньше знал, то интерфейс и enum не наследуются от обжекта, однако после такого вот
кода я запутался

IComparable F;
F.ToString(); // или любой другой метод object, написал для демонстрации
Console.WriteLine(typeof(IComparable).BaseType) //пустая строка


Путаница в том, что во-первых откуда у интерфейса и enum взялись встроенные методы
object ? А во-вторых, я уж было смирился с этим, однако последняя строка выводит пустой
экран вместо имени базового класса. Как это можно понять, подскажите пожалуйста, если
учесть, что якобы интерфейс наследуется от обжекта ?

Далее встретил у него в книге "CLR VIA C# 4.5" стр 167 (предпоследний блок кода примера)
фразу в этом коде 

using System;
internal struct Point : IComparable
{
    private Int32 m_x, m_y;
    // Конструктор, просто инициализирующий поля
    public Point(Int32 x, Int32 y)
    {
        m_x = x;
        m_y = y;
    }
    // Переопределяем метод ToString, унаследованный от System.ValueType
    public override String ToString()
    {
        // Возвращаем Point как строку (вызов ToString предотвращает упаковку)
        return String.Format("({0}, {1})", m_x.ToString(), m_y.ToString());
    }
    // Безопасная в отношении типов реализация метода CompareTo
    public Int32 CompareTo(Point other)
    {
        // Используем теорему Пифагора для определения точки,
        // наиболее удаленной от начала координат (0, 0)
        return Math.Sign(Math.Sqrt(m_x * m_x + m_y * m_y) -
                         Math.Sqrt(other.m_x * other.m_x + other.m_y * other.m_y));
    }
    // Реализация метода CompareTo интерфейса IComparable
    public Int32 CompareTo(Object o)
    {
        if (GetType() != o.GetType()) 
            throw new ArgumentException("o is not a Point");
        // Вызов безопасного в отношении типов метода CompareTo
        return CompareTo((Point)o);
    }
}
public static class Program
{
    public static void Main()
    {
        // Создаем в стеке два экземпляра Point
        Point p1 = new Point(10, 10);
        Point p2 = new Point(20, 20);
        // p1 НЕ пакуется для вызова ToString (виртуальный метод)
        Console.WriteLine(p1.ToString()); // "(10, 10)"
        // p1 ПАКУЕТСЯ для вызова GetType (невиртуальный метод)
        Console.WriteLine(p1.GetType()); // "Point"
        // p1 НЕ пакуется для вызова CompareTo
        // p2 НЕ пакуется, потому что вызван CompareTo(Point)
        Console.WriteLine(p1.CompareTo(p2)); // "-1"
        // p1 ПАКУЕТСЯ, а ссылка размещается в c
        IComparable c = p1;
        Console.WriteLine(c.GetType()); // "Point"
        // p1 НЕ пакуется для вызова CompareTo
        // Поскольку в CompareTo не передается переменная Point,
        // вызывается CompareTo(Object), которому нужна ссылка
        // на упакованный Point
        // c НЕ пакуется, потому что уже ссылается на упакованный Point
        Console.WriteLine(p1.CompareTo(c)); // "0"
        // c НЕ пакуется, потому что уже ссылается на упакованный Point
        // p2 ПАКУЕТСЯ, потому что вызывается CompareTo(Object)
        Console.WriteLine(c.CompareTo(p2));// "-1"
        // c пакуется, а поля копируются в p2
        p2 = (Point)c;
        // Убеждаемся, что поля скопированы в p2
        Console.WriteLine(p2.ToString());// "(10, 10)"
    }
}


// c пакуется, а поля копируются в p2 однако как он может паковаться, если он ссылается
на уже запакованную структуру ?? Ведь когда интерфейсу присваиваешь тип значение то
происходит упаковка (ранее в коде интерфейсу присвоили тип значение)
    


Ответы

Ответ 1



Интерфейс действительно не наследуются от object. Строго говоря, интерфейсы - это не совсем типы. Вот что об этом говорит спецификация (пункт 3.4.5): The members of an interface are the members declared in the interface and in all base interfaces of the interface. The members in class object are not, strictly speaking, members of any interface (§13.2). However, the members in class object are available via member lookup in any interface type Иными словами, если то же самое сказать языком Бродского и Достоевского, то интерфейсы не наследуются от object, однако компилятор любезно позволяет нам использовать члены object'a без прямого приведения типа. Наверное просто ради удобства, так как в C# все так или иначе является объектом, а потому имеет ссылки на эти методы Что же касается enum'ов, то тут вы не правы. Они как раз наследуются от object опосредованно через System.ValueType, о чем нам также любезно сообщает спецификация (п. 3.4.3): The members of an enumeration are the constants declared in the enumeration and the members inherited from the enumeration’s direct base class System.Enum and the indirect base classes System.ValueType and object Что же касается второй части вопроса, про упаковку-распаковку, то здесь скорее всего имеет место ошибка перевода или же самой книги. В строке, о которой вы говорите, происходит распаковка (то есть преобразование экземпляра ссылочного типа к типу-значению), в чем легко убедиться, посмотрев IL-код. Для такого C#-кода Point pt = new Point(); IComparable a = pt; Point p2 = (Point) a; соответствующий IL будет примерно таков: ldloca.s pt initobj MyNamespace.Point ldloc.0 box MyNamespace.Point stloc.1 ldloc.1 unbox.any MyNamespace.Point pop ret как можно видеть, операция упаковки (box) только одна, и за ней следует распаковка (unbox)

четверг, 19 декабря 2019 г.

Зачем нужен атрибут [Serializable] и что он делает?

#c_sharp #clr


У всех классов, экземпляры которых должны/могут быть сохранены через BinaryFormater,
обязан быть этот самый атрибут [Serializable]. Зачем он нужен? 

Нет, конечно, ясно, что он говорит среде CLR, мол, тип есть сериализуемый, поэтому,
будь добра (среда) и сериализируй!

В моем представлении, почти на пальцах, этот атрибут помогает MSV дописать и реализовать
GetObjectData(SerializationInfo info, StreamingContext context) из интерфейса ISerializable
для каждого поля в классе. Например, так:

    public override void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("Name", name);
        info.AddValue("BDate", BDate);
        info.AddValue("Price", Price);
    }


И реализовать конструктор для десериализации, вот так:

    public Engineer(SerializationInfo info, StreamingContext context) : base(info.GetString("Name"),
info.GetDateTime("BDate"))
    {
        Price = info.GetDecimal("Price");

    }


НО, даже если и интерфейс ISerializable ты реализовал, то атрибут все равно нужен.
CA2237: MarkISerializableTypesWithSerializable

Что же он все таки делает, так сказать, под капотом?
    


Ответы

Ответ 1



В дополнение к ответу @Alexander Petrov, процитирую MSDN (в собственном переводе): Применяйте атрибут SerializableAttribute к типу для того, чтобы указать, что экземпляры этого типа разрешено сериализовывать. Если хотя бы один тип в графе сериализуемых объектов (то есть, тип корневого объекта или любого объекта, на который он внутри ссылается прямо или косвенно) не имеет этого атрибута, CLR выбросит SerializationException. Это предотвращает ошибки, связанные с непредусмотренной сериализацией типов, которые для сериализации вовсе не предназначены. Почему такое необходимо для бинарной сериализации? Дело в том, что бинарная сериализация нарушает инкапсуляцию и работает напрямую с закрытыми полями. При этом, в отличие от вызова сеттера для публичного свойства, легко может быть нарушен инвариант типа. Например, если каждый экземпляр класса получает при создании уникальный номер, то бинарная десериализация склонирует этот самый номер, нарушив тем самым логику программы. Чтобы избежать таких проблем, для участия типа в сериализации CLR требует осознанного решения программиста о том, что каждый конкретный может быть безопасно сериализирован. (Информация частично взята из этого ответа.)

Ответ 2



Поискав объяснения и как следует подумав, я окончательно укрепился в мнении, которое давно подозревал: этот атрибут служит намёком не столько BinaryFormatterу, сколько человеку. Не сериализуй что попало! Например, при разработке на Windows Forms (когда вся логика в баттон-кликах, а данные - в полях форм) у новичков часто возникает соблазн сохранять внешний вид и вообще все данные программы с помощью сериализации главной формы. А что, просто же! Однако, если бы это было реализовано, то это привело бы к сохранению сотен, а то и тысяч, по большей части пустых (неизменённых с дефолтного значения) свойств форм со всеми вложенными контролами (в том числе изображения). Что, конечно же, избыточно и ненужно. Соответственнно, когда разработчик пишет класс, содержащий поля/свойства с данными, он должен определить, можно ли (нужно ли) его свободно (де)сериализовать и пометить при необходимости этим атрибутом. Другой разработчик, использующий этот класс, по наличию/отсутствию атрибута поймёт, что с ним можно делать. При необходимости ему придётся создать класс, специально предназначенный для хранения отдельных (и только нужных!) данных.

Ответ 3



Добавлю еще немного информации к размышлению. Класс может быть сериализуемым и при этом не реализовывать интерфейс ISerializable [Serializable] class A { public int Value; } В этом случае будет применена сериализация по-умолчанию, т.е. будут сериализованы публичные поля и свойства. Но нужно учесть, что для последующей десериализации у класса должен быть конструктор по-умолчанию (без параметров), иначе вылетит исключение. В особых случаях требуется и атрибут, и реализация интерфейса. Опустим случай, когда разработчику нужно контролировать процесс исходя из соображений логики класса и возьмем пример где это необходимо независимо от желания разработчика. [Serializable] class B { public B Pred; public B Next; } По-умолчанию, если при сериализации встречается публичное поле содержащее объект, этот объект также сериализуется. К чему это приводит: в примере класса выше, это приведет к бесконечной рекурсии и выбросу StackOverflowException если существует два и более связанных объектов класса B т.к. каждый объект имеет ссылку на соседа, а тот, в свою очередь, имеет обратную ссылку. В данном случае этот класс обязан реализовать интерфейс ISerializable, не смотря на внешнюю простоту и отсутствие указаний со стороны компилятора. Ок, двусвязный список видно сразу. Возьмем более сложный пример: [Serializable] class A { public B Item; } [Serializable] class B { public C Item; } [Serializable] class C { public A Item; } Сериализация любого из этих объектов может привести к выбросу StackOverflowException если объекты, хранящиеся в свойствах Item образуют замкнутую цепочку. Не смотря на простоту примера, аналогичные конструкции встречаются довольно часто и наличие циклического графа объектов может быть далеко не очевидным. Так как атрибуты не наследуются и требуется чтобы все задействованные при сериализации объекты имели тип помеченный атрибутом Serializable, вероятность случайно сериализовать объект, который является частью цикла и тем самым вызвать переполнение стека сведена к минимуму, за исключением случаев синтетических примеров, как приведены выше, и явной ошибки(диверсии?) программиста пометившего атрибутом Serializable все что попалось под руку.

Ответ 4



Двунаправленные списки, в том числе круговые, прекрасно сериализуются. Вот класс, который приведен как тот, что выбрасывает StackOverFlowException, сериализуем! Без пользовательской сериализации и других ухищрений. Как, думаете, сериализуются коллекции? А они все Serializable! Сейчас написал класс вот такого типа: [Serializable] public sealed class MyObjectCircle { public int Data; public MyObjectCircle Next = null; public MyObjectCircle Prev = null; } Конечно, заполнил его данными, примерно так: var obj = new MyObjectCircle(data1); var objNext = new MyObjectCircle(data2, null, obj); var objPrev = new MyObjectCircle(data3, obj); obj.Next = objNext; obj.Prev = objPrev; И что, он не сериализуется? Сериализуется, Бинарный форматтер, как надо. Десериализуется тоже без проблем. private void BtSerialize_Click ( object sender, EventArgs e ) { int.TryParse ( textBox6.Text, out var data1 ); int.TryParse(textBox7.Text, out var data2); int.TryParse(textBox8.Text, out var data3); var obj = new MyObjectCircle(data1); var objNext = new MyObjectCircle(data2, null, obj); var objPrev = new MyObjectCircle(data3, obj); obj.Next = objNext; obj.Prev = objPrev; var formatter = new BinaryFormatter ( ); Stream stream = new FileStream ( "serializing.bin", FileMode.Create ); formatter.Serialize ( stream, obj ); stream.Close ( ); TBInfo.Text = @"Сериализовано в ""serializing.bin"""; } } Серьезно, зачем сериализатору сериализировать то, что уже сериализовано? Я надеюсь,я правильно понял комментарий rdorn.