提问人:Zoey Hewll 提问时间:11/7/2023 最后编辑:Zoey Hewll 更新时间:11/9/2023 访问量:30
具有调用方定义的错误处理策略的单一实现
Single implementation with caller-defined error handling strategy
问:
我想使用几种不同的策略来处理错误,从而实现一个复杂的操作。为了简单起见,假设该操作是批量文件删除。 如果删除文件时出错,我可能需要做一些事情来响应:
- 立即退出,不要再删除任何文件
- 记录失败并继续删除其他文件
- 将失败添加到某些失败删除的集合中并继续,并在操作完成后返回该集合
这似乎不太适合例外和尝试捕获,因为它迫使我预先决定策略,这意味着我将为每个策略提供不同版本的核心实现。
使用异常的示例:
// Quitting strategy
void DeleteAllQuitting(IEnumerable<FileInfo> files) {
foreach (var file in files) {
file.Delete();
}
}
// Logging strategy
void DeleteAllLogging(IEnumerable<FileInfo> files, ILogger log) {
foreach (var file in files) {
try {
file.Delete();
log.Info(file);
} catch (IOException e) {
log.Warn(file, e);
} catch (SecurityException e) {
log.Warn(file, e);
}
}
}
// Collecting strategy
Failure[] DeleteAllCollecting(IEnumerable<FileInfo> files) {
return files.Select(file => {
try {
file.Delete();
return new Success(file, e);
} catch (IOException e) {
return new Failure(file, e);
} catch (SecurityException e) {
return new Failure(file, e);
}
}).OfType<Failure>().ToArray();
}
它们都具有相同的核心逻辑(调用每个元素),仅在如何处理该核心操作的异常方面有所不同。
每个新战略都需要一个全新的实施。
当然,在这种情况下,重复是最小的,但希望你能想象更复杂的操作,其中重复将非常重要(例如递归删除目录)。file.Delete()
我能想到的最好的办法是使用生成器,在操作失败或成功时懒惰地产生结果,允许调用者仅使用一个核心实现中止、恢复、记录、收集或任何其他可能的策略。 但我在 C# 标准库中没有遇到过这样的东西,所以我的印象是这不是惯用的,也许我错过了更好的方法。
使用生成器的示例:
// Core implementation
IEnumerable<Result> DeleteAll(IEnumerable<FileInfo> files) {
foreach (var file in files) {
Result result;
try {
file.Delete();
result = new Success(file);
} catch (Exception e) {
result = new Failure(file, e);
}
yield return result;
}
}
// Quitting strategy
void DeleteAllQuitting(IEnumerable<FileInfo> files) {
foreach (var fail in DeleteAll(files).OfType<Failure>()) {
throw fail.exception;
}
}
// Logging strategy
void DeleteAllLogging(IEnumerable<FileInfo> files, ILogger log) {
foreach (var result in DeleteAll(files)) {
switch (result){
case Failure f:
var e = f.exception;
if (e is IOException || e is SecurityException) {
log.Warn(f.file, e);
} else {
throw e;
}
break;
case Success s:
log.Info(s.file);
break;
}
}
// Collecting strategy
Failure[] DeleteAllCollecting(IEnumerable<FileInfo> files) {
return DeleteAll(files)
.OfType<Failure>()
.Where(fail => fail.exception is IOException || fail.exception is SecurityException)
.ToArray();
}
作为替代方法,我还考虑创建一个提供 OnFail 和 OnSuccess 方法的接口,这些方法将传递给 DeleteAll 方法。 缺点是,它需要实现者定义他们自己的类(我认为不能像我所做的那样与代码一起嵌套),并且它会反转控制流,因此调用者中的本地控制流需要是处理程序类中的手写状态机。
// Core implementation
void DeleteAll(IEnumerable<FileInfo> files, IErrorStrategy handler) {
foreach (var file in files) {
Exception ex = null;
try {
file.Delete();
} catch (Exception e) {
ex = e;
}
if (ex is null) {
handler.success(file);
} else {
handler.fail(file, ex);
}
}
}
// Quitting strategy
void DeleteAllQuitting(IEnumerable<FileInfo> files) {
class Thrower : IErrorStrategy {
void fail(FileInfo _, Exception e) {
throw e;
}
}
DeleteAll(files, new Thrower());
}
// Logging strategy
void DeleteAllLogging(IEnumerable<FileInfo> files, ILogger log) {
class Logger : IErrorStrategy {
void success(FileInfo file) {
log.Info(file, e);
}
void fail(FileInfo file, Exception e) {
if (e is IOException | e is SecurityException) {
log.Warn(file, e);
} else {
throw e;
}
}
}
DeleteAll(files, new Logger());
}
// Collecting strategy
Failure[] DeleteAllCollecting(IEnumerable<FileInfo> files) {
class Collector : IErrorStrategy {
List<Exception> errors;
void fail(FileInfo file, Exception e) {
if (e is IOException | e is SecurityException) {
errors.Add(e)
} else {
throw e;
}
}
}
var collector = new Collector();
DeleteAll(files, collector);
return collector.errors;
}
请原谅这个漫无边际的问题,但我想它的核心是:有没有我错过的设计做得更好?或者发电机解决方案是我们拥有的最好的解决方案吗?
补遗:
关于我所说的“倒置控制流”这个话题, 它不太可能出现在我上面描述的情况中, 但这个概念是,生成或使用一系列值的任意代码可以表示为内部或外部迭代器。
- 外部迭代器的特征是从序列中提取(我反复调用迭代器 - 尽管在 C# 中将其拆分为枚举器和枚举器)。
it.next()
it.Current
it.MoveNext()
- 内部迭代器的特点是依次将每个项目推送到它(我的方法被反复调用)。
with_next(Item)
一个可以被认为是另一个的反转,因为理论上可以将任意外部迭代器转换为等效的内部迭代器,反之亦然。
当前者可以表示为过程代码时,后者被表示为状态机,以便“记住”它在控制流中的位置。
(我理解 C# 生成器方法(即 )实际上出于同样的原因被编译为状态机)yield return
在大多数简单的 for 循环中,它们看起来基本相同: 作为外部迭代器:
for (var item in iter) {
do_something(item);
}
// compiles to something like
while (iter.has_next()) {
do_something(it.next());
}
作为内部迭代器:
iter.ForEach(new Foo());
class Foo {
void with_next(Item item) {
do_something(item);
}
}
但是使用任意逻辑,它可能会变得更加复杂。我在下面提供了一个相当病态的例子,但我的主要目标是让呼叫者自由,这应该是一个有效的策略。 作为外部迭代器:
if (a(it.next())) {
b(it.next());
c(it.next());
} else {
d(it.next());
}
作为内部迭代器:(请原谅,因为我不习惯用 C# 手写状态机)
try {
iter.ForEach(new Foo());
} catch Done; // exit early if the state machine asks to
enum Step {
Test,
Pass1,
Pass2,
Fail1,
End,
}
class Foo {
Step current_step = Step.Test;
void with_next(Item item) {
switch (current_step) {
case Step.Test:
current_step = a(item) ? Step.Pass1 : Step.Fail1;
break;
case Step.Pass1:
b(item);
current_step = Step.Pass2;
break;
case Step.Pass2:
c(item);
current_step = Step.End;
break;
case Step.Fail1:
d(item);
current_step = Step.End;
break;
case Step.End:
throw new Done());
}
}
}
回到上面概述的问题:
- 提供 的版本严格对应于内部迭代:处理程序上的方法大致对应于内部迭代器中的专用方法。
IErrorHandler
with_next
- 迭代惰性结果的版本实际上可以免费使用外部或内部迭代。Collecting 变体确实通过 LINQ 使用内部迭代,而其他变体则通过循环使用外部迭代。
foreach
答: 暂无答案
评论
Action<FileInfo, Exception>