Does the ICollection<T>.Add()
-implementation of arrays break the Liskov substitution principle? The method results in a NotSupportedException
, which does break the LSP, IMHO.
string[] data = new string[] {"a"};
ICollection<string> dataCollection = data;
dataCollection.Add("b");
This results in
Unhandled exception: System.NotSupportedException: Collection was of
a fixed size.
I found a pretty similar question concerning Stream
-implementations. I open a separate question, because this case is pretty different: Liskov substitution principle and Streams.
The difference here is that ICollection
does not provide a CanAdd
-Property or such thing, as the Stream
-class does.
I can see why you'd think so. There's a function that expects a collection, and it expects it to be modifiable. Passing an array will make it fail, so clearly you can't substitute the interface with this particular implementation, right?
Is it a problem? Maybe. It depends on how often you expect ideals to hold. Are you going to use an array instead of a collection by accident and then be surprised ten years later that it breaks down? Not really. The type system .NET applications use isn't perfect - it doesn't tell you this particular ICollection<T>
usage requires the collection to be modifiable.
Would .NET be better off if arrays didn't pretend to implement ICollection<T>
(or IEnumerable<T>
, which they also don't "really" implement)? I don't think so. Is there a way to keep the convenience of arrays "being" ICollection<T>
that would also avoid the same LSP violation? Nope. The underlying array would still be fixed-length - at best, you'd be violating more useful principles instead (like the fact that reference types are not expected to have referential transparency).
But wait! Let's look at the actual contract of ICollection<T>.Add
. Does it allow for a NotSupportedException
to be thrown? Oh yes - quoting MSDN:
[NotSupportedException is thrown if ...] The ICollection is read-only.
And arrays do return true when you query IsReadOnly
. The contract is upheld.
If you consider Stream
not to break LSP because of CanWrite
, you must consider arrays to be valid collections, since they have IsReadOnly
, and it is true
. If a function accepts a read-only collection and tries adding to it, it's an error in the function. There's no way to specify this explicitly in C#/.NET, so you have to rely on other parts of the contract than just types - e.g. the documentation for the function should specify that a NotSupportedException
(or ArgumentException
or whatever) is thrown for a collection that is readonly. A good implementation would do this test right at the start of the function.
One important thing to note is that types aren't quite as constrained in C# as in the type theory where LSP is defined. For example, you can write a function like this in C#:
bool IsFrob(object bobicator)
{
return ((Bob)bobicator).IsFrob;
}
Can bobicator
be substituted with any supertype of object
? Clearly not. But it just as clearly isn't a problem of the poor Frobinate
type - it's an error in the IsFrob
function. In practice, a lot of code in C# (and most other languages) only works with objects far more constrained than would be indicated by the type in the method signature.
An object only violates the LSP if it violates the contract of its supertype. It cannot be responsible for other code violationg LSP. And often you'll find it quite pragmatic to make code that doesn't perfectly hold up under LSP - engineering is, and always has been, about trade-offs. Weigh the costs carefuly.
No, as it's not a class - the relationship between an interface and the implementing class is not the same as the relationship between super and subclass.
LSP specifically applies to behaviour of code which implies implementation - an interface has no implementation so LSP does not apply.
It is, however, a violation of the Interface Segregation Principle which says that you should rather compose interfaces to avoid unimplemented methods.