Freigeben über


Lazy Evaluation requires you to think outside the box

I was watching a presentation today and there was a point brought up that I found really interesting.

Say you have the following LINQ code:

    1: int[] iArray = new int[] { 5, 4, 3, 2, 1, 0 };
    2:  
    3: IEnumerable<int> results = iArray.Select(i => 100 / i);
    4:  
    5: foreach (int i in results)
    6:     Console.WriteLine(i.ToString());

Pretty simple, right.  We are taking the values in an int array and dividing each into 100 and then outputting the result.  But what happens if the starting array contains a 0, as in the sample above?  This will cause a DivideByZeroException.

Thinking about this using typical exception logic, then all we need to do is wrap the arithmetic statement in a try/catch block.  This would look like:

    1: int[] iArray = new int[] { 5, 4, 3, 2, 1, 0 };
    2: IEnumerable<int> results = null;
    3:  
    4: try
    5: {
    6:     results = iArray.Select(i => 100 / i);
    7: }
    8: catch (DivideByZeroException)
    9: {
   10:     // Handle DBZE here
   11: }
   12:  
   13: foreach (int i in results)
   14:     Console.WriteLine(i.ToString());

OK, fixed right?  Well when you run this code, you still will get the DivideByZeroException. 

The reason is that LINQ uses lazy evaluation when executing the statement.  Essentially, this means that the LINQ statements are not executed until they are actually needed.  In fact, if you ran the following code you will never get the DivideByZeroException:

    1: int[] iArray = new int[] { 5, 4, 3, 2, 1, 0 };
    2:  
    3: IEnumerable<int> results = iArray.Select(i => 100 / i);
    4:  
    5: // Do other work without referencing "results"
    6: ...

This is because of lazy evaluation.  Since the "result" object is never used, the computation contained in the Select() query is never executed.

Going back to the original example, the following shows the correct way to apply exception handling to the code:

    1: int[] iArray = new int[] { 5, 4, 3, 2, 1, 0 };
    2:  
    3: IEnumerable<int> results = iArray.Select(i => 100 / i);
    4:  
    5: // Do exception handling around the reference
    6: try
    7: {
    8:     foreach (int i in results)
    9:         Console.WriteLine(i.ToString());
   10: }
   11: catch (DivideByZeroException)
   12: {
   13:     // Handle DBZE here
   14: }

This will catch the exception from the LINQ query.  As you can see, the methodology for doing this is somewhat different than the standard though of catching exceptions.  In fact, if you enumerate the "result" collection again later in the code, it will be reevaluated and if not wrapped in a try/catch block, it will cause another DivideByZeroException:

    1: int[] iArray = new int[] { 5, 4, 3, 2, 1, 0 };
    2:  
    3: IEnumerable<int> results = iArray.Select(i => 100 / i);
    4:  
    5: try
    6: {
    7:     foreach (int i in results)
    8:         Console.WriteLine(i.ToString());
    9: }
   10: catch (DivideByZeroException)
   11: {
   12:     Console.WriteLine("DBZE caught");
   13: }
   14:  
   15: // This will throw an exception
   16: foreach (int j in results)
   17:         Console.WriteLine(j.ToString());

You can think of the LINQ expression as simply a definition of what is to be queried, and that statement is not evaluated until the reference to that expression is actually enumerated against.  This is counter to the typical way of doing things and it can get you in trouble if you are not careful.

Comments

  • Anonymous
    August 07, 2008
    The comment has been removed