I sometimes end up having to write a method or property for a class library for which it is not exceptional to have no real answer, but a failure. Something cannot be determined, is not available, not found, not currently possible or there is no more data available.
I think there are three possible solutions for such a relatively non-exceptional situation to indicate failure in C# 4:
- return a magic value that has no meaning otherwise (such as
null
and-1
); - throw an exception (e.g.
KeyNotFoundException
); - return
false
and provide the actual return value in anout
parameter, (such asDictionary<,>.TryGetValue
).
So the questions are: in which non-exceptional situation should I throw an exception? And if I should not throw: when is returning a magic value perferred above implementing a Try*
method with an out
parameter? (To me the out
parameter seems dirty, and it is more work to use it properly.)
I'm looking for factual answers, such as answers involving design guidelines (I don't know any about Try*
methods), usability (as I ask this for a class library), consistency with the BCL, and readability.
In the .NET Framework Base Class Library, all three methods are used:
- return a magic value that has no meaning otherwise:
Collection<T>.IndexOf
returns -1,StreamReader.Read
returns -1,Math.Sqrt
returns NaN,Hashtable.Item
returns null;
- throw an exception:
Dictionary<,>.Item
throws KeyNotFoundException,Double.Parse
throws FormatException; or
- return
false
and provide the actual return value in anout
parameter:
Note that as Hashtable
was created in the time when there were no generics in C#, it uses object
and can therefore return null
as a magic value. But with generics, exceptions are used in Dictionary<,>
, and initially it didn't have TryGetValue
. Apparently insights change.
Obviously, the Item
–TryGetValue
and Parse
–TryParse
duality is there for a reason, so I assume that throwing exceptions for non-exceptional failures is in C# 4 not done. However, the Try*
methods didn't always exist, even when Dictionary<,>.Item
existed.
Best Answer
I don't think that your examples are really equivalent. There are three distinct groups, each with it's own rationale for its behaviour.
StreamReader.Read
or when there is a simple to use value that will never be a valid answer (-1 forIndexOf
).The examples you provide are perfectly clear for cases 2 and 3. For the magic values, it can be argued if this is a good design decision or not in all cases.
The
NaN
returned byMath.Sqrt
is a special case - it follows the floating point standard.