假设我想强制执行一条规则:
每次在函数中调用"StartJumping()"时,必须在返回之前调用"EndJumping()".
当开发人员编写代码时,他们可能只是忘记调用EndSomething - 所以我想让它易于记忆.
我只能想到一种方法:它滥用"using"关键字:
class Jumper : IDisposable {
public Jumper() { Jumper.StartJumping(); }
public void Dispose() { Jumper.EndJumping(); }
public static void StartJumping() {...}
public static void EndJumping() {...}
}
public bool SomeFunction() {
// do some stuff
// start jumping...
using(new Jumper()) {
// do more stuff
// while jumping
} // end jumping
}
Run Code Online (Sandbox Code Playgroud)
有一个更好的方法吗?
Eri*_*ert 14
基本上问题是:
你发现当你这样做时会很痛.我的建议不是试图找到减少伤害的方法,而是试图找到一种不首先做痛苦的方法.
我很清楚这有多难.当我们在v3中将lambdas添加到C#时,我们遇到了一个大问题.考虑以下:
void M(Func<int, int> f) { }
void M(Func<string, int> f) { }
...
M(x=>x.Length);
Run Code Online (Sandbox Code Playgroud)
我们怎么能成功地绑定它?好吧,我们做的是尝试两者(x是int,或x是字符串)并查看哪些(如果有的话)给我们一个错误.不给出错误的那些成为重载解析的候选者.
编译器中的错误报告引擎是全局状态.在C#1和2还没有过,我们不得不说"绑定这整个身体的方法来确定是否有任何错误的目的的情况,但不报告错误 ".毕竟,在这个程序,你就不会想要得到的错误"诠释不具有的属性称为长度",你希望它发现,做的一份说明,并没有举报.
所以我所做的就是你所做的.开始抑制错误报告,但不要忘记停止抑制错误报告.
它是可怕的.我们真正应该做的是重新设计编译器,以便输出语义分析器的错误,而不是编译器的全局状态.但是,很难通过依赖于全局状态的数十万行现有代码来解决这个问题.
无论如何,还有别的想法.您的"使用"解决方案具有在抛出异常时停止跳转的效果.这是正确的做法吗?它可能不是.毕竟,已经抛出了意外的,未处理的异常.整个系统可能是大规模的不稳定.在这种情况下,您的内部状态不变量都不会实际上是不变的.
以这种方式看待它:我改变了全球状态.然后我得到了一个意外的,未处理的异常.我知道,我想我会再次改变全球状态!那会有所帮助!看起来像一个非常非常糟糕的主意.
当然,这取决于对全球状态的突变.如果它是"开始向用户再次报告错误",那么对于未处理的异常,正确的做法就是再次向用户报告错误:毕竟,我们需要报告错误,编译器只有一个未处理的异常!
另一方面,如果突变到全局状态是"解锁资源并允许它被不值得信任的代码观察和使用",那么自动解锁它可能是一个非常糟糕的想法.这个意外的,未处理的异常可能是对您的代码进行攻击的证据,来自攻击者非常希望您现在可以解除对全局状态的访问,因为它处于易受攻击且不一致的状态.
我不同意埃里克:什么时候这样做取决于具体情况.有一次,我正在重新设计一个大型代码库,以包含对自定义图像类的所有访问的获取/发布语义.图像最初分配在不动的内存块中,但我们现在能够将图像放入允许移动的块中(如果尚未获取的话).在我的代码中,对于已经解锁的内存块而言,这是一个严重的错误.
因此,执行此操作至关重要.我创建了这个类:
public class ResourceReleaser<T> : IDisposable
{
private Action<T> _action;
private bool _disposed;
private T _val;
public ResourceReleaser(T val, Action<T> action)
{
if (action == null)
throw new ArgumentNullException("action");
_action = action;
_val = val;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~ResourceReleaser()
{
Dispose(false);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
_disposed = true;
_action(_val);
}
}
}
Run Code Online (Sandbox Code Playgroud)
这允许我做这个子类:
public class PixelMemoryLocker : ResourceReleaser<PixelMemory>
{
public PixelMemoryLocker(PixelMemory mem)
: base(mem,
(pm =>
{
if (pm != null)
pm.Unlock();
}
))
{
if (mem != null)
mem.Lock();
}
public PixelMemoryLocker(AtalaImage image)
: this(image == null ? null : image.PixelMemory)
{
}
}
Run Code Online (Sandbox Code Playgroud)
这反过来让我写这个代码:
using (var locker = new PixelMemoryLocker(image)) {
// .. pixel memory is now locked and ready to work with
}
Run Code Online (Sandbox Code Playgroud)
这是我需要的工作,快速搜索告诉我,我需要它在186个地方,我可以保证永远不会解锁.而且我必须能够做出这样的保证 - 否则可能会冻结我客户端堆中的大量内存.我做不到.
但是,在我处理PDF文档加密的另一种情况下,所有字符串和流都以PDF字典加密,除非它们不是.真.有一些边缘情况,加密或解密字典是不正确的,所以在流出一个对象时,我这样做:
if (context.IsEncrypting)
{
crypt = context.Encryption;
if (!ShouldBeEncrypted(crypt))
{
context.SuspendEncryption();
suspendedEncryption = true;
}
}
// ... more code ...
if (suspendedEncryption)
{
context.ResumeEncryption();
}
Run Code Online (Sandbox Code Playgroud)
那么为什么我选择这个而不是RAII方法呢?好吧,在...更多代码中发生的任何异常都意味着你已经死在水中.没有恢复.没有恢复.你必须从一开始就重新开始,并且需要重建上下文对象,所以它的状态无论如何都被冲洗了.相比之下,我只需要执行此代码4次 - 错误的可能性比内存锁定代码少,如果我将来忘记了,生成的文档将立即被破坏(失败)快速).
因此,当你绝对肯定要有括号内的电话并且不能失败时选择RAII.如果做其他事情是微不足道的话,不要打扰RAII.
如果您需要控制范围操作,我会添加一个方法,该方法Action<Jumper>包含在跳线实例上包含所需的操作:
public static void Jump(Action<Jumper> jumpAction)
{
StartJumping();
Jumper j = new Jumper();
jumpAction(j);
EndJumping();
}
Run Code Online (Sandbox Code Playgroud)
在某些情况下(即,当操作最终都发生时)可以使用的另一种方法是创建一系列具有流畅接口和一些最终Execute()方法的类.
var sequence = StartJumping().Then(some_other_method).Then(some_third_method);
// forgot to do EndJumping()
sequence.Execute();
Run Code Online (Sandbox Code Playgroud)
Execute()可以向下链接并强制执行任何规则(或者您可以在构建打开序列时构建关闭序列).
这种技术优于其他技术的一个优点是您不受范围规则的限制.例如,如果您想根据用户输入或其他异步事件构建序列,则可以执行此操作.
杰夫
您试图实现的目标通常称为面向方面的编程(AOP)。在C#中使用AOP范例进行编程并不容易-还是可靠的...。在某些狭窄情况下,CLR和.NET框架中直接内置了一些功能,这些功能使AOP成为可能。例如,当您从ContextBoundObject派生一个类时,可以使用ContextAttribute在CBO实例上的方法调用之前/之后注入逻辑。您可以在此处查看如何完成此操作的示例。
派生CBO类很烦人且具有限制性-还有另一种选择。您可以使用PostSharp之类的工具将AOP应用于任何C#类。PostSharp比CBO灵活得多,因为它实际上是在后期编译步骤中重写您的IL代码。尽管这看起来有些可怕,但它非常强大,因为它使您能够以几乎可以想象的任何方式编织代码。这是一个基于您的使用场景的PostSharp示例:
using PostSharp.Aspects;
[Serializable]
public sealed class JumperAttribute : OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionArgs args)
{
Jumper.StartJumping();
}
public override void OnExit(MethodExecutionArgs args)
{
Jumper.EndJumping();
}
}
class SomeClass
{
[Jumper()]
public bool SomeFunction() // StartJumping *magically* called...
{
// do some code...
} // EndJumping *magically* called...
}
Run Code Online (Sandbox Code Playgroud)
PostSharp 通过重写已编译的IL代码以包含运行在类和方法中定义的代码的指令来实现魔力。JumperAttributeOnEntryOnExit
对于您而言,PostSharp / AOP是否比“重新使用” using语句更好的选择对我来说还不清楚。我倾向于同意@Eric Lippert的观点,using关键字混淆了代码的重要语义,并}在using块的末尾对符号施加了副作用和语义指导-这是意外的。但这与将AOP属性应用于代码有什么不同吗?它们还在声明性语法后面隐藏了重要的语义……但这就是AOP的重点。
我全心全意地同意Eric的观点是,重新设计代码以避免这种全局状态(如果可能)是最好的选择。它不仅避免了强制正确使用的问题,而且还可以帮助避免将来的多线程挑战-全局状态非常容易受到影响。