Как обеспечить использование возвращаемого значения метода в С#?

У меня есть часть программного обеспечения, написанная с плавным синтаксисом. Цепочка метода имеет окончательное "окончание", перед которым в коде не делается ничего полезного (думаю, что генерация запросов 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", указав, что THAT-метод выполняет "настоящую работу" программного обеспечения.

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

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

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

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

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

Ответ 1

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

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

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

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

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

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

Ответ 2

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

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

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

Ответ 3

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

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) 

Ответ 4

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

Ответ 5

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

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

  • Восстановите интерфейс немного, чтобы передать концепцию более точно. Вызовите функцию init что-то вроде "prepareQuery" или "AAA" или "initRememberToCallSaveOrThisWontDoAnything". (Последнее - преувеличение, но может потребоваться сделать точку).

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

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

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

Ответ 6

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

Ответ 7

Используйте параметр out! Необходимо использовать все out.

Изменить: я не уверен, что это поможет, tho... Это сломало бы свободный синтаксис.