I recently inherited a C# code base at work. I agree that C# is a powerful, productive, and mature language, but as someone who has been programming mainly in Go, Python, and a bit of Zig over the past few years, there are a few things that feel like a regression in C#:
- The absence of free-floating functions (everything is an object, and I must use static methods on static classes).
- When “using” a namespace, the convention is to make all the symbols in the “used” namespace available directly in the current namespace. That makes harder to know where symbols are coming from when reading code. And to avoid name collisions, people tend to use longer names. The convention in Go or Rust is to prefix most symbols with the package they are coming from. But perhaps I’m missing something and need to read/write more C#.
- The syntax for doc comments with tags such as <summary> is super verbose. I don’t see the point. Docs are working great without these tags in other languages. Reminds me of my J2EE days.
- I really prefer the name before type syntax (used in all new languages such as Go, TypeScript, Go, Swift, Kotlin, Zig, etc.) to the type before name syntax (used in C#, C, C++, Java, Dart).
C# started as a language tightly aligned with C++/Java, but has since moved to be something else entirely that is highly capable.
I assume that free-floating functions are global functions. You can achieve something similar by "global using". Put this in a file and tug it away somewhere:
"global using static YourStaticClass;"
Now you can call all the static methods on the class everywhere.
As for the using vs. naming convention, most people use the IDE and hover the mouse over the type to see its identity. Once you get proficient with the IDE, you can do it using shortcuts. However, if you really want to prefix all types with a shorthand of the package it came from, you can do it with aliases in C#.
I use VSCode with the C# LSP, but I prefer to immediately see where a name comes from by reading it, rather than hovering over it. That's why I prefer to avoid global.
Regarding imports, I guess I could do something like `using c = my.namespace.ClassWithMyStaticMethods`, but I suppose it's not idiomatic in C#.
Better is `using static my.namespace.ClassWithMyStaticMethods` that gets you exactly the consuming syntax you want, even though the methods still need to be static on that class.
> I assume that free-floating functions are global functions.
In most languages they're bound to some scope, like package, module, etc. I'm not familiar with C#, but I assume there it would be scoped in a namespace.
Yes, I'd like to be able to define functions directly under the namespace. In C#, it seems the only way to do this in to define a static method in a class, the class being part of a namespace.
Speaking as an old C# hand:
1) I don’t think static methods/classes are that big a deal. There’s a free-floating syntax for short scripts but I don’t regard this as much of a problem. There’s also the rarely used using static syntax.
2) The common convention assumes you’re using VS or Rider and can just mouse over to find where things come from. So you’re not missing anything except possibly an IDE.
3) Yeah, it is. But again, the IDE integration is great.
4) can’t argue with you there. The C-style syntax was a mistake that’s been replicated in way too many places.
1) It's just a bit verbose. I think I just rewired my brain around a programming style that is not OOP anymore.
2) As I wrote in another comment, my editor supports hovering with the C# LSP, but after years of reading Python, Go, Rust, and Zig, I got used to be able to see where a name comes from by simply reading it, rather than having to hover over it.
3) My problem with the verbose doc comments is not about writing, but reading.
As you wrote, all of that is not a big deal, but just feels like a step backward.
Yeah static types as containers for functions just means the hierarchy becomes easier. In A.B.C.D() you know A.B is a namespace, C is a type and D() is a static method. With free functions you’d be unsure if this is a free method in the A.B.C namespace or not. Doing reflection over all functions would require iterating types as well as the magical unnamed ”global type”. It’s just a special case that doesn’t carry its own weight. Especially with the newer syntax where you can omit the declaration of the type in top level programs so static void Main() is a valid program.
I don't see how "attaching" functions to types is easier than attaching them directly to namespaces. I don't see either why I would need to use reflection to iterate over all the functions and methods in a namespace, but I agree that if I would need that, then yes it would require adjusting the way it works today. The problem comes from the CLR where all code and data must be part of a class. C# simply mirrors that design choice. That's why C# does not offer free-floating functions. F#, which is also relying on the CLR, solved the problem by introducing the concept of "modules", in addition to the concept of namespaces. Namespaces in F# are like namespaces in C#. All code and data must be part of a module, which is part of a namespace. Under the hood, at the CLR level, modules are implemented as static classes.
> I don't see how "attaching" functions to types is easier than attaching them directly to namespaces.
There's basically no difference between a static class and a namespace, so you're right, it's not easier, but then this kind of dissolves your whole argument too: just put your functions in static class or two, what's the big deal? It's just like a namespace after all.
The wider problem is that to do a thing you have to start thinking about the design of IDoAThing and a implementation ThingDoer before you've even worked out exactly what to do yet.
The C# tooling is very refactor-friendly. I commonly just build the class then pull out the interface later.
C# does want to have interfaces though, and gravitates to the common interfaces - core services - composition/DI root architecture, with lots of projects in the solution to provide separation of concerns. I think it works very well generally for business software at least, but I hear plenty of grumbling about 'complexity' so it's not for everyone.
'ThingDoer' is a naming convention from Go (I'm not a fan of the fact that we have to live with the Delphi-ism that is IMyThing but at least it's just one letter).
There is no requirement to define new interfaces except for a specific coding style in a team. It is best for most components to stay as plain and simple as possible, and for the module and component level testing to be applied with as little mocking as possible (because, most of the time, it is an anti-pattern imposed on us by what is sometimes named "London's school of testing" which is just bad practice and whoever perpetuates it should apologize).
Honestly, every point you mention in my opinion is positive.
- if you feel like using static classes and methods, you should spend more time thinking about architecture
- namespaces are just what they are
- in modern IDEs just write /// before a member declaration, it will insert the whole comment block for you
Static methods in modern C# are either extension methods or some other very rare cases like mathematical functions.
The super complicated comments become readable and useful when you hover the mouse cursor over something. There are also tools that can parse those comments and create documentation.
- The absence of free-floating functions (everything is an object, and I must use static methods on static classes).
- When “using” a namespace, the convention is to make all the symbols in the “used” namespace available directly in the current namespace. That makes harder to know where symbols are coming from when reading code. And to avoid name collisions, people tend to use longer names. The convention in Go or Rust is to prefix most symbols with the package they are coming from. But perhaps I’m missing something and need to read/write more C#.
- The syntax for doc comments with tags such as <summary> is super verbose. I don’t see the point. Docs are working great without these tags in other languages. Reminds me of my J2EE days.
- I really prefer the name before type syntax (used in all new languages such as Go, TypeScript, Go, Swift, Kotlin, Zig, etc.) to the type before name syntax (used in C#, C, C++, Java, Dart).