C#构造函数中的虚方法调用 - 如何重构?

3
我有一个针对数据库无关的游标操作的抽象类。 基于此,有一些类实现了处理特定于数据库的方法的抽象方法。
问题是,基类构造函数需要调用抽象方法 - 当调用构造函数时,它需要初始化特定于数据库的游标。
我知道为什么不应该这样做,我不需要那个解释!
这是我的第一个实现,显然行不通 - 这是做法上的“错误方式”。 重写的方法访问派生类中尚未实例化的字段:
public abstract class CursorReader
{
    private readonly int m_rowCount;
    protected CursorReader(string sqlCmd)
    {
         m_rowCount = CreateCursor(sqlCmd); //virtual call !
    }
    protected abstract int CreateCursor(string sqlCmd);

    //...other (non-abstract) methods that assume a cursor exists
}

public class SqlCursorReader : CursorReader
{
    private SqlConnection m_sqlConnection;

    public SqlCursorReader(string sqlCmd, SqlConnection sqlConnection)
    {
        m_sqlConnection = sqlConnection;     //field initialized here
    }
    protected override int CreateCursor(string sqlCmd)
    {
        //uses not-yet-initialized member *m_sqlConnection*
        //so this throws a NullReferenceException
        var cursor = new SqlCursor(sqlCmd, m_sqlConnection); 
        cursor.Create();
        return cursor.Count();
    }
}

我会跟进我的尝试来修复这个问题...

更新

覆盖的方法CreateCursor()在数据库中创建了一个实际的游标。这对于许多被省略在类中的方法的正确运行至关重要。
CreateCursor()必须在基本构造函数中调用,以便在构造函数返回时该类处于一致状态。我已经稍微更新了上面的代码以反映这一点。

5个回答

4
你可以始终拥有一个延迟属性来获取计数。
public abstract class CursorReader
{
    private int? m_rowCount;
    protected CursorReader()
    {

    }
    protected abstract int CreateCursor(string sqlCmd);
    protected int RowCount {
      get {
          if (m_RowCount == null)
          {
             m_RowCount = CreateCursor(sql);
          }
          return m_RowCount.Value;
      }

    }
}

@hunter,这是同样的事情。对于“Nullable”类型,我喜欢检查是否为“null”。 - Daniel A. White
CursorReader 知道 sql 是什么吗? - hunter
你的getter方法阻止了JIT使用内联特性。 - CodeTherapist
@CSharper:嗯,可以使用“return (rowCount ?? (rowCount = CreateCursor(sql))).Value”进行单行(和内联)操作,所以这不是什么大问题。 - Patrick
我并不是说你不能把它写在一行上!但 JIT 只能对简单的 getter 进行内联。而你的代码并不简单。 - CodeTherapist
显示剩余2条评论

1

怎么样:

public abstract class CursorReader
{
    private int? m_rowCount = null;
    private int rowCount { get { return m_rowCount = m_rowCount ?? CreateCursor(sqlCmd); } }
    protected CursorReader() { }
    protected abstract int CreateCursor(string sqlCmd);
}

CursorReader 知道 sqlCmd 是什么吗? - hunter
@hunter 这只是来自 OP 的一小段代码。我假设它是暴露出来的,但真正的抽象类并不是这样的。 - Paul Fleming

1

你可能需要将你的逻辑分成构造函数和初始化。

public abstract class CursorReader
{
    private readonly int m_rowCount;
    protected CursorReader()
    {

    }

    protected void Init()
    {
         m_rowCount = CreateCursor(sqlCmd); //virtual call !
    }

    protected abstract int CreateCursor(string sqlCmd);
}

这将要求您在每个新实例上调用Init(),但这是我能想到的最好的解决方案。

请注意,像您提到的那样,Init可以从派生类中调用,但我认为从调用代码中调用它会更简单。有很多类型使用这种模式,即使它需要更多的代码,也不是一个坏习惯。


“从调用代码中调用会更简单”是什么意思?另外,请检查我的问题更新——在构造对象时应该真正调用CreateCursor()。也许可以看一下我的答案,它似乎朝着类似的方向发展。 - Cristian Diaconescu

1

这是我考虑的第二个方向:

为了避免鸡生蛋、蛋生鸡的问题,同时允许快速创建游标,需要进行一些抽象。

在基类中,虚调用尝试访问派生类中尚未初始化的字段。因此,让我们将创建游标的功能提取到另一个类中。基类对其如何创建不感兴趣,它只是更大算法中的一步。

对我来说,这种方法的要点看起来类似于策略模式 - 主类知道算法的一般步骤,而步骤的实际实现细节在运行时插入。

public interface ICursorCreator {
    int CreateCursor(string sqlCmd);
}
public abstract class CursorReader
{
    private readonly int m_rowCount;
    protected CursorReader(string sqlCmd, ICursorCreator creator)
    {
         m_rowCount = creator.CreateCursor(sqlCmd); //no longer a virtual call 
    }
    //protected abstract int CreateCursor(string sqlCmd);//no longer needed

    //...other (non-abstract) methods that assume a cursor exists
}

//move the logic of creating a cursor in a separate class, and pass an instance of that to the base class. 
public SqlCursorCreator: ICursorCreator {
    private SqlConnection m_sqlConnection;
    public SqlCursorCreator(SqConnection conn){
        m_sqlConnection = conn;
    }
    public int CreateCursor(string sqlCmd)
    {
        var cursor = new SqlCursor(sqlCmd, m_sqlConnection); 
        cursor.Create();
        return cursor.Count();
    }
}

public class SqlCursorReader : CursorReader
{
    //private SqlConnection m_sqlConnection;//no longer needed

    //by saving the connection in the factory, it will be available when needed later
    public SqlCursorReader(string sqlCmd, SqlConnection sqlConnection)
        :this(sqlCmd, new SqlCursorCreator(sqlConnection))
    { }
    protected SqlCursorReader(string sqlCmd, SqlCursorCreator creator)
        : base(sqlCmd, creator)
    { }
}

0

第一次尝试是将虚拟调用移出基类构造函数,但它有一些缺点:

  • m_rowCount不再是只读的
  • 派生类和所有未来的派生类都需要调用基类的Initialize()方法。

-

public abstract class CursorReader
{
    private int m_rowCount;//no longer read-only
    protected CursorReader()
    {
        //no virtual call here
    }
    protected abstract int CreateCursor(string sqlCmd);
    protected void Initialize()
    {
         //virtual call moved here
         m_rowCount = CreateCursor(sqlCmd); //virtual call !
    }
}
public class SqlCursorReader : CursorReader
{
    private SqlConnection m_sqlConnection;

    public SqlCursorReader(string sqlCmd, SqlConnection sqlConnection)
    {
        m_sqlConnection = sqlConnection;

        //the derived classes NEED to call the base class' Initialize() 
        Initialize();
    }
    protected override int CreateCursor(string sqlCmd)
    {
        //uses not-yet-initialized member m_sqlConnection
        var cursor = new CustomCursor(sqlCmd, m_sqlConnection); 
        return cursor.Count();
    }
}

我特别不喜欢第二个要点...


这是一个答案还是一个更新?看起来应该只是问题的一部分。 - Kendall Frey
@Kendall 这是一个可能的答案。我希望能得到一些建设性的批评。 - Cristian Diaconescu

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