There are few tasks a developer enjoys more than writing documentation. It’s a thrilling journey into the exciting world of code formatting and variable naming conventions, right up there with untangling someone else’s regular expressions. So, you can imagine my sheer, unadulterated joy when I was asked to create a "Best Practice" document for my team's C# projects.
Okay, my sarcasm meter is now switched off. It was, in fact, a necessary and useful exercise. While C# isn't the language that sings me to sleep at night, it's a powerful tool (and a country mile better than Java). The goal was to create a shared map for our team, a guide to writing code that our future selves wouldn't want to travel back in time to prevent. The document covers the essentials: SOLID principles, consistent naming conventions, and key architectural patterns like Dependency Injection. It also provides guardrails for C#-specific features, like the right way to use async/await and how to query data with LINQ without accidentally DDOSing your own database.
![]() |
C# |
The result is a common language for quality. It makes our code reviews more productive and helps everyone, from senior to junior, stay on the same page.
Opening Pandora's Box
With the C# guide complete, a dangerous thought crept in over the weekend: "I wonder what this would look like for my other languages?" This was a classic case of a weekend project spiralling into a minor obsession. I fired up my editors and, in a fit of what I can only describe as productive procrastination, began creating similar guides for Lazarus/FreePascal, Go, Arduino C++, and Cerberus-X.
What started as a simple comparison turned into a fascinating exploration of programming language philosophy. The exercise proved that while principles like DRY (Don't Repeat Yourself) are universal, the "best" way to implement them is anything but.
A Tale of Five Philosophies
The way a language handles common problems tells you a lot about its personality. The differences are most stark in a few key areas.
Memory Management: From Butler Service to DIY Survival
How a language manages memory fundamentally changes how you write code.
- C#: Has a garbage collector, which is like a butler who tidies up after you. It’s convenient, but you still need to know the rules. You have to explicitly tell the butler about any special (unmanaged) items using IDisposable, otherwise, they'll be left lying around.
- Arduino/C++: This is the survivalist end of the spectrum. You have a tiny backpack with 2KB of RAM, which is less memory than a high-resolution emoji. Every byte is sacred. Heap allocation is a dangerous game of Jenga that leads to fragmentation and mysterious crashes. The Arduino String object is a notorious trap for new players, munching on your limited memory. Here, best practice isn't just a good idea; it's the only thing keeping your project from collapsing.
- Go: Also has a garbage collector, but it’s more of a silent partner. The language and its idioms are designed in such a way that you rarely have to think about memory management. It just works.
Cerberus
Cerberus-X: As another high-level language, Cerberus-X handles memory automatically. The developer's main responsibility isn't freeing memory, but ensuring its state is predictable. The most crucial best practice is to always use the Strict directive. This is the "no more mystery values" setting, as it enforces that all variables must be initialized before use , saving you from the bizarre bugs that come from variables defaulting to 0 or an empty string in non-strict mode.- Lazarus & FreePascal: The "Choose Your Own Adventure" Model
This is where things get really interesting. FreePascal offers a mixed model for memory management, letting you pick the right tool for the job.
- The Classic Approach: This is pure manual control. Every object you create with .Create is your responsibility, and you must personally ensure it is destroyed with a corresponding .Free call. The try..finally block is your non-negotiable safety net to guarantee that cleanup happens, even when errors occur. It’s the ultimate "you made the mess, you clean it up" philosophy.
FreePascal cheetah
The LCL Ownership Model: The Lazarus Component Library gives you a helping hand, especially for user interfaces. When you create a component, you can assign it an Owner (like the form it sits on). The Owner then acts like a responsible parent: when it gets destroyed, it automatically frees all the child components it owns. You should not manually .Free a component that has an owner.- The Modern Approach: To make life even easier, FreePascal supports Automatic Reference Counting (ARC) for interfaces. When an object is assigned to an interface variable, a counter is incremented. When that variable goes out of scope, the counter is decremented , and once it hits zero, the object is automatically freed. This brings the convenience of garbage collection to your business objects, drastically reducing the risk of memory leaks.
Concurrency: An Assembly Line vs. The Office Worker
- C#: async/await feels like delegating a task. You ask a subordinate (Task) to do something, and you can either wait for the result (await) or carry on with other work. It's efficient and clean.
Go
Go: Go's model is more like an automated assembly line. You have multiple workers (goroutines) and a system of pneumatic tubes (channels) connecting them. Workers perform their small task and send the result down a tube to the next worker, all happening simultaneously.- Arduino/C++: You're a solo act on a mission. There are no threads, so you can't do two things at once. The entire game is to never stop moving. You check a sensor, update a light, check a button, and repeat, all in a lightning-fast loop(). A delay() is your worst enemy because it brings everything to a grinding halt.
- Lazarus/FreePascal: This is the classic office worker. To avoid freezing the UI during a long operation, you spawn a TThread to do the heavy lifting in the background. When the worker thread needs to update a label on the screen, it can't just barge in. It has to use TThread.Synchronize or TThread.Queue to politely tap the UI thread on the shoulder and ask it to make the change safely.
- Cerberus-X: This is the resourceful indie developer. It doesn't have the fancy built-in machinery of async/await. To achieve non-blocking operations, it falls back on the fundamental tools, letting the developer build their own solution using threading or designing methods with callbacks.
Error Handling: The Town Crier vs. The Smoke Signal
- C# & Friends: Languages like C#, Lazarus, and Cerberus-X prefer the "town crier" approach of exceptions. When something goes wrong, they shout about it loudly, and a try...catch block is expected to handle the commotion.
- Go: Go has trust issues. It prefers you to look before you leap. Functions return an error value alongside their result, forcing you to confront the possibility of failure at every single step.
Arduino C++ - Arduino/C++: When your code is running on a chip in a field, how does it cry for help? It uses a smoke signal. There's no console, so robust error handling involves returning status codes or, in a critical failure, entering a safe state and blinking an LED in a specific pattern—a primitive but effective "blink of death" to signal an error code.
Up for Grabs
This dive into different programming paradigms was a blast. It’s a powerful reminder that there’s no single "best" language, only the right tool for the job, with its own unique set of best practices.
I’ve cleaned up all five documents and made them available for download. If you work in any of these languages, I hope they can be of some use to you. Now if you'll excuse me, I think I see a dusty corner of the internet where a language is just begging for a best practice guide. It's a sickness, really.
Grab them, use them, and happy coding!
No comments:
Post a Comment