Tuesday, January 20, 2026

Rethinking my past dislike of C#

In response to my previous post.


We don't use "goto", of course, because we want our iteration to be structured.

But "for" is only for collection iteration with indexing. For everything else, we use "while", even though "for" gives the reader the accumulator variable, means of accumulation, and end condition upfront. And then we scatter in a few "break"s to confuse the hurried reader further.

Still annoyed at the limited idiom of "for".


We use "for" instead of "foreach" for performance

On lists that could have been arrays. We always call "ToList", never "ToArray".

ToArray often builds a list first, then converts it to an array, which takes an extra copy operation. ToArray could be worth it if you're just using Select or other length-preserving transformations.

Also, most codebases don't use "for" instead of "foreach" for performance. That's not idiomatic.


We don't need discriminated unions. Classes and interfaces are enough for any situation.

And then we "switch" on them and have to repair bugs having to do with missing cases that were created later.

Now that I am older and wiser, I realize that my greatest mistake was when I thought that inheritance might have been worth using to get exhaustive matching. If you are programming in C#, learn to accept that you don't get exhaustive matching. If you use inheritance, you will end up with so many layers of indirection that you never quite know where to find the code that does the thing. Don't do polymorphism. Kill it on sight. You are allowed to use "if" and to switch on enums. That is all.

C# is supposedly getting DUs one of these days. I wonder whether that will change my opinion.


We don't use static fields and singleton classes. Instead, we use dependency injection frameworks to ensure that only one instance of each class is generated and that all other classes have access to that instance. Which is different.

I'm withholding judgment on DI until I have been bitten by global objects. To figure this one out, I'll need to use more global objects.


We don't have an order of compilation below the project level. Every class in a project, unless it is private to another class, is accessible from every other class. We deal with circular class dependencies by not worrying about them. Code goes wherever happens to feel best at the moment.

I haven't thought about this in a while. F#'s file ordering was quite nice.

I think that this problem probably mostly goes away if you understand what your program does. Of course, this requires writing a program that a human can understand.

The non-F# solution is to split the program into small units with narrow interfaces. Maybe that is why the early C programmers encouraged us to split functionality into separate executables and communicate between them with plain text. Your interface can't be very complex if you have to parse all input and output.


"IConvertsFooToBar" is supposed to be prettier than "Func<Foo, Bar>" for some reason.

Granted, "Func" is an ugly word. But I guess we had already used up all the punctuation elsewhere.

And we need to have separate "Func" and "Action" delegates since you can't have a value of type "void".

Same answer as for polymorphism: C# wasn't designed for passing functions. Don't do that.

Don't pass interfaces, either, if you can help it. Refer to the previous comment on avoiding polymorphism. Flat code is easy to read.


We use static typing so that we can be sure that all representable cases are valid.

Unless they're null. Anything could be null. (Except structs, but we don't use structs.)

A defined value representing "no value" is highly useful. This is a sub-problem of understanding your program's flow so that you know the range of possible values at any point. This, again, requires writing an understandable program.


"default". Without "default", you have to make two versions of every generic method, one for classes and one for structs. With "default", we still have to make two versions of every generic method since the two have completely different behaviors. For classes, it returns "null", which might blow up your program if you're not on guard. For structs, it returns an instance in which every field is set to its own "default", which is significantly worse since it's very difficult to detect.

Now that I have written a little C, "default" makes sense. Now it is the distinction between structs and classes that I don't understand. Why can I only allocate structs on the stack and classes on the heap? Isn't allocation strategy more related to data access and lifetime than to data type? I have yet to explore functional programming in C, but at least I can pass a reference to stack-allocated data.


Exceptions are unexceptional. We wrap deep calls in try/catch because something in there will probably throw an exception at some point, and just because there was an exception doesn't mean that we need to crash now.

Catching all exceptions from a function call is evil; you don't know exactly where the exception was thrown or how the program state has changed. You must at least take some effort to recover rather than continuing blithely on as though no error had occurred.

That's more a pitfall than a language problem, though F#'s native Either type provides a much nicer alternative to exceptions than C# does.


Everything is a "manager". "Manager" is pretty close to synonymous with "class" when you think about it.

This is an anti-pattern, but also C#'s fault for making us wrap all code in classes while telling us that each class should do one thing. How do you name a class and method that do one thing? WidgetFrobber.FrobWidget? WidgetFrobber.Frob? WidgetFrobber.Do? FrobWidget.Execute? I usually go with WidgetFrobation.Do, but it still feels wrong. Organizing code by verbs is often better than organizing it by nouns.


I don't mind C# as much as I did then; I have since spent much time learning to simplify code to avoid the features that make C# painful to use. C# has records now, which take most of the headache out of classes. If you avoid polymorphism of any sort--interfaces, inheritance, or passing delegates--and structure your code as a straightforward imperative program, then modern C# isn't so bad.

No comments:

Post a Comment

Rethinking my past dislike of C#

In response to  my previous post . We don't use "goto", of course, because we want our iteration to be structured. But "f...