Managing memory with reference and value types

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.

Note

If a struct uses types that are not of the struct type for any of its fields, then those fields will be stored on the heap!

These are the most common struct types in .NET Core:

  • Numbers: byte, sbyte, short, ushort, int, uint, long, ulong, float, double, decimal
  • Miscellaneous: char, bool
  • System.Drawing: Color, Point, Rectangle

Almost all the other types in .NET Core are class types, including string.

Note

You cannot inherit from struct.

Defining a struct type

Add a class file named DisplacementVector.cs to the Ch07_PacktLibrary project.

Note

There isn't an item template in Visual Studio 2017 for struct, so you must use class and then change it manually.

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)

Tip

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.

Releasing unmanaged resources

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.

Note

For this topic, I will show some code examples, but you do not need to create them in your current project.

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 
      } 
    } 

Note

Do not confuse a finalizer aka destructor with a deconstructor. A destructor releases resources, that is, it destroys an object. A deconstructor returns an object split up into its constituent parts and uses the new C# 7 deconstruction syntax.

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; 
      } 
    } 

Note

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.

Ensuring that dispose is called

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(); 
    } 
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.123.106