如何返回一个依赖于一系列using语句的对象?

4

I'd like to write a method similar to this one:

C Make()
{
    using (var a = new A())
    using (var b = new B(a))
    {
        return new C(b);
    }
}

这是不好的,因为当该方法返回时,c 保留了一个对已释放对象的引用。

请注意:

  • A 实现了 IDisposable 接口。
  • B 实现了 IDisposable 接口。
  • C 不实现 IDisposable 接口,因为 C 的作者声明 C 不拥有 b 对象。

2
你不需要手动释放它们 - using 语句会自动处理。这就是 using 的作用。问题是当 C 尝试使用 b 时,它已经被释放了吗? - D Stanley
你的意思是说,你在使用“C”期间不想处理“a”和“b”吗? - sstan
@DStanley 是的,但是当使用返回的变量时,你会得到一个ObjectDisposedException异常(取决于实现方式)。OP:如果你想要一个断开连接的场景,你需要指示客户端处理b的释放。你可以通过让C来处理并使C : IDisposable来实现。参见Return an object created by USING等。 - CodeCaster
@DStanley 在离开Make()时无法处理'b',因为它被'c'使用。 - Shmoopy
@nodots 是的,C 在其整个生命周期中都在使用它的 b 实例。 - Shmoopy
显示剩余2条评论
3个回答

3

这是一个文档很重要的例子,与类的合同有关。

当然,您已经意识到了这一点,因为您说:

C 的作者表示,C 不拥有 b

这意味着您无法完全实现您想要的。您目前的做法可能不正确,因为在新的 C 返回之前,ab 将立即被处理。

您需要对此代码进行一些重构。要么更改 Make(),使其接受类型为 B 的参数;调用者将继续负责 BC 的生命周期。要么编写一个实现 IDisposable 并包装 ABC 并通过属性公开 C 的新类。

如果您拥有类型C,您可以考虑修改它,以允许它可选地接管b的所有权。这是.NET本身中相当常见的模式。例如,请参阅XmlReaderSettings.CloseInput

2

这样做是不好的,因为我不能从外部引用ab并处置它们。

只有在您将对ab保留引用并可以从代码外部处置它们时,才会出现问题。然而,这些对象是可处置的,因为C没有创建它们或获取所有权转移,所以应该在完成构造函数时从AB中获取所需内容,并且不要保留对可处置对象的引用:

class C {
    private readonly string x;
    private readonly int y;
    public C(B b) {
        // Using b here is OK
        x = b.X;
        y = b.Y;
        // We are done with b, so when b is disposed, C will not break
    }
}

不幸的是,C 在其生命周期中保留对其 b 的引用,并期望调用方在不再需要 C 时处理它。

如果您无法控制 C,请为其创建一个 IDisposable 包装器,接管 B 并在不再需要 C 时进行处理:

class WrapC : IDisposable {
    private readonly B b;
    public C C { get; private set; }
    public WrapC (B b) {
        this.b = b;
        C = new C(b);
    }
    public void Dispose() {
        b.Dispose();
    }
}

移除 Busing 语句,并在使用完 WrapC 后进行释放。


不幸的是,C在其整个生命周期中保留了对它的b的引用,并期望调用者在不再需要C时将其处理掉。 - Shmoopy

0

你的情况很类似于我在查询数据库时经常看到的情况。为了分离逻辑,有时会看到这样的代码:

var reader = ExecuteSQL("SELECT ...");
while (reader.Read()) // <-- this fails, because the connection is closed.
{
    // process row...
}

public SqlDataReader ExecuteSQL(string sql)
{
    using (SqlConnection conn = new SqlConnection("..."))
    {
        conn.Open();
        using (SqlCommand cmd = new SqlCommand(sql, conn))
        {
            return cmd.ExecuteReader();
        }
    }
}

但是,当然,那是行不通的,因为当 SqlDataReaderExecuteSQL 方法返回时,连接已经关闭(处理)。

因此,上述的替代方案利用委托来实现关注点分离,但仍允许代码正常工作:

ExecuteSQL(reader => {
    // write code that reads from the SqlDataReader here.
});

public void ExecuteSQL(string sql, Action<SqlDataReader> processRow)
{
    using (SqlConnection conn = new SqlConnection("..."))
    {
        conn.Open();
        using (SqlCommand cmd = new SqlCommand(sql, conn))
        {
            using (SqlDataReader reader = cmd.ExecuteReader())
            {
                while (reader.Read())
                {
                    processRow(reader);
                }
            }
        }
    }
}

所以,也许你可以在你的情况下尝试类似的东西?像这样:

MakeAndProcess(c => {
    // write code that uses C here.
    // This will work, because A and B are not disposed yet.
});

public void MakeAndProcess(Action<C> processC)
{
    using (var a = new A())
    using (var b = new B(a))
    {
        processC(new C(b));
    }
}

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