Как обеспечить использование возвращаемого значения метода в C #? - PullRequest
10 голосов
/ 23 марта 2011

У меня есть часть программного обеспечения, написанная с использованием свободного синтаксиса. Цепочка методов имеет определенное «окончание», перед которым на самом деле ничего полезного в коде не делается (например, NBuilder или генерация запросов Linq-to-SQL, фактически не затрагивающая базу данных, пока мы не перебираем наши объекты, скажем, с помощью ToList () ).

Проблема, с которой я столкнулся, заключается в том, что среди других разработчиков возникает путаница по поводу правильного использования кода. Они пренебрегают вызовом метода "окончания" (таким образом, никогда "ничего не делают")!

Меня интересует принудительное использование возвращаемого значения некоторых моих методов, чтобы мы никогда не могли "завершить цепочку", не вызвав этот метод "Finalize ()" или "Save ()" это на самом деле делает работу.

Рассмотрим следующий код:

//The "factory" class the user will be dealing with
public class FluentClass
{
    //The entry point for this software
    public IntermediateClass<T> Init<T>()
    {
        return new IntermediateClass<T>();
    }
}

//The class that actually does the work
public class IntermediateClass<T>
{
    private List<T> _values;

    //The user cannot call this constructor
    internal IntermediateClass<T>()
    {
        _values = new List<T>();
    }

    //Once generated, they can call "setup" methods such as this
    public IntermediateClass<T> With(T value)
    {
        var instance = new IntermediateClass<T>() { _values = _values };
        instance._values.Add(value);
        return instance;
    }

    //Picture "lazy loading" - you have to call this method to
    //actually do anything worthwhile
    public void Save()
    {
        var itemCount = _values.Count();
        . . . //save to database, write a log, do some real work
    }
}

Как видите, правильное использование этого кода будет выглядеть примерно так:

new FluentClass().Init<int>().With(-1).With(300).With(42).Save();

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

new FluentClass().Init<int>().With(-1).With(300).With(42);

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

Логические ошибки, подобные этим, очень трудно обнаружить, и, конечно, они компилируются, поскольку вполне допустимо вызывать метод с возвращаемым значением и просто «притворяться», что он возвращает void. Visual Studio не волнует, если вы это сделаете; Ваше программное обеспечение все еще будет компилироваться и запускаться (хотя в некоторых случаях я считаю, что оно выдает предупреждение). Это отличная возможность, конечно. Представьте себе простой метод «InsertToDatabase», который возвращает идентификатор новой строки в виде целого числа - легко видеть, что в некоторых случаях нам нужен этот идентификатор, а в некоторых случаях мы можем обойтись без него.

В случае этого программного обеспечения, безусловно, нет никаких причин отказываться от этой функции «Сохранить» в конце цепочки методов. Это очень специализированная утилита, и единственное преимущество - последний шаг.

Я хочу, чтобы чье-то программное обеспечение не работало на уровне компилятора , если они вызывают «With ()», а не «Save ()».

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

Примечание: Альтернативный способ достижения этой цели, который мне уже предлагался, - это написать набор юнит-тестов для обеспечения соблюдения этого правила и использовать что-то вроде http://www.testdriven.net для их привязки. компилятору. Это приемлемое решение, но я надеюсь на что-то более элегантное.

Ответы [ 7 ]

9 голосов
/ 23 марта 2011

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

Одна потенциальная опция, которая может помочь, однако, состоит в том, чтобы настроить ваш класс, только в DEBUG , чтобы иметь финализатор, который регистрирует / выбрасывает / и т.д. если бы Save() никогда не вызывали. Это может помочь вам обнаружить эти проблемы во время выполнения во время отладки, а не полагаться на поиск кода и т. Д.

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

4 голосов
/ 23 марта 2011

Для использования обратного вызова вам могут потребоваться определенные методы, например:

new FluentClass().Init<int>(x =>
{
    x.Save(y =>
    {
         y.With(-1),
         y.With(300)
    });
});

Метод with возвращает некоторый конкретный объект, и единственный способ получить этот объект - вызвать x.Save (), которыйсам по себе имеет функцию обратного вызова, которая позволяет вам устанавливать неопределенное количество операторов.Итак, init принимает что-то вроде этого:

public T Init<T>(Func<MyInitInputType, MySaveResultType> initSetup) 
1 голос
/ 04 мая 2011

После долгих размышлений, проб и ошибок выясняется, что создание исключения из метода Finalize () для меня не сработало. По-видимому, вы просто не можете этого сделать; исключение уничтожается, потому что сборка мусора работает недетерминированно. Мне также не удалось заставить программу автоматически вызывать Dispose () из деструктора. Комментарий Джека В. объясняет это хорошо; вот ссылка, которую он разместил, для избыточности / акцента:

Разница между деструктором и финализатором?

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

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

Я закончил тем, что использовал Mono.Cecil , чтобы отразить на вызывающей сборке (код, вызывающий мое программное обеспечение). Обратите внимание, что System.Reflection было недостаточно для моих целей, потому что он не может точно определять ссылки на методы, но мне все еще нужно (?) Использовать его для получения самой "вызывающей сборки" (Mono.Cecil остается недокументированным, поэтому возможно, мне просто нужно больше ознакомиться с ним, чтобы вообще покончить с System.Reflection; это еще предстоит выяснить ....)

Я поместил код Mono.Cecil в метод Init () , и теперь структура выглядит примерно так:

public IntermediateClass<T> Init<T>()
{
    ValidateUsage(Assembly.GetCallingAssembly());
    return new IntermediateClass<T>();
}

void ValidateUsage(Assembly assembly)
{
    // 1) Use Mono.Cecil to inspect the codebase inside the assembly
    var assemblyLocation = assembly.CodeBase.Replace("file:///", "");
    var monoCecilAssembly = AssemblyFactory.GetAssembly(assemblyLocation);

    // 2) Retrieve the list of Instructions in the calling method
    var methods = monoCecilAssembly.Modules...Types...Methods...Instructions
    // (It's a little more complicated than that...
    //  if anybody would like more specific information on how I got this,
    //  let me know... I just didn't want to clutter up this post)

    // 3) Those instructions refer to OpCodes and Operands....
    //    Defining "invalid method" as a method that calls "Init" but not "Save"
    var methodCallingInit = method.Body.Instructions.Any
        (instruction => instruction.OpCode.Name.Equals("callvirt")
                     && instruction.Operand is IMethodReference
                     && instruction.Operand.ToString.Equals(INITMETHODSIGNATURE);

    var methodNotCallingSave = !method.Body.Instructions.Any
        (instruction => instruction.OpCode.Name.Equals("callvirt")
                     && instruction.Operand is IMethodReference
                     && instruction.Operand.ToString.Equals(SAVEMETHODSIGNATURE);

    var methodInvalid = methodCallingInit && methodNotCallingSave;

    // Note: this is partially pseudocode;
    // It doesn't 100% faithfully represent either Mono.Cecil's syntax or my own
    // There are actually a lot of annoying casts involved, omitted for sanity

    // 4) Obviously, if the method is invalid, throw
    if (methodInvalid)
    {
        throw new Exception(String.Format("Bad developer! BAD! {0}", method.Name));
    }
}

Поверьте мне, реальный код выглядит даже хуже, чем мой псевдокод ....: -)

Но Mono.Cecil может быть моей новой любимой игрушкой.

Теперь у меня есть метод, который отказывается запускать свое основное тело, если вызывающий код не «обещает» также вызвать второй метод впоследствии. Это похоже на странный кодовый контракт. Я на самом деле думаю о создании этого универсального и многоразового использования. Будет ли кто-нибудь из вас использовать такую ​​вещь? Скажите, если бы это был атрибут?

1 голос
/ 28 марта 2011

Я могу придумать три несколько решений, не идеальных.

  1. AIUI, что вам нужно, это функция, которая вызывается, когда временная переменная выходит из области видимости (например, когда она становится доступной для сборки мусора, но, вероятно, еще некоторое время не будет собираться мусором). (См .: Разница между деструктором и финализатором? ) Эта гипотетическая функция скажет: «Если вы создали запрос в этом объекте, но не вызвали save, выведите ошибку». C ++ / CLI вызывает это RAII, а в C ++ / CLI существует понятие «деструктор», когда объект больше не используется, и «финализатор», который вызывается, когда он, наконец, собирается мусором. Очень странно, что в C # есть только так называемый деструктор, но это only , вызываемый сборщиком мусора (это было бы допустимо для платформы, чтобы вызывать ее раньше, как если бы она частично очищала объект немедленно, но AFAIK это не делает ничего подобного). Так что вам нужен деструктор C ++ / CLI. К сожалению, AIUI это отображает на концепцию IDisposable, которая предоставляет метод dispose (), который можно вызывать при вызове деструктора C ++ / CLI или при вызове деструктора C # - но AIUI вам все равно придется вызывать «dispose» "вручную, что побеждает точку?

  2. Измените интерфейс немного, чтобы передать концепцию более точно. Вызовите функцию инициализации, например, «prepareQuery», «AAA» или «initRememberToCallSaveOrThisWontDoAnything». (Последнее является преувеличением, но, возможно, это необходимо для пояснения).

  3. Это скорее социальная проблема, чем техническая проблема. Интерфейс должен облегчать правильные действия, но программисты должны знать, как использовать код! Соберите всех программистов вместе. Объясните это раз и навсегда этот простой факт. При необходимости попросите их всех подписать лист бумаги, в котором говорится, что они понимают, и если они умышленно продолжат писать код, который ничего не делает, они хуже, чем бесполезны для компании и будут уволены.

  4. Повторите то, как операторы связаны, например. пусть каждая из функций промежуточного класса собирает агрегатный объект промежуточного класса, содержащий все параметры (в большинстве случаев это уже было (?)), но требуется, чтобы инициализирующая функция исходного класса принимала это в качестве аргумента, а не имела их за ним следует цепочка, и вы можете сохранить, а другие функции возвращают два разных типа классов (с практически одинаковым содержимым), а init может принимать только класс правильного типа.

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

1 голос
/ 23 марта 2011

Что если вы сделали так, чтобы Init и With не возвращали объекты типа FluentClass?Пусть они вернутся, например, UninitializedFluentClass, который оборачивает FluentClass объект.Затем вызов .Save(0 для объекта UnitializedFluentClass вызывает его для обернутого объекта FluentClass и возвращает его.Если они не вызывают Save, они не получают объект FluentClass.

0 голосов
/ 24 марта 2011

Используйте параметр out!Все out s должны быть использованы.

Редактировать: Я не уверен, что это поможет, хотя ... Это нарушит свободный синтаксис.

0 голосов
/ 23 марта 2011

В режиме отладки помимо реализации IDisposable вы можете установить таймер, который будет выдавать исключение через 1 секунду, если метод результата не был вызван.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...