2

Suppose we have a source IEnumerable sequence:

IEnumerable<Tuple<int, int>> source = new [] {
    new Tuple<int, int>(1, 2),
    new Tuple<int, int>(2, 3),
    new Tuple<int, int>(3, 2),
    new Tuple<int, int>(5, 2),
    new Tuple<int, int>(2, 0),
};

We want to apply some filters and some transformations:

IEnumerable<int> result1 = source.Where(t => (t.Item1 + t.Item2) % 2 == 0)
                                 .Select(t => t.Item2)
                                 .Select(i => 1 / i);
IEnumerable<int> result2 = from t in source
                           where (t.Item1 + t.Item2) % 2 == 0
                           let i = t.Item2
                           select 1 / i;

These two queries are equivalent, and both will throw a DivideByZeroException on the last item.

However, when the second query is enumerated, the VS debugger will let me inspect the entire query, thus very handy in determining the source of the problem.

VS Debugger is useful

However, there is no equivalent help when the first query is enumerated. Inspecting into the LINQ implementation yields no useful data, probably due to the binary being optimized:

LINQ is too optimized

Is there a way to usefully inspect the enumerable values up the "stack" of IEnumerables when not using query syntax? Query syntax is not an option because sharing code is impossible with it (ie, the transformations are non trivial and used more than once).

felipe
  • 662
  • 4
  • 16

1 Answers1

1

But you can debug the first one. Just insert a breakpoint on any one of the lambdas and you're free to inspect the values of the parameters or whatever else is in scope.

enter image description here

When debugging you can then inspect the values of (in the case of breaking within the first Where) t, t.Item1, etc.

enter image description here

As for the reason that you can inspect t when performing the final select in your second query, but not your first, it's because you haven't created equivalent queries. The second query you wrote, when written out by the compiler, will not generate something like your first query. It will create something subtly, but still significantly, different. It will create something like this:

IEnumerable<int> result1 = source.Where(t => (t.Item1 + t.Item2) % 2 == 0)
                        .Select(t => new
                        {
                            t,
                            i = t.Item2,
                        })
                        .Select(result => 1 / result.i);

A let call doesn't just select out that value, as the first query you wrote does. It selects out a new anonymous type that pulls out the value from the let clause as well as the previous value, and then modifies the subsequent queries to pull out the appropriate variable. That's why the "previous" variables (i.e. t are still in scope at the end of the query (at both compile time and runtime; that alone should have been a big hint to you). Using the query I provided above, when breaking on the select, you can see the value of result.t through the debugger.

Servy
  • 202,030
  • 26
  • 332
  • 449
  • Yes, but if the sequence is large then we will get lots of unrelated hits. And we can't (always) put an appropriate condition on the breakpoint, because we don't know (yet) what the problem is! The question is "how did 'i' become zero?", and it is not always trivial to determine the corresponding break condition in the Where clause. – felipe Aug 26 '13 at 17:54
  • Marking as answer, it appears there are no alternatives to creating intermediate types. Thanks! – felipe Aug 27 '13 at 23:01
  • @felipe As I said, that's what's going on under the hood in the query syntax case. This is, more or less, what the compiler is translating that query into, so it's not like this code here is less performant or anything like that. – Servy Aug 28 '13 at 13:48
  • It probably not less performant, but it is more cumbersome. Either I can't share the pre-processing, or I have to create some intermediate class to hold all intermediate data (I can't return anonymous types!), and then in the final result remove the intermediate type. – felipe Aug 28 '13 at 14:27
  • @felipe Sure, my point is simply that you're not being "saved" from this in query syntax. Both methods are using the same solution. It is *the* solution to this problem. It's not like you're missing out on something better that that query syntax is doing under the hood. – Servy Aug 28 '13 at 14:33
  • It appears I'm not explaining myself correctly. By sharing queries, I mean that the first Where+Select in the OP was done in one function (because it is used in multiple sites), and the final Select was done in a second function (refining the data obtained from the first function). In this situation I would have to make the first function return an intermediate type that contains all the query variables. That is what I think is cumbersome. – felipe Aug 28 '13 at 20:35