Debugging Features in C# 3.0 Part 1

Overview

C# 3.0 introduces many new constructs and opens entirely new ways of thinking and developing code. In this article I will talk about the new debugging features that make it easy to see the running code and better understand it. In my experience one spends as much time writing the initial code as debugging it later. In fact stepping though the code and inspecting variables and expressions is one of the best ways to understand it. So its not just the original writer but many more who will eventually debug it. To this end there are many new things that you will notice when you start debugging in a C# 3.0 project, I will enumerate few of them in this part of the article.

Results View

The primary aim of the results view is to provide a array like view for Enumerable objects. This allows the user to see the items that are the results of the Query. The Query/Enumeration might be on a local collection(Linq/Xlinq/iterators) or from a remote store(Dlinq/customProviders). The idea here is not to change/hide the object's default structure while adding a simple way to inspect the result. This is quite different from debugger proxies which hide the actual view of the object under a "raw" node and show the users the custom view. The rational being that there are many useful properties/fields on the users query and we wanted to add this functionality without loosing all that information. This brings up 2 interesting questions.

1. When is this applied?

The Results View is applied to an object/struct if

  1. It does not have a Debugger type proxy 
  2. Does not implement IList or ICollection (they already have items view)  
  3. It does implement IEnumerable or IEnumerable<T>.

2. What does the View look like?

  1. For a object whose compile time type is an interface of type IEnumerable, IEnumerable<T>, IQueryable or IQueryable<T>, we hide the derived node and show the runtime type’s  members directly under the object  .
  2. A new node called “Results View” is added to the Expression which when expanded will enumerate the Enumerable.    
  3. The Results View's value column warns the user that expanding will change the state of the object being inspected.
  4. A new format specifier is added to represent the view and can be accessed by typing object/expression, results.

E.g. for query like

int[] array  = new int[]{ 23, 3,54, 8, 10, 39, 87, 3, 7};
var q = from i in array.AsQueryable()
        let y = i * i
        let z = y * y
        select new { y, i, z };

System.Diagnostics.Debugger.Break();

here we can see the Results View Node is added as a child of the Query q. Though q has a static type of IQueryable<SomeAnontype>, we directly show the members of the actual runtime type. The value column has a important message for the user, stating that expanding this view will enumerate the object.If the base of the expression in watch is also an enumerable the results view is added to it. If a field/property which implements IEnumerable is hidden then the results view inherits the hidden property from its parent.

Finally we can also access the results view by using the results specifier “, results”.

There are some exceptions to the rule, Sytem.String is excluded from having this view though it implements IEnumerable.

Extension Methods

Extension methods can now be used when calling an instance method in the watch and the immediate window. This is very useful if you want to  see

  1. Which extension method will get called if the statement was placed here in the code.
  2. The results of a Extension method not present in the code being executed.
  3. Limit the number of elements returned by a query, before expanding its results.
  4. Most importantly since Queries boil down to extension methods, we can use the watch/immediate like scratch pad to try out some Ad-hoc Query like behavior.  

Consider the following code

    public class Program
    {
        static void Main(string[] args)
        {
            int[] array = new int[] { 1, 23, 45, 67, 12 };      // simple in memory array

            Func<Func<int, bool>, IEnumerable<int>> where = array.Where<int>;  //curry an Extension method

            System.Func<int, int> f = Program.identity;
            System.Diagnostics.Debugger.Break();
        }

        public static bool Eval(int i){ return i > 5; }

        public static bool Eval1(int val) { return val < 50; }

        public static int identity(int x) { return x; }
    }

Trim the Enumerable before inspecting it ( very useful if this call will be remoted to the provider of the extension method)

Calling an Extension methods with a method (predicate) as argument

Using Curried delegate from the code

Chaining extension methods to archive Ad-hoc query like execution

This assumes that the assembly contain the extension method is already loaded by an earlier runtime call.

All in all using these 2 features in combination allows users to do some powerful analysis on a  program while it executes. Though not as powerfully as a full fledged Query Analyzer with lambdas, it does allows the user to do very interesting stuff. I am interested in knowing potential pain points that you come across, things that are in your way when debugging etc.

In the coming article i will cover Stepping, Anonymous-types, Range-variables and Xlinq Support.

kick it on DotNetKicks.com

Comments

  • Anonymous
    May 29, 2007
    You've been kicked (a good thing) - Trackback from DotNetKicks.com

  • Anonymous
    May 29, 2007
    The comment has been removed

  • Anonymous
    May 29, 2007
    Does the debugger take into account that not all enumerators have a set number or results, or that the results might be quite large?  You might have an enumerable that returns an unending sequence of something.  The debugger should be able to handle the "accidental" selection of the results view w/o freezing the IDE.

  • Anonymous
    May 30, 2007
    Thanks Matthieu and yes there will be intellisence in the watch and the immediate window for extension methods and in fact i will update the article to mention/show it. I understand Onovotny, an Enumeration can certainly be infinite and there is no clear way of identifying this. An Infinite Enumeration will behave just like a function that times out when entered in the watch. In fact i will be metioning this in my next article on steeping. The other case to think of is when the enumeration yields zero results, we give an error message for that ,again i will update the article to mention this. thanks guys and keep it comming

  • Anonymous
    May 30, 2007
    Sree, Maybe it's too late, but I hope not -- how about an attribute that can go on enumerators (and enumerable methods -- yield return) that's a debugger hint not to eval.  Something Like DebuggerNoEnumerateAttribute.  

  • Anonymous
    June 01, 2007
    The comment has been removed

  • Anonymous
    June 20, 2007
    Welcome to the XXVIII Community Convergence. In these posts I try to wrap up events that have occurred

  • Anonymous
    April 04, 2011
    Hi Sree, I am very confused by your statement, that Results View is shown when the object Does not implement IList or ICollection "because they already have items view", I am not seeing that they indeed have one: If I do something like " BitArray bitty = new BitArray(new[] {false, false, true, false});" and look at bitty in the debugger, I don't get to see the items contains within it even though BitArray is an ICollection. Even if I try to force the debugger to show me the items by evaluating "bitty, results", then I get a message "Only Enumerable types can have Results View", even though bitty is of course also an IEnumerable. Can you please explain this? Thanks very much for your excellent blog!