When writing a method, you have a choice, when something goes wrong, to return some kind of failure code or throw an exception. In general, you throw an exception when the error is outside the normal workflow—or if you expect that the immediate caller won’t be able to cope with it. Occasionally, though, it can be best to offer both choices to the consumer. An example of this is the int type, which defines two versions of its Parse method:
public int Parse (string input); public bool TryParse (string input, out int returnValue);
If parsing fails,Parsethrows an exception;TryParse returnsfalse.
You can implement this pattern by having theXXX method call theTryXXX method as follows:
public return-type XXX (input-type input) { return-type returnValue; if (! TryXXX (input, out returnValue)) throw new YYYException (...) return returnValue; }
The atomicity pattern
It can be desirable for an operation to be atomic, where it either successfully completes or fails without affecting state. An object becomes unusable when it enters an indeterminate state that is the result of a half-finished operation. finally blocks facilitate writing atomic operations.
In the following example, we use anAccumulatorclass that has anAddmethod that adds an array of integers to its fieldTotal. TheAddmethod will cause anOverflowException ifTotalexceeds the maximum value for anint. TheAdd method is atomic, either successfully updatingTotalor failing, which leavesTotalwith its former value.
class Test { static void Main() { Accumulator a = new Accumulator (); try { a.Add (4, 5); // a.Total is now 9 a.Add (1, int.MaxValue); // will cause OverflowException } catch (OverflowException) { Console.WriteLine (a.Total); // a.Total is still 9 } } }
In the implementation ofAccumulator, theAdd method affects theTotalfield as it executes. However, if anything goes wrong during the method (e.g., a numeric overflow, a stack overflow, etc.),Totalis restored to its initial value at the start of the method.
public class Accumulator { public int Total;
public void Add(params int[] ints) { bool success = false; int totalSnapshot = Total; try { foreach (int i in ints) { checked { Total += i; } } success = true; } finally { if (! success) Total = totalSnapshot; } } }
Alternatives to exceptions
As with int.TryParse, a function can communicate failure by sending an error code back to the calling function via a return type or parameter. Although this can work with simple and predictable failures, it becomes clumsy when extended to all errors, polluting method signatures and creating unnecessary complexity and clutter. It also cannot generalize to functions that are not methods, such as operators (e.g., the division operator) or properties. An alternative is to place the error in a common place where all functions in the call stack can see it (e.g., a static method that stores the current error per thread). This, though, requires each function to participate in an error-propagation pattern that is cumbersome and, ironically, itself error-prone.
An enumerator is a read-only, forward-only cursor over a sequence of values. An enumerator is an object that either:
ImplementsIEnumeratororIEnumerator<T>
Has a method namedMoveNext for iterating the sequence, and a property calledCurrentfor getting the current element in the sequence
Theforeachstatement iterates over an enumerable object. An enumerable object is the logical representation of a sequence. It is not itself a cursor, but an object that produces cursors over itself. An enumerable object either:
ImplementsIEnumerableorIEnumerable<T>
Has a method namedGetEnumeratorthat returns an enumerator
IEnumeratorandIEnumerableare defined inSystem.Collections.
IEnumerator<T>andIEnumerable<T>are defined inSystem.Collections.Generic.
The enumeration pattern is as follows:
class Enumerator // typically implements IEnumerator or IEnumerator<T> { public IteratorVariableType Current { get {...} } public bool MoveNext() {...} }
class Enumerable // typically implements IEnumerable or IEnumerable<T> { public Enumerator GetEnumerator() {...} }
Here is the high-level way of iterating through the characters in the word “beer” using aforeachstatement:
foreach (char c in "beer") Console.WriteLine (c);
Here is the low-level way of iterating through the characters in “beer” without using aforeachstatement:
var enumerator = "beer".GetEnumerator();
while (enumerator.MoveNext()) { var element = enumerator.Current; Console.WriteLine (element); }
Theforeachstatement also acts as ausing statement, implicitly disposing the enumerator object.
Whereas a foreach statement is a consumer of an enumerator, an iterator is a producer of an enumerator. In this example, we use an iterator to return a sequence of Fibonacci numbers (where each number is the sum of the previous two):
using System; using System.Collections.Generic;
class Test { static void Main() { foreach (int fib in Fibs(6)) Console.Write (fib + " "); }
static IEnumerable<int>Fibs(int fibCount) { for (int i = 0, prevFib = 1, curFib = 1; i < fibCount; i++) { yield return prevFib; int newFib = prevFib+curFib; prevFib = curFib; curFib = newFib; } } }
OUTPUT: 1 1 2 3 5 8
Whereas areturnstatement expresses “Here’s the value you asked me to return from this method,” ayield return statement expresses “Here’s the next element you asked me to yield from this enumerator.” On eachyieldstatement, control is returned to the caller, but the callee’s state is maintained so that the method can continue executing as soon as the caller enumerates the next element. The lifetime of this state is bound to the enumerator, such that the state can be released when the caller has finished enumerating.
An iterator is a method, property, or indexer that contains one or more yield statements. An iterator must return one of the following four interfaces (otherwise, the compiler will generate an error):
Theyield breakstatement indicates that the iterator block should exit early, without returning more elements. We can modifyFooas follows to demonstrate: