Заблуждения начинающих C# разработчиков. Пытаемся ответить на стандартные вопросы

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



    #1. Мантра про 3 поколения в любой ситуации


    Это скорее неточность, чем заблуждение. Вопрос про «сборщик мусора в C#» для разработчика стал классикой и уже мало кто не начнет на него бойко отвечать про концепцию поколений. Однако, почему-то, мало кто обращает внимание на то, что великий и ужасный сборщик мусора — часть рантайма. Соответственно, я бы дал понять, что не пальцем пихан, и спросил бы про какую среду исполнения идет речь. По запросу «сборщик мусора в c#» в интернетах можно найти более, чем много похожих сведений. Однако мало кто упоминает, что данная информация относится к CLR/CoreCLR (как правило). Но не стоит забывать и про Mono, легковесный, гибкий и встраиваемый рантайм, который занял свою нишу в мобильной разработке (Unity, Xamarin) и используется в Blazor. И для соответствующих разработчиков я бы посоветовал поинтересоваться подробностями устройства сборщика в Mono. Например, по запросу «mono garbage collector generations», можно увидеть, что поколения всего два — nursery и old generation (в новеньком и модном сборщике мусора — SGen).

    #2. Мантра про 2 стадии сборки мусора в любой ситуации


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

    Однако все изменилось, когда народ огня развязал войну с появлением CoreCLR и исходников сборщика. Код сборщика для CoreCLR был целиком взят из CLR версии. Никто с нуля его не писал, соответственно, почти все, что можно узнать из исходников CoreCLR, будет верно и для CLR. Теперь, чтобы понять, как что-то работает, достаточно зайти на github и найти это в исходном коде или прочитать readme. Там же можно увидеть, что существует 5 фаз: маркировки, планирования, обновления ссылок, компактинга (удаление с перемещением) и удаление без перемещений(перевести это дело сложно). Но формально это можно разделить на 3 этапа — маркировка, планирование, чистка.

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

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

    #3. Выделение памяти на куче так же быстро, как и на стеке


    Опять же, скорее неточность, чем абсолютная неправда. В общем случае, конечно, разница в скорости выделения памяти минимальна. Действительно, в лучшем случае, при bump pointer allocation, выделение памяти — лишь сдвиг указателя, как и на стеке. Однако на выделение памяти в куче могут повлиять такие факторы, как присваивание нового объекта в поле старого (что затронет write barrier, обновляющий card table — механизм, позволяющий отслеживать ссылки из старшего поколения в младшее), наличие финализатора(необходимо добавить тип в соответствующую очередь) и др. Так же, возможно, объект будет записан в одну из свободных дыр в куче (после сборки без дефрагментации). А нахождение такой дыры происходить хоть и быстро, но, очевидно, медленнее, чем простой сдвиг указателя. Ну и разумеется каждый созданный объект приближает очередную сборку мусора. И на очередной процедуре выделения памяти она может случится. Что, естественно, займет некоторое время.

    #4. Определение ссылочного, значимого типов и упаковки через понятия стека и кучи


    Прямо классика, которая, к счастью, уже не так часто встречается.

    Ссылочный тип располагается в куче. Значимый на стеке. Наверняка многие слышали эти определения очень часто. Но мало того, что это лишь частичная правда, так и определять понятия через протекшую абстракцию — не лучшая идея. За всеми определениями предлагаю обращаться к стандарту CLI — ECMA 335. Для начала стоит уточнить, что типы описывают значения. Так, ссылочный тип определяется следующим образом — значение, описываемое ссылочным типом (ссылка) указывает на расположение другого значения. Для значимого типа значение им описываемое является автономным(самосодержащим). Про то, где располагаются те или иные типы ни слова. Это является протекшей абстракцией, которую все же следует знать.

    Значимый тип может располагаться:

    1. В динамической памяти (куче), если он является частью объекта, расположенного в куче, или в случае упаковки;
    2. На стеке, если он является локальной переменной/аргументом/возвращаемым значением метода;
    3. В регистрах, если то позволяет размер значимого типа и другие условия.

    Ссылочный тип, а именно, значение на которое указывает ссылка, на текущий момент располагается в куче.

    Сама же ссылка может располагаться там же, где и значимый тип.

    Упаковка также не определяется через места хранения. Рассмотрим краткий пример.

    Код C#
        public struct MyStruct
        {
            public int justField;
        }
        
        public class MyClass
        {
         	public MyStruct justStruct;   
        }
        
        public static void Main() 
    	{
            MyClass instance = new MyClass();
            object boxed = instance.justStruct;        
        }
    


    И соответственный IL код для метода Main

    Код IL
        1: nop
        2: newobj instance void C/MyClass::.ctor()
        3: stloc.0
        4: ldloc.0
        5: ldfld valuetype C/MyStruct C/MyClass::justStruct
        6: box C/MyStruct
        7: stloc.1
        8: ret
    


    Так как значимый тип является частью ссылочного очевидно, что располагаться он будет в куче. И шестая строка дает ясно понять, что мы имеем дело с упаковкой. Соответственно, типичное определение «копирование из стека в кучу» дает сбой.

    Чтобы определить, что есть упаковка, для начала стоит сказать, что для каждого значимого типа CTS(common type system) определяет ссылочный тип, который называется упакованным типом. Так, упаковка — операция над значимым типом, создающая значение соответствующего упакованного типа, содержащего побитовую копию оригинального значения.

    #4. События — отдельный механизм


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

    К сожалению, зачастую под событием понимается некий отдельный инструмент, тип, механизм. Особенно этому способствует тип из BCL EventHandler, имя которого наталкивает на мысли о том, что это что-то отдельное.

    Определение события стоит начать с определения свойств. Я уже давно провел для себя такую аналогию, а недавно увидел, что она проведена и в спецификации CLI.

    Свойство определяет именованное значение и методы, которые обращаются к нему. Звучит довольно очевидно. Переходим к событиям. CTS поддерживает события так же, как и свойства, НО методы для доступа отличаются и включают методы для подписки и отписки от события. Из спецификации языка C# — класс определяет событие… что напоминает объявление поля с добавлением ключевого слова event. Тип этого объявления должен быть типом делегата. Спасибо стандарту CLI за определения.

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

    #5. Управляемые и неуправляемые ресурсы. Финализаторы и IDisposable


    Абсолютная путаница существует при работе с этими ресурсами. Этому во многом способствует интернет с тысячей статей о правильной реализации паттерна Dispose. Собственно, в этом паттерне ничего криминального нет — модифицированный шаблонный метод под конкретный случай. Но вопрос в другом — нужен ли он вообще. Почему-то, у части людей возникает непреодолимое желание реализовать финализатор на каждый чих. Скорее всего причиной этого является не полное понимание, что есть «неуправляемый ресурс». И строчки про то, что в финализаторах, как правило, освобождаются неуправляемые ресурсы из-за этого неполного понимания проходят мимо и не остаются в голове.

    Неуправляемый ресурс — ресурс, который не является управляемым (как бы это странно ни было). А управляемый ресурс, в свою очередь, тот, который выделяется и высвобождается CLI автоматически через процесс который называется сборка мусора. Это определение я нагло содрал из стандарта CLI. Но если попытаться объяснить проще, то неуправляемые ресурсы — те, про которые не знает сборщик мусора. (Строго говоря мы можем давать сборщику немного информации про такие ресурсы с помощью GC.AddMemoryPressure и GC.RemoveMemoryPressure, это может оказать влияние на внутренние самотьюнинги сборщика). Соответственно, он не сможет сам позаботиться об их освобождении и поэтому мы должны сделать это за него. И подходов к этому может быть много. И чтобы код не пестрил от разнообразия воображения разработчиков, используются 2 общепринятых подхода.

    1. Интерфейс IDisposable (и его асинхронная версия IAsyncDisposable). Мониторится всеми анализаторами кода, так что забыть про его вызов сложно. Предоставляет единственный метод — Dispose. И поддержку компилятора — оператор using. Отличный кандидат на тело метода Dispose — вызов аналогичного метода одного из полей класса или освобождение неуправляемого ресурса. Вызывается явно пользователем класса. Наличие данного интерфейса у класса подразумевает, что по окончании работы с экземпляром, нужно вызвать этот метод.
    2. Финализатор. По своей сути является страховкой. Вызывается неявно, в неопределенное время, во время сборки мусора. Замедляет выделение памяти, работу сборщика мусора, продлевает время жизни объектов минимум до следующей сборки, а то и дольше, но зато вызывается сам, даже если его никто не вызывал. Из-за своей недетерминированной природы, в нем должны освобождаться только неуправляемые ресурсы. Также можно встретить примеры, в которых финализатор применялся для воскрешения(resurrection) объекта и организации пула объектов таким образом. Однако такая имплементация пула объектов — однозначно плохая идея. Как и пытаться логировать, кидать исключения, обращаться к базе и тысячи подобных действий.

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

    И вопреки утверждению, что Dispose нарушает концепцию, в которой CLR сделает все за нас, заставляет делать что-то самим, о чем-то помнить и тд, скажу следующее. При работе с неуправляемыми ресурсами, надо быть готовым, что они никем, кроме вас, не управляются. И вообще, ситуации, при которых данные ресурсы будут использоваться в ентерпрайсе, почти не встречаются. И в большинстве случаев можно обойтись замечательными классами обертками, вроде SafeHandle, который обеспечивает критическую финализацию ресурсов, предотвращая их преждевременную сборку.

    Если же в вашем приложении по тем или иным причинам много ресурсов, требующих дополнительных действий для освобождения, то стоит взглянуть на отличный паттер компании JetBrains — Lifetime. Но не стоит его применять при виде первого же IDisposable объекта.

    #6. Стек потока, стек вызовов, вычислительный стек и
    Stack <T>


    Последний пункт добавил смеха ради, не думаю, что есть те, кто относит последнее к предыдущим двум. Однако путаницы с тем, что такое стек потока, стек вызовов и вычислительный стек довольно много.

    Стек вызовов — структура данных, а именно стек, для хранения адресов возврата, для возвращения из функций. Стек вызовов — понятие больше логическое. Оно не регламентирует где и как должна храниться информация для возврата. Получается, что стек вызовов — самый обычный и родной нам стек [т.е.
     Stack<T>
    :trollface:]. В нем же хранятся локальные переменные, через него передаются параметры и в нем же сохраняются адреса возврата при вызове инструкции CALL и прерываний, которые впоследствии используются инструкцией RET для возврата из функции/прерывания. Идем дальше. Одним из основных приколов потока является указатель на инструкцию, которая выполняется далее. Поток поочереди выполняет инструкции, объединяющиеся в функции. Соответственно у каждого потока есть стек вызовов. Таким образом получается, что стек потока и есть стек вызовов. То есть стек вызовов данного потока. Вообще, он также упоминается и под другими именами: программный стек, машинный стек.

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

    Вычислительный стек (evaluation stack). Как известно, код C# компилируется в IL код, который входит в состав результирующих DLL (в самом общем случае). И как раз в основе среды выполнения, поглощающей наши DLL и выполняющей IL код лежит стек-машина. Почти все IL инструкции оперируют неким стеком. Например, ldloc загружает локальную переменную под определенным индексом на стек. Здесь под стеком понимается некий виртуальный стек, ведь в итоге эта переменная может с высокой вероятностью оказаться и в регистрах. Арифметические, логические и др IL инструкции оперируют с переменными со стека и кладут результат туда же. То бишь вычисления производятся через этот стек. Таким образом получается, что вычислительных стек — абстракция в рантайме. К слову, многие виртуальные машины основаны на стек-машине.

    #7. Больше потоков — быстрее код


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

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

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

    C# Language Specification — ECMA-334
    Просто хорошие источники:
    Konrad Kokosa — Pro .NET Memory Management
    CLI specification — ECMA-335
    CoreCLR developers about runtime — Book Of The Runtime
    От Станислава Сидристого про финализацию и прочее — .NET Platform Architecture
    Поделиться публикацией

    Похожие публикации

    Комментарии 60

      +13
      #1 Мантра про 3 поколения в любой ситуации

      По-моему это почти всегда вопрос чисто для собеседования. Мне кажется, потому что Рихтера дальше сборщика мусора большинство не осилили.
      В C# очень мало способов работать со сборщиком мусора и реально редки кейсы, когда знание про то, что он есть хоть как-то помогает. Другими словами, раньше ваш проект закроют, чем вы дойдёте до тонкой оптимизации кода под сборщик мусора.

        0
        Тут даже не про тонкую оптимизацию речь. Такие знания наиболее полезны, когда от пользователей повалили дампы с жалобами на «повисшую» софтиную. Раскопать, кто же наинвочил в UI'ный поток, без знания о сборке мусора практически невозможно, особенно если QA не может воспроизвести проблему, а напрямую отдебажить не представляется возможным ввиду объёма кода и кол-ва потоков.
          +2
          UI'ный поток

          Специфика конкретного UI-фреймворка. Следуйте гайдлайнам и не будет таких проблем. Понятно, что у разных UI-фреймворков свои тонкости.
          Я никогда не работал с WPF и Unity, а в asp.net таких проблем не было и нет, там проблемы свои, но я за 10 лет ни разу не встречался с проблемами от GC в самых разных проектах.
            0
            Это не проблема фрэймворка — это проблема кода приложения. Многие фрэймворки требуют обращения к UI'ным объектам только в UI'ном потоке, просто потому что STAThread — тяжкое наследие COM'а. Во многих фрэймворках и библиотеках есть Invoke() и BeginInvoke()
              0

              Сейчас с async-await стало чуть легче писать подобный код.

                0
                Многие фрэймворки требуют обращения к UI'ным объектам только в UI'ном потоке, просто потому что STAThread — тяжкое наследие COM'а.
                В iOS изначально можно было обращаться к UI из любого потока. Правда если так делать, то потом получаешь такие состояния UI, о которых даже подумать не мог.
          +16
          Среди зеленых юниоров распространено мракобесие по поводу очевидных (только что прочитавшему книгу про память) вещей

          Зачем вообще спрашивать джуна про количество поколений CG?

            +27
            Потому что собеседующие не знают что спрашивать
              0
              Такие вопросы задают чтобы выявить хоть кого-то, кто интересуется языком да и программированием вообще. Не важно, что отверят — главное что-б не «а я учииил...» и глаза такие большие большие… у дядьки весом под центнер и седой бородой.
                +1
                Нет, это вопрос из списка «10 банальных вопросов для собеседования программиста на языке C#». Это совсем не про любознательность и интерес к программированию.
                Чтобы определить интерес к программированию надо смотреть как и что человек рассказывает про прошлую работу/опыт/проекты/дипломы и как он рассказывает про то, чем хотел бы заниматься. Для этого интервьюиру нужно иметь богатый опыт разработки и кругозор в технологиях, коим большая часть 23-летних тимлидов, разумеется, не обладает.
                  0

                  А вот и нет. Врать про прошлые проекты, где в реальности на совещаниях сидел да одну фичу годами точил может (почти) каждый. А спросишь его элементарный синтаксис — всё поплыл. Вы удивитесь, но примерно 80% на вопрос о garbage collector или GetHashCode не отвечают вообще ничего.


                  Кровавый энтерпрайз за дофига зарплаты.

                    +1
                    Я не очень понимаю как можно врать про нюансы и сложности использования тех или иных технологических/архитектурных решений на прошлых проектах. Если человек легко может рассказать что, почему и как было использовано, то все ок.
                0
                Нет, потому что собеседующий хочет показать свою значимость и «крутизну».
              +6
              Ну как сказать… Я вот почти сразу на грабли наступил. Дело было так. Детишки попросили соорудить игрушечный камин для кукольного домика, чтоб огонь горел как настоящий. У меня под рукой была платка Raspberry Pi да трехдюймовый мониторчик. Делов на вечер, но дело там осложнялось тем, что мониторчик был на шине SPI со специальным API и библиотекой. Короче, для упражнения я написал программку на C# и она там под mono крутилась, показывая в цикле фреймы из видео. Где-то кадров 15 в секунду я получил. Все работало, но падало через некоторое время с переполнением памяти, хотя утечек там на вид не было. Там вся программа-то полсотни строк была от силы. И решилось, как ни странно, вставкой небольшой паузы в несколько миллисекунд в цикл. Судя по всему, сборщик мусора банально не получал процессорного времени.
                0

                Если мониторчик типа ili9341, то для него есть fbtft и больше 15 кадров в секунду с него получить проблематично

                0
                Все гуд, только Вам очень повезло с зелеными джунами — те которые попадались мне (а последнее время ещё чаще стали попадаться) на подобные темы в таком ракурсе точно общаться не готовы… Справедливости ради я от них подобных нюансов, наверное, и не жду даже…
                  +3

                  В статье вопросы не для джуна. А детали имплементации сборщика мусора в разных рантаймах могут не знать и синьоры.

                    0
                    неплохо бы знать, что эти разные рантаймы таки есть.
                  +9
                  Ни один из рассмотренных вопросов начинающим нафиг не сдался.
                  А вы точно специалист?
                    +4
                    Стек вызовов — структура данных, а именно стек, для хранения адресов возврата, для возвращения из функций. Стек вызовов — понятие больше логическое. Оно не регламентирует где и как должна храниться информация для возврата. Получается, что стек вызовов — самый обычный и родной нам стек т.е. Stack T
                    Каждое предложение, на мой взгляд, ложное.
                    Стек поддерживаемый CPU хранит еще значения регистров до изменения этих регистров новой функцией.
                    Может до JIT оно больше логическое, но вряд ли, но после JIT наверняка используется машино-зависимый стек поддерживаемый CPU, то есть точно не Stack T, то есть именно регламентируется где и как должна хранится информация для возврата, что бы CPU это понял.
                    Конечно если Stack T — вдруг стал виртуальной оберткой над поддерживаемым CPU стеком, то конечно это могло бы быть правдой, но нет.
                      +2
                      А есть ещё архитектуры, отличные от x86, которые не хранят адреса возвратов. Например ARM, на котором .NET тоже уже вроде как работает.
                        0
                        Я не знаком с асемблером ARM. Там что приходится самому выдумывать где хранить адреса возвратов и текущий контекст состояния CPU? На ARM может тогда и подойдет Stack T, но может и на ARM проще указатель стека «двигать», что бы не использовать Stack T потому что наверняка у Stack T всякие излишние аллокации памяти есть?
                          0
                          Нет, там есть специальный регистр — LR (Link Register) — куда записывается адрес возврата. А его уже можно куда-нибудь потом переложить.
                            0

                            Иными словами, адрес возврата всё равно хранится в стеке, просто конкретное место хранения в кадре определяется не архитектурой процессора, а компилятором.

                              0
                              Почти. Адрес возврата хранится в специальном регистре, но чтобы его повторно использовать, его надо сохранить, например в стек. Этот механизм похож на какой-нибудь jmp в конце метода, когда инлайнится вызов другой функции, но обладает несколько большей гибкостью.
                      +1
                      Когда я устраивался в качестве джуниора, на собеседовании были вопросы про сборку мусора (в т.ч. я рассказал про 3 поколения, о которых вы упомянули), как работает GC, boxing/unboxing и т.д. Я бы не сказал, что это что-то супер сложное… Обычно, такие вопросы не вызывают проблем у тех, кто дочитал книгу Рихтера :)
                        0
                        Не уверен что джун вообще должен знать о поколениях сборщика мусора. Да и Рихтера даже не обязательно читать и помнить все прочитанное. Вот дальше да, будет нужно.
                          0
                          Джуны джунам рознь. Никто не говорит, что это знать прямо необходимо и пригодится в работе уже завтра. Эти вопросы вполне можно провалить и успешно устроиться на работу. Задача интервьюера — найти «дно» знаний у кандидата, оценить его кругозор, найти области, в которых он «плавает» и наоборот прокачан сильнее необходимого. Для этого диапазон вопросов должен быть достаточно широк. Особенно это ценно при получении первой коммерческой работы.
                            0
                            Честно слово, на предыдущем собеседовании (в другой компании) меня даже спросили, читал ли я Рихтера. После чего, я серьезно взялся за книги, и… Но об этом я, в скором времени, напишу во второй части своей статьи :)
                              0
                              Мне гораздо интереснее другое — многим из тех, кто «читал Рихтера потому что все говорят надо его читать» вообще пригодились эти знания в повседневной работе?
                                +1
                                Не скажу вам за тех многих, но, мне кажется, любая (адекватная) прочитанная книга в этой области, по крайней мере, позволяет немного меньше ошибаться, и тратить рабочее время
                                  0
                                  Да. Инфа там полезная, но подача довольно тяжёлая. Я не думаю, что читать рихтера джунам — это хороший план.
                                    +1
                                    Согласен с вами насчет полезности и тяжелой подачи для начинающих. Но ведь никто не говорит, что его книга должна быть первой прочитанной по .NET :) В моем случае, я сначала читал Троелсена и информацию на сайте metanit
                            0
                            Пункты конечно интересные и занимательные. Но на мой взгляд человеку, только начинающему писать на С#, знать про них не то чтобы надо. Там на мой взгляд другие вещи гораздо важнее.

                            Да и не для начинающих часть из пунктов интересна только в определённых ситуациях/проектах. И в этих случаях на мой взгляд всё равно надо регулярно поглядывать не меняется ли ситуация с новыми версиями и/или библиотеками
                              +1
                              человеку, только начинающему писать на С#

                              это не джун, это школьник/студент. Максимальная позиция — интерн. Джун — это человек, который пишет код, который потом попадёт в продакшн. И этот код должен быть предсказуемым и поддерживаемым. Когда «джун» случайно увидит в интернетах слово «рефлексия» и начнёт пихать её во все щели в проекте — тогда будут проблемы. Это основы. Это одна несчастная книга, которую можно осилить за полгода и писать код сознательно а не «скомпилилось? ничёси я крутой, коммичу»
                                +7
                                Во первых давайте посмотрим на название статьи и увидим что там не ни слова про «джунов»/«миддлов»/«сениоров», а стоит «Заблуждения начинающих C# разработчиков».
                                А во вторых если взять конкретно те пункты, которые автор описал в своей статье, то они не особо нужны ни джунам, ни даже миддлам. И уж точно они менее важны чем куча других вещей, которые полезно знать людям пишущим на С# и о которых в статье нет ни слова.
                                  0
                                  Ну я согласен, некоторые пункты из этой статьи не нужны даже мидлам, да и синьёры используют эти познания только на собеседованиях)
                                  Но вот пункты про IDisposable и про количество потоков — прям наболевшее. Кто-то где-то пишет using на общий ресурс, и всё, ищи потом, почему конекшен закрылся внезапно. Ну а с потоками доходило до того что ресурсов на их создание уходило на несколько порядков больше, чем на саму задачу.
                              +7
                              Это скорее не заблуждения джунов, а укоренившиеся вредные вопросы с собеседований.
                              Именно там это любят спрашивать и именно там кочуют неправильные ответы, которые тиражируются на всяких форумах и бложиках. Мне порой казалось, что собеседующие как раз с бложиков и понатягивали эти вопросы.

                              Как-то раз интервьюер мне спрашивал про разницу между значимыми и ссылочными типами. Я ему рассказал про разницу синтаксиса, про конструкторы, про ссылки на кучу, про способы передачи и упаковку… А он мне, «есть ещё одна разница»… Я чуть потупил, потом говорю про выделение на стеке и в куче. Он радостно подтвердил, что именно это хотел услышать, но я сломал ему систему — рассказал про то, что локальная переменная всегда на стеке (хотя иногда и в регистре сразу), а вот её содержимое может и в куче, если это ссылочный тип. Хуже того, если поле значимого типа часть ссылочного типа, то значение окажется сразу в куче. А если в локальной переменной значимого типа поле ссылочного типа, то значение оного будет снова в куче. А совсем худо от того, что компилятору/оптимизатору может хватить ума весь ссылочный тип уложить на стек, если он коротко живёт.
                              И чтобы уж совсем его добить, я спросил «Как думаете, а поле типа int в классе претерпевает boxin/unboxing»? (да, это был не первый тупой вопрос на этом собеседовании и я был уже немного злой).

                              Мораль: всё зло от соцсетей :)
                              Точнее от того, что джуны не книжки читают, а их краткие пересказы (и это касается не только программирования), не пишут экспериментальный код, а смотрят в ютубе, как это делает кто-то другой :(
                                0
                                А что бы вы советовали почитать? Часто спрашивают, но я в свое время по С# кроме Шилдта и доки ничего не читал.
                                  0
                                  Что читать — зависит от специализации. Лично моё мнение — в программировании читать книжки по языкам почти бесполезно, если не писать код. Хорошо, когда есть конкретная задача или pet-project.

                                  Если пока ещё нет конкретной цели, но интересен C#, можно начать c официальных туториалов, или с этой подборки.

                                  Я на .net пишу с 2002. Тогда книжек не было — я шерстил msdn, rsdn. Писал, ковырял много чего. Потом избранные главы из разных авторитетных книжек, по мере выявления потребности.
                                  Но я вообще по книжкам учил только Basic (89-й), C по Кернигану и Ритчи (90-й), да C++ по Страуструпу (92-й). Всё остальное — хэлпы, мануалы, доки и метод научного тыка… Да, блин, я английский выучил по докам :)
                                    0
                                    Да в наше время даже сам Рихтер говорит: «зачем вам книжки писать, если вы их не читаете». Кстати, Рихтер, по сравнеию с Шилдтом пишет ужасно: что на русском, что в оригинале реально снотворное.
                                    Самое лучшее, пока что есть, это статьи и официальная дока. Microsoft очень по этому поводу запарились и привели её в божеский вид.
                                    Стоит читать framework design guidelines.
                                      0
                                      Рихтер же пишет то, чего в доках нет — про хрупкую внутреннюю кухню.

                                      … реально снотворное.
                                      Самое лучшее, пока что есть, это статьи и официальная дока.

                                      Стоит читать framework design guidelines.

                                      Просытня советов — это разве не скучно?
                                        0
                                        Нет framework design guidelines это zero bullshit и очень близко к «земле».
                                        Людям скучно читать что-то не про них (Сильмариллион какой-нибудь), а framework design guidelines — это именно про то с чем мы сталкиваемся каждый день.
                                        Ну вот это например: docs.microsoft.com/en-US/dotnet/standard/design-guidelines/choosing-between-class-and-struct
                                        чудо же.
                                          0
                                          Близость к «земле» — это просто выжимка best practice, с минимумом ответов на вопрос: «Почему?», хотя темы поднимаются те же.
                                          Тоже неплохо (для того, кому нужно завтра проект поддерживать, а технологию он впервые видит и нет времени читать тысячестраничные талмуды), но я бы не сказал, что это даже сейчас может быть достойным заменителем более подробным книгам.

                                          Как мне кажется, не столько времени уходит на само чтение книг, сколько на усвоение информации. Много взаимосвязей, которые бывают неочевидны даже после первого прочтения. Шилдт же проще потому, что у него процент воды к информации выше (и это не плохо, ибо позволяет мозгу выиграть время на усваивание) и материал проще. А Рихтер рискует стать настольной книгой, хотя бы на какое-то время, где даже после первого прочтения придется возвращаться к отдельным главам за вновь появившимися вопросами, когда очередной пласт информации уляжется.
                                  0
                                  Финализатор. По своей сути является страховкой. Вызывается неявно, в неопределенное время, во время сборки мусора.
                                  Если точнее, то GC только помещает объект в очередь финализации. Из этой очереди объекты забирает отдельный поток финализации, который и вызывает финализатор.
                                    0
                                    Также пытаться работать с диском из многих потоков — самоубийство. Диск и без того является тормозящим фактором многих программ, которые с ним работают. Если пытаться работать с ним из многих потоков, то надо забыть о быстродействии.


                                    Для SSD проблема становится гораздо менее острой, в большинстве случаев можно не заморачиваться.
                                      +1
                                      А потом в инструкции к программе появится «работает только на ssd»? :)
                                      0
                                      Также пытаться работать с диском из многих потоков — самоубийство. Диск и без того является тормозящим фактором многих программ, которые с ним работают. Если пытаться работать с ним из многих потоков, то надо забыть о быстродействии.

                                      С одной стороны хорошо, а с другой — не всегда. Можно на выходе получить софт, который тормозит, потому что форматируется дискета.


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

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

                                        Можно работать с одни диском из одного не UI потока, например. Данная цитата вовсе не означает, что вся программа должна быть однопоточной
                                        +3
                                        Самое большое заблуждение начинающих C# разработчиков, это думать что знание всех нюансов работы GC, памяти, финализатора и прочих «низкоуровневых штук» рантайма автоматом сделает из них «не начинающих» C# разработчиков.
                                          0
                                          Однако такая имплементация пула объектов — однозначно плохая идея. Как и пытаться логировать, кидать исключения, обращаться к базе и тысячи подобных действий.

                                          Вообще, в финализаторе логировать часто хорошая идея. Потому что вызов финализатора свидетельствует о том, что разработчик забыл Dispose (Естественно, там не забыть GC.SuppressFinalize или вляпать флажок).
                                            +1
                                            Это черевато непредсказуемыми падениями: порядок вызова финализаторов недетерминирован, равно как и порядок убийства объектов. Закрываешь окно приложения, а оно тебе НА! CurrentDomain_UnhandledException.
                                            Чтобы гарантировать, что Dispose() таки вызвали, нужно тесты писать.
                                              +1
                                              Если вызвали Dispose, то финалайзер не должен вызываться. А если его забыли — то явный крэш приложения лучше, чем тихая утечкая ресурсов, которую потом отлавливать по обрывочным сведениям.
                                              Я как-то из-за забытого диспоза при установке приложения клиенту, поехал не на поезд, а в гостиницу. И да, конечно же приложение не говорило, что кто-то забыл диспоз на 78-ой строчке в конкретном файле, и то, что забыт именно диспоз выяснилось потом.
                                                0

                                                Тем не менее, гарантируется что у обычных объектов финализаторы вызываются раньше чем у наследников CriticalFinalizerObject.


                                                Что означает, в том числе, безопасность обращения к любым SafeHandle в "логирующем" финализаторе.


                                                Ну и между SafeHandle отношения владения можно выстраивать при помощи подсчёта ссылок (DangerousAddRef/DangerousRelease), но тут уже надо внимательно всё проверять.

                                                  0
                                                  Угу, только чаще всего для логирования используют что-нибудь вроде log4net, а для логирования пишут что-то типа Logger.Warn(«object not disposed»).
                                                  А логгер этот не простой, а «золотой» — использует
                                                  abstract class LoggerWrapperImpl {}
                                                  в качестве таргета для строчек лога. Это нифига не наследник SafeHandle — вполне может уже прибит в момент работы финализатора.
                                                    0

                                                    А зачем он вообще реализует финализатор?

                                                      0
                                                      Кто, абстрактный класс? Он его не реализует. А вот реализуют ли его наследники финализатор я знать не могу: это вопрос к тому, кто его будет имплементить, или уже заимплементил. Однако в моём случае он ссылается на PatternLayout, который тоже не SafeHandle.
                                              0
                                              Господи, вот хоть бы раз за мою долгую карьеру мне как-то пригодилось знание про поколения GC. Кроме как на собеседованиях — вообще ни разу не нужно было. И ещё туча таких же вопросов чисто собеседовательных.
                                              В реальной жизни самое важное это вообще умение декомпозировать задачи, про которое ни на одном собеседовании не спросили и не спросят, потому что оно «не про язык программирования».
                                                0
                                                Ну, мне поколения часто пригождались, правда больше встрявал в LOH, но и другие ситуации, связанные с «утечками» памяти тоже приходилось расследовать. Но большинству программистов нафиг не надо, да. Вопросы про это бестолковые.
                                                0
                                                Также пытаться работать с диском из многих потоков — самоубийство. Диск и без того является тормозящим фактором многих программ, которые с ним работают. Если пытаться работать с ним из многих потоков, то надо забыть о быстродействии.

                                                Это может быть два физически разных диска, тогда одним потоком читаем, другим пишем. Это быстрее чем одним потоком оба действия.
                                                Еще бывает и внутреннее распараллеливание потоков записи или чтения (обычно для SSD), тогда последовательная запись данных одним потоком может оказаться еще и медленнее 2-4 потоков.
                                                В третьих у дисков иногда бывает кэш и множество коротких операций чтения/записи работают сильно быстрее в многопоточном режиме, если укладываются в размер кэша.
                                                Если кто в курсе про файловые системы, то у каждого файла есть метаданные (имя, размер, атрибуты, время создания, изменения, доступа, и данные распределения на диске). ОС с целью обеспечения сохранности структур файловой системы все время фиксирует на диск их изменения для каждого файла. То есть один поток вынужден ждать когда ОС зафиксирует очередное изменение на диск, а много потоков могут разделять это время.
                                                Конечно если паттерн обращений на диск не может параллелиться, а зачастую это так, то лучше это делать одним потоком.

                                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                Самое читаемое