Creating instances in C# is a bit different because there’s a difference between value types and reference types. You’ll be able to navigate your objects with ease after this episode.
You first learn how to place your C# classes in their own .cs files and how this is different than creating separate .cpp and .h files as in C++. You also have the ability in C# to declare your classes, structs, and even methods to be partial which allows you to divide them between multiple files.
Then, you learn how to determine which types are value types and which are reference types. This is a new concept that at first seems similar to references in C++ but it’s actually a very fundamental difference in C#.
Value type variables in methods always go to the stack directly and get their own instance. So if you have two instances of a struct type and you assign one of the variables to the other, what you end up doing is copying the contents of the one to the other.
But a reference type just gives you a pointer that lives on the stack which actually points to the instance in the heap. C# doesn’t call this a pointer and it ends up looking very much like a reference in C++. If you create two instances of a class type and assign one of the variables to the other, what you end up with is both variables now point to the same instance. The values of the data inside the instance did not change and in fact, one of the instances will now be eligible for garbage collection.
That’s all there is really to deleting instances in C#, you just forget about them and the garbage collector will clean it up at some later point. You can always implement Disposable and call the Dispose method if you have any action you need to perform before then. Just make sure to call Dispose while your reference still refers to the instance.
Listen to the full episode or read the full transcript below for more.
C# puts everything into source files called cs files and you normally create a cs file named after your class, struct, or interface that’s defined by that file. So if you have a class called Hero, then you would have just a Hero.cs file that defines the Hero class. If this was C++, then you would have separate Hero.h and Hero.cpp files.
C# needs everything in cs files because the managed nature of the language relies on the runtime having access to everything. There’s no need to keep the implementation in a separate file. You can actually have a class split between multiple cs files in C# and this is really useful if, say, part of your class definition is generated by some automated tool while you write the rest. You don’t want your hand-written code getting overwritten when the tool creates an update. But the main thing is that no matter how many cs files are used to make a class definition, the runtime needs access to all of them.
C# also supports structs which in C++ are almost identical to classes but are quite different in C#. Other than some limitations placed on structs, the biggest difference between the two in C# is that structs are value types while classes are reference types.
You’ll see this difference when you create instance variables of your class or struct types.
Before explaining the differences though, I want to mention that all the types in C# are either value or reference types. You’ll get a feel for which are which after a while but here’s the breakdown:
Value types consist of structs, chars, bools, dates, enumerations, and all the numeric types.
Reference types consist of classes, arrays, strings, and delegates.
I know, we haven’t discussed all of these types. I’m trying to keep this episode focused on just what you need to know to be able to understand what happens when you create and dispose instances. Knowing that there are two main classifications of types that behave differently is enough for now.
When you declare a variable in a method and give it a type and a name, you’re always creating that variable on the stack. This is true in C++ too because in C++ when you allocate a variable on the heap with operator new that returns a pointer, your variable is actually a pointer to the type that was just created. And that pointer lives on the stack. The instance of the class that was created is on the heap but in order to get to it, you must still go through a pointer. Without that pointer, you would have no way to know where in the heap your instance was created. But where C++ allows you to create anything directly on the stack, including class instances, C# says “no way”. You can do this in C++ because C++ has no distinction between value types and reference types. Sure C++ has values, and it has types, and it has references. But in C++ any type can be a reference or not.
C# makes a distinction which might at first seem sort of arbitrary. I mean, why is a date a value type and not a reference type? It mostly comes down to how the types are typically used. Small types or types that you don’t normally change once created are value types. Don’t worry, it still seems sort of arbitrary to me too.
Okay, so in C#, if you declare a variable inside of a method that’s a struct, then the data for that struct instance will live on the stack. And this is important. It will live on the stack even if you initialize it with the operator new. Operator new for structs, really just skips over the part about allocating memory in the heap and just puts the instance on the stack and then calls the constructor. It also doesn’t return a pointer. You can work with the variable instance directly. All value types behave like this.
Now for the Hero class type, when you declare a variable inside of a method of this type, you have to use operator new. There’s no way for you to put that class instance on the stack directly. What you get on the stack is a reference to the instance that operator new placed on the heap. It looks like a local variable that you can access directly and you can. C# does not expose pointers. But really, a pointer is exactly what you have.
The interesting behavior comes when you try assigning your variables to each other.
If you declare two struct variables and then assign one to the other, you start out with two separate variables each with their own values and end up with two separate variables only now they each have the same values because of the assignment. If you then change a property on one of the variables, it has no effect on the other. They are still two independent instances of the struct that each live on the stack.
But try this with the Hero class and you get a much different result. Let’s start out with two Hero variables called redHero and blueHero and create each by calling operator new. This will give you two reference variables that live on the stack and each reference will point somewhere in the heap where the red and blue Hero instances actually live. Now if you were to write code that said “redHero is assigned blueHero”, what happens? You end up with both the redHero and the blueHero references both pointing to the same blueHero instance on the heap. You no longer have anything that points to the redHero instance.
If this was C++, you would have just created a memory leak. But we’re talking about C# here and this is exactly how C# behaves. The redHero instance is not leaked. True, you can’t get to it anymore. But the runtime still knows about it.
The next time the garbage collector runs, it’ll notice that there’s no way for your program to ever get to the redHero instance and will flag the instance for deletion.
So in C#, when you want to delete something, all you have to do is forget about it. Make sure that you no longer have any references that point to the instance and the garbage collector will take care of the rest.
So does that mean that the garbage collector will delete the object? Maybe. As C# programmers, we’re not able to control this. If your computer is running with lots of extra memory, then the garbage collector might just decide to let the instance stick around a while longer. The garbage collector is only concerned with memory usage.
But what if the redHero instance had something important inside that wasn’t memory but was limited in other ways? Something that you really don’t want to stay locked up inside the orphaned instance for who knows how long?
If this is the case, you really need to make sure that your Hero class implements the IDisposable interface. This will give you access to the Dispose method. You just need to make sure that you call Dispose while you still have the chance.
What you don’t want to do is write code that says, “redHero is assigned blueHero and then say redHero.Dispose” because that will dispose what redHero currently refers to which is now the blueHero instance. You just disposed the instance that both references point to.
What you have to do instead is write code that says, “redHero.Dispose and then redHero is assigned blueHero”
When you write your code in this order, then you’ll Dispose the redHero instance which will free up your important resource. You do this while you still have a reference to the redHero instance. Then when you assign the blueHero to the redHero, you forget all about the redHero instance on the heap. But now you don’t care how long it takes the garbage collector to actually reclaim the memory because now, it’s just some memory.
So far, I’ve been talking about declaring variables in C# as local variables in a method. But what about what happens when you declare that your Hero class needs a class member variable of, say, and int. Integers are value types in C#. If you declare an integer in a method, then it’ll go on the stack. But since this int is part of a class, then it goes on the heap because that’s where all instances of classes go. It just adds to the size of the instance in memory.
Let me end with one more final thought about sizes. In C++, you can control very well and can find out exactly how much memory anything will require. You don’t have the same ability in C#. All you can really say in C# is that as you add more member data to your classes or structs, then the memory needed will also increase. But there’s no way to find out exactly what’s needed. It is after all, managed for you.