#c# #dispose #fluent
#c# #удаление #свободно
Вопрос:
Я разрабатываю свободный API, и его использование примерно так:
IUser user = work
.Timeout(TimeSpan.FromSeconds(5))
.WithRepository(c => c.Users)
.Do(r => r.LoadByUsername("matt"))
.Execute();
Итак, допустим, work
это тип IUnitOfWork
, но метод WithRepository(c => c.Users)
возвращает вызываемый интерфейс IActionFlow<IUserRepository>
, который является IDisposable
.
Когда я вызываю Execute()
и получаю конечный результат, я теряю ссылку на этот IActionFlow<IUserRepository>
экземпляр, поэтому я не могу его удалить.
Каковы недостатки того, что экземпляр dipose сам зависит от Execute()
метода?
Что-то вроде:
public TResult Execute()
{
// ...
Dispose();
return resu<
}
Кажется, что код компилируется просто отлично, но я ищу странное поведение или ошибки, которые могут возникнуть из-за этого. Это вообще плохая практика?
Комментарии:
1. Ну, это просто означает, что вы возвращаете удаленный объект, который действительно выглядит странно :). У плавных интерфейсов есть некоторые проблемы: ocramius.github.io/blog/fluent-interfaces-are-evil здесь есть целое обсуждение шаблона Dispose с плавными интерфейсами: davefancher.com/2015/06/14 /…
Ответ №1:
У вас может быть Using
такой метод:
public static TResult Using<TDisposable, TResult>(Func<TDisposable> factory,
Func<TDisposable, TResult> fn) where TDisposable : IDisposable {
using (var disposable = factory()) {
return fn(disposable);
}
}
Тогда ваш код будет выглядеть следующим образом:
var user = Using(() => work.
Timeout(TimeSpan.FromSeconds(5)).
WithRepository(c => c.Users),
repository => repository.Do(r => r.LoadByUsername("matt")).
Execute());
Это позволит вашему API оставаться свободным, и в то же время вы будете удалять WithRepository
его в тот же момент Execute
, когда он будет завершен.
Комментарии:
1. Мне нравится продуманность решения, но ИМО это излишне.
Ответ №2:
Вариант 1:
Можете ли вы обернуть свой код внутри using
блока, чтобы dispose вызывался автоматически,
using(var repository = work.Timeout(TimeSpan.FromSeconds(5)).WithRepository(c => c.Users))
{
IUser user = repository
.Do(r => r.LoadByUsername("matt"))
.Execute();
}
Таким образом, вашему Execute
методу не нужно вызывать Dispose()
public TResult Execute()
{
// ...
//Dispose();
return resu<
}
Вариант 2:
Вы можете присвоить результат свойству и вернуть объект репозитория, с помощью которого вы можете Dispose
явно вызвать метод.
Что-то вроде этого (может быть переработано дальше),
using(var repository = work
.Timeout(TimeSpan.FromSeconds(5))
.WithRepository(c => c.Users)
.Do(r => r.LoadByUsername("matt"))
.Execute())
{
IUser user = repository.Resu<
//repository.Dispose();
}
//******
public TResult Result { get; set; }
public IActionFlow<IUserRepository> Execute()
{
// ...
//Dispose();
this.Result = resu<
return this;
}
Примечание: После вызова Dispose()
метода сборка мусора может произойти на любом этапе в зависимости от доступных ресурсов сервера. Поэтому вызов этого внутри Execute
метода может привести к неожиданным странным проблемам.
Комментарии:
1. Я могу, но это сделает мой API совсем не свободным
2. @MatiasCicero предоставил альтернативную идею, но должен провести ее дальнейший рефакторинг.
3. Или даже можно обернуть вариант 2 внутри
using
блока
Ответ №3:
Одноразовые объекты должны быть удалены из клиентского кода, один из побочных эффектов, который вы увидите из того, что вы делаете, заключается в том, что когда ваши потребители используют ваш API, они будут получать ошибки анализа кода (если они его используют, многие делают), например: «CA2213: одноразовые поля должны быть удалены» см. https://msdn.microsoft.com/en-us/library/ms182328.aspxдля получения подробной информации об этой ошибке.
Сказав вышеизложенное, вы не должны утилизировать вещи, которые абсолютно необходимы для утилизации в методе «Execute», возьмите эти сценарии:
Сценарий # 1
- У вас есть такие вещи, как открытые соединения, которые вы должны утилизировать, соединения открываются каким-то другим методом, кроме Execute.
- Во время выполнения метода «Do» возникает ошибка, dispose никогда не вызывается, поскольку execute никогда не вызывался, а открытое соединение оставалось открытым. Обратите внимание, что даже если на вашем компьютере никогда не произойдет сбой, это не гарантирует, что он не выйдет из строя при производстве из-за случайных событий, происходящих с машиной из другого потока.
PS операторы using и вся функциональность IDisposable созданы для решения таких ситуаций, заставляя хороших разработчиков распоряжаться независимо от обстоятельств.
Сценарий # 2
- Методы выполнения делают что-то одноразовое, например, открывают соединение, а затем делают что-то еще.
В этом сценарии вам не нужен метод dispose, вы можете просто удалить соединение в методе execute, обернув соединение в оператор using, например:
public void Execute(object whatever){
using (var conn = new Connection()){
//method body
}
}
Если у вас есть сценарий № 1, вы должны позволить клиенту использовать оператор using и должны будете терпеть его отсутствие (деструкторы не будут его вырезать), если у вас есть сценарий № 2, вам вообще не нужно dispose .
Ответ №4:
Мне кажется, что вы пытаетесь избавиться от репозитория.
Если вы хотите сохранить свой API без изменений, я бы посоветовал следующее:
using(var repo = work.Timeout(TimeSpan.FromSeconds(5))
.WithRepository(c => c.Users))
{
IUser user = repo.Do(r => r.LoadByUsername("matt")).Execute();
//Do whatever with user here if lazy loading.
}
Я знаю, что такой тип разбивает ваш fluent API на две части, но вы можете использовать его таким образом, вообще ничего не меняя.
Другим вариантом было бы создать класс-оболочку для результата, который поддерживает ссылку на репозиторий (например, ActionFlowExecution) и реализует IDisposable:
public class ActionFlowExecution<TResult, TRepository> : IDisposable
{
private TRepository _repository;
internal ActionFlowExecution(TResult result, TRepository repository)
{
Result = resu<
_repository = repository;
}
public TResult Result { get; private set; }
public void Dispose()
{
if(_repository != null)
{
_repository.Dispose();
_repository = null;
}
}
}
Затем вы можете вызвать свой API следующим образом:
using(var execution = work
.Timeout(TimeSpan.FromSeconds(5))
.WithRepository(c => c.Users)
.Do(r => r.LoadByUsername("matt"))
.Execute())
{
IUser user = execution.Resu<
//Do whatever with user here
}
Я согласен с другими, что это плохая идея, чтобы объект удалялся изнутри.
Я написал это за пять минут, поэтому могут быть некоторые опечатки, но я думаю, вы получите общую идею.
Ответ №5:
Вы можете безопасно делать то, что упомянули, если не будете использовать этот объект позже. Единственная, но очень важная вещь здесь — это именование. Я бы вызвал метод ExecuteAndDispose
. Другие варианты либо нарушают ваш fluent API, либо просто излишни, а также не просты в использовании.