This year by year increase in features - what I refer to as the "better than Java effect" - is not of concern. They're not going to remove features from the language, because that would break existing code for no reason, and that would be insane. The only reason to remove a feature is if it can cause damage. Maybe Scheme should get rid of call-with-current-continuation, but I can't think of any features C# needs to get rid of. Can you?
These C# issues arguably demonstrate haphazard additions that didn't align with good taste:
* Too many overlapping concepts for referring to code by value and defining them inline: events, delegates, anonymous delegates, and lambdas.
* Lambdas that generate magic types rather than slotting into SAM types. This works great for functional languages, sure, but doesn't fit well into a class-based OOP language.
* `ref` and `out` parameters to appease archaic COM APIs.
* Tuple unpacking, which makes sense independently but then bizzarely tries to integrate with `out` parameters.
* A nice "ex nihlo" object literal syntax that shoots itself in the foot by undermining immutability due to requiring settable fields. (TBF, later versions fixed this IIRC.)
* Inline functions in methods in a language that already has lambdas and methods; it's just bloat.
* `as` casting that yields nulls. Did it really need a whole new syntax just to handle null more concisely specifically for casting?
* `partial` classes. Encouraging even more code generation with features like this is a questionable idea and wasn't necessary in other languages.
* `dynamic`. Even if there are use cases for it, it's strange in an otherwise static language with a top-level Object type anyway. I'm saying this as someone who's perfectly happy with fully dynamic languages like Erlang and Lisp. I just don't see the point of adding it to C# specifically.
* C-style enumerations.
* Properties. Invoking side-effects on something that is syntactically indistinguishable from an attribute read is a bad idea. Auto-generation of Java's verbose getters would be long overdue, but the caller site shouldn't be the same a la C#.
* Extension methods. It seems quite ad hoc compared to static addition of types to common operations in other languages, like imported traits in Rust or typeclasses in Haskell.
* Proposed "shapes". Looks like a good idea by itself but will overlap too much with default method implementation and other existing mechanisms.
* Interfaces prefixed with `I`. Not strictly a language problem but an ecosystem one. I shouldn't need to know what _sort_ of type I'm dealing with, that's '90s Hungarian notation.
* Nullable reference types. Getting rid of null is good, but this proposal became confusing. They mentioned opting in assembly-wide for a while but there was then a conversation about having it just warn in some cases. I need to read the latest literature around this, but it seemed less elegant than Java just adding a monad-like Optional type and not adding loads of special-case operations with question marks everywhere.
Despite this, I still admire a lot of the design decisions behind C#. LINQ was great. `async/await`, despite my belief of its inferiority to Goroutines/Project Loom/Erlang processes, was still a great innovation from Midiori at the time. Value types were obviously right to be implemented early on. Assemblies were a good idea. Private by default rather than package-accessibility was a nice touch, as was the `override` keyword. The C# team are smart people who know what they are doing!
As an aside, I used to be firmly against erased generics, but reading more about the tradeoffs from the likes of Gilad Bracha has caused me to reconsider.
Since C# 1.1 they've only added features that are mostly orthogonal to the existing ones. Secondly, the features they've added aren't half-baked. Sure there's room for improvement, but they behave as you expect them to, and the designers specifically leave room for improvement all the time.
Sure, I hate that everything I want to do still has to be specified within a class / static method; that's Java-related legacy done probably because of marketing reasons, but C# has 2 things I wish for whenever I'm working with Scala ...
- the ability to get the expression tree of a closure. There was a plugin for Scala at some point, but it's dead.
- dynamic typing, which structural typing cannot really replace.
You say chock-full of crap, but IMHO language designers should learn from its evolution, because it was a good one.
Modern C# is way too complicated, and I really dislike that. On the other hand, as I've tested out the new features, I have to admit that most of them are good.
Part of what makes this work is that wherever possible, they made the features compiler-only, which means I can use them without having to update my application to use a newer runtime or newer libraries. This means I don't have to worry that updating my code will break compatibility for existing users.
The other key thing for me has been realizing that a significant portion of the new features enable me to replace old verbose/bad code with new concise code that has desirable properties - it's become easier to reduce duplication and easier to write efficient code that avoids allocations and better exploits the type system.
Some simple examples:
the addition of 'in' parameters which behave like 'ref' but are read-only, which enables the compiler to automatically pass values by-ref when appropriate - this means you can replace existing by-value arguments with by-ref arguments and not break any existing code. It's really great.
they added 'ref' locals and 'ref' return values, which means you can do a clean and efficient mutable version of 'list[i]' like in C++ but without any of the semantic issues (storing the ref is explicit and the compiler prevents you from accidentally introducing memory safety issues.)
generics were expanded to allow you to safely manipulate pointers, which means that high-performance code no longer has to use gross tricks and can now be type-safe. in modern releases you can now finally do arithmetic with generic types as well (though sadly this requires an updated runtime).
The addition of tuple types bothered me a lot too until I realized that the tuples were silently updated to value types, which means writing obvious tuple-based code is actually highly efficient and I don't need to hand-write record types.
the async/await features have some major downsides, but on the other hand the compiler and library design teams built in a bunch of really wise escape hatches to let you work around issues. The whole state machine can be customized to swap out all of the internals, you can define your own types with seamless 'await' support, and the compiler will (in release mode) aggressively turn the state machines into structs so there aren't any allocations. It's really nice and transitioning my code to it has been a huge improvement.
linq is notorious for bad performance, but they made it possible to provide your own implementation of all the query operators so it was possible for me to define my own methods and make my linq queries not allocate at runtime. really nice (though it comes with its own tradeoffs).
It got _better_, way better, much nicer than Java. It's not even remotely gone. Type inference is crap in C#, and they even admitted in some cases, it was due to compiler design problems, not language design[1].
Even local vars can't be type inferred if they're functions, because of C#'s confusing decision to use the same syntax for lambdas and quoted code.
The ASP.NET MVC team resorted to using reflection on anonymous types, because there was no lightweight way to pass in a set of options at runtime. With a more expressive syntax, that'd be needless.
C# has statements that aren't expressions, which really bulks up code and adds flow for no reason. In F#, even "if" and "try" blocks are expressions which again keeps slimming things down, and more importantly, keeps the code simpler.
In one direct "line-by-line" translation (C#->F#), F# reduced the number type annotations I needed by 95% (1/20th).
No pattern matching (and thus no active patterns!), little type inference, no syntax for tuples, no lightweight function syntax, no code nesting, no workflows, (and a weird hardcoded one just for async), no top-level functions, no custom operators, and C# is flat-out downright clunky when compared to F#.
It may be one of those "you don't know what you'll miss until it's gone" kind of deals. I've used C# now and then, even last month on an entire project, and it just feels tiring.
C# continues to quietly release incredible features to little fanfare. I hear the same from the Rust and Zig communities. Really feels like we’re in the golden age of language usability and development.
One thing I’d like to see in C# is the theft of the .. operator from Dart so that you can chain calls on a single object without needing to return that object in each call.
I don't think it's that they've run out of ideas, it's that they're being picky with what they implement as C# is fairly modern but also fairly mature. This isn't like the C++ revolution of the past few years where they've been adding new, interesting, (and sometimes dramatically different) features to a decades old language.
Regardless, this change is very handy. Even if "everything must go in a class" is a bad idea (I think it's fine), the end result of this change, from the library consumer's standpoint, is the same as having functions defined outside a class but within a namespace. It's a great pragmatic change that doesn't rock the boat much.
In line with that are the other mostly minor but time saving changes like simplified null coalescing.
public static string Truncate(string value, int length)
{
return value?.Substring(0, Math.Min(value.Length, length));
}
In previous versions this looks like:
public static string Truncate(string value, int length)
{
return value != null ? value.Substring(0, Math.Min(value.Length, length)) : null;
}
C# shows pretty compellingly that adding features does not have to break existing code. For example, you can use every keyword added after 1.0 as identifier, too.
None of them are killer features. Nor is pattern matching, assorted comprehensions, array slicing, binary literals, bytestrings, tuples, active patterns, records, type inference, immutability, shadowing, nested functions, custom operators, typechecked printf, type providers, workflows[1], sum types, agents, etc. etc.
But it sure all adds up.
Switching for a codebase might not be a wise move for many reasons. But writing new code doesn't have those excuses.
Actually workflows are the closest to a "killer" feature but C# took the most popular, async, and hard-coded it in.
The big features that were added after c#3 are dynamic references and async. They both seem to be quite well done as far as I can tell. Could you comment more specifically on problems you saw?
This is generally accurate, but the last paragraph is a little suspect -- we're iterating on C# 6 right now and using the Roslyn compiler to build the features necessary for it.
I don't think compiler architecture is seriously stopping features in their tracks -- there's only a cost/benefit analysis. 'MichaelGG's suggestion on inferred members, for example, has implications on the contracts of types and usability/user interaction with the language, so it's not just a question of if we can do it, but if we want to do it.
Edit: This is probably too negative of a comment. It's just years of frustration with MS coming out, that's all.
I've not got much inside knowledge. I was an MVP 2003-2005 on C# then CLR and Security. I wasn't very knowledgeable back then, but I don't recall any push for FP style, at all. I think the fact that C# originally didn't have lambdas, then added them with an 8-character keyword says enough.
Implementing better generics later? I doubt it. There's been no CLR changes since v2, as far as the type system or IL goes. So that's 10 years, no additions, just added tweaks here and there. Hell, even now, .NET Native relies on source-level transformations, instead of being implemented at the IL-level.
They've been hyping Rosyln. Great. One famous problem with C#'s compiler is that its design made it very hard to add type inference consistently to the language[1]. They rewrote the compiler, did they fix that? Nope. Even worse: My watch says it's 2015, but VS/C# still doesn't ship a REPL. Come on. (Yeah maybe "C# Interactive" will show up some day now that 2015 has RTM'd. But not today.)
The core of my complaint is that they have a mindset of implementing things as hard-coded scenarios, versus general purpose features. Async. Dynamic. Duck typing. Operators. Even the lambda syntax being ambiguous between expressions and code. Why? Cause they choose an end-user scenario, e.g. LINQ, then implemented just what they need to get that scenario done. That lacks elegance. It adds conceptual overhead.
Java has more popularity because MS decided to shun non-Windows. Essentially no one prefers Java-the-language over C#, but Windows-only is a nonstarter in many cases. My IE comment is saying that, like IE, MS has removed resources and the drive to seriously improve its language tech, as there are no real competitors in their space, language-wise.
P.S. I still think C# is a good language in relative terms and they have brilliant people doing great work on it. And the polish of the tooling - wow, yeah it's amazing. I'm just disappointed that MS doesn't seem to be interested in really upping-the-ante and being a leader here. F# is basically a "best-of-breed" language that'd put them solidly ahead, yet they neglect it.
I learned java in school and used C# professional for year. Almost every useful refactoring I perform in C# is based on functional concepts. You can slap patterns on a mess to provide order or you can introduce functional concepts to remove it, almost entirely.
I am not trying to dismiss patterns, but when you have lambda's you have a free way to add a visitor, command or specification to your code. It is one line instead of three interfaces and a bunch of shared understanding.
Consider,
var even = (new [1,2,3,4]).Select(x => x % 2 == 0);
The select statement gives you a way to interrogate individual objects, in the same way you could if you built up all the plumbing for a visitor pattern. What did I have to add to do this? Nothing. C# is awesome.
A lot of the improvements are just logically extending the language to remove arbitrary restrictions. That's what most of C# 10 and 9 appear to be. So most of the features you wouldn't go out of your way to use, but instead stuff that used to be impossible is now possible. I can't say that I've ever wanted a generic attribute, or a constant interpolated string, but if I did want them, I'd be surprised when they didn't work. C# 8 was the last major new language features.
Nah, I'm not going back to C# and don't plan to stay up to date with it. If you're talking about ref structs, Spans etc, I'm perfectly aware and have used those features in anger (in more ways than one).
Aye - but the annoying thing is that it was never necessary: with a few subtle changes to Linq it’s possible to have allocation-free closures by passing state via hidden parameters on the stack - but just like every language out there we’re now hobbled by decisions made 15 years in the past.
——
On a related note, it’s interesting just how unpopular so many new C# language features are (just by looking at the numbers of Thumbs-down reactions on the GitHub Issues/PRs - stuff like top-level Main. It feels like C#’s LDT wants to be like Swift, but without Swift’s willingness to ditch ill-conceived features after a few years… but I think C# would be well-served by taking an axe to some language-features by now - like keyword-Linq and CLS-compliance (honestly, do any ISAs today still lack hardware support for unsigned ints?)
Example? The biggest addition to C# recently is non-nullable references, which was long overdue even though I haven't had a proper opportunity to use yet, but they don't even change the syntax of the language.
reply