Extension Methods: Helpers for Types You Can’t Edit (in Unity / C#)

Isn’t it frustrating when you can’t edit a class to add a method you really need?

Maybe the class is built into your programming language or game engine, hidden away in a dll file, or buried in 3rd party code simply too risky to alter.  Whichever the case, editing the original class isn’t an option.

This article will take an in-depth look at the various solutions to this common problem. We’ll explore some advantages, limitations, common pitfalls and best practices associated with each approach, and take a detailed look at one option which often gets overlooked: extension methods.

This gets a bit heavy in places but there are some important details here which should get your mental wheels spinning. So refill your coffee and let’s boogie!

Side Note: I don’t claim to have all the answers. No one does! I love hearing other people’s perspectives, so feel free to toss me a bone once you’ve soaked this stuff in.

This is part 1 of a 2-part series. In part 2, I’ll share some useful extension methods you can use for free in your own projects.

The Problem

It’s hard to cover this stuff in general terms, so let’s explore the problem through the lens of a common scenario:

You need to randomize the values of a generic list. This functionality is required in multiple places so a reusable method is needed to avoid repeat code.

A quick scan of the List<T> class confirms that no such method already exists. That’s a shame because it feels like an obvious omission.

It would be nice if you could simply add a Shuffle() method to List<T>, but it’s built into C# / .NET and can’t be edited.

That’s a pain but we do have other options…

  • Extend the class with inheritance
  • Write a helper class (also known as a utility class)
  • Add an extension method

So how do you determine which option is best, and why is an extension method particularly useful here? To answer that question, let’s take a closer look at each option…

Option 1: Extend the Class with Inheritance

On the plus side, extending List<T> and adding a Shuffle() method is dead simple.

Side note: If you don’t know what “<T>” does, don’t worry about it! The class we’re extending just happens to use generics. Rest assured, all the examples here will work with non-generic types as well.

Inheritance in situations like this is bad idea for a few reasons:

  • The original class could be sealed, preventing you from inheriting from it even if you wanted to. That’s fairly common in situations like these. Luckily, this isn’t the case with List<T>.
  • You’re forced to use ShuffleList<T> in place of List<T>. This may seem like a minor annoyance but it could lead to maintenance and risk issues later.
  • Inheritance is a powerful, yet dangerous tool. It should be used with caution and only be considered a viable solution when dealing with the most natural and distinct “is-a” relationships (see composition over inheritance).

Warning: Inheritance requires a very tight form of coupling and hierarchical limitation. As design requirements change, these rigid characteristics can easily lead to risky code refactorsspaghetti code, bugs and design smells (anti-patterns such as CallSuper).

Side note: One could argue that we do have an “is-a” relationship of sorts here, and we do, but ShuffleList<T> is basically just a normal List<T> with additional functionality. Is this relationship really distinct enough to warrant additional taxonomy (hierarchical classification)? Given the fact that we would have just added the method to the original class if we could, probably not.

So inheritance doesn’t seem like a good option in this case. Let’s move on to the next one…

Option 2: Write a Helper Class

A helper class is often the preferred option. The choice between helper class and extension method is largely situational and sometimes boils down to personal preference.

I tend to use extension methods…

  • When the method feels like an omission in the original class.
  • For small additions.
  • For code readability.

I tend to use helper classes…

  • When the method seems best grouped with other methods sharing a singular purpose or responsibility.
  • When the method leverages additional variables or methods outside of its scope (non-static helper class preferred).
  • When composition is a better fit for information hiding or encapsulation reasons (non-static helper class required).

Implementation of the helper class is super easy. We just add the method to a new class and pass our list into it.

To make things more convenient, developers often make both the class and method static. This way they don’t have to instantiate the helper class.

But keep in mind that static methods are procedural by nature and can break object oriented design principles when misused. Like inheritance, static methods are a powerful tool which becomes dangerous in the wrong hands.

Static methods…

  • are totally safe when the method is pure (a given input always results in the same output).
  • are still reasonably safe when simple randomness is involved (as in our Shuffle example).
  • should avoid using non-local variables and other static methods. These are design smells.
  • should never use variables beyond the class scope, nor “unsafe” static methods. Refactor these immediately.
  • should not be used if you may need an instance of the class later on. This kind of refactoring can be a nightmare!

Used carelessly, static methods can and will bite you. Follow the above guidelines and generally prefer non-static alternatives.

Moving on, developers still need to remember that the helper class is at their disposal.

That sounds obvious but a single class is easy to overlook in a large codebase, especially when a colleague may have been the one to write it. If the class is declared within a namespace other than that of the extended type, they may not realize it’s even there.

The calling syntax is also bit cumbersome, but that’s a minor issue.

Overall, the helper class seems like a decent option, but wouldn’t it be cool if you could just do this?

Read on!

Option 3: Add an Extension Method

Implementing an extension method is just like the static helper class, except you add “this” preceding the first parameter in the method declaration.

What “this” does here is tell the compiler that it can infer the first parameter as the calling object.

That’s really useful because it allows you to call the static method as if it were just another instance method of that class.

How cool is that!?

Notice how you no longer need to specify the generic value type or pass in the list. That’s because first parameter in the method declaration is now treated as the class instance and the compiler already knows its type.

You’re not limited to extending classes either. You could just as easily extend an int, string, enum and so on.

What’s extra cool here is that the extension method will show up in the type’s IntelliSense drop-down, just like its instance methods. That makes the extension method hard to overlook, which can’t be said of the other options we’ve explored.

IntelliSenseExtensionMethod

This ninja magic does come with a few limitations, however…

  • The class containing the extension method cannot be generic or nested. No big deal there.
  • The class and extension methods must be static. Also no big deal, though this does mean that any other methods or non-local variables you use must also be static (or your code won’t compile).
  • The same cautions regarding static methods still technically apply (see previous section). However, they do tend to be far more forgiving in practice because they are called just like instance methods. You can’t use (misuse) them from areas which don’t have access to the class instance. In effect, they function in a more object oriented way.
  • You can’t override instance methods. This one can be pain since you won’t even get a compiler error! It’ll just run the instance method instead. Your extension method needs a unique signature to distinguish it from the instance method.

Full Code

Congratulations! You stuck it through to the near-end. I think that entitles you to the final code, don’t you?

There are a couple small, yet important changes here. See if you can find them!

The “using” keyword has been replaced with a “namespace” block.

Also note that the class is declared within the same namespace as the extended type.

Either way we’ll get access to the type we’re extending, but there are a few reasons I prefer this for extension methods:

  1. Declaring classes within a namespace is a general best practice. It forces you think about what kind of scope the class should have and helps keep your project organized.
  2. It makes more sense conceptually. We’re supplementing the extended type so it makes sense for both to share the same scope.
  3. Other developers won’t have to remember to include another namespace (or have prior knowledge of its existence). You could just omit the namespace, making it globally accessible, but that’s a bad idea (unless you’re extending a type which is also globally accessible).

Side Note: Sometimes helper classes can also benefit from sharing a namespace with the type they act upon. This isn’t always the case, however. For example, if the utility class is meant to add a feature set, rather than simply provide tools to supplement existing features, then a separate namespace may be preferred. That way you can choose whether or not you need that feature set.

List<T> has been replaced with IList<T> as the extended type.

But why? Well, because the object oriented design principle, “program to an interface, not an implementation”, is good advise.

Basically, we’re loosening up coupling by relying on the most basic type we can. By doing that, our Shuffle() method now works with anything which implements the IList<T> interface, not just List<T>.

This makes it easier to swap out our List<T> implementations later on, without having to alter our extension method.

Hey, this also works with arrays now! Neat, huh?

Organizational Tips

The name of the extension class mirrors that of the extended type, with “Extensions” appended to the end.

Adopting this best practice helps enforce small, well-defined, extension classes.

One bad practice I see happening time and time again is the use of one file for all extension methods.

Developers usually justify this by claiming that having them all in one place makes them easy to find. However, the opposite happens as your codebase grows and more extension methods are added.

Inevitably, the extensions file becomes a cluttered mess and you’re often forced to scrub the file to find what you’re looking for.

A better way keep your extensions organized is with your project file / folder structure.

ExtensionMethodFileStructure

Notice that all extension methods are still “in one place”,  the “ExtensionMethods” folder.

Subfolders are created to mirror the namespaces each class belongs to. Any extension classes sharing a namespace logically share a folder.

And if you were to later add an extension class for something in the “System.Collections” namespace, there’s already a folder for that. You don’t have to think much about where it should live.

This not only helps keep things tidy and easy to find, but also reinforces the best practices I’ve just mentioned.

If you try to add an extension file and can’t adhere to the namespace file structure, then it’s probably a clue to you that it should really be something like a helper class instead.

Closing Words

In this article, we explored various ways you can supplement a class you can’t edit with additional functionality. We covered the pros and cons of each, as well as several pitfalls and general best practices.

I know it was a lengthy process, but I hope you learned something useful.

Feel free to leave me a comment, share this article or buy me a coffee. A few seconds of your time would make my entire day.

– John Hutchinson

 

This is part 1 of a 2-part series. In part 2, I’ll share some useful extension methods you can use for free in your own projects.

Leave a Reply

You are allowed to enter 1 URL(s) in the comment area.

5 × one =