Русский
Русский
English
Статистика
Реклама

Csharp.net

Перевод Поиск, устранение и предупреждение утечек памяти в C .NET 8 лучших практик

12.01.2021 16:20:24 | Автор: admin

Для будущих студентов курса Разработчик C# и всех интересующихся подготовили перевод полезного материала.

Также приглашаем поучаствовать в
открытом вебинаре на тему Методы LINQ, которые сделают всё за вас на нем участники обсудят шесть представителей семейства технологий LINQ, три составляющих основной операции запроса, отложенное и немедленное выполнение, параллельные запросы.


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

Умение обнаруживать, исправлять и предупреждать утечки памяти очень важный навык. Здесь я перечислю 8 лучших практик, используемых мной и моими коллегами старшими .NET разработчиками, которые и вдохновили меня написать эту статью. Эти методы научат вас определять, когда в приложении возникает утечка памяти, находить и исправлять ее. Наконец, я включил в статью стратегии для мониторинга и отчета об утечках памяти в уже развернутых программах.

Что из себя представляют утечки памяти в .NET

В среде со сборкой мусора термин утечка памяти представляется немного контринтуитивным. Как может произойти утечка памяти, когда есть сборщик мусора (GC garbage collector), который берет на себя задачу высвобождения памяти?

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

Вторая причина заключается в том, что вы каким-то образом выделяете неуправляемую память (без сборки мусора) и не освобождаете ее. Сделать это не так уж и сложно. В самой .NET есть множество классов, которые выделяют неуправляемую память. Практически все, что связано с потоками, графикой, файловой системой или сетевыми вызовами, делает это под капотом. Обычно эти классы реализуют метод Dispose, который освобождает память (мы поговорим об этом позже). Вы можете легко выделить неуправляемую память самостоятельно с помощью специальных классов .NET .Например, Marshal или PInvoke (пример этого будет ниже).

Давайте же перейдем к моему списку лучших практик:

1. Обнаружение утечек памяти с помощью окна средств диагностики

Если вы перейдете в Debug | Windows | Show Diagnostic Tools, вы увидите это окно. Как и я когда-то, вы, вероятно, уже видели это окно после установки Visual Studio, сразу же закрыли его и никогда больше о нем не вспоминали. Окно средств диагностики может быть весьма полезным. Оно может помочь вам легко обнаружить 2 проблемы: утечки памяти и GC Pressure (давление на сборщик мусора).

Когда у вас есть утечки памяти, график использования памяти процессом (Process Memory) выглядит следующим образом:

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

В случае GC Pressure, график использования памяти процессом выглядит следующим образом:

GC Pressure это когда вы создаете и удаляете новые объекты настолько быстро, что сборщик мусора просто не успевает за вами. Как вы видите на картинке, объем потребляемой памяти близок к своему пределу, а сборка мусора происходит очень часто.

С помощью этого метода вы не сможете найти определенные утечки памяти, но вы навскидку можете обнаружить, что у вас есть проблема с утечкой памяти, что само по себе уже несет пользу. В Visual Studio Enterprise окно средств диагностики также включает встроенный профилировщик памяти, который позволяет обнаружить конкретную утечку. Мы поговорим о профилировании памяти в третьем пункте.

2. Обнаружение утечек памяти с помощью диспетчера задач, Process Explorer или PerfMon

Второй самый простой способ обнаружить серьезные проблемы с утечками памяти с помощью диспетчера задач (Task Manager) или Process Explorer (от SysInternals). Эти инструменты могут показать объем памяти, который использует ваш процесс. Если она постоянно увеличивается со временем, возможно, у вас утечка памяти.

PerfMon немного сложнее в использовании, но у него есть хороший график потребления памяти с течением времени. Вот график моего приложения, которое бесконечно выделяет память, не освобождая ее. Я использую счетчик Process | Private Bytes.

Обратите внимание, что этот метод заведомо ненадежен. Вы можете наблюдать увеличение потребления памяти только потому, что еще не отработал сборщик мусора. Также стоит вопрос об общей и приватной памяти, поэтому вы можете упустить утечки памяти и/или диагностировать утечки, которые не являются вашими собственными (объяснение). Наконец, вы можете принять утечку памяти за GC Pressure. В этом случае у вас нет утечек памяти, но вы создаете и удаляете объекты так быстро, что сборщик мусора не поспевает за вами.

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

3. Использование профилировщика памяти для обнаружения утечек

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

Вот несколько довольно известных профилировщиков для .NET: dotMemory, SciTech Memory Profiler и ANTS Memory Profiler. Также есть бесплатный профилировщик, если у вас стоит Visual Studio Enterprise.

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

Вы можете увидеть, сколько аллоцировано экземпляров каждого типа, сколько памяти они занимают и путь ссылки на GC Root.

GC Root это объект, который сборщик мусора не может освободить, поэтому все, на что ссылается GC Root, также не может быть освобождено. Статические и локальные объекты, текущие активные потоки, являются GC Roots. Подробнее об этом читайте в статье Сборка мусора в .NET.

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

  1. Начните с какого-либо состояния бездействия (Idle state) в вашем приложении. Это может быть Главное меню или что-то в этом роде.

  2. Сделайте снапшот с помощью профилировщика памяти, присоединившись к процессу или сохранив дамп.

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

  4. Сделайте второй снапшот.

  5. Сравните оба снапшота с помощью своего профилировщика.

  6. Изучите New-Created-Instances, возможно, это утечки памяти. Изучите path to GC Root и попытайтесь понять, почему эти объекты не были освобождены.

Вот отличное видео, где в профилировщике памяти SciTech сравниваются два снапшота, в результате чего обнаруживается утечка памяти:

4. Используйте Make Object ID для поиска утечек памяти

В моей последней статье 5 методов, позволяющих избежать утечек памяти из-за событий в C# .NET, которые вы должны знать, я показал способ найти утечку памяти, поместив точку останова в класс Finalizer. Я покажу вам похожий метод, который еще проще в использовании и не требует изменения кода. Здесь используется функция отладчика Make Object ID и окно непосредственной отладки (Immediate Window).

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

  1. Поместите точку останова туда, где создается экземпляр класса.

  2. Наведите курсор на переменную, чтобы открыть всплывающую подсказку отладчика, затем щелкните правой кнопкой мыши и используйте Make Object ID. Вы можете ввести в окне Immediate $1, чтобы убедиться, что Object ID был создан правильно.

  3. Завершите сценарий, который должен был освободить ваш экземпляр от ссылок.

  4. Спровоцируйте сборку мусора с помощью известных волшебных строчек кода.

GC.Collect();GC.WaitForPendingFinalizers();GC.Collect();

5. В появившемся окне непосредственной отладки введите $1. Если оно возвращает null, значит, сборщик мусора собрал ваш объект. Если нет, у вас утечка памяти.

Здесь я отлаживаю сценарий с утечкой памяти:

А здесь я отлаживаю аналогичный сценарий, в котором нет утечек памяти:

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

Важно: этот метод не работает в отладчике .NET Core 2.X (проблема). Принудительная сборка мусора в той же области, что и выделение объекта, не освобождает этот объект. Вы можете сделать это, приложив немного больше усилий, спровоцировав сборку мусора в другом методе вне области видимости.

5. Избегайте известных способов заиметь утечки памяти

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

Вот некоторые из наиболее распространенных подозреваемых:

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

  • Кэширование любой тип механизма кэширования может легко вызвать утечку памяти. Кэширую информацию в памяти, в конечном итоге он переполнится и вызовет исключение OutOfMemory. Решением может быть периодическое удаление старых элементов или ограничение объема кэширования.

  • Привязки WPF могут быть опасными. Практическое правило всегда выполнять привязку к DependencyObject или к INotifyPropertyChanged. Если вы этого не сделаете, WPF создаст сильную ссылку на ваш источник привязки (то есть ViewModel) из статической переменной, что приведет к утечке памяти. Дополнительную информацию о WPF утечках можно найти в этом полезном треде StackOverflow.

  • Захваченные члены. Может быть достаточно очевидно, что метод обработчика событий подразумевает, что на объект ссылаются, но когда переменная захвачена анонимным методом на нее также ссылаются. Вот пример такой утечки памяти:

public class MyClass{    private int _wiFiChangesCounter = 0;     public MyClass(WiFiManager wiFiManager)    {        wiFiManager.WiFiSignalChanged += (s, e) => _wiFiChangesCounter++;    }
  • Потоки, которые никогда не завершаются. Live Stack каждого из ваших потоков считается GC Root. Это означает, что до тех пор, пока поток не завершится, любые ссылки из его переменных в стеке не будут собираться сборщиком мусора. Это также включает таймеры. Если обработчик тиков вашего таймера является методом, то объект метода считается ссылочным и не собирается. Вот пример такой утечки памяти:

public class MyClass{    public MyClass(WiFiManager wiFiManager)    {        Timer timer = new Timer(HandleTick);        timer.Change(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));    }     private void HandleTick(object state)    {        // do something    }

Подробнее об этом читайте в моей статье 8 способов вызвать утечки памяти в .NET.

6. Используйте шаблон Dispose для предотвращения утечек неуправляемой памяти

Ваше приложение .NET постоянно использует неуправляемые ресурсы. Сама платформа .NET в значительной степени полагается на неуправляемый код для внутренних операций, оптимизации и Win32 API. Каждый раз, когда вы используете потоки, графику или файлы, например, вы, вероятно, исполняете неуправляемый код.

Классы .NET Framework, использующие неуправляемый код, обычно реализуют IDisposable. Это связано с тем, что неуправляемые ресурсы должны быть явно освобождены, а это происходит в методе Dispose. Ваша единственная задача не забыть вызвать метод Dispose. Если возможно, используйте для этого оператор using.

public void Foo(){    using (var stream = new FileStream(@"C:\Temp\SomeFile.txt",                                       FileMode.OpenOrCreate))    {        // do stuff     }// stream.Dispose() will be called even if an exception occurs

Оператор using за кулисами преобразует код в оператор try / finally, где метод Dispose вызывается в finally.

Но даже если вы не вызовете метод Dispose, эти ресурсы будут освобождены, поскольку классы .NET используют шаблон Dispose. Это означает, что если Dispose не был вызван раньше, он вызывается из Finalizer, когда объект собирается сборщиком мусора. То есть, если у вас нет утечки памяти и действительно вызывается Finalizer.

Когда вы сами выделяете неуправляемые ресурсы, вам определенно следует использовать шаблон Dispose. Вот пример:

public class MyClass : IDisposable{    private IntPtr _bufferPtr;    public int BUFFER_SIZE = 1024 * 1024; // 1 MB    private bool _disposed = false;     public MyClass()    {        _bufferPtr =  Marshal.AllocHGlobal(BUFFER_SIZE);    }     protected virtual void Dispose(bool disposing)    {        if (_disposed)            return;         if (disposing)        {            // Free any other managed objects here.        }         // Free any unmanaged objects here.        Marshal.FreeHGlobal(_bufferPtr);        _disposed = true;    }     public void Dispose()    {        Dispose(true);        GC.SuppressFinalize(this);    }     ~MyClass()    {        Dispose(false);    }}

Смысл этого шаблона разрешить явное удаление ресурсов. А также чтобы добавить гарантии того, что ваши ресурсы будут удалены во время сборки мусора (в Finalizer), если Dispose() не был вызван.

GC.SuppressFinalize(this) также имеет важное значение. Она гарантирует, что Finalizer не будет вызван при сборке мусора, если объект уже был удален. Объекты с Finalizer-ами освобождаются иначе и намного дороже. Finalizer добавляется к F-Reachable-Queue, которая позволяет объекту пережить дополнительную генерацию сборщика мусора. Есть и другие сложности.

7. Добавление телеметрии памяти из кода

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

Из самого приложения мы можем получить много информации. Получить текущую используемую память очень просто:

Process currentProc = Process.GetCurrentProcess();var bytesInUse = currentProc.PrivateMemorySize64;

Для получения дополнительной информации вы можете использовать PerformanceCounter класс, который используется для PerfMon:

PerformanceCounter ctr1 = new PerformanceCounter("Process", "Private Bytes", Process.GetCurrentProcess().ProcessName);PerformanceCounter ctr2 = new PerformanceCounter(".NET CLR Memory", "# Gen 0 Collections", Process.GetCurrentProcess().ProcessName);PerformanceCounter ctr3 = new PerformanceCounter(".NET CLR Memory", "# Gen 1 Collections", Process.GetCurrentProcess().ProcessName);PerformanceCounter ctr4 = new PerformanceCounter(".NET CLR Memory", "# Gen 2 Collections", Process.GetCurrentProcess().ProcessName);PerformanceCounter ctr5 = new PerformanceCounter(".NET CLR Memory", "Gen 0 heap size", Process.GetCurrentProcess().ProcessName);//...Debug.WriteLine("ctr1 = " + ctr1 .NextValue());Debug.WriteLine("ctr2 = " + ctr2 .NextValue());Debug.WriteLine("ctr3 = " + ctr3 .NextValue());Debug.WriteLine("ctr4 = " + ctr4 .NextValue());Debug.WriteLine("ctr5 = " + ctr5 .NextValue());

Доступна информация с любого счетчика perfMon, чего нам хватит с головой.

Однако вы можете пойти еще дальше. CLR MD (Microsoft.Diagnostics.Runtime) позволяет проверить текущую кучу и получить любую возможную информацию. Например, вы можете вывести все выделенные типы в памяти, включая количество экземпляров, пути к корням и так далее. Вы в значительной степени реализовали профилировщик памяти из кода.

Чтобы получить представление о том, чего можно достичь с помощью CLR MD, ознакомьтесь с DumpMiner Дуди Келети.

Вся эта информация может быть записана в файл или, что еще лучше, в инструмент телеметрии, такой как Application Insights.

8. Тестирование на утечки памяти

Профилактическое тестирование на утечки памяти незаменимая практика. И это не так уж и сложно. Вот небольшой шаблон, который вы можете использовать:

[Test]void MemoryLeakTest(){  var weakRef = new WeakReference(leakyObject)  // Ryn an operation with leakyObject  GC.Collect();  GC.WaitForPendingFinalizers();  GC.Collect();  Assert.IsFalse(weakRef.IsAlive);}

Для более глубокого тестирования профилировщики памяти, такие как .NET Memory Profiler от SciTech и dotMemory, предоставляют тестовый API:

MemAssertion.NoInstances(typeof(MyLeakyClass));MemAssertion.NoNewInstances(typeof(MyLeakyClass), lastSnapshot);MemAssertion.MaxNewInstances(typeof(Bitmap), 10);

Заключение

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

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


- Узнать подробнее о курсе Разработчик C#.

- Зарегистрироваться на открытый вебинар Методы LINQ, которые сделают всё за вас.

Подробнее..

Перевод Реализуем глобальную обработку исключений в ASP.NET Core приложении

20.02.2021 18:04:03 | Автор: admin

В преддверии старта курса C# ASP.NET Core разработчик подготовили традиционный перевод полезного материала.

Также рекомендуем посмотреть вебинар на тему
Отличия структурных шаблонов проектирования на примерах. На этом открытом уроке участники вместе с преподавателем-экспертом познакомятся с тремя структурными шаблонами проектирования: Заместитель, Адаптер и Декоратор.


Введение

Сегодня в этой статье мы обсудим концепцию обработки исключений в приложениях ASP.NET Core. Обработка исключений (exception handling) одна из наиболее важных импортируемых функций или частей любого типа приложений, которой всегда следует уделять внимание и правильно реализовывать. Исключения это в основном средства ориентированные на обработку рантайм ошибок, которые возникают во время выполнения приложения. Если этот тип ошибок не обрабатывать должным образом, то приложение будет остановлено в результате их появления.

В ASP.NET Core концепция обработки исключений подверглась некоторым изменениям, и теперь она, если можно так сказать, находится в гораздо лучшей форме для внедрения обработки исключений. Для любых API-проектов реализация обработки исключений для каждого действия будет отнимать довольно много времени и дополнительных усилий. Но мы можем реализовать глобальный обработчик исключений (Global Exception handler), который будет перехватывать все типы необработанных исключений. Преимущество реализации глобального обработчика исключений состоит в том, что нам нужно определить его всего лишь в одном месте. Через этот обработчик будет обрабатываться любое исключение, возникающее в нашем приложении, даже если мы объявляем новые методы или контроллеры. Итак, в этой статье мы обсудим, как реализовать глобальную обработку исключений в ASP.NET Core Web API.

Создание проекта ASP.NET Core Web API в Visual Studio 2019

Итак, прежде чем переходить к обсуждению глобального обработчика исключений, сначала нам нужно создать проект ASP.NET Web API. Для этого выполните шаги, указанные ниже.

  • Откройте Microsoft Visual Studio и нажмите Create a New Project (Создать новый проект).

  • В диалоговом окне Create New Project выберите ASP.NET Core Web Application for C# (Веб-приложение ASP.NET Core на C#) и нажмите кнопку Next (Далее).

  • В окне Configure your new project (Настроить новый проект) укажите имя проекта и нажмите кнопку Create (Создать).

  • В диалоговом окне Create a New ASP.NET Core Web Application (Создание нового веб-приложения ASP.NET Core) выберите API и нажмите кнопку Create.

  • Убедитесь, что флажки Enable Docker Support (Включить поддержку Docker) и Configure for HTTPS (Настроить под HTTPS) сняты. Мы не будем использовать эти функции.

  • Убедитесь, что выбрано No Authentication (Без аутентификации), поскольку мы также не будем использовать аутентификацию.

  • Нажмите ОК.

Используем UseExceptionHandler middleware в ASP.NET Core.

Чтобы реализовать глобальный обработчик исключений, мы можем воспользоваться преимуществами встроенного Middleware ASP.NET Core. Middleware представляет из себя программный компонент, внедренный в конвейер обработки запросов, который каким-либо образом обрабатывает запросы и ответы. Мы можем использовать встроенное middleware ASP.NET Core UseExceptionHandler в качестве глобального обработчика исключений. Конвейер обработки запросов ASP.NET Core включает в себя цепочку middleware-компонентов. Эти компоненты, в свою очередь, содержат серию делегатов запросов, которые вызываются один за другим. В то время как входящие запросы проходят через каждый из middleware-компонентов в конвейере, каждый из этих компонентов может либо обработать запрос, либо передать запрос следующему компоненту в конвейере.

С помощью этого middleware мы можем получить всю детализированную информацию об объекте исключения, такую как стектрейс, вложенное исключение, сообщение и т. д., а также вернуть эту информацию через API в качестве вывода. Нам нужно поместить middleware обработки исключений в configure() файла startup.cs. Если мы используем какое-либо приложение на основе MVC, мы можем использовать middleware обработки исключений, как это показано ниже. Этот фрагмент кода демонстрирует, как мы можем настроить middleware UseExceptionHandler для перенаправления пользователя на страницу с ошибкой при возникновении любого типа исключения.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  {      app.UseExceptionHandler("/Home/Error");      app.UseMvc();  } 

Теперь нам нужно проверить сообщение об исключении. Для этого откройте файл WeatherForecastController.cs и добавьте следующий экшн-метод, чтобы пробросить исключение:

[Route("GetExceptionInfo")]  [HttpGet]  public IEnumerable<string> GetExceptionInfo()  {       string[] arrRetValues = null;       if (arrRetValues.Length > 0)       { }       return arrRetValues;  } 

Если мы хотим получить подробную информацию об объектах исключения, например, стектрейс, сообщение и т. д., мы можем использовать приведенный ниже код в качестве middleware исключения

app.UseExceptionHandler(                  options =>                  {                      options.Run(                          async context =>                          {                              context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;                              context.Response.ContentType = "text/html";                              var exceptionObject = context.Features.Get<IExceptionHandlerFeature>();                              if (null != exceptionObject)                              {                                  var errorMessage = $"<b>Exception Error: {exceptionObject.Error.Message} </b> {exceptionObject.Error.StackTrace}";                                  await context.Response.WriteAsync(errorMessage).ConfigureAwait(false);                              }                          });                  }              );  

Для проверки вывода просто запустите эндпоинт API в любом браузере:

Определение пользовательского Middleware для обработки исключений в API ASP.NET Core

Кроме того, мы можем написать собственное middleware для обработки любых типов исключений. В этом разделе мы продемонстрируем, как создать типичный пользовательский класс middleware. Пользовательское middleware также обеспечивает гораздо большую гибкость для обработки исключений. Мы можем добавить стекатрейс, имя типа исключения, код ошибки или что-нибудь еще, что мы захотим включить как часть сообщения об ошибке. В приведенном ниже фрагменте кода показан типичный пользовательский класс middleware:

using Microsoft.AspNetCore.Http;    using Newtonsoft.Json;    using System;    using System.Collections.Generic;    using System.Linq;    using System.Net;    using System.Threading.Tasks;        namespace API.DemoSample.Exceptions    {        public class ExceptionHandlerMiddleware        {            private readonly RequestDelegate _next;                public ExceptionHandlerMiddleware(RequestDelegate next)            {                _next = next;            }                public async Task Invoke(HttpContext context)            {                try                {                    await _next.Invoke(context);                }                catch (Exception ex)                {                                    }            }        }    } 

В приведенном выше классе делегат запроса передается любому middleware. Middleware либо обрабатывает его, либо передает его следующему middleware в цепочке. Если запрос не успешен, будет выброшено исключение, а затем будет выполнен метод HandleExceptionMessageAsync в блоке catch. Итак, давайте обновим код метода Invoke, как показано ниже:

public async Task Invoke(HttpContext context)  {      try      {          await _next.Invoke(context);      }      catch (Exception ex)      {          await HandleExceptionMessageAsync(context, ex).ConfigureAwait(false);      }  }  

Теперь нам нужно реализовать метод HandleExceptionMessageAsync, как показано ниже:

private static Task HandleExceptionMessageAsync(HttpContext context, Exception exception)  {      context.Response.ContentType = "application/json";      int statusCode = (int)HttpStatusCode.InternalServerError;      var result = JsonConvert.SerializeObject(new      {          StatusCode = statusCode,          ErrorMessage = exception.Message      });      context.Response.ContentType = "application/json";      context.Response.StatusCode = statusCode;      return context.Response.WriteAsync(result);  } 

Теперь, на следующем шаге, нам нужно создать статический класс с именем ExceptionHandlerMiddlewareExtensions и добавить приведенный ниже код в этот класс,

using Microsoft.AspNetCore.Builder;  using System;  using System.Collections.Generic;  using System.Linq;  using System.Threading.Tasks;    namespace API.DemoSample.Exceptions  {      public static class ExceptionHandlerMiddlewareExtensions      {          public static void UseExceptionHandlerMiddleware(this IApplicationBuilder app)          {              app.UseMiddleware<ExceptionHandlerMiddleware>();          }      }  }  

На последнем этапе, нам нужно включить наше пользовательское middleware в методе Configure класса startup, как показано ниже:

app.UseExceptionHandlerMiddleware();

Заключение

Обработка исключений это по сути сквозная функциональность для любого типа приложений. В этой статье мы обсудили процесс реализации концепции глобальной обработки исключений. Мы можем воспользоваться преимуществами глобальной обработки исключений в любом приложении ASP.NET Core, чтобы гарантировать, что каждое исключение будет перехвачено и вернет правильные сведения, связанные с этим исключением. С глобальной обработкой исключений нам достаточно в одном месте написать код, связанный с обработкой исключений, для всего нашего приложения. Любые предложения, отзывы или запросы, связанные с этой статьей, приветствуются.


Узнать подробнее о курсе C# ASP.NET Core разработчик.

Посмотреть вебинар на тему Отличия структурных шаблонов проектирования на примерах.

Подробнее..

Связность кода на примере генератора ASCII графиков, утилита для операций с интервалами и demo на Blazor WebAssembly

18.03.2021 00:13:34 | Автор: admin

Работа с периодами может быть запутанной. Представьте, что у вас бухгалтерское приложение. И вам нужно получить периоды, когда сотрудник работал по графику 2 через 2 до индексации зарплаты. При этом нужно учитывать отпуска, смены графиков работы, увольнения/восстановления, переходы в другие отделы и прочие кадровые мероприятия. Эта информация хранится в виде приказов, у которых есть Дата начала действия и Дата конца, т.е. у вас есть периоды времени, с которыми нужно производить операции.

Например найти пересечение всех интервалов:

          2              5         7         9                  |--------------|         |---------|        0              3    4         6    7              10  |--------------|    |---------|    |--------------|        1              4    5              8                  |--------------|    |--------------|             Result          2    3                   7    8                       |----|                   |----|// Для упрощения восприятия даты заменены на числа

Для решения подобных задач и предназначена анонсируемая утилита.

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

Оказалось, что с помощью полученной ASCII генерилки можно рисовать еще и графики y=f(x).

Рис 1. Демонстрация генерации ASCII графиков на Blazor WebAssembly. Можно указывать несколько произвольных функции зависимости y от x.

Анонс утилиты для работы с интервалами IntervalUtility

GitHub, NuGet, интерактивное demo на Blazor WebAssembly.

Ниже пара примеров работы утилиты. Больше примеров на GitHub.

Найти пересечения коллекций интервалов

var arrayOfArrays = new[] {    new[] { new Interval<int>(2,5), new Interval<int>(7, 9) },    new[] { new Interval<int>(0,3), new Interval<int>(4, 6),             new Interval<int>(7, 10) },    new[] { new Interval<int>(1,4), new Interval<int>(5, 8) },};var intervalUtil = new IntervalUtil();var res = intervalUtil.Intersections(arrayOfArrays);// => [2,3], [7,8]          2              5         7         9                 |--------------|         |---------|       0              3    4         6    7              10 |--------------|    |---------|    |--------------|       1              4    5              8                 |--------------|    |--------------|            Result          2    3                   7    8                      |----|                   |----| 

Вычитание коллекции интервалов

var intervalsA = new[] { new Interval<int>(1, 5), new Interval<int>(7, 10) };var intervalsB = new[] { new Interval<int>(0, 2), new Interval<int>(3, 5),                          new Interval<int>(8, 9) };var intervalUtil = new IntervalUtil();var res = intervalUtil.Exclude(intervalsA, intervalsB);// => [2,3], [7,8], [9,10]     1                   5         7              10     |-------------------|         |--------------|0         2    3         5              8    9|---------|    |---------|              |----|Result          2    3                   7    8    9    10          |----|                   |----|    |----|

На этом полезная часть статьи заканчивается. Дальше будет способ генерации бесполезных ASCII графиков.

Генератор ASCII графиков

Интерактивное demo на Blazor WebAssembly.

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

Основная идея: рисовать ASCII блоками фиксированного размера, по аналогии с пикселями

Нужен способ отображения символов (или блоков символов). Пусть изображение строится блоками одинаковой длины. Так удобнее. Можно сказать что один блок - один пиксель. Пиксели тоже половинами рисовать нельзя. Высота блока также фиксирована - одна строка.

Точно также как и у пикселя, у каждого блока есть позиция:

  • номер строки,

  • номер колонки или, другими словами, номер блока в строке.

Нужен способ последовательно запрашивать/генерировать блоки, указывая номер строки и номер блока в строке.

public class DrawerProcessor {    public void Draw(        // запрашивает блок по номеру строки и номеру блока в строке        Func<int, int, DrawerBlock> blockDraw,         // вызывается когда блок можно отрисовать        Action<string, bool> onBlockDraw) {        int row = 0;        int blockIndex = 0;        var done = false;        while (!done) {            var block = blockDraw(row, blockIndex);            switch (block.Command) {                case DrawerCommand.Continue:                    blockIndex = blockIndex + block.Displacement;                    break;                case DrawerCommand.NewLine:                    row = row + 1;                    blockIndex = 0;                    break;                case DrawerCommand.End:                    done = true;                    break;            }            onBlockDraw(block.Value, done);        }    }}public class DrawerBlock {    public string Value { get; set; }    public DrawerCommand Command { get; set; }    // Смещение индекса блока,    // Value может содержать один или несколько блоков    // Если в Value два блока индекс следующего блока надо сместить на 2    public int Displacement { get; set; } = 1;}

DrawerProcessor только следит за текущей позицией блока. DrawerProcessor не принимает никаких решений:

  • он не формирует содержимое блоков,

  • не решает начать новую строку или нет,

  • не определяет продолжать отрисовку или прервать.

Использование DrawerProcessor:

var drawer = new DrawerProcessor();drawer.Draw(    (row, blockIndex) => {        // вся логика отрисовки в одном куске кода        if (row == 3)            return new DrawerBlock {                 Command = DrawerCommand.End             };        if(blockIndex == 3)            return new DrawerBlock {                Value = Environment.NewLine,                Command = DrawerCommand.NewLine            };        return new DrawerBlock {            Value = $"[{row},{blockIndex}]",            Command = DrawerCommand.Continue        };    },    (blockStr, isEnd) => Console.Write(blockStr));Вывод[0,0][0,1][0,2][1,0][1,1][1,2][2,0][2,1][2,2]// Листинг 1. Использование DrawerProcessor.// Не лучший вариант для сложной логики рисования - вся отрисовка// в одном куске кода (в делегате  (row, blockIndex) => { .. }),// который будет разрастаться.

Зачем может потребоваться выводить сразу двойные блоки, и следовательно, зачем нужно свойство DrawerBlock.Displacement? Предположим - длина блока 1 символ, нужно отобразить интервалы и подписи к концам:

8         11|---------|

11 - это два символа. Отрисовывать 11 двумя разными блоками сложно - нужно запомнить, что пишем 11, 1 уже написали - значит сейчас выводим еще 1 . А если надо написать трехзначное число? Проще сразу нарисовать 11 и пропустить отрисовку следующего блока: установить DrawerBlock.Displacement = 2.

Выражаясь наукообразно: если рисовать 11 двумя разными блоками, появляется необходимость хранить состояние (знать, что рисовал предыдущий блок и возможно пред-предыдущий). Увеличивается связь между блоками (предыдущий блок нарисовал 1 - значит сейчас надо нарисовать еще 1), т.е. увеличивается "связность кода". Связность приводит к усложнению.

Немного удобства:

static class Block {    public static DrawerBlock Continue(string val, int displacement = 1)                 => new() {            Command = DrawerCommand.Continue,            Value = val,            Displacement = displacement        };    public static DrawerBlock End() =>        new() { Command = DrawerCommand.End };    public static DrawerBlock NewLine() =>        new() {             Command = DrawerCommand.NewLine,             Value = Environment.NewLine         };}

Теперь такой код:

return new DrawerBlock {    Value = Environment.NewLine,    Command = DrawerCommand.NewLine};

можно писать короче:

return Block.NewLine();

Рисуем несвязанными между собой блоками

В листинге 1 вся логика отрисовки находится в одном куске кода (в делегате (row, blockIndex) => { .. }). Пока этой логики не много, это наглядно и удобно. Но при разрастании, появлении новых требований/условий - код делегата (row, blockIndex) => { .. } будет расти и усложняться.

Пример: в листинге 1 рисовали кирпичики с цифрами:

[0,0][0,1][0,2][1,0][1,1][1,2][2,0][2,1][2,2]

теперь нужно писать цифры только на кирпичиках в шахматном порядке:

[   ][0,1][   ][1,0][   ][1,2][   ][2,1][   ]

Придется править и усложнять код делегата (row, blockIndex) => { .. }.

Выражаясь наукообразно: произойдет нарушение принципа открытости/закрытости - каждое новое требование влечет за собой необходимость изменения ранее написанного кода, т.е. код не открыт для расширения - не можем расширить логику отрисовки. И код не закрыт для изменений - придется изменять уже написанный код.

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

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

// блок "конец"DrawerBlock end(int row, int blockIndex) =>    row == 3 ? Block.End() : null;// блок "новая строка"DrawerBlock newLine(int row, int blockIndex) =>    blockIndex == 3 ? Block.NewLine() : null;// блок "кирпичик с цифрами"DrawerBlock brick(int row, int blockIndex) =>     Block.Continue($"[{row},{blockIndex}]");

При рисовании нужно просто перебирать функции блоков:

public class BlockDrawer {    readonly DrawerProcessor _DrawerProcessor;    public BlockDrawer(DrawerProcessor drawerProcessor) {        _DrawerProcessor = drawerProcessor             ?? throw new ArgumentNullException(nameof(drawerProcessor));    }    public void Draw(        IReadOnlyCollection<Func<int, int, DrawerBlock>> blockDrawers,         Action<string, bool> onBlockDraw) {        _DrawerProcessor.Draw(                (row, blockIndex) => {                    foreach (var bd in blockDrawers) {                        var block = bd(row, blockIndex);                        if (block != null)                            return block;                    }                    return                         new DrawerBlock { Command = DrawerCommand.End };                },                onBlockDraw            );    }}

Пример использования:

// обратите внимание: порядок блоков важенvar blockDrawers = new Func<int, int, DrawerBlock>[] {     end,    newLine,    brick};var drawer = new DrawerProcessor();var blockDrawer = new BlockDrawer(drawer);blockDrawer.Draw(    blockDrawers,    (blockStr, isEnd) => Console.Write(blockStr));

Сейчас программа рисует тоже самое, что программа в листинге 1 - только кирпичики с цифрами.

Добавим кирпичики без цифр:

static void Main(string[] args) {    DrawerBlock end(int row, int blockIndex) => ...;    DrawerBlock newLine(int row, int blockIndex) => ...;    DrawerBlock brick(int row, int blockIndex) => ...;    // кирпичик без цифр    DrawerBlock brickEmpty(int row, int blockIndex) =>        ((row + blockIndex) % 2 == 0) ? Block.Continue($"[   ]") : null;    var blockDrawers = new Func<int, int, DrawerBlock>[] {         end,        newLine,        brickEmpty, // важно вставить перед brick        brick    };    var drawer = new DrawerProcessor();    var blockDrawer = new BlockDrawer(drawer);    blockDrawer.Draw(        blockDrawers,        (blockStr, isEnd) => Console.Write(blockStr));}Результат[   ][0,1][   ][1,0][   ][1,2][   ][2,1][   ]  // Листинг 2. Использование DrawerProcessor.// За отрисовку каждого блока отвечает отдельная функция.// Функции захардкожены в main - не лучший варинт,// если блоков будет много или они будут более сложные.

Выражаясь наукообразно: использован паттерн цепочка обязанностей. Функции рисования блоков вызываются по цепочке. Функции не связаны между собой. Можно добавлять новые блоки, не изменяя написанный код. Однако отметим, что слабая связь между блоками все же есть - важно вызывать их в нужной последовательности.

В листинге 2 функции захардкожены в методе Main. Нет возможности добавить новую функцию рисования без изменения метода Main. Исправим. Пусть для каждой функции рисования блока будет отдельный класс.

public interface IContextDrawerBlock<TDrawerContext> {    int Priority { get; }    DrawerBlock Draw(int row, int blockIndex, TDrawerContext context);}

В интерфейс блоков рисования добавлен context, чтобы была возможность задавать разные параметры отрисовки. Например, ниже класс блока конца отрисовки узнает о максимальном кол-ве строк через context.RowCount:

class EndDrawer : IContextDrawerBlock<SampleDrawContext> {    public int Priority => 10;    public DrawerBlock Draw(int row, int blockIndex,        SampleDrawContext context)               => row == context.RowCount ? Block.End() : null;}

Как и с функциями, классы блоков надо последовательно перебрать:

public class ContextBlockDrawer<TDrawerContext> {    readonly IReadOnlyCollection<IContextDrawerBlock<TDrawerContext>> _BlockDrawers;    readonly BlockDrawer _Drawer;    public ContextBlockDrawer(        BlockDrawer drawer,         IReadOnlyCollection<IContextDrawerBlock<TDrawerContext>> blockDrawers) {        _Drawer = drawer ?? throw ...        _BlockDrawers = blockDrawers?.Any() == true            ? blockDrawers.OrderBy(bd => bd.Priority).ToArray()            : throw ...    }    public void Draw(TDrawerContext drawerContext,        Action<string, bool> onBlockDraw) {        var drawers = _BlockDrawers.Select(bd => {            DrawerBlock draw(int row, int blockIndex) =>                 bd.Draw(row, blockIndex, drawerContext);            return (Func<int, int, DrawerBlock>)draw;        })        .ToArray();        _Drawer.Draw(drawers, onBlockDraw);    }}

Теперь пример с кирпичиками будет выглядеть так:

// Создание ContextBlockDrawervar drawer = new DrawerProcessor();var blockDrawer = new BlockDrawer(drawer);var blockDrawers = new IContextDrawerBlock<SampleDrawContext>[] {    new EndDrawer(),    new EndLineDrawer(),    new BrickEmptyDrawer(),    new BrickDrawer(),};var ctxBlockDrawer = new ContextBlockDrawer<SampleDrawContext>(    blockDrawer,     blockDrawers);// использование ContextBlockDrawerctxBlockDrawer.Draw(    new SampleDrawContext {        RowCount = 3,        BlockCount = 3    },    (blockStr, isEnd) => Console.Write(blockStr));// Листинг 3. Использование ContextBlockDrawer.// Каждая функция рисования блока в отдельном классе.// Введен контекст рисования.// Классы блоков выглядят не связанными между собой,// однако это не совсем так - мешает необходимость задавать Priority.

Обратите внимание: каждый класс блока имеет свойство Priority, поэтому при создании нового блока придется посмотреть какие Priority у других блоков. По крайней мере приоритет нового блока не должен быть выше блока конца. Т.е. на самом деле между блоками есть связь (хотя и не очень сильная).

Наращиваем функционал переиспользуя старый код, стараемся при переиспользовании минимизировать связи

Посмотрите на код создания ContextBlockDrawer в листинге 3. ContextBlockDrawer использует (переиспользует) в своей работе BlockDrawer и получает его экземпляр через конструктор. BlockDrawer, в свою очередь, использует (переиспользует) DrawerProcessor, и также получает его через конструктор.

Выражаясь наукообразно:

ContextBlockDrawer ->зависит от (читай использует)-> BlockDrawer -> зависит от -> DrawerProcessor.

Получение зависимости через конструктор называется агрегацией.

В листинге 3 при создании ContextBlockDrawer фактически реализовано внедрение зависимостей. При этом самый явный (читай лучший) вариант: внедрение зависимостей через конструктор:

- открывая код класса, сразу видно его зависимости (все зависимости в одном месте - в конструкторе)

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

Для сравнения: еще зависимость можно внедрять, например, через поле или получать через сервис локатор:

// внедрение зависимости через поле // (хуже внедрения через конструктор)var ctxBlockDrawer = new ContextBlockDrawer();ctxBlockDrawer.BlockDrawer = blockDrawer;// получение зависимости через сервис локатор// (хуже внедрения через конструктор)public class ContextBlockDrawer<TDrawerContext> {    ...    public void Draw(TDrawerContext drawerContext,         Action<string, bool> onBlockDraw) {                    ...        var blockDrawer = ServiceLocator.Get<BlockDrawer>();        ...    }}// Варианты получения зависимостей которые лучше избегать

Кроме получения зависимости BlockDrawer через конструктор (агрегация), можно прямо в ContextBlockDrawer создать экземпляр BlockDrawer (применить композицию):

public class ContextBlockDrawer<TDrawerContext> {    readonly IReadOnlyCollection<IContextDrawerBlock<TDrawerContext>>  _BlockDrawers;    readonly BlockDrawer _Drawer;    public ContextBlockDrawer(        IReadOnlyCollection<IContextDrawerBlock<TDrawerContext>> blockDrawers) {        // композиция        var drawer = new DrawerProcessor();        _Drawer = new BlockDrawer(drawer);        ...    }

тогда связь между ContextBlockDrawer и BlockDrawer будет более сильной: ContextBlockDrawer не просто будет использовать BlockDrawer, но и должен будет знать как создать BlockDrawer. И знать о зависимостях BlockDrawer(о DrawerProcessor). Т.е. связность кода возрастет, а вместе с ней возрастет и сложность системы.

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

Декартова система координат

Рисовать графики зависимости y от x с помощью ContextBlockDrawer можно, но неудобно:

public interface IContextDrawerBlock<TDrawerContext> {    int Priority { get; }    DrawerBlock Draw(int row, int blockIndex, TDrawerContext context);}

Draw принимает row и blockIndex. Рисование идет справа налево и сверху вниз. Для графиков y от х было бы удобней:

public interface IСartesianDrawerBlock<TDrawerContext> {    int Priority { get; }    DrawerBlock Draw(float x, float y, TDrawerContext context);}

Например, этот блок рисует прямую:

class LineDrawer : IСartesianDrawerBlock<СartesianDrawerContext> {    public int Priority => 40;    public DrawerBlock Draw(float x, float y,        СartesianDrawerContext context) {        var y1 = x; // уравнение прямой y=x                // если вычисленное y1 равно текущей позиции y        // (c учетом округления)        if (Math.Abs(y1 -y) <= context.Rounding)            return Block.Continue("#");                    return null;    }}

Нужно сделать возможным использовать новый IСartesianDrawerBlock с уже реализованным ContextBlockDrawer. Нужен адаптер, который стыкует Draw(int row, int blockIndex, TDrawerContext context) и DrawerBlock Draw(float x, float y, TDrawerContext context):

public class СartesianDrawerAdapter<TDrawerContext> :     IContextDrawerBlock<TDrawerContext>    where TDrawerContext : IСartesianDrawerAdapterContext {    readonly IСartesianDrawerBlock<TDrawerContext> _cartesianDrawer;    public СartesianDrawerAdapter(    IСartesianDrawerBlock<TDrawerContext> cartesianDrawer) {        _cartesianDrawer = cartesianDrawer ?? throw ...    }        public int Priority => _cartesianDrawer.Priority;    public DrawerBlock Draw(int row, int blockIndex, TDrawerContext context) {        float x = blockIndex / context.Scale + context.XMin;        float y = context.YMax - row / context.Scale;        return _cartesianDrawer.Draw(x, y, context);    }}public interface IСartesianDrawerAdapterContext {    public float Scale { get; }    public float XMin { get; }    public float YMax { get; }}

Пример использования СartesianDrawerAdapter - график прямой:

// создание ctxBlockDrawer для декартовой системы координатvar drawer = new DrawerProcessor();var blockDrawer = new BlockDrawer(drawer);var blockDrawers = new IСartesianDrawerBlock<СartesianDrawerContext>[] {        new EndDrawer(),        new EndLineDrawer(),        new LineDrawer(),        new EmptyDrawer()    }    .Select(dd =>         // применение адаптера        new СartesianDrawerAdapter<СartesianDrawerContext>(dd))    .ToArray();var ctxBlockDrawer = new ContextBlockDrawer<СartesianDrawerContext>(    blockDrawer,    blockDrawers);// использование ctxBlockDrawerctxBlockDrawer.Draw(new СartesianDrawerContext {    XMin = -2,    XMax = 30,    YMin = -2,    YMax = 8,    Scale = 5,    Rounding = 0.1F},(blockStr, isEnd) => Console.Write(blockStr));

Выражаясь наукообразно: при переходе от IContextDrawerBlock к IСartesianDrawerBlock использован паттерн адаптер - СartesianDrawerAdapter.

Использование можно сделать красивее:

// создание...var ctxBlockDrawer = ...var asciiDrawer =     new AsciiDrawer<СartesianDrawerContext>(ctxBlockDrawer);// использованиеasciiDrawer    // вот тут стало красивее    .OnBlockDraw((blockStr, isEnd) => Console.Write(blockStr))    .Draw(new СartesianDrawerContext {        XMin = -2,        XMax = 30,        ...    });// Листинг 4. Создание и использование AsciiDrawer.

Код AsciiDrawer:

public class AsciiDrawer<TDrawerContext> {    readonly ContextBlockDrawer<TDrawerContext> _ContextBlockDrawer;    readonly Action<string, bool> _onBlockDraw;    public AsciiDrawer(        ContextBlockDrawer<TDrawerContext> contextBlockDrawer,         Action<string, bool> onBlockDraw = null) {        _ContextBlockDrawer = contextBlockDrawer ?? throw ...        _onBlockDraw = onBlockDraw;    }    public AsciiDrawer<TDrawerContext> OnBlockDraw(        Action<string, bool> onBlockDraw) {        // создаем новый экземпляр        // возвращать this (return this) небезопасно при многопоточности        return new AsciiDrawer<TDrawerContext>(            _ContextBlockDrawer,             onBlockDraw);    }    public void Draw(TDrawerContext context) {        if (_onBlockDraw == null)                throw new InvalidOperationException("Use .OnBlockDraw to set up draw output");        _ContextBlockDrawer.Draw(context, _onBlockDraw);    }}

Выражаясь наукообразно: AsciiDrawer сделан неизменным - нет методов, которые бы изменяли его состояние. OnBlockDraw возвращает новый экземпляр (а не this). Неизменность делает класс безопасным для многопоточности.

Все SingleInstance

В листинге 4 все, что под комментарием Создание, можно вынести в регистрацию IoC контейнера. В приложении просто получать и использовать готовый AsciiDrawer.

Объекты ASCII рисовальщика не хранят состояний, еще они неизменяемы - значит можно без опасений использовать одни и те же инстансы в разных местах. В том числе наши объекты можно зарегистрировать в IoC контейнере как SingleInstance.

В итоге в Blazor WebAssembly демонстрации по клику на кнопке Run следующий код:

var res = new StringBuilder();AsciiDrw    .OnBlockDraw((blockStr, isEnd) => {        res.Append(blockStr);        if (isEnd)            // обновление UI            Res = res.ToString();    })    .Draw(new FuncsDrawerContext {        // поля формы        Rounding = Rounding,        Scale = Scale,        XMin = Xmin,        XMax = Xmax,        YMin = Ymin,        YMax = Ymax,        // функции для y от x        Functions = funcs    });

В демонстрации используются следующие блоки:

new EndDrawer(),new EndLineDrawer(),new FuncsDrawer(), // рисует функции указанные в контекстеnew XAxisDrawer(), // рисует ось Xnew YAxisDrawer(), // рисует ось Ynew EmptyDrawer()

Можно еще придумать:

  • блок, который закрашивает площадь под графиком,

  • блок который выводит шкалу на осях,

  • блок, который подписывает точки пересечения графиков.

Заключение

Как видите, даже школьную задачку можно серьезно запутать - главное знать принципы и паттерны.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru