如何在接口或基类中定义具有固定大小的数组

本文关键字:数组 定义 接口 基类 | 更新日期: 2023-09-27 18:36:32

我正在做一个宾果游戏,在宾果游戏中,一张卡片是一个 5x5 的数字网格(减去"自由"中心空间)。我正在寻找一种以强类型方式表示 5x5 网格的方法。它可能是整数、布尔值、某个类等的 5x5 网格。我最初的想法是使用 2 维数组,但这有一个问题,即我不允许指定大小,因此在传递对象时,其他类无法知道它应该有 5 行和 5 列。

例如,我可能会创建接口:

public interface ICard
{
    int[,] cells { get; }
}

但是这里没有明确指出整数数组有 5 行和 5 列。 此外,为了定义要匹配的模式,我可能会使用 5x5 的布尔网格,所以我希望它看起来更像这样:

public interface ICard<T>
{
    T[,] cells { get; }
}

那么我如何更改它以返回一个强类型的 Card 对象,该对象强制执行应该只有 5 行和 5 列的规则,并使其显而易见。我认为我的问题的答案是创建一个新的 Card 类,但我不确定如何以一种优雅的方式强制执行并使其明显地表明它是一个 5x5 网格。

任何想法都值得赞赏。谢谢。

派生答案

放开拳头,感谢所有这么快提供答案的人。基于所有答案,我想出了一些混合方法。这是我最终做的事情:

创建了一个新的泛型矩阵接口和类:

public interface IMatrix<T>
{
    int NumberOfColumns { get; }
    int NumberOfRows { get; }
    T GetCell(int column, int row);
    void SetCell(int column, int row, T value);
}
public class Matrix<T> : IMatrix<T>
{
    protected readonly T[,] Cells;
    public int NumberOfColumns { get; }
    public int NumberOfRows { get; }
    public Matrix(int numberOfColumns, int numberOfRows)
    {
        NumberOfColumns = numberOfColumns;
        NumberOfRows = numberOfRows;
        Cells = new T[numberOfColumns, numberOfRows];
    }
    public T GetCell(int column, int row)
    {
        ThrowExceptionIfIndexesAreOutOfRange(column, row);
        return Cells[column, row];
    }
    public void SetCell(int column, int row, T value)
    {
        ThrowExceptionIfIndexesAreOutOfRange(column, row);
        Cells[column, row] = value;
    }
    private void ThrowExceptionIfIndexesAreOutOfRange(int column, int row)
    {
        if (column < 0 || column >= NumberOfColumns || row < 0 || row >= NumberOfRows)
        {
            throw new ArgumentException($"The given column index '{column}' or row index '{row}' is outside of the expected range. Max column range is '{NumberOfColumns}' and max row range is '{NumberOfRows}'.");
        }
    }
}

然后,我的实际 Card 对象在构造函数中获取一个 IMatrix,并验证它是否具有预期的行数和列数:

public interface ICard
{
    int NumberOfColumns { get; }
    int NumberOfRows { get; }
    ICell GetCellValue(int column, int row);
    bool Mark(int number);
    bool Unmark(int number);
}
public class Card : ICard
{
    // A standard Bingo card has 5 columns and 5 rows.
    private const int _numberOfColumns = 5;
    private const int _numberOfRows = 5;
    private IMatrix<ICell> Cells { get; } = new Matrix<ICell>(_numberOfColumns, _numberOfRows);
    public Card(IMatrix<ICell> numbers)
    {
        if (numbers.NumberOfColumns != NumberOfColumns || numbers.NumberOfRows != NumberOfRows)
            throw new ArgumentException($"A {numbers.NumberOfColumns}x{numbers.NumberOfRows} matrix of numbers was provided for the Card with ID '{id}' instead of the expected {NumberOfColumns}x{NumberOfRows} matrix of numbers.", nameof(provider));
        for (int column = 0; column < NumberOfColumns; column++)
        {
            for (int row = 0; row < NumberOfRows; row++)
            {
                var number = numbers.GetCell(column, row);
                var value = (column == 2 && row == 2) ? new Cell(-1, true) : new Cell(number);
                Cells.SetCell(column, row, value);
            }
        }
    }
    public int NumberOfColumns => _numberOfColumns;
    public int NumberOfRows => _numberOfRows;
    public ICell GetCellValue(int column, int row) => Cells.GetCell(column, row).Clone();
    public bool Mark(int number)
    {
        var cell = GetCell(number);
        if (cell != null)
        { 
            cell.Called = true;
        }
        return cell != null;
    }
    public bool Unmark(int number)
    {
        var cell = GetCell(number);
        if (cell != null)
        {
            cell.Called = false;
        }
        return cell != null;
    }
    ...
}

我喜欢这种方法,因为它通过 IMatrix 属性使行和列的数量变得明显,并允许我轻松地添加另一个 LargeCard 类,该类可以采用 10x10 矩阵或我需要的任何内容。由于它们都使用接口,因此应该意味着需要最少的代码更改。此外,如果我决定在内部使用 List 而不是多维数组(可能是出于性能原因),我需要做的就是更新 Matrix 类实现。

如何在接口或基类中定义具有固定大小的数组

如果您需要类似cell[row, column],这可能是一个建议:

static void Main()
{
    var card = new Card();
    card.cells[3, 2] = true;
    Console.WriteLine(card.cells[2, 4]); // False
    Console.WriteLine(card.cells[3, 2]); // True
    Console.WriteLine(card.cells[8, 9]); // Exception
}
public interface ICard
{
    Cells cells { get; set; }
}
public class Card : ICard
{
    Cells _cells = new Cells();
    public Cells cells { get { return _cells; } set { _cells = value; } }
}
public class Cells : List<bool>
{
    public Cells()
    {
        for (int i = 0; i < 25; i++)
        {
            this.Add(false);
        }
    }
    public virtual bool this[int row, int col]
    {
        get
        {
            if (row < 0 || row >= 5 || col < 0 || col >= 5) throw new IndexOutOfRangeException("Something");
            return this[row * 5 + col];
        }
        set
        {
            if (row < 0 || row >= 5 || col < 0 || col >= 5) throw new IndexOutOfRangeException("Something");
            this[row * 5 + col] = value;
        }
    }
}

无法指定方法或属性返回的数组的实际大小。更好的方法是让代码处理任何大小的数组,并使用 Array.GetUpperBound 方法在运行时确定实际大小的实际大小。

bool[,] cells = obj.cells;
for(int i = 0; i <= cells.GetUpperBound(0); i++) {
    for(int j = 0; j <= cells.GetUpperBound(1); j++) {
        // Do something with cells[i,j] 
    }
}

另外,我会将接口更改为使用方法而不是属性:

public interface ICard<T>
{
    T[,] GetCells();
    T GetCell(int row, column);
}

为了确保卡是固定大小,您可以将数组的大小传递到实现ICard的构造函数中:

public class Card : ICard
{    
    ...
    public const int MaxRows = 5;
    public const int MaxColumns = 5;
    private readonly int _rows;
    private readonly int _columns;
    public Card(int rows, int columns)
    {
        if(columns > MaxColumns || rows > MaxRows) 
        {
            throw new ArgumentExcetion(...);
        }
    }
    ...
    public int Rows { get { return _rows; } }
    public int Columns { get { return _columns; } } 
}

这样,您可以限制卡的最大大小。然后,如果您对允许的最大大小改变主意,请更改MaxRowsMaxColumns,一切都应该继续工作。如果你想要不同大小的卡片,那么只需将不同的值传递到构造函数中。

如果您一直想使用固定大小,请添加默认构造函数,如下所示:

public Card()
    : this(MaxRows, MaxColumns)
{
}

无论您选择哪种实现方式,都不应该公开该信息。您已经有卡的接口。但是你需要的是几种能够以安全的方式提供细胞的方法。例如索引器:

public Cell this[int row, int column]
{
   // check for boundaries
   return _cells[row, column];
}

或其他一些帮助程序方法,例如:

public IEnumerable<Cell> GetRow(int row)
{
    // return all cells of specified row
}

和一个属性 CardSize,表示卡片上有多少列和行。CardSize 应该是在构造函数中初始化的只读信息。

当我在考虑它时,您将需要另一种卡方法,例如"SetCell","MoveCell","ClearCell"。(对不起,我不知道宾果游戏是如何工作的。

我最初的想法是使用 2 维数组,但这有一个问题,即我不允许指定大小,因此在传递对象时,其他类无法知道它应该有 5 行和 5 列。

不要使用数组。

我会这样写:

public class Cell
{
  public int Value { get; set; }
  public bool Flagged { get; set; }  // terrible name imho
}
public class Board
{
  private List<List<Cell>> Rows;
  private List<List<Cell>> Columns;      
  public Board(IEnumerable<Cell> cells)
  {
    if (cells == null)
      throw new ArgumentNullException();
    if (cells.Count() != 25)
      throw new ArgumentException("cells must contain exactly 25 cells");
    // additional logic to make sure you don't have same values
    // or to many numbers in a specific range etc
    var orderedCells = cells.OrderBy(c => c.Value);
    Columns = new List<List<Cell>>()
    {
      orderedCells.Take(5).ToList(),
      orderedCells.Skip(5).Take(5).ToList(),  
      orderedCells.Skip(10).Take(5).ToList(),  
      orderedCells.Skip(15).Take(5).ToList(), 
      orderedCells.Skip(20).Take(5).ToList()  
    }
    Rows = Enumerable.Range(0,5)
      .Select(i => new List<Int>()
        {
          Columns.ElementAt(0).Skip(i).First(),
          Columns.ElementAt(1).Skip(i).First(),
          Columns.ElementAt(2).Skip(i).First(),
          Columns.ElementAt(3).Skip(i).First(),
          Columns.ElementAt(4).Skip(i).First(),
        })
      .ToList();
  }
  public IEnumerable<Cell> GetRow(int index)
  {
    if (index < 0 || index >= Rows.Count())
      throw new ArgumentOutOfRangeException();
    return Rows.ElementAt(index);
  }

  public IEnumerable<Cell> GetColumn(int index)
  {
    if (index < 0 || index >= Rows.Count())
      throw new ArgumentOutOfRangeException();
    return Rows.ElementAt(index);
  }

  public Cell GetCell(int column, int row)
  {
    if (column < 0 || column >= Columns.Count())
      throw new ArgumentOutOfRangeException();
    if (row < 0 || row >= Rows.Count())
      throw new ArgumentOutOfRangeException();

    return Rows.ElementAt(row).ElementAt(column);
  }
}

创建一个从 List 派生的自定义列表类型,在默认构造函数中强制执行容量,例如

class ListOfFives<T> : List<T>
{
  public ListOfFives<T>(): base(capacity:5)
  { 
  }
}

然后在您的界面中使用它

public interface ICard
{
    ListOfFives<ListOfFives<T>> Cells { get; }
}

出于显而易见的原因,您需要使用列表而不是数组。给出一个显式的类型名称来解释约束将使列表的实现者和用户一目了然。