A team was proposing a method that was at its essence a Read(n)
that takes the maximum number of items to read. The reality was more complicated than that: The parameter n was really a configuration property on an operation, but it had the same meaning: It set the maximum number of items to return.
The team said that their method returns E_
if you pass n = 0.
I argued that this is an incorrect design: If somebody asks for “at most zero items”, then you should succeed and give them zero items. Zero is at most zero.
For example, maybe the program calculates the size of its window, divides by the height of an item, and requests only as many items as fit in the window without overflowing. After all, there’s no point fetching data that you never use.
And then the user resizes the window so small that no items fit, so the division rounds down to zero, and the program asks for zero items and crashes because “somebody” decided that it was wrong to ask for zero items.
Let them ask for zero items. Give them nothing.
Edge cases are hard, so remove edge cases from the interface.
Related reading: Embracing the power of the empty set in API design (and applying this principle to selectors and filters).
This is exactly why LINQ methods in C# are wrong to penalize callers with a NullReferenceException when passed a null source. Null is the most efficient version of an empty collection, and if you have to check it everywhere, you can’t really write fluent call chains efficiently. Any method that implicitly converts a null input to a non-null empty collection would be a blessing to any business software I’ve ever worked with.
>> There is no such rule or it would have been imposed from the start when there were barely any backwards-compatibility concerns
There is no rule because extension methods are just static methods with a well-defined format. The guidelines have always stated, and continue to do so, that extension methods SHOULD behave just like instance methods in terms of null UNLESS the method itself makes it explicitly clear that it handles it. You can go back to the original documentation on this, the design guidelines written around extension methods and even the original code analysis tools.
This is not an...
An empty collection is not the same as no collection.
Instance-like methods are intended to be called on non-null targets in C#, so any extension method should mimic that behavior unless specifically designed for the other case. Linq methods can be implemented on the instance and not from an extension, so throwing on null is the consistent choice.
@Joe Beans:
>In C++ you can use null pointers to find field offsets
NULL and nullptr_t are not always zero. It depends on your platform implementation. Address 0 points to the interrupt table or reset vector on many platforms. It is well likely to point to a valid memory region.
It is more robust to use `std::offsetof` or compiler builtins to fetch field offsets.
@Joe Beans: “That’s just your opinion. In practical terms and under the same principles as this blog post, they’re [empty collections and null references are] exactly the same. ”
They are not. A null reference means “There is no collection.” An empty collection means “I have a collection that has nothing in it.” If you allow null references to be returned when collections are expected, then everybody has to add a special case to check for null, and most will forget. See the linked article.
That's just your opinion. In practical terms and under the same principles as this blog post, they're exactly the same. There is no such rule or it would have been imposed from the start when there were barely any backwards-compatibility concerns. In C++ you can use null pointers to find field offsets but in C# the same pointer math gets a null reference exception even without reading or writing from the memory location....which tells me that they can and do enforce null when they want to. I created a LINQ method called WithoutNulls() that returns an empty collection if the...
Yes, far too many developers feel a strange compulsion to treat empty collections as a special case... returning null instead of empty if nothing is found, or checking if a collection is empty before iterating over it, that sort of thing. It's pointless, creating extra work for no benefit.
That said, your example does hint at one case where such sanity checks are meaningful - pagination. If I ask for a page of size 0, the obvious corollary is that I want to know how many such pages of data there are... at which point we're back to division-by-zero, and...
I’d argue that return an error on zero is the correct behaviour, it’s notifying the caller that they have a bug in their code. No-one goes to Amazon and orders zero batteries, or asks for zero litres of petrol at the pump, or goes to a takeaway to place an order for zero burgers, so passing in zero to a function like this should alert the caller via an error return, not continue as if nothing unusual was going on.
Requesting zero items is still useful for the containing type. The Windows Runtime's Windows.Security.Cryptography.CryptographicBuffer class suffers from this design flaw as described in Going beyond the empty set: Embracing the power of other empty things. I reviewed all the calls I make to the affected methods and substituted the null return value with an empty buffer, and relied on CryptographicBuffer.GenerateRandom(0) to produce that empty buffer while being type-indistinguishable from the intended Windows.Storage.Streams.IBuffer interface type (instead of constructing an empty Windows.Storage.Streams.Buffer instance type, which could be distinguishable).
This return value might be implemented using a private type with custom method...
I’ll argue that’s it’s easy to do this exact thing: you put two kinds of batteries into the shopping cart, then find out which one you really need. So you go and set the number requested of that battery to zero.
“No-one…goes to a takeaway to place an order for zero burgers”
But people do go to, say, Starbucks and order zero drinks (if you’re meeting somebody there for an interview).
Computer programs are not human beings. Analogies of what a person would or wouldn't do don't really work. Would you crash and burn if something unexpected happened in your life?
You may have missed it, but Raymond has given an example in the post where asking for zero items is not a bug. Whenever your caller arrives at your parameter value via a calculation, they may happen to arrive at one of the edge cases.
To put it in different terms: you either handle a request for zero items yourself, or you force all your callers to do their own zero checks...
Completely agree about eliminating edge cases and aggravating exceptions/errors like that, but following convention is more important. For example, in many APIs a limiting value of 0 means "unlimited" so if that's the case here then this method should behave the same.
Of course, sometimes -1 means unlimited. Or any negative number. Or you're supposed to use . Or . Etc, etc. One of my favorites is the conflict in .NET between and .
I like having an explicit way to signal that a response should be unlimited, but admit that just using a Very...
I don’t really see a conflict between TimeSpan.MaxValue and Timeout.Infinite. The latter is used for (Win32) APIs like Sleep(DWORD dwMilliseconds), for which any overload taking a TimeSpan is a convenience method. Under the hood, TimeSpan is a LONGLONG value, in which the binary value represented by Timeout.Infinite is a perfectly valid negative value. As the binary value of (DWORD)-1 is all bits set, the best possible equivalent for a TimeSpan would be TimeSpan.MaxValue, which also has all bits set.
A slight issue with this are other method contracts. For example when reading from a byte stream, the method could promise to return 0 if and only if the stream is at the end, so either it reads at least one element or indicates the end by returning 0. That is incompatible with reading 0 bytes, so you end up with an edge case either way.
A slight issue with this slight issue is that I believe that reading from a byte stream is not a good example.
Requesting 0 bytes is a valid operation, so only returning 0 at the end of the stream is questionable in the first place. To give two primary examples of this, both the UCRT's fread and Windows' ReadFile treat reading 0 bytes as a success. The return of fread is 0, but ferror is not set. ReadFile shows that the bytes read is 0, but it doesn't return FALSE.
An argument could be made for 0 being returned only when the...