Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to define an array with a fixed size in an interface or base class

Tags:

arrays

c#

I'm making a Bingo game, and in Bingo a card is a 5x5 grid of numbers (minus the "free" center space). I'm looking for a way of representing that 5x5 grid in a strongly-typed way. It might be a 5x5 grid of ints, bools, some class, etc. My initial thought was to use a 2 dimensional array, but this has the problem that I'm not allowed to specify the size so when passing the object around, there's no way for other classes to know that it should have 5 rows and 5 columns.

For example, I might create the interface:

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

But no where here does it explicitly state that the integer array has 5 rows and 5 columns. Also, for defining a pattern to match, I'll likely be using a 5x5 grid of booleans, so I would want it to look something more like this:

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

So how can I change this to instead return a strongly-typed Card object that enforces the rules that there should only be 5 rows and 5 columns, as well as makes it obvious. I think the answer to my question is to create a new Card class, but I'm not sure how to do it in an elegant way that enforces and makes it obvious that it's a 5x5 grid.

Any thoughts are appreciated. Thanks.

Derived Answer

Fist off, thanks to everyone who provided answers so quickly. Based on all of the answers, I came up with a bit of a hybrid approach. Here's what I ended up doing:

Created a new generic Matrix interface and class:

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}'.");
        }
    }
}

My actual Card object then takes an IMatrix in the constructor and verifies that it has the expected number of Rows and Columns:

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;
    }

    ...
}

I like this approach because it makes the number of Rows and Columns obvious via the IMatrix properties, and allows me to easily add another LargeCard class down the road that can take a 10x10 matrix or whatever I need. Since they are all using interfaces it should mean minimal code change would be required. Also, if I decide that internally I want to use a List instead of a multi-dimensional array (maybe for performance reasons), all I need to do is update the Matrix class implementation.

like image 291
deadlydog Avatar asked Feb 27 '16 08:02

deadlydog


2 Answers

If you need something like cell[row, column] this maybe a suggestion:

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;
        }
    }
}
like image 131
Thanh Nguyen Avatar answered Oct 28 '22 12:10

Thanh Nguyen


There is no way of specifying the actual size of an array returned by a method or property. A better approach would be to have the code handle any size array and use the Array.GetUpperBound method to determine at runtime what the actual size actually is.

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] 
    }
}

Also, I would change the interface to use a method rather than a property:

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

To ensure that the card is a fixed size you could pass the size of the array into a constructor that implements 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; } } 
}

This way you can restrict the maximum size of the card. Then if you change your mind about the maximum size allowed, then change MaxRows and MaxColumns and everything should continue to work. If you want different sized cards, then you simply pass different values into the constructor.

If you always wanted to use the fixed size, then add a default constructor like so:

public Card()
    : this(MaxRows, MaxColumns)
{
}
like image 41
MotoSV Avatar answered Oct 28 '22 11:10

MotoSV