There are two categories of memory: stack memory and heap memory. Stack memory is fast but limited and heap memory is slow but plentiful.
There are two C# keywords that you use to create object types: class
and struct
. Both can have the same members. The difference between the two is how memory is allocated.
When you define a type using class, you are defining a reference type. This means that the memory for the object itself is allocated on the heap, and only the memory address of the object (and a little overhead) is stored on the stack.
When you define a type using struct
, you are defining a value type. This means that the memory for the object itself is allocated on the stack.
These are the most common struct
types in .NET Core:
byte
, sbyte
, short
, ushort
, int
, uint
, long
, ulong
, float
, double
, decimal
char
, bool
Color
, Point
, Rectangle
Almost all the other types in .NET Core are class
types, including string
.
Add a class file named DisplacementVector.cs
to the Ch07_PacktLibrary
project.
Modify the file as shown in the following code:
namespace Packt.CS7 { public struct DisplacementVector { public int X; public int Y; public DisplacementVector(int initialX, int initialY) { X = initialX; Y = initialY; } public static DisplacementVector operator +( DisplacementVector vector1, DisplacementVector vector2) { return new DisplacementVector(vector1.X + vector2.X, vector1.Y + vector2.Y); } } }
In the Ch07_PeopleApp
project, in the Main
method, add the following code:
var dv1 = new DisplacementVector(3, 5); var dv2 = new DisplacementVector(-2, 7); var dv3 = dv1 + dv2; WriteLine($"({dv1.X}, {dv1.Y}) + ({dv2.X}, {dv2.Y}) = ({dv3.X}, {dv3.Y})");
Run the application and view the output:
(3, 5) + (-2, 7) = (1, 12)
Good Practice
If all the fields in your type use 16 bytes or less of stack memory, your type only uses struct
types for its fields, and you will never want to derive from your type, then Microsoft recommends that you use a struct
. If your type uses more than 16 bytes of stack memory, or if it uses class types for its fields, or if you might want to inherit from it, then use class
.
In the previous chapter, we saw that constructors can be used to initialize fields and that a type may have multiple constructors.
Imagine that a constructor allocates an unmanaged resource, that is, anything that is not controlled by .NET. The unmanaged resource must be manually released because .NET cannot do it for us.
Each type can have a single finalizer (aka destructor) that will be called by the CLR when the resources need to be released. A finalizer has the same name as a constructor, that is, the type name, but it is prefixed with a tilde (~
), as shown in the following example:
public class Animal { public Animal() { // allocate an unmanaged resource } ~Animal() // Finalizer aka destructor { // deallocate the unmanaged resource } }
This is the minimum you should do in this scenario. The problem with just providing a finalizer is that the .NET garbage collector requires two garbage collections to completely release the allocated resources for this type.
Though optional, it is recommended to also provide a method to allow a developer who uses your type to explicitly release resources so that the garbage collector can then release the object in a single collection.
There is a standard mechanism to do this in .NET by implementing the IDisposable
interface, as shown in the following example:
public class Animal : IDisposable { public Animal() { // allocate unmanaged resource } ~Animal() // Finalizer { if (disposed) return; Dispose(false); } bool disposed = false; // have resources been released? public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposed) return; // deallocate the *unmanaged* resource // ... if (disposing) { // deallocate any other *managed* resources // ... } disposed = true; } }
There are two Dispose
methods. The public
method will be called by a developer using your type. The Dispose
method with a bool
parameter is used internally to implement the deallocation of resources, both unmanaged and managed. When the public Dispose
method is called, both unmanaged and managed resources need to be deallocated, but when the finalizer runs, only unmanaged resources need to be deallocated.
Also, note the call to GC.SuppressFinalize(this)
---this is what notifies the garbage collector that it no longer needs to run the finalizer and removes the need for a second collection.
When someone uses a type that implements IDisposable
, they can ensure that the public Dispose
method is called with the using
statement, as shown in the following code:
using(Animal a = new Animal()) { // code that uses the Animal instance }
The compiler converts your code into something like the following, which guarantees that even if an exception occurs, the Dispose
method will still be called:
Animal a = new Animal(); try { // code that uses the Animal instance } finally { if (a != null) a.Dispose(); }
3.138.123.106