It would be very useful to have ToString overrides on our generic collections, to make it easier for a user in the debugger to see at a glance what's inside of them.
This could probably be mostly implemented in terms of a simple helper method for IEnumerable<T> that goes something like this:
return string.Join(enumerable.Select(obj => obj == null ? "null" : obj.ToString()), ", ");
Any thoughts on this? Good idea? Bad idea? (Please forgive me if the LINQ syntax isn't perfect; I don't do much work with lambdas in C#.)
On the MSDN page for Object.ToString() it lists some points about implementing ToString that I think enumerating through the types would break:
Personally I think we should look at the System.Diagnostics.DebuggerDisplayAttribute that most of the classes have and simply have ToString return that. For example List<T> shows Count = X.
@SamuelEnglard Granted, a large collection could produce a long string, but I'm curious about your second point. In what scenario would iterating over a collection and calling ToString() on each member produce observable side effects? Both of these operations are supposed to be read-only.
@SamuelEnglard The length issue could be solved by displaying only the first _n_ (say, 100) items from the collection. I think that would still help with debugging, while not causing issues for huge collections.
@masonwheeler while I do agree that both action should have no "real" side effects they can have them. A common one would be lazy loading memory. I've built many classes that if ToString is a heavy operation I cache the result. While not directly observable it does change the debugging environment.
A second point that also answers @sivarv too is that doing that will create lots of allocations. Every object in the collection will allocate a string. Even if we use a better memory strategy inside the collection ToString there will still be all those allocations. For example I have about 700 contacts in my address book. That's 700 allocations!
@SamuelEnglard 700 allocations?!? gasp That's... that's...
...nothing at all on modern hardware, really. Just this morning I was running dotMemory trying to figure out why a certain piece of code was spending about 40% of its time in GC. Turns out it was doing a few billion (yes, with a B) allocations over a period of about 2 minutes.
@masonwheeler It's nothing on my i7 with 24GB of memory sure, but on a RP2? Remember .NET Core runs on very low end devices too. (And G-d save you if you're on an Android Watch)
@SamuelEnglard If your ToString() breaks the requirements, then I think it's fine if List<T>.ToString() breaks them by calling your ToString(). It's your fault for breaking them.
And how come the debugger directly calling ToString() on your object is fine, while calling it indirectly through List<T> is a problem?
Also, few hundred (see my previous comment for why it would probably be less than 700) allocations every time you step in the debugger (i.e. at most about once a second) sounds perfectly acceptable to me, even on weak HW.
@SamuelEnglard A Raspberry Pi 2 has a full GB of RAM and enough hardware to play back HD movies. As for "Android watches,"
The days of ultra-low-end resource-constrained systems are over; we live in an age when $9 can get you this much hardware! _That's today's ultra-low-end system,_ and it wouldn't even blink at 700 string allocations.
If your ToString() breaks the requirements, then I think it's fine if List
.ToString() breaks them by calling your ToString() . It's your fault for breaking them.
A good point.
And how come the debugger directly calling ToString() on your object is fine, while calling it indirectly through List
is a problem?
Because the developer has chosen to do it. When VS just shows me the count I then chose to open the list. When ToString does it I don't get to decide if the collection should or should not be enumerated.
I concede on the performance issue.
And how come the debugger directly calling ToString() on your object is fine, while calling it indirectly through List is a problem?
Because the developer has chosen to do it.
The same thing happens when you have a local variable of your type. Or when your type is returned from a method. Do those also count as the developer "choosing" to do it? Or is there something different between calling ToString() on a single object when you have a local of your type and calling it on a small number of objects when you have List<YourType> (how small is up for debate, I proposed 100 above)?
To me there is but I admit that is very subjective.
I think a smaller number than 100 should be used but I think long run that could work.
I would though raise the question of if the System.Diagnostics.DebuggerDisplayAttribute should match the ToString here or not?
How do you ensure that your ToString() terminates. This is, after all, a legal collection.
List<object> bottomLess = new List<object>();
bottomLess.Add(bottomLess);
@AtsushiKan That's evil. Maybe it's okay not to solve that? For example, anonymous objects in VB don't:
Dim x = New With {.X = Nothing}
x.X = x
Calling x.ToString() here causes StackOverflowException, attempting to display it in debugger stops debugging. Though that experience could certainly be improved, and you could even argue it's a bug.
Given that the proposal to add ToString() is for better debugger view, all of the collections have DebuggerDisplay attribute on them which is what is displayed on hover over the locals, the ToString() override in Debugger will only be useful, only when calling collection.ToString() explicitly in watch, at which point you could use lambda in intermediate window to display the elements. Moreover, solving the string length issue by just showing the first few elements is a non-standard heurisitic, what if the most interesting objects are the recently added ones..
@masonwheeler Please feel free to reopen the issue if you have any new usage scenarios/data to support the proposal.
Most helpful comment
How do you ensure that your ToString() terminates. This is, after all, a legal collection.