Почему "использование" улучшает производительность С#

Похоже, что в большинстве случаев компилятор С# мог вызвать Dispose() автоматически. Как и в большинстве случаев используемого шаблона, вы можете:

public void SomeMethod()
{
    ...

    using (var foo = new Foo())
    {
        ...
    }

    // Foo isn't use after here (obviously).
    ...
}

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

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

Теперь почему это не делается? Разве это не улучшит характеристики (если разработчики... грязные).

Ответы

Ответ 1

Несколько пунктов:

Вызов Dispose не повышает производительность. IDisposable предназначен для сценариев, в которых вы используете ограниченные и/или неуправляемые ресурсы, которые не могут быть учтены во время выполнения.

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

Рассмотрим, например, тестер factory, который принимает Stream и десериализует экземпляр класса.

public class Foo
{
    public static Foo FromStream(System.IO.Stream stream) { ... }
}

И я называю это:

Stream stream = new FileStream(path);

Foo foo = Foo.FromStream(stream);

Теперь я могу или не хочу, чтобы Stream удалялся, когда метод завершается. Если Foo factory считывает все необходимые данные из Stream и больше не нуждается в этом, тогда я хочу, чтобы он был удален. Если объект Foo должен удерживаться в потоке и использовать его за время его существования, я бы не захотел его удалить.

Аналогично, что касается экземпляров, которые извлекаются из чего-то другого, кроме конструктора, например Control.CreateGraphics(). Эти экземпляры могут существовать вне кода, поэтому компилятор не будет удалять их автоматически.

Предоставление пользовательского контроля (и предоставление идиомы, такой как блок using) делает пользователя понятным и облегчает обнаружение мест, где экземпляры IDisposable не удаляются должным образом. Если компилятор автоматически удалял некоторые экземпляры, то отладка была бы намного сложнее, поскольку разработчик должен был расшифровать, как правила автоматического удаления применяются к каждому блоку кода, который использовал объект IDisposable.

В конце концов, существует две причины (по соглашению) для реализации IDisposable для типа.

  • Вы используете неуправляемый ресурс (это означает, что вы вызываете вызов P/Invoke, который возвращает что-то вроде дескриптора, который должен быть выпущен другим вызовом P/Invoke)
  • У вашего типа есть экземпляры IDisposable, которые должны быть удалены, когда это время жизни объекта завершено.

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

Ответ 2

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

Позвольте мне немного повторить это. Автоматически вызывать Dispose() не так просто. Это также не будет напрямую увеличивать производительность. Подробнее об этом чуть позже.

Если у вас есть следующий код:

public void DoSomeWork(SqlCommand command)
{
    SqlConnection conn = new SqlConnection(connString);

    conn.Open();

    command.Connection = conn;

    // Rest of the work here
 }

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

Явный вызов Dispose() или использование блока using ясно указывает ваше намерение и заставляет вещи правильно очищаться.

Теперь вернемся к производительности. Просто вызов Dispose() объекта не гарантирует увеличения производительности. Метод Dispose() используется для "очистки" ресурсов, когда вы закончите с Object.

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

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

Ответ 3

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

Неустойчивые объекты также могут вызывать беспокойство.

Кроме того, using() {....} более читабельна и интуитивно понятна, что немаловажно с точки зрения ремонтопригодности.

Как инженеры или программисты, мы стремимся быть эффективными, но это редко бывает ленивым.

Ответ 4

Посмотрите MSDN Artilce для С#, использующего выражение Оператор using - это всего лишь короткое сокращение, чтобы избежать попыток и наконец, выделить места. Вызов утилиты не является низкоуровневой функциональностью, такой как Garbage Collection.

Как вы можете видеть, использование переведено в.

{
  Font font1 = new Font("Arial", 10.0f);
  try
  {
    byte charset = font1.GdiCharSet;
  }
  finally
  {
    if (font1 != null)
      ((IDisposable)font1).Dispose();
  }
}

Как компилятор узнает, куда положить блок finally? Вызывает ли это это на сборке мусора?

Коллекция Garabage не происходит, как только вы оставляете метод. Прочтите эту статью статью о сборке мусора, чтобы лучше ее понять. Только после того, как нет ссылок на объект. Ресурс может быть привязан намного дольше, чем нужно.

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

Ответ 5

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

Ответ 6

Я думаю, что вы думаете о финализаторах. Финализаторы используют синтаксис деструктора в С#, и они вызываются автоматически сборщиком мусора. Финализаторы подходят только для использования при очистке неуправляемых ресурсов.

Dispose предназначен для ранней очистки неуправляемых ресурсов (а также может использоваться для очистки управляемых ресурсов).

Обнаружение на самом деле сложнее, чем кажется. Что делать, если у вас есть такой код:


  var mydisposable = new...
  AMethod(mydisposable);
  // (not used again)

Возможно, что некоторый код в AMethod привязан к ссылке на myDisposable.

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

  • Возможно, myDisposable подписывается на событие внутри AMethod (тогда у издателя событий есть ссылка на myDisposable)

  • Возможно, еще один поток порожден AMethod

  • Возможно, mydisposable становится "закрытым" анонимным методом или выражением lamba внутри AMethod.

Все эти вещи затрудняют понимание того, что ваш объект больше не используется, поэтому Dispose позволяет разработчику сказать "хорошо, я знаю, что теперь безопасно запускать мой код очистки";

Помните также, что dispose не освобождает ваш объект - только GC может это сделать. (У GC есть волшебство, чтобы понять все описанные мной сценарии, и он знает, когда нужно очистить свой объект, и если вам действительно нужен код для запуска, когда GC не обнаружит ссылок, вы можете использовать финализатор). Однако будьте осторожны с финализаторами - они предназначены только для неуправляемых распределений, которыми принадлежит ваш класс.

Подробнее об этом можно прочитать здесь: http://msdn.microsoft.com/en-us/magazine/bb985010.aspx и здесь: http://www.bluebytesoftware.com/blog/2005/04/08/DGUpdateDisposeFinalizationAndResourceManagement.aspx

Если вам нужна неуправляемая очистка ручек, прочитайте также о SafeHandles.

Ответ 7

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

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

Ответ 8

Мне кажется сложным для G.C. знать, что вы больше не будете использовать эту переменную в том же методе. Очевидно, что если вы оставите метод и не будете больше ссылаться на переменную, то G.C. будет распоряжаться им. Но используя using в вашем примере, сообщает G.C. что вы уверены, что больше не будете использовать эту переменную после.

Ответ 9

using statement не имеет ничего общего с производительностью (если вы не избегаете утечки ресурсов/памяти как производительности).

Все, что он делает для вас, - это гарантия того, что метод IDisposable.Dispose вызывается на объект, о котором идет речь, когда он выходит за пределы области видимости, даже если в блоке использования произошло исключение.

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

Конечно, если метод Dispose() реализован плохо, оператор using обеспечивает нулевое преимущество.

Ответ 10

Я думаю, что OP говорит: "Зачем беспокоиться" используя ", когда компилятор должен иметь возможность легко справиться с этим".

Я думаю, что OP говорит, что

public void SomeMethod() 
{ 
    ... 

    var foo = new Foo();

    ... do stuff with Foo ... 


    // Foo isn't use after here (obviously). 
    ... 
} 

должен быть эквивалентен

public void SomeMethod() 
{ 
    ... 

    using (var foo = new Foo())
    {
    ... do stuff with Foo ... 
    }

    // Foo isn't use after here (obviously). 
    ... 
} 

потому что Foo не используется снова.

Ответ, конечно, заключается в том, что компилятор не может легко его обработать. Сбор мусора (что магически называет "Dispose()" в .NET) - очень сложное поле. Просто потому, что символ не используется ниже, это не означает, что переменная не используется.

Возьмем этот пример:

public void SomeMethod() 
{ 
    ... 

    var foo = new Foo();
    foo.DoStuffWith(someRandomObject);
    someOtherClass.Method(foo);

    // Foo isn't use after here (obviously).
    // Or is it?? 
    ... 
} 

В этом примере someRandomObject и someOtherClass могут ссылаться на то, что указывает Foo, поэтому, если мы позвоним Foo.Dispose(), он сломает их. Вы говорите, что просто представляете себе простой случай, но единственный "простой случай", в котором вы предлагаете работать, - это случай, когда вы не вызываете никаких вызовов из Foo и не пропускаете Foo или любого из его членов ни к чему другому - эффективно, когда вы даже не используете Foo вообще, и в этом случае вам, вероятно, не нужно его объявлять. Даже тогда вы никогда не сможете быть уверены, что какое-то отражение или хакерство событий не получили ссылки на Foo только по самому его созданию, или что Foo не зацепил себя чем-то еще во время своего конструктора.

Ответ 11

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