Как использовать возвращаемое значение метода в C#?
у меня есть часть программного обеспечения, написанная с беглым синтаксисом. Цепочка методов имеет окончательное "окончание", перед которым в коде фактически ничего полезного не делается (подумайте, что 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 ответов
Я не знаю, как обеспечить это на уровне компилятора. Он часто запрашивается для объектов, которые реализуют IDisposable
также, но на самом деле не подлежит исполнению.
один потенциальный вариант, который может помочь, однако, настроить ваш класс,только в DEBUG, чтобы иметь финализатор, который регистрирует / бросает/etc. если Save()
никогда не назывался. Это может помочь вам обнаружить эти проблемы во время отладки, а не полагаться на поиск кода и т. д.
однако, убедитесь, что в режиме выпуска это не используется, так как это приведет к накладным расходам производительности, так как добавление ненужного финализатора очень плохо сказывается на производительности GC.
для использования обратного вызова могут потребоваться определенные методы:
new FluentClass().Init<int>(x =>
{
x.Save(y =>
{
y.With(-1),
y.With(300)
});
});
метод with возвращает определенный объект, и единственный способ получить этот объект-вызвать x.Save (), который сам имеет обратный вызов, который позволяет настроить неопределенное количество операторов with. Таким образом, init принимает что-то вроде этого:
public T Init<T>(Func<MyInitInputType, MySaveResultType> initSetup)
Я думаю три несколько решений, не идеально.
AIUI то, что вы хотите, - это функция, которая вызывается, когда временная переменная выходит из области видимости (например, когда она становится доступной для сбора мусора, но, вероятно, еще не будет собрана в течение некоторого времени). (См.: разница между деструктором и финализатором?) эта гипотетическая функция скажет: "если вы построили запрос в этом объекте, но не вызвали сохранить, произвести ошибку". C++ / CLI вызывает этот RAII, и в C++ / CLI есть концепция "деструктора", когда объект больше не используется, и "финализатор", который вызывается, когда он, наконец, собирает мусор. Очень смущает, что C# имеет только так называемый деструктор, но это только вызывается сборщиком мусора (было бы допустимо, чтобы фреймворк вызывал его раньше, как если бы он немедленно частично очищал объект, но AFAIK этого не делает). Так что вы хотели бы это деструктор C++ / CLI. К сожалению, AIUI это сопоставляется с концепцией IDisposable, которая предоставляет метод dispose (), который может быть вызван, когда будет вызван деструктор C++/CLI или когда деструктор c# вызывается-но AIUI вам все равно нужно вызвать "dispose" вручную, что побеждает точку?
рефакторинг интерфейс немного, чтобы передать концепцию более точно. Вызовите функцию init что - то вроде "prepareQuery" или " AAA " или "initRememberToCallSaveOrThisWontdoanything". (Последнее является преувеличением, но, возможно, необходимо сделать это).
Это скорее социальная проблема, чем техническая проблема. Интерфейс должны сделать его легким сделать правильную вещь, но программисты должны знать, как использовать код! Соберите всех программистов вместе. Объясните раз и навсегда этот простой факт. Если необходимо, пусть они все подпишут лист бумаги, говоря, что понимают, и если они умышленно продолжайте писать код, который ничего не делает, они хуже, чем бесполезны для компании и будут уволены.
возиться с тем, как операторы соединяются, например. каждая из функций intermediateClass собирает совокупный объект intermediateclass, содержащий все параметры (вы в основном делаете это уже (?)) но требуйте, чтобы init-подобная функция исходного класса принимала это как аргумент, а не приковывала их после него, а затем вы может иметь save, а другие функции возвращают два разных типа классов (с практически одинаковым содержимым), и init принимает только класс правильного типа.
тот факт, что он еще проблема предполагает, что либо ваши коллеги нуждаются в полезном напоминании, либо они довольно суб-паритет, или интерфейс был не очень ясным (возможно, его отлично, но автор не понял, что было бы неясно, если бы вы использовали его только мимоходом, а не узнав это), или вы сами неправильно поняли ситуацию. Техническое решение было бы хорошо, но вы, вероятно, должны подумать о том, почему возникла проблема и как более четко общаться, вероятно, попросив кого-то старшего.
после долгих размышлений и проб и ошибок выясняется, что выбрасывание исключения из метода Finalize() не сработает для меня. По-видимому, вы просто не можете этого сделать; исключение съедается, потому что сбор мусора работает недетерминированно. Я также не смог заставить программное обеспечение автоматически вызывать Dispose() из деструктора. Комментарий Джека В. объясняет это хорошо; вот ссылка, которую он опубликовал, для избыточности / акцента:
в разница между деструктором и финализатором?
изменение синтаксиса для использования обратного вызова было умным способом сделать поведение надежным, но согласованный синтаксис был исправлен, и мне пришлось работать с ним. Наша компания все о беглых цепях метода. Я также был поклонником решения "out parameter", чтобы быть честным, но опять же, суть в том, что подписи метода просто не могли измениться.
полезная информация о моей конкретной проблеме включает в себя факт что мое программное обеспечение только запускаться как часть набора модульных тестов - поэтому эффективность не является проблемой.
Я решил было использовать Mono.Сесил!--12--> отразить по вызывающей сборки (кода вызова моей программы). Обратите внимание, что было недостаточно для моих целей, потому что он не может точно указать ссылки на метод, но мне все еще нужно(?) использовать его для получения самой "вызывающей сборки" (Монофонический.Сесил остается недокументированным, так что, возможно, мне просто нужно познакомиться с ним поближе, чтобы покончить с системой.Отражение вообще; это еще предстоит увидеть....)
Я поставил Mono.Сесил!--12--> код 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));
}
}
поверьте мне, фактический код еще уродливее, чем мой псевдокод.... :-)
Но Моно.Сесил, возможно, мой новый фаворит. игрушка.
теперь у меня есть метод, который отказывается запускать свое основное тело, если вызывающий код "обещает" также вызвать второй метод позже. Это похоже на странный кодовый контракт. Вообще-то, я подумываю о том, чтобы сделать это общим и многоразовым. Кому-нибудь из вас это пригодится? Скажем, если бы это был атрибут?
что делать, если вы сделали это так Init
и With
не возвращать объекты типа FluentClass
? Пусть они вернутся, например,UninitializedFluentClass
, который обертывает
в режиме отладки рядом с реализацией IDisposable вы можете настроить таймер, который выдаст исключение через 1 секунду, если resultmethod не был вызван.
использовать ! Все out
s необходимо использовать.
Edit: я не уверен, что это поможет, tho... Это нарушило бы свободный синтаксис.