Deconstructor in C#

Deconstructor in C#

I have often heard developers use deconstructor and destructor interchangeably while these words have completely two different meanings. In this article, I would like to talk about deconstructors in detail and have a look at its usages but let’s first have a quick look at destructors.

What is a destructor?

Destructors in C# are methods that are automatically called by the garbage collector when an object is no longer needed. They are used to perform cleanup tasks such as releasing unmanaged resources (e.g., file handles, network connections) or disposing of disposable objects.

Destructors define like a method without any parameters and modifiers. A destructor has following characteristics:

  • No Parameters: A destructor cannot have any parameters or return types.
  • No Access Modifiers: You cannot explicitly define an access modifier for a destructor. It is always protected.
  • Non-Inheritable: Destructors are not inherited and cannot be called directly.
  • Invoked Automatically: The garbage collector (GC) calls the destructor when it determines that there are no more references to an object.
  • Rarely Used: They are typically used for classes that work with unmanaged resources (e.g., file streams, database connections).

Let’s see how to use a destructor in action:

using System;
using System.IO;

class FileManager
{
     private FileStream? _fileStream;
     private string _filePath;

     // Constructor
     public FileManager(string filePath)
     {
          _filePath = filePath;
         fileStream = new FileStream(filePath, FileMode.OpenOrCreate);
          Console.WriteLine($"File '{_filePath}' opened.");
     }

     // Destructor (Finalizer)
     ~FileManager()
     {
          if (_fileStream != null)
          {
              _fileStream.Close();
              Console.WriteLine($"File '{_filePath}' closed in destructor.");
          }
     }

     // Method to write to the file
     public void WriteData(string data)
     {
          if (_fileStream != null)
          {
              byte[] bytes = System.Text.Encoding.UTF8.GetBytes(data);
              _fileStream.Write(bytes, 0, bytes.Length);
              Console.WriteLine("Data written to file.");
          }
     }
}

class Program
{
     static void Main(string[] args)
     {
          // Create an instance of FileManager
          FileManager fileManager = new FileManager("example.txt");

          // Write data to the file
          fileManager.WriteData("Hello, world!");

          // After the Main method finishes, the destructor will be called automatically
          // when the object is garbage collected
     }
}        

In this example ~FileManager() is a destructor and is used to close the file stream when garbage collector is going to remove an object of type FileManager.

While destructors are used for cleanup, they are often not the best choice. Instead, the IDisposable interface with a Dispose method is recommended in most cases, as it allows the developer to manage resource cleanup more explicitly and deterministically.

Best Practice is combining Dispose with a Destructor. Here is a better version of the FileManager class:

using System;
using System.IO;

class FileManager: IDisposable
{
     private FileStream? _fileStream;
     private string _filePath;
     private bool _disposed = false;  // Track whether resources have been released

     // Constructor
     public FileManager(string filePath)
     {
          _filePath = filePath;
          _fileStream = new FileStream(_filePath, FileMode.OpenOrCreate);
          Console.WriteLine($"File '{_filePath}' opened.");
     }

     // Method to write to the file
     public void WriteData(string data)
     {
          if (_fileStream != null)
          {
               byte[] bytes = System.Text.Encoding.UTF8.GetBytes(data);
               _fileStream.Write(bytes, 0, bytes.Length);
               Console.WriteLine("Data written to file.");
          }
     }

     // Implement IDisposable
     public void Dispose()
     {
          Dispose(true);
          GC.SuppressFinalize(this);  // Suppress finalization since cleanup is done
     }

     protected virtual void Dispose(bool disposing)
     {
          if (!_disposed)
          {
        	if (disposing)
        	{
                    // Release managed resources here
                    if (_fileStream != null)
                    {
                         _fileStream.Close();
                         _fileStream = null;
                    }
               }

        	// Release unmanaged resources here (if any)
        	_disposed = true;
          }     
     }

     // Destructor (Finalizer)
     ~FileManager()
     {
          Dispose(true); 
          GC.SuppressFinalize(this); // Suppress finalization since cleanup is done
     }
}        

Ok, enough about destructor, and let’s move to our main topic, deconstructor.

What is a Deconstructor?

Deconstructor is introduced in C# 7. A deconstructor is a special method used to decompose an object into its individual components, which allows for unpacking or extracting values from an object instance into separate variables. This concept is useful when you want to quickly retrieve multiple properties of an object without manually accessing them one by one.

Deconstructors are not destructors (which are used for cleanup). Instead, deconstructors are for extracting or destructuring objects.

Somehow we can say that a deconstructor is the opposite of a constructor. while a constructor mostly takes a set of parameters and assigns them to fields, a deconstructor does the reverse and assigns fields back to a set of variables.

How to deine a Deconstructor?

A deconstructor has the following characteristics:

  • Public method: A deconstructor is defined as a public method with a specific name (Deconstruct).
  • Out parameters: It returns values using out parameters for each component of the object you want to extract.
  • Flexible unpacking: It allows developers to unpack object properties into individual variables in a concise and readable way.

public void Deconstruct(out T1 value1, out T2 value2, ...)
{
     value1 = this.Property1;
     value2 = this.Property2;
     // And so on...
}        

Here is a sample of deconstructor:

public class Person
{
     public string FirstName { get; }
     public string LastName { get; }
     public int Age { get; }

     // Constructor
     public Person(string firstName, string lastName, int age)
     {
          FirstName = firstName;
          LastName = lastName;
          Age = age;
     }

     // Deconstructor (Deconstruct method)
     public void Deconstruct(out string firstName, out string lastName, out int age)
     {
          firstName = FirstName;
          lastName = LastName;
          age = Age;
     }
}        

Once a class has a deconstructor, you can use tuple-like syntax to extract values from an object:

var person = new Person("John", "Doe", 30);        

// Deconstruct the person into individual variables

var (firstName, lastName, age) = person;
Console.WriteLine($"First Name: {firstName}, Last Name: {lastName}, Age: {age}");        

This allows for a clean and readable way of extracting the values of multiple properties from an object in one step, as seen with tuple deconstruction.

Note: If you don’t need a variable, you can use discard symbol “_” to escape it like:

var (firstName, lastName, _) = person;        

Note: If you want to deconstruct a type that you didn’t author it, you can use deconstructor as an extension function.

Benefits of deconstructors

  • Concise Syntax: Deconstructors make it easier and more concise to extract values from an object.
  • Readability: Using a tuple deconstruction syntax improves the readability of the code.
  • Flexibility: It allows the developer to define how the object is decomposed into separate values, and what properties/fields should be extracted.

A practical example

Imagine you are working on a travel planning application. You have a list of Location objects, each representing a place with a name, latitude, and longitude. You want to compute the distance between pairs of locations in the list.

Let's start by defining the Location class, including a deconstructor to make it easier to unpack the location details:

public class Location
{
     public string Name { get; }
     public double Latitude { get; }
     public double Longitude { get; }

     public Location(string name, double latitude, double longitude)
     {
          Name = name;
          Latitude = latitude;
          Longitude = longitude;
     }

     // Deconstructor to unpack the location details
     public void Deconstruct(out string name, out double latitude, out double      longitude)
     {
          name = Name;
          latitude = Latitude;
          longitude = Longitude;
     }
}        

Using the Deconstructor to Calculate Distances

Now, you want to calculate the distance between all pairs of locations in a list. The Haversine formula is commonly used to calculate the distance between two points on the Earth given their latitude and longitude.

Here’s how you can use the deconstructor to simplify the process:

using System;
using System.Collections.Generic;

public static void Main()
{
     var locations = new List<Location>
     {
         new Location("New York", 40.7128, -74.0060),
         new Location("Los Angeles", 34.0522, -118.2437),
         new Location("Chicago", 41.8781, -87.6298),
     };

     // Calculate and display distances between each pair of locations
     for (int i = 0; i < locations.Count; i++)
     {
         for (int j = i + 1; j < locations.Count; j++)
         {
             var (name1, lat1, lon1) = locations[i];
             var (name2, lat2, lon2) = locations[j];
             double distance = CalculateHaversineDistance(lat1, lon1, lat2, lon2);
             Console.WriteLine($"Distance between {name1} and {name2}: {distance:F2} km");
         }
     }
}

// Haversine formula to calculate distance between two coordinates
public static double CalculateHaversineDistance(double lat1, double lon1, double lat2, double lon2)
{
     const double R = 6371; // Radius of the Earth in kilometers
     var lat = (lat2 - lat1) * Math.PI / 180.0;
     var lon = (lon2 - lon1) * Math.PI / 180.0;
     var a = Math.Sin(lat / 2) * Math.Sin(lat / 2) +
             Math.Cos(lat1  Math.PI / 180.0)  Math.Cos(lat2  Math.PI / 180.0) 
             Math.Sin(lon / 2) * Math.Sin(lon / 2);

     var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
     return R * c;
}        

If you run the program, the output will show the distances between the pairs of cities:

Distance between New York and Los Angeles: 3935.75 km
Distance between New York and Chicago: 1145.41 km
Distance between Los Angeles and Chicago: 2804.66 km        

Conclusion

A deconstructor in C# is a powerful feature that allows you to decompose an object into its individual properties or fields using a Deconstruct method. It provides a clean and concise way to work with object values, especially when dealing with objects that have multiple properties.


要查看或添加评论,请登录

Amir Doosti的更多文章

  • Plotting in C# (Part 4 - ScottPlot)

    Plotting in C# (Part 4 - ScottPlot)

    ScottPlot is an open-source, .NET-based charting library designed for creating high-performance, interactive plots in…

  • Plotting in C# (Part 3 - OxyPlot)

    Plotting in C# (Part 3 - OxyPlot)

    OxyPlot is a lightweight, open-source plotting library designed specifically for .NET applications, supporting…

    2 条评论
  • Plotting in C#.Net (Part2 - LiveCharts2)

    Plotting in C#.Net (Part2 - LiveCharts2)

    LiveCharts is a versatile and modern charting library that supports a variety of charts and visualizations with smooth…

  • Plotting in C#.Net (Part 1 - General)

    Plotting in C#.Net (Part 1 - General)

    Plotting is a crucial tool for data analysis, visualization, and communication. There are many reasons why we need to…

    2 条评论
  • Half-Precision floating point in C#

    Half-Precision floating point in C#

    Recently I encountered a problem in a system where we needed to use floating point but we had just two bytes memory for…

    3 条评论
  • Working with Excel files in .Net

    Working with Excel files in .Net

    Using Excel files in software applications is common for several reasons, as they provide a practical and versatile…

  • ReadOnly Collections vs Immutable Collections

    ReadOnly Collections vs Immutable Collections

    In C#, both readonly collections and immutable collections aim to prevent modifications to the collection, but they…

  • CancellationToken in C#

    CancellationToken in C#

    As I promised, in this Article we will discover CancellationToken and all its reated types and usage. The…

    2 条评论
  • Signaling in C#

    Signaling in C#

    Introduction to Signaling Signaling is a concept used in computer science to manage communication and coordination…

    4 条评论
  • Partitioning vs. Sharding vs. Replication

    Partitioning vs. Sharding vs. Replication

    Partitioning and sharding are two crucial techniques employed to enhance performance, scalability, and manageability…

    2 条评论

社区洞察

其他会员也浏览了