I have been working in a C# codebase for the last 4-5 months. I've summed up my experience with the language like this: C# is a really good language. C#'s stdlib however is subpar.
For the stdlib there is no one single problem it's death by a thousand cuts in a lot of various ways. Over reliance on inheritance for polymorphism which limits composability in a lot of ways. Too little use of interfaces.
Some of this seems like it's due to backwards compatibility concerns but it definitely degrades the developer experience with the language. Which is a shame because c# has a lot to offer.
I'm a C# developer of 10+ years. Your concerns are valid and no lost on many people. A class example being IList<T> and List<T>
I was once told that a lot of the original libs were effectively ported from C++ libs by C++ devs in a hurry. There are nuget packages with decent data structure / collections missing from the core BCL.
Interfaces like IEnumerable<T>, IComparable<T> and IEquatable<T> I find to be extremely useful and if you're following interface segregation principle and not chucking round the actual concrete implementations. I concede though that the BCL would be richer if one didn't have to think about this stuff too much.
I guess there's enough experienced C# devs around to know how to avoid those pitfalls. As the BCL is shrunk I expect it will be easier to substitute any structures your not happy with, with alternatives.
In a hurry is an understatement. Generics were allegedly implemented by Don Syme before the initial release of C#, but higher ups were more keen on shipping a substandard product because Generics were "only for academics".
Some of C#'s interfaces are simple and well designed, but the OOP model used is kind of weak for composition of them. For example, one would often like an IIndexable interface to cover both arrays and lists, or an numeric interface for which we can perform common operations over any number type. Without the ability to extend existing implementations with new interfaces, we're stuck using various patterns and wrappers to get the abstraction we need.
I was a C# developer for about 6 years before I got tired of the same old problems and decided it's not for me. I still use it occasionally when required, but not if other options are available.
> For example, one would often like an IIndexable interface to cover both arrays and lists
I’ve thought about this.
To be useful, an IIndexable would need get and set methods for the index, and a Length/Count property. You would need to explicitly implement it on something like the array class since you wouldn’t want to lose the semantic difference between Length (O(1)) and Count (O(n)).
But, the existence of Func<T, TResult> and Action<T1, T2> would largely obsolete the utility of such and interface.
Let xs be an “indexable” object of T (e.g. T[] or List<T>). Then any method that expects to only access an object could just accept lambda of type int -> T. So, you’d just pass it
n => xs[n].
Similarly, methods that expect to mutate the object could take a lambda of type (int, T) -> (). So would just pass something like
(n, x) => xs[n] = x.
This is nicer because it makes explicit what the method will do to the indexable.
I don't think there is the distinction between Length and Count you draw - Count is O(1) for most collections, even LinkedList<T> maintains the number of elements in the list in a separate field. You could say Length does not change during the lifetime of an object while Count may do so, but I am not even sure if this distinction is always honored.
Arrays actually do implement IList<T>; they did think of that. I guess the problem is that it's not discoverable. I'd second your call for an interface on numbers (including some way to "genericise" TryParse).
I guess the problem is that it's not discoverable.
I think you are absolutely right. While the reflection is available (and the documentation, in general, is not at all bad), discoverability is still weak.
Arrays implementing IList<T> is a clear example of a broken abstraction though - arrays have no need for Add, Remove and whatnot that come with IList - it only needs indexing and count.
We normally think of Strings as being a special case of an array, but arguably String<T> should be a fundamental datatype, and we could do away with Array altogether. In that case, having operators for add and remove doesn't seem so crazy, especially in light of the fact that vectors often outperform doubly linked list for insertion and deletion on modern hardware[1].
Who provides a better standard library? I am a C# developer and have not that much exposure to other standard libraries, but from what I used, .NET is by far my favorite one.
There's a fair number of patterns which are now outdated, because they were created before, e.g. Generics.
So the
bool Int32.TryParse(string value, out number)
method would probably nowadays be better done as
Int? Int32.TryParse(string value)
Similarly, WinForms has loads of different events of different types with individual delegates created for them. Nowadays you'd use Func and Action instead.
And they would not make arrays covariant again, the reflection API would surely use IEnumerable<T> instead of arrays, there are a couple of glitches in the inheritance hierarchy of the collections...but that doesn't really make it a bad standard library.
Nullable is not an adequate replacement for Maybe/Option, one because it is constrained only to value types, which makes it useless generically anyway, and secondly, because you can easily "bypass" it and attempt to call .Value while skipping .HasValue, causing an exception to be thrown. C# lacks the necessary features to have a useful Maybe type - namely exhaustive pattern matching and the Unit type.
A better pattern used in say, F#, would be to return the tuple of <bool, T>, which could handle reference types and value types uniformly unlike Nullable. That won't slide in C# though as there's no syntactic sugar for tuples, and calling .Item1, .Item2 is too "inelegant" for the regular programmer.
For now though, the bool Try_(_, out _ _) is still the preferred method by most.
Func and Action are certainly useful, but sometimes having a named delegate type is still preferable because you can capture the purpose of the delegate in it's name, rather than resorting to doc comments nobody is going to read anyway.
You don't need it for reference types, because those can just return a null.
I agree that it would be nice if C# checked that you dealt with the null case of the return - Resharper will happily warn me if I haven't, but it would be nice if the base compiler did. Maybe in the future!
>You don't need it for reference types, because those can just return a null.
The point is that it is not necessarily correct to conflate the nullability of the parse with the correctness of the parse. If I wanted to parse a string into a Foo, null might be a valid value for a successful parse to return, say because the rule is that the string "(null)" maps to a null Foo.
Nullable is the perfect solution for TryParse. The InvalidOperationException on null is the equivalent of the NullPointerException you would expect for a reference class TryParse method.
I have implemented TryParse methods on many reference types with exactly these semantics - object on success, null on failure (often with details logged at DEBUG).
What's frustrating is how few things got the needed TLC after generics and nullables were added.
Also, I'm constantly frustrated how many core libs freak the heck out over a null string vs. an empty string. Obviously there are places to distinguish those, but if I'm trying to create a nullable int out of the text that is not one of them.
That doesn't work for a standard library - it may make you happy, but the next developer may have very different needs. Int32.TryParse() does not distinguish between null and empty strings and it takes only a minute or two to create an extension method and the problem is solved once and forever.
public static class StringExtensions
{
public static Int32? ParseAsNullableInt32(this String self, Int32? fallback = null)
{
Int32 result;
return Int32.TryParse(self, out result) ? result : fallback;
}
}
Yes, that's the obviously implementation, but so many older framework libs that have not been replaced still use serializers that, when given a Nullable type, fail to use that obvious implementation and blow up on empty strings.
In the case of the XML Serializer, that means that if I declare a class with Nullable<int> Foo {get;set;}
and
<Foo />
appears in the XML body, then it blows up. The WebForms stuff is similarly pathetic about empty strings. There is no useful distinction between empty strings and nulls when we're talking about dates or numbers or something like that, so throwing an exception in this case is just plain wrong. It's not even protecting backwards compatibility because pre-2.0 C# didn't even have nullable ints.
Likewise Async. The basic file IO and ADO.NET APIs can be wrapped up in awaitable structures but you pretty much have to do it yourself. For most purposes if you really want to make good use of C#'s cleverest language features you have to write wrapper classes around .NET to make it work.
They've also done a terrible job of making a first class Windows UI library for .NET; WinForms was always clunky and limited in the subset of MFC/GDI it exposed; WPF was too radical and didn't make sufficient use of the underlying native capabilities of Windows - and the WinRT story has been confused from day 1.
WPF is so much better than (almost) everything else out there (that I am aware of). Yes, it is very different and switching from WinForms is hard to say the least, but it gets so many things right, is extremely powerful and really fun to use once you wrapped your head around it.
Java's library is on the whole better than C#'s. But if you want an example of a stdlib that really sets the bar then I would point to Go. In particular the io packages. They are so composable and simple and easy to use that it makes me want to cry at their beauty sometimes. Go has one of the best written stdlib's of any language I've ever used.
For the stdlib there is no one single problem it's death by a thousand cuts in a lot of various ways. Over reliance on inheritance for polymorphism which limits composability in a lot of ways. Too little use of interfaces.
Some of this seems like it's due to backwards compatibility concerns but it definitely degrades the developer experience with the language. Which is a shame because c# has a lot to offer.