有没有一种方法可以告诉编译器,某个特定的代码块总是会被执行?

4
我们的代码库中有以下结构,用于确保特定资源在使用后被处理:
using (var disposableThing = DisposableThing.Begin())
{
    // do something

    disposableThing.Finish(); // must always be called
}

这是它使用的一个例子:
List<int> ids;

using (var disposableThing = DisposableThing.Begin())
{
    ids = disposableThing.GetSomeIds();

    disposableThing.Finish();
}

DoSomethingElseWith(ids);

由于这种模式如此常见,我们在DisposableThing上编写了一个方法来封装它:
static void ExecuteWithFinish(Action<DisposableThing> action)
{
    using (var disposableThing = Begin())
    {
        action(disposableThing);

        disposableThing.Finish();
    }
}

这使我们可以将第二个样本重写为:
// #4
List<int> ids;

DisposableThing.ExecuteWithFinish(disposableThing =>
{
    ids = disposableThing.GetSomeIds();
});

DoSomethingElseWith(ids); // compiler error "Use of unassigned local variable 'ids'"

但编译器拒绝编译该代码,因为它无法知道在ExecuteWithFinish完成后(或抛出异常,这将防止执行DoSomethingElseWithids是否总是被分配。

  • 我知道我可以添加一个重载的ExecuteWithFinish,从传入的Func返回值,但这很丑陋。
  • 我知道我可以子类化DisposableThing并覆盖其Dispose方法以调用Finish,这比每次构造一个委托更干净、更整洁、更快速(这可能是我最终会做的)。

但出于我的自我启发和“如果”的精神,有没有可能通知甚至欺骗编译器允许#4中的代码按原样运行?

编辑:是的,我知道我可以写List<int> ids = null;并完全规避这个问题,但(a)我不想执行不必要的赋值(b)我希望尽可能少地改变代码。


5
我注意到disposableThing.Finish()的注释中写着“必须始终调用”。即使do something抛出异常,这个说法是否仍然成立?如果是这样的话,你应该将整个Begin() Finish() Dispose()转化为一个单独的类,并使用一个using语句进行管理。 - Matthew Watson
只是一般性的评论:为什么不使用IDisposable模式,这似乎比使用Finish方法重新发明机制更明显?将Finish中的代码放入您的Dispose方法中(或者仅从Dispose调用Finish)。这样,您就可以避免编译器错误,并获得语言支持,因为Finish中的代码将在using语句结束时自动调用。 - Dirk Vollmar
@Joey 你希望是这样的 - 但在这种情况下,为什么OP的代码明确调用它呢? - Matthew Watson
1
赋值为 null 不是一种不必要的赋值;它甚至不会在堆上创建实例,因此基本上是免费的。 - Sefe
1
请注意,使用未初始化的本地变量无法生成经过验证和管理的代码,因此编译器在这里束手无策 - 它不能轻易相信你。在IL级别上,本地变量在使用之前必须被明确赋值,并且运行时不执行代码流分析,出于效率和复杂性(以及代理、动态代码等可能性)的明显原因。虽然可以对未经验证的代码放宽限制,但您可以看到为什么这不是高优先级的事情。 - Jeroen Mostert
显示剩余3条评论
2个回答

5
我会用不同的方法来解决这个问题。
我假设你必须在调用Dispose()之前调用Finish()方法。也许这是一个草率的假设,这也引出了一个问题:为什么你不把Finish()的功能加入到Dispose()中呢?但是...
首先,创建一个接口来封装带有Finish()方法的可处理对象:
public interface IDisposableThingWithFinish : IDisposable
{
    void Finish();
}

请把你的 DisposableThing 类改成实现 IDisposableThingWithFinish 接口。

这样,你就可以写一个可释放对象类,将调用 Finish()Dispose() 封装在一起:

public sealed class DisposingFinisher : IDisposable
{
    readonly IDisposableThingWithFinish _item;

    public Disposing(IDisposableThingWithFinish item)
    {
        if (item == null)
            throw new ArgumentNullException(nameof(item));

        _item = item;
    }

    public void Dispose()
    {
        try
        {
            _item.Finish();
        }

        finally
        {
            _item.Dispose();
        }
    }
}

您可以像这样使用Finisher:
using (var disposableThing = new DisposingFinisher(DisposableThing.Begin()))
{
    // Do something.
}

0
一次简单的空值赋值就可以避免编译器警告,如编译器错误CS0165所述。
List<int> ids = null;

完全正确,但我想避免不必要的赋值,根据我的编辑。 - Ian Kemp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接