You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As an initial step, I thought a love/hate/wish list of C# would not be too bad. Plenty of subjectivity here, but oh well. Love/hate should not be taken too seriously, it could be called like/dislike, even nitpicks went into the hate category.
Love in C#
Reflection: The ability to inspect types makes writing serializers and other inspection tools super easy. And .NET has one of the most powerful reflection systems I've seen so far.
Good BCL: .NET languages ship with batteries included. It's not so lacking as a C++ standard library.
No header files: Mainly coming from the C and C++ world, it's a breath of fresh air not having to keep up 2 files for a module.
Source Generators: They make metaprogramming much more powerful than in C++. We can generate complex constructs from simple, declarative notations, all compile-time.
Inheritance-based type/pattern matching: In most languages discriminated unions are implemented as tagged unions, which is fine, as long as you don't want inheritance. Once you introduce some kind of subtyping, the two features become somewhat redundant.
Amazing tooling support: This is a huge one. VS and Rider are extremely helpful. Great static analysis, suggestions, code-fixes, refactorings. You can include extra analyzers just as you would include a package, not even mentioning having an API to implement custom analyzers.
Switch expression: After the dreadful switch statement, the switch expression is a breath of fresh air.
Hate in C#
Lots of legacy quirks that makes the language feel kind of dirty at some places. And they are not going away because of backwards compatibility. Some of these are:
Covariant arrays
0 literal implicitly converts to other integral types
Query-based LINQ syntax
The amount of new constructs is getting way out of hand and it seems like adding new thing to fix old ones is becoming the trend (which really didn't work out for C++).
An entirely new syntax for classes (records) that add equality, hash and print. Why not add the placement-syntax to classes and allow classes to auto-implement the equality and hash? Both derive constructs and tag types exist for this and I can't believe it would have been too much work for the compiler. The new record structs will make this even worse.
There is readonly struct but no readonly class. These modifiers seem to fly all around randomly, applicable to some things, while not to others, also making it very inconsistent.
Properties are completely separated from methods for some reason.
If the closed hierarchies proposal hits the scene, sealed and closed will enforce the same thing but on different scopes, making the feature redundant.
No variadic generics: Things like a type-safe OneOf for an arbitrary amount of types is kind of doomed.
No const generics: Implementing an N dimensional vector or an NxM matrix means either letting the dimensions be dynamic in a runtime value - type-unsafe - or writing/generating each case - not flexible, cumbersome.
Interfaces really only prescribe member-level constraints: There is no way to enforce implementing an addition operator for a type, or even adding type as a required member. This often means externalizing factory functions into factory objects, using reflection, or having some janky post-construction initializer.
There are plenty of problems with Source Generators:
They can't modify existing code: As much as I love the power SGs bring to the table, not allowing modification is a major drawback. They can't for example rewrite LINQ for you or act as proper decorators. They also make you litter partial all around. There is also no plan to remove this constraint, because they pose a security concern in the way they were implemented.
They work on strings: This is a really-really odd solution and the experience is very poor, compared to systems like Rusts quote.
Source Generators and dependencies suck together. Having a Source Generator include either a generation-time or runtime dependency is insanity. All because they decided to make it piggyback on the existing analyzers framework. Look at the hoops you need to jump for just a single case. Want to include a local project? Forget it, just include the sources recursively.
Because of all this, a nice auto-implementation for INPC without a weaver is still unlikely.
When having to implement Equals/GetHashCode it's painful and there's crud that needs to be done but the compiler won't do it for you (like overriding bool object.Equals(object? other)). Admittedly, this is not something that has to be done but I've never written code where it is not desired, hence this is likely a holdback for a 0.1% case or something.
Similar pattern is when I implement IComparable and I want to also have IEquatable (which is common), again there is boilerplate. Again, this is not desired for partial ordering, but most cases are not partial ordering.
There is covariance for overrides of base elements, but none for interface implementations (AFAIK this is getting resolved partially).
Default interface implementations are really bad. They almost feel like they gave us some compositional elements or traits, where you can externally define behavior, but no, only when you reference the type as the interface.
The new keyword feels like it was just brought in because Java had it. Why not just type out the constructor name?
There is no type-parameter inference, resulting in things like the snippet below, despite having both var and new() for inference. Imagine only having to type new Dictionary<_, _>() (or something even shorter) there.
Implementing the dispose pattern is horrific and there is no way to enforce disposal.
The switch statement. It's horrible. I usually avoid it at all cost.
Miss from C#
Closed type hierarchies, which would bring in DUs without the warning or useless default case in switch-cases.
A derive macro, which would auto-implement the common patterns correctly, in place. Records could just become classes that derive equality and debug-print or something.
User-defined, AST-based decorators. Instead of SGs that can act anywhere as long as it's new code, it could go the other way: only let the decorator act on the code it annotates, but allow to modify it. This could be a compile-time function, very much like Rust procedural macros.
A better construct instead of interfaces (traits or mixins):
Nonmember constraints (static and type constraints), which are also usable as generic constraints.
Proper default implementations, maybe unchangeable ones. For example, you'd only have to implement T1+T2, and T2+T1 would be automatically implemented, and there would be no way to override that. (just an example, not necessary in this exact case)
Externally implementable, maybe as long as the implementer type or the implemented trait definition is owned by the user.
Higher-kinded-types: I'm really not sure or sold on this yet, maybe type fields in traits would resolve most needs and this feature wouldn't be justified anymore. Rust only has type fields in traits and still manages to provide a fully typed LINQ-like API.
Way better type inference, mainly for generic parameters and constructors.
Constant computations: Since SGs require compile-time code execution anyway, compile-time evaluating some expressions would really benefit at some places. Maybe speed up serialization of known types, allows a wider range of arguments for attributes to pass, ...
The text was updated successfully, but these errors were encountered:
As an initial step, I thought a love/hate/wish list of C# would not be too bad. Plenty of subjectivity here, but oh well. Love/hate should not be taken too seriously, it could be called like/dislike, even nitpicks went into the hate category.
Love in C#
Hate in C#
records
) that add equality, hash and print. Why not add the placement-syntax to classes and allow classes to auto-implement the equality and hash? Both derive constructs and tag types exist for this and I can't believe it would have been too much work for the compiler. The newrecord struct
s will make this even worse.readonly struct
but noreadonly class
. These modifiers seem to fly all around randomly, applicable to some things, while not to others, also making it very inconsistent.sealed
andclosed
will enforce the same thing but on different scopes, making the feature redundant.OneOf
for an arbitrary amount of types is kind of doomed.partial
all around. There is also no plan to remove this constraint, because they pose a security concern in the way they were implemented.Equals
/GetHashCode
it's painful and there's crud that needs to be done but the compiler won't do it for you (like overridingbool object.Equals(object? other)
). Admittedly, this is not something that has to be done but I've never written code where it is not desired, hence this is likely a holdback for a 0.1% case or something.IComparable
and I want to also haveIEquatable
(which is common), again there is boilerplate. Again, this is not desired for partial ordering, but most cases are not partial ordering.new
keyword feels like it was just brought in because Java had it. Why not just type out the constructor name?var
andnew()
for inference. Imagine only having to typenew Dictionary<_, _>()
(or something even shorter) there.Miss from C#
T1+T2
, andT2+T1
would be automatically implemented, and there would be no way to override that. (just an example, not necessary in this exact case)The text was updated successfully, but these errors were encountered: