1

I have been struggling with this issue for a while and I am not sure if it's even possible to deal without some magic. I have a class container that takes generics and stores them and might use other procedures. However, I struggle to extend my ToString() function that should take the generic elements and printing them if they are themselves a collection.

Here is my MCE:

namespace MyNamespace
{
    public class Container<T>
    {
        public T value1;
        public T value2;

        public Container()
        {
            value1 = default;
            value2 = default;
        }

        public Container(T firstValue, T secondValue)
        {
            value1 = firstValue;
            value2 = secondValue;
        }

        ...

        public override string ToString()
        {
            // Check if the type is a collection/enumerable and attempt to convert the elements of that collectiont/enumerable to String.
            if (typeof(T).GetInterface(nameof(IEnumerable<T>)) != null)
            {
                return string.Join(", ", value1) +"; " + string.Join(", ", value2); // THIS IS THE PROBLEM
            }

            // This part works
            return value1.ToString() + ", " + value2.ToString();
        }
    }
}

This manages to work when the generic is a basic datatype (int, float, uint, etc) but giving it a collection will just give back the collection's name. This is weird because extracting said collection from my class and using the same method does actually work as intended.

Container<uint[]> exampleContainer = new Container<uint[]>(new uint[4] { 0, 1, 2, 3 }, new uint[4] { 4, 5, 6, 7 });
Console.WriteLine("This doesn't work " + exampleContainer.ToString()); // This doesn't work System.UInt32[]; System.UInt32[]
Console.WriteLine("But this does " + string.Join(", ", exampleContainer.value1) + " " + string.Join(", ", exampleContainer.value2)); // But this does 0, 1, 2, 3 4, 5, 6, 7

Is there something I am missing here?

Note: I am using .Net framework but I am curious of how this applies to a general C# case

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
Jorge Diaz
  • 491
  • 5
  • 11
  • `if (typeof(T).GetInterface(nameof(IEnumerable)) != null)` did you check if this resolves to true, actually? And if it does, `value1` and `value2` are still `T`s not `IEnumerable`s. – Fildor Apr 18 '23 at 15:08
  • Have you actually set breakpoints in your code and stepped through it to see what it's doing? For instance, is it even entering your `if` block, as Fildor asked? – rory.ap Apr 18 '23 at 15:18
  • 1
    And to address your statement "I am using .Net framework but I am curious of how this applies to a general C# case" -- you have it backwards. .NET is the general, C# is just one language among a handful that you can use to write code targeting .NET. – rory.ap Apr 18 '23 at 15:20
  • @rory.ap Having said that, a careful reading of the C# spec does not mention .NET. This is because the designers wanted to run it on JVM also, but that never happened. – Charlieface Apr 18 '23 at 15:27
  • `typeof(T).GetInterface(nameof(IEnumerable)` checks whether the elements of your container are `IEnumerable`s yielding... elements of your container. I have no idea what you are trying to do, but it does not sound right. – Mike Nakis Apr 18 '23 at 15:28
  • @Fildor I did set break points and go through. It falls into the correct if branch for the given case. – Jorge Diaz Apr 21 '23 at 11:40
  • @rory.ap I think your question also falls on the same answer as fildor. On the subject of C# I think this is a larger question. However, my belief is that C# is the language that targets different .NET frameworks. Among them .NET Core and (the annoyingly named) .NET Framework which is what the question tag specifies. – Jorge Diaz Apr 21 '23 at 11:45
  • @JorgeDiaz "(the annoyingly named) .NET Framework" I'm wondering if you realize that for at least a decade, .NET Framework was all there was. I don't think they would have wanted to rename it when they came up with .NET Core, that would have been really confusing. – rory.ap Apr 21 '23 at 14:11

2 Answers2

4

You can check for non-generic IEnumerable (also and option to type check against it with type testing operators and pattern matching):

if (typeof(IEnumerable).IsAssignableFrom(typeof(T))
{
   // T is collection
   ...
}

And then use some LINQ with cast to IEnumerable:

var value1AsString = string.Join(", ", ((IEnumerable) value1).Cast<object>());;

Full code looking something like:

if (typeof(IEnumerable).IsAssignableFrom(typeof(T))
{
    return string.Join(", ", ((IEnumerable) value1).Cast<object>()) +"; " + string.Join(", ", ((IEnumerable) value2).Cast<object>()); 
}
// ...

Note that there are some types (for example string) which will pass this check but you potentially would not want to process as a collection. Also for collection of value types this will not be very performant approach due to boxing (though it can be mitigated if you really need to).

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • 2
    When using it don't forget that there are infinite enumerables... Just make sure your codebase does not have one before calling that "ToString" on one :) – Alexei Levenkov Apr 18 '23 at 17:50
  • 1
    @Guru Stron , Thank you this is the actual answer and very well explained. I didn't realize you could cast to the general IEnumerable. Will keep in mind your warning about string since I definately don't want it to pass. – Jorge Diaz Apr 21 '23 at 12:03
  • 1
    @JorgeDiaz was glad to help! Also read the Alexei Levenkov's comment about infinite/big enumerables, possibly you will want to limit number of elements in the output. – Guru Stron Apr 21 '23 at 12:05
  • @Guru Stron oh I wasn't sure what Alexei meant but yes, output of giant collections could definately cause problems – Jorge Diaz Apr 21 '23 at 13:45
1

This does the job:

public override string ToString()
{
    if (value1 is IEnumerable<object> e1 && value2 is IEnumerable<object> e2)
    {
        return $"{String.Join(", ", e1)}; {String.Join(", ", e2)}";
    }
    return $"{value1}, {value2}";
}

If I test it with this:

var c = new Container<string[]>()
{
    value1 = new string[] { "A", "B", },
    value2 = new string[] { "C", "D", },
};

Console.WriteLine($"{c}");

Then I get A, B; C, D printed to the console.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172