Блоки итераторов, Часть шестая: почему запрещён небезопасный код?
Есть три хороших причины не разрешать блоки unsafe в блоках итераторов.
Во-первых, это весьма маловероятный сценарий. Цель блоков итераторов – в облегчении написания итератора, который обходит некоторый абстрактный тип данных. Это, скорее всего, будет полностью управляемый код; так что сценарий не входит в проект.
Во-вторых, этот сценарий представляет дикое смешение «уровней». Вы трактуете уровень абстракции языка программирования как «расстояние от машины», на котором находятся его возможности. Небезопасные манипуляции с указателями – это настолько близко к машине, насколько это возможно в C#. Вы работаете без страховки, читая и записывая сырые байты в виртуальном адресном пространстве процесса. Вся защита верифицируемой типобезопасности полностью отключена, и полная ответстственность за каждый бит адресного пространства пользовательского режима лежит на вас.
В противоположность этому, блоки итераторов находятся на самом высоком уровне абстракций в C# 2.0. Нет прямо очевидного отображения исходного кода в сгенерированный; это волшебная шляпа, которая делает то, что нужно, потому что компилятор выполняет некоторые весьма значительные трансформации с кодом, порождая классы и реализуя интерфейсы за вас.
Смешение этих двух уровней абстракции в одном методе выглядит действительно плохой идеей.
В-третьих, нам бы пришлось провести некоторую дополнительную работу, чтобы заставить блоки «fixed» работать корректно, работу, которая бы полностью противоречила нашим рекомендациям по корректному и эффективному применению этих блоков.
Я полагаю, что должен кратко пояснить, что делает блок «fixed». Когда вы вызываете неуправляемый код, которому нужно, скажем, записать по неуправляемому адресу начала массива 16-битных символов, вам надо каким-то образом сказать сборщику мусора «эй, ты, GC, да-да, ты, который там вон в том другом потоке – если тебя посетит вдруг желание реорганизовать памать для улучшения эффективности, то мне нужно, чтобы ты, пожалуйста, не двигал пока этот массив, а то неуправляемый код в этом потоке собирается писать по его адресу». Эта операция называется «зафиксировать» или «пришпилить» объект.
Зафиксировать и оставить много всего на долгое время – явно плохая идея. Сборщик мусора работает так хорошо потому, что он может реорганизовывать память для повышения эффективности; фиксация блоков памяти между сборками мусора снижает способность сборщика делать свою работу. Поэтому мы рекомендуем вам фиксировать как можно меньше данных и на как можно более короткое время.
CLR предоставляет нам два способа что-то зафиксировать. Трудный, гибкий, дорогой способ – явно получить GCHandle для объекта, сказать сборщику мусора «этот объект зафиксирован», взять адрес объекта, сделать то, что нужно, и «отпустить» его.
Лёгкий способ – пометить локальную переменную как «зафиксированную переменную» при помощи инструкции «fixed». Компилятор C# сгенерирует специальный код, который помечает переменную как «зафиксированную». Когда сборщик мусора выполняет сборку, ему, очевидно, нужно посмотреть на все переменные в текущем фрейме стека, потому что все эти переменные сейчас «живы». Когда сборщик видит, что переменная зафиксирована, то он не только замечает, что она жива, он также делает себе пометку не двигать во время сборки содержимое того, на что эта переменная указывает. Это дешёвая и лёгкая альтернатива явному получению GCHandle, но она требует, чтобы предмет вопроса был локальной переменной.
В блоке итератора, локальные переменые реализованы путём вытягивания их в поля. Поскольку мы не можем фиксировать поля, нам бы пришлось изменить генерацию кода для блоков fixed так, чтобы она получала GCHandle, фиксировала его, и гарантировала, что рано или поздно он будет отпущен.
Это уже достаточно плохо; теперь задумайтесь, что происходит, когда вы делаете yield, пока объект зафиксирован. Между yield и отпусканием объекта может пройти произвольное время! Это прямо нарушает нашу рекомендацию фиксировать всё на как можно более короткое время.
По всем перечисленным причинам, в блоках итераторов не разрешается небезопасный код. В том маловероятном случае, когда вам нужно обращаться по указателям в блоке итератора, вы всегда можете переместить небезопасный код в отдельный вспомогательный метод и вызывать его из блока итератора.
***********
Я надеюсь, вам понравилось это маленькое путешествие в странные уголки блоков итераторов. Это – сложная возможность, с большим количеством интересных точек принятия проектных решений.
Я лечу на юг Канады, чтобы провести время с моей семьёй на населенных бобровыми акулами берегах огромного внутреннего моря, известного как озеро Гурон, так что несколько следующих невероятных приключений будут записаны заранее. До встречи после моего возвращения.