Помощник по переплетению кода для стандартного шаблона Dispose? - PullRequest
7 голосов
/ 21 января 2012

Я недавно читал Effective C # и несколько других подобных книг / блогов, и когда я говорю о стандартном шаблоне Dispose 1002 * (который я уже использую), все они рекомендуют использовать класс . располагайте переменную (как определено в этом примере кода MSDN) в начале каждого метода. По сути, чтобы гарантировать, что после вызова Dispose любая попытка использовать объект приведет к исключению ObjectDisposedException. Это имеет смысл, но это огромный объем ручного труда в достаточно большой базе кода, и он полагается на то, что люди не забывают это делать. Поэтому я ищу лучший путь.

Недавно я столкнулся и начал использовать notifypropertyweaver , который автоматически заполняет весь шаблонный код вызова обработчиков PropertyChanged (работает как задача msbuild, поэтому не требует дополнительной зависимости доставки). Интересно, кто-нибудь знает подобное решение для стандартного шаблона утилизации. По сути, он принимает имя переменной как config (bool disposed в примере MSDN) и добавляет следующий код к каждому методу, который не является финализатором или именем Dispose в каждом классе, реализующем IDisposable:

if(disposed)
  throw new ObjectDisposedException();

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

Разъяснение цели
Большая потребность в этом заключается не в том, чтобы использовать «лучшие практики», а в том, что у нас есть пользователи, неправильно управляющие жизненными циклами наших объектов. Прямо сейчас они просто получают NullReference или что-то подобное из базового ресурса, что может означать, что у нас есть ошибка в нашей библиотеке, я хочу сообщить им, что именно они создают проблему и как они ее решают (учитывая, что я нахожусь в положении, чтобы знать). Таким образом, предположения, что пользователи наших Типов - это те, кто должен позаботиться об этом, не очень продуктивны здесь.

Ответы [ 2 ]

2 голосов
/ 26 января 2012

Обновление: Мой первоначальный ответ на самом деле не ответил на вопрос, поэтому вот еще одна попытка ...

Чтобы помочь решить корневую проблему, разработчики забыли бросить ObjectDisposedExceptions,возможно, автоматическое модульное тестирование поможет.Если вы хотите строго соблюдать требование, чтобы все методы / свойства немедленно генерировали ObjectDisposedException, если Dispose уже был вызван, то вы можете использовать следующий модульный тест.Просто укажите сборки, которые вы хотите проверить.Вероятно, вам потребуется изменить метод IsExcluded, если необходимо, и имитация объекта может работать не во всех случаях.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using MbUnit.Framework;
using Moq;

[TestFixture]
public class IDisposableTests
{
    [Test]
    public void ThrowsObjectDisposedExceptions()
    {
        var assemblyToTest = Assembly.LoadWithPartialName("MyAssembly");

        // Get all types that implement IDisposable
        var disposableTypes = 
            from type in assemblyToTest.GetTypes()
            where type.GetInterface(typeof(IDisposable).FullName) != null
            select type;

        foreach (var type in disposableTypes)
        {
            // Try to get default constructor first...
            var constructor = type.GetConstructor(Type.EmptyTypes);

            if (constructor == null)
            {
                // Otherwise get first parameter based constructor...
                var constructors = type.GetConstructors();

                if (constructors != null &&
                    constructors.Length > 0)
                {
                    constructor = constructors[0];
                }
            }

            // If there is a public constructor...
            if (constructor != null)
            {
                object instance = Activator.CreateInstance(type, GetDefaultArguments(constructor));

                (instance as IDisposable).Dispose();

                foreach (var method in type.GetMethods())
                {
                    if (!this.IsExcluded(method))
                    {
                        bool threwObjectDisposedException = false;

                        try
                        {
                            method.Invoke(instance, GetDefaultArguments(method));
                        }
                        catch (TargetInvocationException ex)
                        {
                            if (ex.InnerException.GetType() == typeof(ObjectDisposedException))
                            {
                                threwObjectDisposedException = true;
                            }
                        }

                        Assert.IsTrue(threwObjectDisposedException);
                    }
                }
            }
        }
    }

    private bool IsExcluded(MethodInfo method)
    {
        // May want to include ToString, GetHashCode.
        // Doesn't handle checking overloads which would take more
        // logic to compare parameters etc.
        if (method.Name == "Dispose" ||
            method.Name == "GetType")
        {
            return true;
        }

        return false;
    }

    private object[] GetDefaultArguments(MethodBase method)
    {
        var arguments = new List<object>();

        foreach (var parameter in method.GetParameters())
        {
            var type = parameter.ParameterType;

            if (type.IsValueType)
            {
                arguments.Add(Activator.CreateInstance(type));
            }
            else if (!type.IsSealed)
            {
                dynamic mock = Activator.CreateInstance(typeof(Mock<>).MakeGenericType(type));
                arguments.Add(mock.Object);
            }
            else
            {
                arguments.Add(null);
            }
        }

        return arguments.ToArray();
    }
}

Исходный ответ : не выглядиткак будто есть что-то вроде NotifyPropertyWeaver для IDisposable, поэтому, если вы хотите, вам нужно создать подобный проект самостоятельно.Потенциально вы можете сэкономить небольшую работу, имея базовый класс Disposable, как в этой записи блога .Тогда у вас есть только одна строка в верхней части каждого метода: ThrowExceptionIfDisposed().

Однако ни одно из возможных решений не кажется правильным и не представляется необходимым.В общем, бросать ObjectDisposedException не нужно так часто.Я выполнил быстрый поиск в Reflector, и ObjectDisposedException генерируется непосредственно только 6 типами в BCL, а для образца вне BCL System.Windows.Forms имеет только один тип, который выбрасывает: Cursor при получении дескриптора.

По сути, вам нужно только выбросить ObjectDisposedException, если вызов вашего объекта завершится неудачей, в частности, потому что Dispose уже был вызван, например, если вы установите какое-то поле в null, которое требуется для метода или свойства,ObjectDisposedException будет более информативным, чем случайное NullReferenceException, но, если вы не очищаете неуправляемые ресурсы, обычно нет необходимости устанавливать для набора полей значение null.В большинстве случаев, если вы просто вызываете Dispose для других объектов, они могут бросить ObjectDisposedException.

Вот простой пример, который вы можете явно бросить ObjectDisposedException:

public class ThrowObjectDisposedExplicity : IDisposable
{
    private MemoryStream stream;

    public ThrowObjectDisposedExplicity()
    {
        this.stream = new MemoryStream();
    }

    public void DoSomething()
    {
        if (this.stream == null)
        {
            throw new ObjectDisposedException(null);
        }

        this.stream.ReadByte();
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (this.stream != null)
            {
                this.stream.Dispose();
                this.stream = null;
            }
        }
    }

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

С кодом выше, хотя на самом деле нет необходимости устанавливать поток в нуль.Вы можете просто положиться на тот факт, что MemoryStream.ReadByte() сгенерирует ObjectDisposedException самостоятельно, как показано в коде ниже:

public class ThrowObjectDisposedImplicitly : IDisposable
{
    private MemoryStream stream;

    public ThrowObjectDisposedImplicitly()
    {
        this.stream = new MemoryStream();
    }

    public void DoSomething()
    {
        // This will throw ObjectDisposedException as necessary
        this.stream.ReadByte();
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            this.stream.Dispose();
        }
    }

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

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

Бросок ObjectDisposedException не требуется часто и долженТщательно продуманный, поэтому инструмент для создания кода, который вам нужен, вероятно, не нужен.Используйте библиотеки Microsoft в качестве примера, посмотрите, какие методы, которые реализуют, на самом деле выдают ObjectDisposedException, когда тип реализует IDisposable.

Примечание: GC.SuppressFinalize(this); строго не требуется, так как естьнет финализатора, но он остался там, так как подкласс может реализовывать финализатор.Если класс был помечен как sealed, тогда GC.SupressFinalize можно было бы безопасно удалить.

2 голосов
/ 25 января 2012

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

  1. Люди забудут реализовать шаблон
  2. Вы стремитесь избежать затрат на его многократную реализацию в большой базе кода

Чтобы покрыть затраты на переписывание очень похожего кода, я предлагаю вам создать и использовать фрагменты кода в Visual Studio.Проблема, которую пытаются решить фрагменты кода, находится на втором месте в списке выше.Инструкции см. В этой статье MSDN .

Проблема с № 1 в приведенном выше списке заключается в том, что шаблон IDisposable не всегда необходим в 100% ваших классов.Это мешает статическому анализу «поймать», должен ли данный класс быть одноразовым.(Отредактируйте: подумав об этом в течение минуты, на самом деле это будет не так уж сложно проверить. Если ваш текущий класс содержит экземпляры, которые являются IDisposable, ваш класс должен быть IDisposable)

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

...