Рассмотрим ключевое слово "одноразовые" в С#

Каково ваше мнение о том, как одноразовые объекты внедряются в .Net? И как вы решаете повторяемость реализации IDisposable классов?

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

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

В С#, например, что, если мой класс, который должен реализовать одноразовую семантику, может быть объявлен следующим образом:

public class disposable MyDisposableThing
{
    ~MyDisposableThing()
    {
        // Dispose managed resources
    }
}

В этом случае компилятор может легко сгенерировать реализацию интерфейса IDisposable. Деструктор ~ MyDisposableThing может быть преобразован в фактический метод Dispose, который должен освобождать управляемые ресурсы.

Промежуточный код С# будет выглядеть следующим образом:

public class MyDisposableThing : IDisposable
{
    private void MyDisposableThingDestructor()
    {
        // Dispose my managed resources
    }

    ~MyDisposableThing()
    {
        DisposeMe(false);
    }

    public void Dispose()
    {
        DisposeMe(true);
        GC.SuppressFinalize(this);
    }

    private bool _disposed;
    private void DisposeMe(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // Call the userdefined "destructor" 
                MyDisposableThingDestructor();
            }
        }
        _disposed = true;
    }
}

Это приведет к значительно более чистым кодам, меньшему тиражированию кода и постоянному способу утилизации управляемых ресурсов. Реализация IDisposable вручную будет по-прежнему поддерживаться для краевых случаев и неуправляемых ресурсов.

Обеспечение правильного расположения экземпляров - еще одна проблема. Рассмотрим следующий код:

private string ReadFile(string filename)
{
    var reader = new StreamReader();
    return reader.ReadToEnd(filename);
}

Переменная считывателя никогда не выделяет область действия метода, но придется ждать, пока GC ее утилизирует. В этом случае компилятор может вызвать ошибку, чтобы объект StreamReader не был явно удален. Эта ошибка заставит разработчика обернуть его в оператор using:

private string ReadFile(string filename)
{
    using (var reader = new StreamReader())
    {
        return reader.ReadToEnd(filename);
    }
}

Ответы

Ответ 1

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

Я согласен с тем, что одноразовость могла быть выведена из "шаблонного" мира и на язык С#, как мы это делали, например, с помощью свойств getters и seters (которые представляют собой стандартизацию шаблона с использованием методов getter и setter), или события (которые стандартизируют идею хранения делегата и называют его, когда что-то интересное происходит.)

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

Хотя это, безусловно, хорошая идея, я не думаю, что она встречает бар. Было бы неплохо, я согласен, но это не позволяет создавать действительно богатые новые сценарии.

Ответ 2

В дополнение к другим ответам существует проблема того, насколько это должно реализоваться и что люди должны ожидать от него? Скажем, я объявил свой класс следующим образом:

public disposable class MyClass
{
    readonly AnotherDisposableObject resource = new AnotherDisposableObject();

    ~MyClass()
    {
        this.resource.Dispose();
    }

    public void DoStuff()
    {
        this.resource.SomeMethod();
    }
}

Тогда что бы вы ожидали, если вызывающий вызывал DoStuff после того, как экземпляр был удален? Если компилятор автоматически вставляет что-то вроде

if (this.disposed) throw new ObjectDisposedException();

в начале каждого метода, потому что вы объявили класс как одноразовый?

Если да, то как насчет случаев, когда методы явно разрешены для вызова после того, как объект расположен (например, MemoryStream.GetBuffer)? Вам нужно было бы ввести новое ключевое слово, которое указывало бы это на компилятор, например. public useafterdispose void ...?

Если нет, то как вы объясняете людям, что новое ключевое слово реализует для вас часть кода котельной плиты, но что им все еще нужно написать код, чтобы проверить, находится ли объект в каждом методе? Более того, как они могут это проверить, потому что вся информация состояния о том, был ли объект размещен, автоматически генерируется! Теперь им нужно отслеживать собственный флаг в методе ~MyClass, который отменяет половину работы, которую должен выполнить компилятор для вас.

Я думаю, что в качестве специфического языкового шаблона в концепции слишком много дыр, и он только пытается решить одну конкретную проблему. Теперь то, что может решить весь этот класс задач в универсальном режиме, это mixins (т.е. Одноразовый mixin), и эта языковая функция обычно может использоваться повторно для разных понятий (например, Equatable mixin, Comparable mixin и т.д.). То, куда мои деньги поедут.

Ответ 3

Лично я считаю, что поддержка IDisposable вполне приличная в текущей версии .NET. Наличие ключевого слова using в значительной степени превращает его в конструкцию первого класса для меня.

Я признаю, что существует определенный код шаблона, но этого недостаточно, чтобы гарантировать новые возможности языка. (Автообновленные свойства были хорошим примером функции, которую попросили представить.) Вы пропустили важный момент в своем сообщении, что этот "шаблонный" код не всегда то, что вам нужно. В основном вам нужно распоряжаться неуправляемыми ресурсами вне блока if (disposing).

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

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

Ответ 4

Я полностью согласен с тем, что IDisposable нужен лучший язык suppprt. Здесь мой вариант из него с того момента назад. Детали, вероятно, ошибочны, но С++/CLI служит для этого довольно хорошей моделью. К сожалению, это смущает ад из программистов на С#, когда я показываю им примеры в С++/CLI. Но это уже "правильно" с точки зрения реализации; нам просто нужен новый синтаксис в С#.

Даже самое простейшее приложение Windows Forms имеет в нем метод Dispose, который генерируется мастером и является хрупким перед лицом изменений с помощью неэксперта. Идея составлять компоненты вместе, так что один компонент может "владеть" несколькими другими, настолько фундаментален, что IDisposable практически неизбежен, и, к сожалению, кажется, что несколько страниц большинства книг объясняют, как правильно его реализовать.

Существующий оператор using заботится о стороне клиента. Где нам нужно больше поддержки языка на стороне реализации.

Некоторые из полей класса являются ссылками на вещи, которые класс "владеет", а некоторые не принадлежат. Поэтому мы должны иметь возможность отмечать поле как принадлежащее.

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

Ответ 5

Я понимаю, что это старый поток, но есть что-то, что было упущено.

Dispose pattern как в С#, так и в Java нарушает фундаментальную причину отсутствия детерминированного деструктора.

MyObject A = new MyObject()
MyObject B = A;
A.Dispose();

Каково сейчас состояние B? Что делать, если владелец B действительно не хотел, чтобы он был убран. Теперь у вас такая же проблема на С++, где вам нужно отслеживать все ссылки на объекты, на которые вы держите.

IDisposable действительно действителен только в контексте using() и обеспечивает очистку ресурсов, если возникает исключение.

Существуют шаблоны проектирования, но это не IDisposable

@Peter Да, я утверждаю, что шаблон Dipsosable имеет ошибку. При реализации одноразового шаблона обычно не предназначено просто распоряжаться ресурсами ОС с идеей возможности продолжить использование объекта, который был удален. Используя шаблон Disposable вне try {} finally {} в Java или using() в .NET, вы нарушаете одну из причин наличия GC. Я не говорю, что память течет. Я говорю, что теперь у вас есть другие части кода, которые имеют ссылку на объект, который был удален. Теперь бремя ответственности за разработчика, чтобы проверить, был ли объект удален до каждого вызова, или, по крайней мере, поймать ObjectDisposedException.

Давайте посмотрим на глупый пример:

FileStream stream = new FileStream (@"c:\mylog.txt");
logger.setStream (stream);

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

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

using (FileStream stream = new FileStream (@"c:\mylog.txt"))
    { logger.setStream (stream); }

или

FileStream stream = new FileStream (@"c:\mylog.txt");
logger.setStream (stream);
stream.Dispose();

Одноразовый шаблон не ссылается на количество ресурсов. Разработчик теперь должен осознавать, кто владеет объектом и кто несет ответственность за его очистку. Реальная проблема заключается в том, что при вызове Dispose() нормальное поведение является недействительным для всего объекта, не позволяя ему использоваться.

Ответ 6

IMHO, языки .net имеют серьезный недостаток в обработке iDisposable, а это то, что нет хорошего способа обработки инициализаторов, которые генерируют исключения. Если только "утечка" копии объекта, находящегося в процессе строительства, или одноразовых объектов в нем, нет способа очистить любые объекты iDisposable, которые были созданы (либо в инициализаторе, либо в конструкторе базового уровня) до того, как выдается инициализатор.

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

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

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

Ответ 7

Хорошо, вам нужно понять разницу между управляемой и неуправляемой памятью. Проще говоря, деструкторы стиля С++ не будут работать в управляемом мире С#, потому что нет никакой гарантии, когда объект получает сбор мусора, поэтому вы никогда не узнаете, когда вызовет деструктор, что сделает вещи очень непредсказуемыми.

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

Вот почему С# не может иметь деструкторы.