Рассмотрим одноразовое ключевое слово в C # - PullRequest
13 голосов
/ 28 мая 2009

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

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

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

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

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

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

Промежуточный код C # будет выглядеть так:

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);
}

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

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

Ответы [ 7 ]

24 голосов
/ 28 мая 2009

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

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

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

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

6 голосов
/ 03 июня 2009

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

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, который отменяет половину работы, которую компилятор должен сделать для вас.

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

5 голосов
/ 28 мая 2009

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

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

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

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

4 голосов
/ 27 июля 2009

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

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

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

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

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

2 голосов
/ 18 февраля 2011

Я понимаю, что это старая ветка, но кое-что упущено.

Шаблон размещения в C # и Java нарушает фундаментальную причину отсутствия детерминированного деструктора.

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

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

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

Для этого есть шаблоны проектирования, но это не IDisposable

@ Peter Да, я утверждаю, что шаблон Dipsosable имеет ошибку. При реализации шаблон Disposable обычно не подразумевает просто распоряжение ресурсами ОС с идеей возможности продолжать использовать объект, который был удален. Используя шаблон Disposable вне try {} finally {} в Java или используя () в .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 (), нормальным поведением является недействительность всего объекта, предотвращая его использование.

0 голосов
/ 01 октября 2010

ИМХО, у языков .net есть большой недостаток в обработке iDisposable, который заключается в том, что нет хорошего способа обработки инициализаторов, которые выдают исключения. До тех пор, пока кто-то не «утечет» копию строящегося объекта или одноразовых объектов в нем, невозможно очистить любые объекты iDisposable, которые были созданы (в инициализаторе или конструкторе базового уровня), до того, как инициализатор сгенерирует.

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

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

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

0 голосов
/ 28 мая 2009

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

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

По этой причине в c # не может быть деструкторов.

...