What are the technical challenges of C# programming?

Features and Challenges of C#

In terms of the language itself, I believe that there are several key points to consider:

  • You need to break free from the stereotypes of object-oriented programming imposed by other classical object-oriented languages.
  • You need to realize that C# is not a purely object-oriented language, but a multi-paradigm language.
  • It has both automatic and manual control capabilities, allowing for manipulation at a low level.

For example, in C#:

  • static can be abstract, and even virtual.
  • Interfaces are essentially dynamic traits, rather than base classes for a type. Therefore, the interface constraints on generics determine how the methods of the generic parameters are dispatched.
  • Due to the above point, a type may not be boxed as the interface it implements, but the interface can be used as a generic constraint to allow passing data of this type.
  • The existence of delegates makes functions first-class citizens. The native lambda support (non-interface wrapper implementation) combined with generics (non-erased) enhances the ability to do lambda calculus.
  • Variance makes subtyping more challenging, but it brings about safer and more flexible type abstractions.
  • The class provides default behavior of copying references as well as copying values represented by struct. It also provides native explicit reference semantics with ref.
  • The existence of reference semantics brings about the distinction between l-values and r-values.
  • The combination of struct and explicit ref, especially the requirement to allow explicit ref as fields of struct, introduces the concept of lifetimes. It is necessary to understand the significance and effect of lifetime annotations such as scoped and UnscopedRef.
  • AsyncMethodBuilder, InterpolatedStringHandler, and various type traits under CompilerServices determine how the compiler generates code.
  • Various hardware intrinsics are directly exposed, so you can write code directly against CPU-level primitives, such as Interlocked, SIMD instructions. However, at the same time, you can also choose to use higher-level type abstractions to reduce mental burden.
  • It supports precise control of object memory layout down to the byte level.
  • Opening the door to the native world with unsafe, exposing all the goodies, requires a thorough understanding of computer principles. Of course, you can also choose not to use unsafe.
  • For some advanced development needs, to balance high performance and memory safety, you also need to know how to isolate unsafe under safe abstractions using the facilities provided by the language.

Of course, if you treat C# as just another Java and spend your days writing CRUD operations, then there probably aren’t any challenges. After all, you’ll hardly come across any of the complexities mentioned above.

This is actually a magical aspect of C#. It seems that all the difficulties are isolated and limited to the people who build frameworks, while there is hardly any difficulty for the users of these frameworks. This language is difficult for only a part of people, while it is very simple for another part. In comparison, C++ and Go are two extremes: C++ presents you with all those things, whether you like it or not; Go, on the other hand, says, “I have limited features and you just have to use what I provide, whether you want to or not.”

C#: The Difficulty Lies in Mastering the Language

I believe the most difficult part is being able to master this language.

I have seen too many programmers at a loss when faced with C#, creating all sorts of problems due to their partial understanding of a feature.

Here, let me give you a few examples:

Abusing structs. In fact, I don’t think knowledge about value types should appear in interview questions. If you expect a programmer to correctly use value types, then they must already understand how to manage memory, and there’s no need to ask about the difference between the heap and the stack…

Abusing destructors and Dispose. Similarly, destructors have only one application, which is when you directly allocate unmanaged resources, meaning you already have full command over memory and resources. And 99.999% of programmers do not possess this capability, even many C++ programmers…

Abusing Tasks. Asynchronous operations can only improve throughput in truly asynchronous situations, and what they actually improve is throughput, not performance. Task.Run essentially executes tasks in parallel, not asynchronously. Task is part of the Task Parallel Library, not the async programming model. The principle behind improving performance with asynchrony is that it can achieve greater concurrency with fewer threads. And since the number of threads is shared by the entire operating system, simply making something asynchronous doesn’t magically boost performance. It has specific use cases and is a technique mainly utilized by high-performance library functions. In your business code, all you need to do is avoid synchronously calling asynchronous library functions. If a library function does not provide a synchronous method, it’s better to contact the author instead of directly using .Result.

As for the concern that the flexibility of the syntax in C# leads to unreadable code, that honestly isn’t a problem…

I’ll add more if anything comes to mind…

C# is like a very sharp kitchen knife. In the hands of a chef, it is a powerful weapon, but if you don’t know how to chop vegetables or use a knife and insist on blindly tinkering with it, you are likely to cut your own hand…

Of course, there are certain languages that advocate for simplicity, ensuring a lower limit, and providing programmers with a peeler. Yes, it can indeed be used to make shredded potatoes and is quite safe. But when you need potato slices, you have to first use this tool to shred them, and then use a hydraulic press to flatten the shredded potatoes into slices. You might ask if everyone does it this way, and if it’s the best practice…

What I’m lamenting is that if things continue like this, these people will come to believe that shredding the potatoes first is the right way to obtain potato slices…

Recruitment Challenge

Perhaps the most difficult part lies in how to join our company during the hiring freeze at Microsoft….

The Key to Problem Solving

I’m using this problem to clarify a few things.

The so-called “difficulties” are never provided by a specific language’s grammar. Instead, they lie in how to use a certain mechanism to solve a practical problem. This “mechanism” is not limited to the language itself. If the language doesn’t have built-in support, and you can’t switch languages, you can consider using libraries, invoking ready-made commands in the command line, using Unix sockets with tools written in another language…

If your business is complex and easily leads to tangled, convoluted code, you should reconsider the overall design and apply appropriate design patterns, combined with unit tests (UT) to ensure code quality. At this point, the patterns and language capabilities available to you (such as inheritance, polymorphism, interfaces, abstract classes, scopes, companions, generics…) come into play. Sometimes you may find that a pattern is not needed because it is already well-supported by the syntax of the language, while in other cases, due to language limitations, you may need to work around it.

If you want to avoid the overhead of garbage collection (GC) or the cost of repetitive memory copying, you should learn to layout and manually manage memory, and write well-defined interfaces for other parts of the code to use. If the language provides mechanisms for allocating a large amount of memory and manually managing it, that’s great. However, if it doesn’t, you’ll have to resort to methods like mmap.

If you want better throughput, you should carefully analyze which resources in your code are being wasted. Can you use mechanisms like async/await, tasks, and threads to improve concurrency and storage, while being aware of the competition issues these mechanisms may cause? However, sometimes the scope of concurrency is not limited to the same process, in which case distributed locking is needed. Once you have locks, you need to optimize performance to avoid as much competition as possible, which brings us back to the design itself. Although a language may provide you with some tools for dealing with concurrency, it cannot help you with good design.

If you need ultimate performance and the code generated by the compiler doesn’t meet your needs, you’ll need to learn how CPUs/graphics cards calculate problems and what kind of instructions they use. Can the language provide a mechanism for you to directly write these instructions and pass them to the underlying system? At the same time, you need to be careful to protect pointers from pointing to invalid locations. These require a basic understanding of the underlying operating system.

If a problem can be elegantly solved by a language’s syntax alone, without any other pitfalls, then it’s the simplest problem. You don’t even need to understand what that piece of code means, you can just generate it using GPT and paste it in.

But in reality, many problems cannot be well supported by the syntax of a single language, which is why programmers need to figure out ways to solve them using the available tools.

For example, when dealing with a Feishu document, how can you generate a preview image that is exactly the same as the real web interface? Or, if you want to directly preview the directory tree inside a zip file uploaded to a cloud storage, can the language help you? If not, what can you do?

Of course, this is not to say that learning a language is useless or that people shouldn’t learn it well. It’s about managing resources, roughly grasping the idea behind the language, being able to solve some simple initial problems, and then quickly trying to solve more complex problems, while also thinking about what tools are useful and haven’t been mastered yet.

Instead of approaching it like high school students who are used to cramming the entire curriculum of the last two or three years in one year, obsessing over what will be on the college entrance examination, and doing practice exams every day.

Expression Trees, Emit, Asynchronous Programming, Design Patterns, LINQ

1. Expression Trees

Expression trees can create a method that performs at the same performance level as native methods, equivalent to the high-performance features of scripting languages.

Understanding expression tree parsing allows you to implement your own ORMs, etc., but it requires a lot of knowledge to delve into.

2. Emit

Advanced use of reflection. It compensates for the inability to construct a class using expressions. It requires knowledge of writing IL code in order to use Emit smoothly, and the difficulty is relatively high.

You can construct classes, methods, variables, and all other things.

3. Asynchronous Programming

Used for handling asynchronous operations, such as network requests, file reading and writing, etc. Understanding and correctly using asynchronous programming can improve the performance and responsiveness of your program, but at the same time, it also increases complexity. For example, omitting “await” may cause errors in your program.

Common ones include Task.WhenAll, await async.

4. Design Patterns

C#’s syntax is more advanced. If the code written using traditional design patterns is not the simplest, for example, implementing lazy loading using double-checked locking, C# only needs one line of code:

public class Singleton
{
    private static Lazy<Singleton> instance = new Lazy<Singleton>(() => new Singleton());
    public static Singleton Instance => instance.Value;
}

5. LINQ

This thing may be a bit confusing if you haven’t used it before. There are many syntaxes, but once you remember them and know how to use them, it becomes very simple.

The Difficulty of Programming Language Lies in Code Inside “unsafe” Brackets

Compared to algorithms, there is not much difficulty inherent in any programming language itself. If we must point it out, the difficulty lies in the code inside the “unsafe” brackets, which requires a certain understanding of operating systems and compiler principles, as well as proficiency in C/C++ to write well.

Technological and Conceptual Lag

No difficulty.

Languages are universal, whether object-oriented, procedural, functional, or logical, they are universal in various languages.

Other languages are as they are in C#. C++ considers memory allocation to be a technology; Java, C#, and Go don’t think it’s simple either.

Go considers multithreading to be a technical point, just like C#, Java, and Python.

Java considers NIO to be a technology, and other languages also find NIO interesting.

The only trouble with C# now is that the books and materials in China are too outdated.

This includes a certain online community, which used to be a centralized place for .NET in China. Many bloggers still treat the things they learned when they started in the industry 10 years ago as their specialty, but these materials and concepts are too outdated and not suitable for promoting entry-level learning anymore.

At present, schools' tutorials and training institutions have not kept up. Therefore, many newcomers to .NET are still using technologies and concepts from 10 years ago, or even using “legacy concepts” from .NET 2.

Choosing the Right Code, Avoiding the Abuse of Asynchronous or Abstract Design.

Use appropriate code for the appropriate scenario, rather than showing off with flashy techniques.

The abuse of asynchronous programming can have serious consequences. When the logic needs to be executed in a sequential manner, using async excessively is common.

If a class is straightforward and simple, there is no need to complicate things with abstract designs.

The Best Learning Strategy

Knowledge of C# is like a boundless and vast ocean. Most people can master the basic 1%-5% of CRUD operations, which is sufficient for developing software for daily application, making a living, and transforming small skills into productivity.

If you delve too deeply into C#, you may end up wasting time studying various C# technologies that you never actually use. Instead, you should focus on developing suitable software and making money, rather than getting caught up in an endless cycle of technical learning.

The difficulty lies in effectively managing your limited time and learning scope. It is best to have a superficial understanding of C# and stop there.

Layered Compilation

The core of .NET lies in the CLR/JIT layer, so the technical difficulties also lie here, rather than in the managed framework or managed API function layer.

In terms of C#, it is essentially no different from other languages. It is more about strange operations and syntax sugar, memory model control, and optimizations at the binary level on the JIT side.

Therefore, its difficulties are also the problems faced by most languages. For example, most things at the framework level can be found on Google, and the true difficulties lie in problems that cannot be searched for or unresolved issues encountered by predecessors.

For example, among the following: in my personal opinion, the core part of the JIT’s difficulties is not…, but layered compilation.

Next
Previous