0

Code

Class

using System;
using System.Collections.Generic;

// In case you really, really want to know,
// this class is called Ticket in my real code and it's used as a Tambola ticket.
public class Foo
{
    public int?[,] Value { get; private set; } = new int?[3 , 9]

    public Foo () => Value = InternalGenerate()

    public static Foo Generate () => new() { Value = InternalGenerate() };
    protected static int?[,] InternalGenerate ()
    {
        // Generate a random instance of this class.
    }
}

Usage

class Program
{
    static void Main ()
    {
        Bar( new() );
    }

    static void Bar ( Foo foo )
    {
        int width = foo.GetLength(1);
        int height = foo.GetLength(0);
        int?[] array = foo.Cast<int?>().ToArray(); // convert foo to 1d array.

        // get "corners" of foo
        int tl = (int) array.First(e => e.HasValue);
        int fl = (int) array.Take(width).Last(e => e.HasValue);
        int lf = (int) array.Skip(( height - 1 ) * width).First(e => e.HasValue);
        int ll = (int) array.Last(e => e.HasValue);
        // this code comes from
        // stackoverflow.com/questions/68661235/get-corners-of-a-2d-array
    }
}

This code will fail.


Question

I have a class called Foo. It's actual value should be a 2d array of type int? - however, when I try to use an instance of Foo as a int?[,] (look at Usage section of code), it fails (which is expected).

So, how do I make it possible to use an instance of Foo as if it is a collection, instead of having to use {instance of Foo}.Value?
I'm not sure, but I believe this is called a wrapper.


What I've tried

I tried to inherit Foo from IEnumerable - but I've never created a custom collection type before. So, after hours of reading tutorials and staring at C#'s source code, I finally resorted to asking a question here, on stack overflow.

BUT, as I said, I wasn't able to implement IEnumerable - doing that may still be the answer to my question. In that case, please tell me how to do so.


This is probably my longest question yet

just-a-hriday
  • 148
  • 1
  • 13
  • It's a lot easier to inherit from an existing implementation, rather than having to implement the interface yourself. While it's not exactly a true multidimensional collection you could try: `public class Foo : List>` – Mikal Schacht Jensen Aug 09 '21 at 07:33
  • Well you can add your own `GetLength` method, and your own indexer (https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/indexers/) - it's hard to know whether that's all you need though. – Jon Skeet Aug 09 '21 at 07:33
  • 1
    @MikalSchachtJensen: Implementing `IEnumerable` is really easy via iterators; deriving from `List` is usually not a great idea in my experience. (It often exposes operations you don't want, for example.) – Jon Skeet Aug 09 '21 at 07:34
  • Your use-case with Cast<> can be handled with the non-generic Enumerator from Value. That is however not very efficient, using boxing. So convince us of a better more realistic use-case. – H H Aug 09 '21 at 07:58
  • Meaning: expand on `// do some stuff with array.` – H H Aug 09 '21 at 08:00
  • The line of code `int?[] array = foo.Cast() as int?[];` will NOT work, even if `foo` itself is a 2D array of `int?`. The result will always be null, so you will have to rethink your question. (Maybe it should be `int?[] array = foo.Cast().ToArray();`.) – Matthew Watson Aug 09 '21 at 08:26
  • @MatthewWatson Oh - okay. I'll fix that. – just-a-hriday Aug 10 '21 at 03:07
  • @HenkHolterman I did that, too. – just-a-hriday Aug 10 '21 at 03:22
  • Related: https://stackoverflow.com/q/68661235/60761 – H H Aug 10 '21 at 05:53
  • @hirday: so you already had an answer... What exactly is new here? – H H Aug 10 '21 at 05:55
  • @Henk That's a completely unrelated question. – just-a-hriday Aug 10 '21 at 07:26

2 Answers2

0

Here's what I eventually landed with:


Class

using System;
using System.Collections;
using System.Collections.Generic;

// In case you really, really want to know,
// this class is called Ticket in my real code and it's used as a Tambola ticket.
public class Foo
{
    protected int?[,] Value { get; set; } = new int?[3 , 9];

    public Foo () => Value = InternalGenerate();

    public static Foo Generate () => new() { Value = InternalGenerate() };
    protected static int?[,] InternalGenerate ()
    {
        // Generate a random instance of this class.
    }

    public IEnumerator<int?> GetEnumerator () => (IEnumerator<int?>) Value.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator () => Value.GetEnumerator();


    public int? this[int r , int c]
    {
        get => Value [r , c];
        private set => Value [r , c] = value;
    }

    public const rowCount = 3;
    public const columnCount = 9;
}

Usage

class Program
{
    static void Main ()
    {
        Bar( new() );
    }

    static void Bar ( Foo foo )
    {
        int?[] array = foo.Cast<int?>().ToArray(); // convert foo to 1d array.

        //get "corners" of foo
        int topLeft = (int) array.First(e => e.HasValue);
        int topRight(int) array.Take(columnCount).Last(e => e.HasValue);
        int bottomLeft = (int) array.Skip(( rowCount - 1 ) * columnCount).First(e => e.HasValue);
        int bottomRight = (int) array.Last(e => e.HasValue);
    }
}
just-a-hriday
  • 148
  • 1
  • 13
0

Actually there are still a few errors or problems in your code:

  • some syntax errors (missing semi-colons)
  • rowCount and columnCount do not necessarily reflect the actual values for your 'Valye' array.
  • public IEnumerator<int?> GetEnumerator () => (IEnumerator<int?>) Value.GetEnumerator(); will throw an 'InvalidCast' runtime exception.

In the code below those issues are solved:

public class Foo: IEnumerable<int?>
{
   public int?[,] Value { get; private set; } = new int?[3, 9];

   public int? this[int x, int y]
   {
       get => Value[x, y];
       set => Value[x, y] = value;
   }

   public Foo() => Value = InternalGenerate();

   public static Foo Generate() => new() { Value = InternalGenerate() };
   
   protected static int?[,] InternalGenerate()
   {
      // Generate a random instance of this class.
      return new int?[3, 9]
        {
            {1,2,3,4,5,6,7,8,9 },
            {9,8,null,6,5,4,3,2,1 },
            {1,2,3,4,5,6,7,8,9 }
        };
   }

   public int GetLength(int dimension) => Value.GetLength(dimension);

   public IEnumerator<int?> GetEnumerator()
   {
       foreach (var item in Value)
       {
           yield return item;
       }
   }

   IEnumerator IEnumerable.GetEnumerator()
   {
     return Value.GetEnumerator();
   }

}

Then you can instantiate a variable of type Foo (not an int?[,]) and use it as you want:

    static void Bar(Foo foo)
    {
        int width = foo.GetLength(1);
        int height = foo.GetLength(0);
        var array = foo; // convert foo to 1d array.

        // get "corners" of foo
        int tl = (int)array.First(e => e.HasValue);
        int fl = (int)array.Take(width).Last(e => e.HasValue);
        int lf = (int)array.Skip((height - 1) * width).First(e => e.HasValue);
        int ll = (int)array.Last(e => e.HasValue);
        // this code comes from
        // stackoverflow.com/questions/68661235/get-corners-of-a-2d-array
    }

If you want to be able to cast Foo to an int?[,], you will need to implement the cast-operations as well (if that is really necessary).

Johan Donne
  • 3,104
  • 14
  • 21