Pull to refresh

Comments 42

Скоро С++ и С# встретятся и влюблятся :) Очень жду дитя, которое они породят :)

C++/CLI? Оно настолько одиноко, что никому не нужно за очень редким исключением :)

Они близкие родственники. Повод задуматься.

Лучше явно очистить ресурсы как можно скорее (вызвав Close, Dispose или оператор using), вместо того чтобы ждать неявной очистки, которая произойдёт «когда-нибудь» (когда сама среда запустит финализаторы).

А точнее, которая может произойти никогда. MSDN намекает на то, что Dispose нужен мягко говоря для другого — не просто что-то закрыть, а освободить неуправляемые ресурсы, которые иначе освобождены никогда не будут.

У автора правильно написано. Освобождение ресурса может быть написано в финализаторе без всякого Dispose. Паттерн нужен в первую очередь для детерминированной очистки ресурсов.

Пардон, "детерминированного освобождения ресурсов"

А если
public struct something : IDisposable

то в каком именно финализаторе он освободится, в несуществующем?

Ещё раз про статью в msdn: там описано исходное назначение паттерна — детерминированное освобождение ресурса. О том что stack-only struct поддерживает только явную очистку захваченных ресурсов у автора также указано в статье, но в msdn не об этом. А для приведённого вами примера характерно использование Dispose для работы с управляемыми ресурсами через Dispose (например, локальный захват локера на запись и его высвобождение в Dispose). В том же случае, если вы ссылаетесь на неуправляемый ресурс в структуре (обычной как в примере, не ссылочной), то это как минимум может привести к нарушению семантики владения ресурсом при копировании структуры (неявном опять таки). Да, согласен, в данном случае финализатора нет и есть хорошая возможность выстрелить в ногу из-за отсутствия финализатора и Dispose тут как бы решает проблему, но, тем ни менее, это не отменяет исходное назначение паттерна.

Еще раз о том, что написано у автора:
вместо того чтобы ждать неявной очистки, которая произойдёт «когда-нибудь»

И о моей поправке:
А точнее, которая может произойти никогда

Если не вызывать Dispose явно (или через using), то очистка может не произойти никогда — если это struct (кстати, любая struct). И вы получите утечку памяти/хэндла/еще каких гадостей. Мне кажется, вы совершенно проигнорировали мою исходную позицию, и вместо этого теперь пытаетесь сказать мне то же самое.
И дело тут, кмк, не в
Лучше явно очистить ресурсы как можно скорее

А в том, что если Dispose не вызвать — вы потенциально простреливаете себе ногу.
Еще раз о том, что написано у автора

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

Я отвечал именно на эту вот фразу:
MSDN намекает на то, что Dispose нужен мягко говоря для другого — не просто что-то закрыть, а освободить неуправляемые ресурсы, которые иначе освобождены никогда не будут

C этим тезисом я не совсем согласен — в msdn о другом, т.к. если посмотреть на пример в статье, то по нему видно, что даже не вызвав Dispose мы всё равно с течением времени освободим ресурс, но недетерминировано (разумеется, при корректной реализации паттерна, приведенной в качестве примера). В msdn-статье нет ни слова о структурах, т.к. для обычных паттерн юзается не по назначению (и, вообще, зачем нужны явные umanaged в структуре(?), явный антипаттерн и источник проблем за редкими исключениями), а ref появились уже позже и тут вызов Dispose обязателен.
Вобщем, я думаю, вопрос исчерпан.

почему 'не совсем согласен'
Немного поясню почему «не совсем согласен»: всё-таки могут быть edge-кейсы, когда ресурс действительно может стать zombie. Если он уплыл в GC/2 и долго там не собирался мусор, а потом приложение завершается, но финализаторы не отрабатывают в установленный таймаут, а ресурсом был ком-объект через DCOM (вне процесса). Или сайт в пуле IIS хостится, а umnaged-ресурс не CFO. Но это скорее нештатные кейсы.
формулировка неверная, но лишь отчасти

С каких пор у boolean появилось, помимо истины/лжи, третье значение «может быть»?
в msdn о другом

Читаем по ссылке:
Provides a mechanism for releasing unmanaged resources.
Перевод: Предоставляет механизм для отчистки неуправляемых ресурсов. Нету ни слова о детерменированности, вы себе ее тут придумываете.
что даже не вызвав Dispose мы всё равно с течением времени освободим ресурс, но недетерминировано

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

Антипаттерн — это использовать публичный протокол, который требует явной очистки ресурсов, и потом эти ресурсы не очищать. Также антипаттерн — это надеяться на детали реализации, что кто-то за вами там что-то почистит когда-нибудь. А еще антипаттерн — это использовать классы и гонять по ссылке то, что может спокойно себе полежать на стэке:
public struct SomeCThing : IDisposable
{
    [DllImport("c.dll")]
    private static extern IntPtr create_c_thing(...);

    private IntPtr handle;
    // Дальше идут обвязочные методы
}
Читаем по ссылке:

Угу, читаем прям вот следующий абзац
Use the Dispose method of this interface to explicitly release unmanaged resources in conjunction with the garbage collector

Не освободим, если это struct. Я это уже говорил, но вы решили об этом забыть.

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

А я разве где-то писал о том, что явно диспозить объект не нужно? Я говорил об исходном назначении паттерна, описанного в статье msdn.
Касательно структур с unmanaged: да, иногда очень надо для совсем критичных мест и писаться этот участок должен очень аккуратно, т.к. поломать стейт unmanaged ресурса очень легко, если он не весь идемпотентный и требуется хранить даже минимально мутабельный стейт внутри объекта, владеющего unamanaged ресурсом. Для общего случая работы с Unmanaged это скорее антипаттерн.

Пример
Before — был какой-то метод с кучей вызовов в Unmanaged
After — кто-то его порефакторил. Визуально — вполне легальный рефакторинг без претензий компилятора.
Т.е. использование unmanaged в структуре накладывает еще и жесткие ограничения на работу с ней через ref (о которых компилятор не парится), а не только на необходимость вызвать Dispose.
ref struct эту проблему тоже не решает, но хотя бы не дает навесить интерфейс и вызывать методы через боксинг (что тоже кораптит стейт), что уже лучше.

    public unsafe struct CustomStream : IDisposable
    {
        private int _size;
        private int _currentPos;
        private IntPtr _ptr;
        private bool _isDisposed;

        public CustomStream(int initialSize)
        {
            _currentPos = 0;
            _size = initialSize;
            _ptr = Marshal.AllocHGlobal(initialSize);
            _isDisposed = false;
        }
        
        public void Append(byte data)
        {
            if (_currentPos < _size)
            {
                Marshal.WriteByte(_ptr, _currentPos, data);
                // тут вот мы изменяем наш стейт
                _currentPos++;
            }
        }

        public void Dispose()
        {
            if (!_isDisposed)
            {
                Marshal.FreeHGlobal(_ptr);
                _isDisposed = true;
            }
        }
    }

    public class Example
    {
        public void Before()
        {
            using (CustomStream stream = new CustomStream(1024))
            {
                stream.Append(0);
                stream.Append(1);
                // всё ОК
            }
        }

        public void After()
        {
            using (CustomStream stream = new CustomStream(1024))
            {
                WriteHeaderBlock(stream);
                WriteClosingBlock(stream); // вот тут уже кораптим стейт
            }
        }

        public void WriteHeaderBlock(CustomStream stream)
        {
            stream.Append(0);
        }

        public void WriteClosingBlock(CustomStream stream)
        {
            stream.Append(1);
        }
    }


Ну, по части освобождения ресурсов и всего что с ним связано в MSDN написано нечто очень странное, а местами — устаревшее.

В реальности метод Dispose может использоваться для чего угодно. При желании, им можно полностью заменить любые повторяющиеся блоки finally, что стало ещё актуальнее с появлением краткой формы оператора using.
Походу надежнее написать try...finally, чтобы избежать возможных сюрпризов от компилятора наподобие (IDisposable)book).Dispose();
ref-структура не может быть упакована, а при таком касте произойдет упаковка. Именно поэтому ref-структуры не поддерживают интерфейсы. В статье об этом написано.
Я привел пример работы using со структурой, во что компилятор решает развернуть код в блоке finally:
 book book = default(book);
        try
        {
        }
        finally
        {
            ((IDisposable)book).Dispose(); //тут сюрприз, если book обычная IDisposable структура
        }


Я же не говорю что он тоже самое делает с ref. Если компилятор работает лучше с ref структурой, то это только хорошо.
Лучше посмотрите IL-код, сгенереный для исходного варианта с using, там нет боксинга и для обычных структур.
Я ниже уже писал, что приведение сгенерится, а упаковки невидно. Причем если будет хоть какая логика, наподобие инициализации поля, то компилятор тупо скопирует значение структуры в новую переменную. И чем вам не понравился после этого вариант с ручным try...finally?
IL-код из блока finally для обоих случаев.
Вариант с юзингом:
IL_000d: ldloca.s b
IL_000f: constrained. C/book
IL_0015: callvirt instance void [System.Runtime]System.IDisposable::Dispose()

Вариант с ручным try-finally:
IL_000e: ldloc.0
IL_000f: box C/book
IL_0014: callvirt instance void [System.Runtime]System.IDisposable::Dispose()
Под ручным то не надо приводить, просто вызвать Dispose. К сожалению даже так используется еще одна переменная структура… Вывод: нельзя использовать этот механизм со структурами.
ref struct не может быть упакована только с точки зрения языка. MSIL ничего о ref struct не знает. Кстати упаковать ref struct не так уж сложно с помощью активатора, если очень хочется.
Я вас огорчу, тут упаковки не будет. Тут будет callvirt предваренный constrained опкодом. И далее зависит от JITа, если если метод определен в структуре, то упаковки не будет, будет прямой вызов метода, а если нет (для структуры это вызов методов ValueType не переопределенных в структуре) то упаковка и вызов виртуального метода.
все как Вы говорите, только вот выглядит так, будто JIT боксирует, судя по результатам
запускал и получал те же результаты на netcore2.2, netcore3.0
Не «боксирует», а копирует. И не JIT, а компилятор.
ох, да, вижу
тогда у меня следующие вопросы:
1) зачем компилятор это делает (копирует переменную)? Это не то, что я ожидаю
2) почему в этом случае Dispose(), который в finally, вызывается через constrained? Ведь просто вызов (не override) метода у структуры — это просто call. Если структура приводится к интерфейсу — тогда будет box, которого я не вижу в finally

В общем: какую магию применяет компилятор по отношению к структурам в using?
Смысл constrained call — в том, чтобы вызвать не просто Dispose, а IDisposable::Dispose, но без приведения типа.

Это важно при явной реализации метода, и не играет никакой роли при публичной реализации.
Это важно при явной реализации метода, и не играет никакой роли при публичной реализации.

вот этого не понял… Что за публичная реализация? Можно пример, в котором видна разница Dispose и IDisposable::Dispose вызовов?

Если метод реализован явно (explicit), т.е. не как public void Dispose(), а как void IDisposable.Dispose() — то вызвать его можно либо через каст к интерфейсу (что для структуры будет упаковкой), либо через constrained call.


А копия делается для того, чтобы не изменилось наблюдаемое поведение.

и все же: зачем компилятор делает копирование?
Компилятор делает защитную копию, потом вызывается callvirt предваренный constraited. Из-за защитной копии, все уверены, что будет упаковка, выберите на шарплабе компиляцию в C# и увидите. однако, у вас в примере в типе только булево поле оно копируется в защитную копию, однако если у вас там ссылка на неуправляемый ресурс, применение после using черевато.
А я и не говорю что в ref будет упаковка.

upd: вы кстати пишите что если структура(не ref), то упаковки не будет. Хочу уточнить, если я обычную структуру (:IDisposable) использую вместе с using, то упаковки не будет?
Никакого обхода нет. Фактически оптимизация заключается в удалении преобразования (о чем написал Эрик в блоге, ссылка есть в его ответе), ну и constrained вызовы специально сделали в cil, как раз для подобного. Естественно, когда вы структуру присваиваете переменной интересного типа и потом вызываете метод, то тут и будут все прелести боксинга.
Примечание. Не забывайте, что в случае ссылочных структур используется только явная очистка, поскольку определение финализаторов для них невозможно.

Сказано так, будто у обычных структур есть финализатор
Довольно не однозначное впечатление от этой фичи языка.
С одной стороны — это (ref структуры) требуется в том случае когда нужна небольшая обертка вокруг неуправляемых ресурсов и эта тонкая обертка позволяет детерминированно освобождать их НО с другой стороны очень нужно что бы компилятор как минимум предупреждал что для такой структуры не был вызван метод Dispose и/или эта структура требует использования using оператора или же студия каким либо иным способом (какой — либо анализатор кода ) давала возможность разрабочтику понять что у него потенциальная утечка ресурсов.
Я бы даже сказал, что у них возможен финализатор. Только вызываться он должен по очистке занятой зоны стека.
Sign up to leave a comment.