© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
G. ByrneTarget C#https://doi.org/10.1007/978-1-4842-8619-7_23

23. Generics

Gerard Byrne1  
(1)
Belfast, Ireland
 

Concept of Generics

In the last chapter, we learned about events and how there is a relationship between events and delegates, which themselves have a strong relationship with methods. We learned that events trigger event handlers, so the action of the event is to trigger a method. We also learned that we have a publish and subscribe relationship where the object that raises the event is the publisher, while the object that handles the event is the subscriber. Now in this chapter, we will look at a very useful concept called generics.

When we see the word generics in C# , we should think of the word general. So, in C# when we define a data type specification that uses generic parameters , we should look on them as substitute parameter types. The real parameter type will replace these generic parameters when we need to use the data type in our code. The C# language allows us to define generic interfaces, generic abstract classes, generic classes, generic events, generic delegates, generic fields, generic methods, etc. The generic type will appear in a set of open and close angle brackets, < >, so we could have something like this in our code:
public class PolicyClass<T>

where T is the parameter type .

Let us now consider a PolicyMatcher class based on a policy matching routine where
  • The PolicyMatcher class has one method called checkIfTheSame().

  • The method only accepts integer values.

  • The method is used to check if the policy numbers passed in are equal.

  • It uses a selection construct to display an appropriate message.

The PolicyMatcher class can be instantiated from within the Main() method and we can then call the checkIfTheSame() method , passing it our two integer values. Now let us code this example.

Let’s code some C# and build our programming muscle.

Add a new project to hold the code for this chapter.
  1. 1.

    Right-click the solution CoreCSharp .

     
  2. 2.

    Choose Add.

     
  3. 3.

    Choose New Project.

     
  4. 4.

    Choose Console App from the listed templates that appear.

     
  5. 5.

    Click the Next button.

     
  6. 6.

    Name the project Chapter23 and leave it in the same location.

     
  7. 7.

    Click the Next button.

     
  8. 8.

    Choose the framework to be used, which in our projects will be .NET 6.0 or higher.

     
  9. 9.

    Click the Create button.

     
Now we should see the Chapter23 project within the solution called CoreCSharp.
  1. 10.

    Right-click the project Chapter23 in the Solution Explorer panel.

     
  2. 11.

    Click the Set as Startup Project option.

     
Notice how the Chapter23 project name has been made to have bold text, indicating that it is the new startup project and that it is the Program.cs file within it that will be executed when we run the debugging.
  1. 12.

    Right-click the Program.cs file in the Solution Explorer window.

     
  2. 13.

    Choose Rename.

     
  3. 14.

    Change the name to Generics1.cs.

     
  4. 15.

    Press the Enter key.

     
  5. 16.

    Double-click the Generics1.cs file to open it in the editor window.

     
We will now create the namespace, the class, and the Main() method within the Generics1 class. We will also create a PolicyMatcher class outside the Generics1 class, and it will contain the method to check if the values are the same. The code is commented to explain what is being done in each line or block of code.
  1. 17.

    Amend the Generics1.cs, as in Listing 23-1.

     
namespace Chapter23
{
  class Generics1
  {
    static void Main(string[] args)
    {
    } // End of Main() method
  } // End of Generics1 class
    class PolicyMatcher
    {
      /*
      A method which is not generic because it has specified 
      the parameter data types. The method cannot be used
      when passing floats etc. It is specific not generic.
      */
      public string checkIfTheSame(int itemOne, int itemTwo)
      {
        if (itemOne.Equals(itemTwo))
        {
          return ($"The {itemOne.GetType()} values {itemOne} and {itemTwo} are equal");
        }
        else
        {
          return ($"The {itemOne.GetType()} values {itemOne} and {itemTwo} are not equal");
        }
      }// End of checkIfTheSame() method
    } // End of PolicyMatcher class
} // End of Chapter23 namespace
Listing 23-1

Class with the Main() method and try block

We will now amend the code to create an instance of the PolicyMatcher class and then call the checkIfTheSame() method , passing it two integer values.
  1. 18.

    Amend the class as in Listing 23-2.

     
 static void Main(string[] args)
 {
 // Instantiate the PolicyMatcher class
 PolicyMatcher myPolicyMatcher = new PolicyMatcher();
 /*
 Call the add method passing it the two policy values
 with the correct data type
 */
 Console.WriteLine(myPolicyMatcher.checkIfTheSame(10000, 20000));
 } // End of Main() method
Listing 23-2

Create instance of PolicyMatcher class and call checkIfTheSame()

  1. 19.

    Click the File menu.

     
  2. 20.

    Choose Save All.

     
  3. 21.

    Click the Debug menu.

     
  4. 22.

    Choose Start Without Debugging.

     
Figure 23-1 shows the console window displaying the details returned from the method.

A series of text on the console window indicates that the application code is functioning properly since it produces the expected results.

Figure 23-1

Method returns its message

  1. 23.

    Press the Enter key to close the console window.

     

We have just shown that this application code works well because it gives us the correct output as shown in Figure 23-1. However, this is a method that is specific in its parameter data types and, while this might be fine for now, because we are only checking integer values, we have a tightly coupled method . Tight coupling in programming is not good, and it would be better if we could make the method generic, so that it could accept different data types. If we could achieve this, we would have loosely coupled code . Thankfully, when we wish to have similar logic for similar types, this is where generics come to the rescue.

G eneric Class, Generic Method, Generic Parameters

  1. 24.

    Right-click the Generics1 filename in the Solution Explorer panel .

     
  2. 25.

    Choose Copy.

     
  3. 26.

    Right-click the Chapter23 project name.

     
  4. 27.

    Choose Paste.

     
  5. 28.

    Right-click the Generics1 – Copy file.

     
  6. 29.

    Choose Rename.

     
  7. 30.

    Name the class Generics2.cs and leave it in the same location.

     
  8. 31.

    Open the Generics2.cs file in the editor window.

     
We will now amend the copied file.
  1. 32.

    Amend the class name in the file to be Generics2 as in Listing 23-3.

     
namespace Chapter23
{
  class Generics2
  {
Listing 23-3

Amended Generics1 class name

  1. 33.

    Amend the PolicyMatcher class name to be PolicyMatcherGeneric as shown in Listing 23-4.

     
Console.WriteLine(myPolicyMatcher.checkIfTheSame(10000, 20000));
    } // End of Main() method
  } // End of Generics2 class
  class PolicyMatcherGeneric
  {
Listing 23-4

Amended PolicyMatcher class name

  1. 34.

    Amend the instantiation to reference PolicyMatcherGeneric, as shown in Listing 23-5.

     
    static void Main(string[] args)
    {
      // Instantiate the PolicyMatcher class
      PolicyMatcherGeneric myPolicyMatcher = new PolicyMatcherGeneric();
Listing 23-5

Amended instantiation to PolicyMatcherGeneric

We will now amend the code so that the class accepts the type <OurGenericType>. OurGenericType is a made-up name, a generic name, and it is used so we can replace it with a specific data type when required. The amended code is shown in Listing 23-6.
    } // End of Main() method
  } // End of Generics2 class
  class PolicyMatcherGeneric<OurGenericType>
  {
Listing 23-6

Class accepts <OurGenericType> type

Now we will amend the method code, so that the two parameters are of the type <OurGenericType>.
  1. 35.

    Amend the code as shown in Listing 23-7.

     
  class PolicyMatcherGeneric<OurGenericType>
    {
      /*
      A method which is not generic because it has specified
      the parameter data types. The method cannot be used
      when passing floats etc. It is specific not generic.
      */
      public string checkIfTheSame(OurGenericType itemOne, OurGenericType itemTwo)
Listing 23-7

Amended method to accept the type <OurGenericType>

Now that we have amended our class, we must change the way we have instantiated it. Here we will be using integer values so we will have the data type shown as <int>, but remember we are dealing with generics so we should be able to use any data type. We will also call the checkIfTheSame() method of the instance, displaying the returned value to the console.
  1. 36.

    Amend the code, as in Listing 23-8.

     
    static void Main(string[] args)
    {
      // Instantiate the PolicyMatcher class
      PolicyMatcherGeneric<int> myPolicyMatcher =
      new PolicyMatcherGeneric<int>();
  /*
  Call the add method passing it the two policy values
  with the correct data type
  */
Console.WriteLine(myPolicyMatcherString.checkIfTheSame( 10000, 20000));
    } // End of Main() method
Listing 23-8

Instantiate the class using the int type

Here we have passed in an int type. If we wished to pass in a string or a double, could we do this? Yes, we have made the class generic by using the <OurGenericType>, and therefore we can instantiate the class using <int>, <string>, <double>, etc.

Now we will instantiate the class again, but this time we will have string types rather than int types. We will then call the checkIfTheSame() method of the instance, displaying the returned value to the console.
  1. 37.

    Amend the code in the Main() method, as in Listing 23-9.

     
 Console.WriteLine(myPolicyMatcher.checkIfTheSame(10000, 20000));
 // Instantiate the PolicyMatcher class for strings
  PolicyMatcherGeneric<string> myPolicyMatcherString =
    new PolicyMatcherGeneric<string>();
  /*
  Call the add method passing it the two policy values
  with the correct data type
  */
Console.WriteLine(myPolicyMatcherString.checkIfTheSame( "PL123456", "PL123456"));
    } // End of Main() method
Listing 23-9

Instantiate the class a second time to accept string type

When we hover over either of the two method calls, we will see that the parameter types have adapted to those specified in our instantiation. Figures 23-2 and 23-3 show this adaptation in action. Amazing!

A series of text on the console window indicates that the types of the parameters have been adjusted to match those specified in the instantiation.

Figure 23-2

Method can accept an int followed by an int

A series of text on the console window indicates that if the same has been modified to accept the string, it will be followed.

Figure 23-3

Method can accept a string followed by a string

The generic class and its generic method have served us well since we can now pass in a policy numeric value or a policy string value and so on and the method still works. No need for separate methods to suit all the different data types . Loosely coupled indeed.
  1. 38.

    Click the File menu.

     
  2. 39.

    Choose Save All.

     
  3. 40.

    Right-click the Chapter23 project in the Solution Explorer panel.

     
  4. 41.

    Choose Properties from the pop-up menu.

     
  5. 42.

    Choose the Generics2 class in the Startup object drop-down list.

     
  6. 43.

    Close the Properties window.

     
  7. 44.

    Click the Debug menu.

     
  8. 45.

    Choose Start Without Debugging.

     
The console window will appear, as shown in Figure 23-4, and display the details returned from both method calls.

A console of using the information that was provided by each of the method calls coded.

Figure 23-4

Method returns the two messages, one for each call to it

  1. 46.

    Press the Enter key to close the console window.

     

Brilliant.

Now that we have an understanding of generics, let us look at what we might see when we look at generics example code on the Microsoft or another website. We will see that often the letter T is used to represent the type. The T is a generic type parameter; T is not a C# reserved keyword. In our Listing 23-7 example, we used the name OurGenericType rather than T. So in our code we could replace the OurGenericType with the letter T and the code should still work fine.

Amend the PolicyMatcherGeneric class code to use the <T> instead of the existing <OurGenericType> and have the method use the T for the type of the parameters as shown in Listing 23-10.
  class PolicyMatcherGeneric<T>
  {
    /*
    A method which is not generic because it has specified
    the parameter data types. The method cannot be used
    when passing floats etc. It is specific not generic.
    */
    public string checkIfTheSame(T itemOne, T itemTwo)
    {
      if (itemOne.Equals(itemTwo))
      {
        return ($"The {itemOne.GetType()} values {itemOne} " +
          $"and {itemTwo} are equal");
      }
      else
      {
        return ($"The {itemOne.GetType()} values {itemOne}" +
          $" and {itemTwo} are not equal");
      }
    }// End of checkIfTheSame() method
Listing 23-10

Class with <T> and method using generic T

In this example our class is generic, PolicyMatcherGeneric<T>. The T allows any type. T is just a placeholder for a type, and therefore our class can represent a collection of objects, the type of which will be dictated when we create the class. An example is shown for reference in Listings 23-11 and 23-12.

The specific type being defined as int:
PolicyMatcherGeneric<int> myPolicyMatcher = new
                                PolicyMatcherGeneric<int>();
Listing 23-11

Type is int

The specific type being defined as string:
PolicyMatcherGeneric<string> myPolicyMatcher = new
                                  PolicyMatcherGeneric<string>();
Listing 23-12

Type is string

Generic Class, Generic Method, Mixed Parameter Types

We will now create another method called checkIfTheSame(), but it will have a specified parameter type , followed by a generic parameter type, followed by a specified data type parameter. This is an example of method overloading.
  1. 47.

    Amend the PolicyMatcherGeneric class to add the new method, as in Listing 23-13.

     
  }// End of checkIfTheSame() method
/*
 A method which is generic because one parameter is of type T.
 This specifies that the parameter data types could be used
 when passing floats, strings etc. as the second argument.
 It is generic.
 */
 public string checkIfTheSame(string itemOne, T itemTwo, double premium)
 {
   /*
    The contains method returns true if the itemTwo is
    contained in itemOne otherwise it returns false
   */
   if (itemOne.Contains(itemTwo.ToString()))
   {
     return ($"The policy value {itemOne} corresponds with the value {itemTwo} and the premium is {premium}");
   }
   else
   {
     return ($"The policy {itemOne} does not correspond with the value {itemTwo} and the premium is {premium}");
   }
 }// End of second checkIfTheSame() method
Listing 23-13

New overloaded method called checkIfTheSame()

We will now call the new overloaded method from within the Main() method. As we are using the int type, we will use the <int> version of the instantiation, which we named as myPolicyMatcher.
  1. 48.

    Amend the Main() method as in Listing 23-14.

     
/*
Call the add method passing it the two policy values
with the correct data type
*/
Console.WriteLine(myPolicyMatcherString.checkIfTheSame("PL123456", "PL123456"));
/*
Call the new method passing it the two policy values
with the correct data type
*/
Console.WriteLine(myPolicyMatcher.checkIfTheSame("PL123456", 123456, 9.99));
    } // End of Main() method
  } // End of Generics2 class
Listing 23-14

Call the overloaded checkIfTheSame()

  1. 49.

    Click the File menu.

     
  2. 50.

    Choose Save All.

     
  3. 51.

    Click the Debug menu.

     
  4. 52.

    Choose Start Without Debugging.

     
The console window will appear, as shown in Figure 23-5, and display the details returned from the overloaded method call, which was passed an int to replace the generic T.

A series of text on the console window indicates that the overloaded method has been executed, and the T was changed to an int to reflect the change.

Figure 23-5

Overloaded method returns its message

  1. 53.

    Press the Enter key to close the console window.

     

Generic Method Only

Think about a RenewalMatcher class that will have a method that can
  • Check if the first parameter is a string.

  • Convert the string to a date to see if it contains the current month:
    • If it does have this month, then the policy is due for renewal and a renewal message will be displayed.

    • Otherwise, a message stating the month of renewal will be displayed.

  • If it is not a date, it must be a double.

  • The first parameter is converted to a double.

  • The second parameter represents the percentage increase, for example, 10 means 10%.

  • The new monthly premium is calculated.

The class is not defined as generic, but the method is made generic and has two generic parameters.
  1. 54.

    Amend the code, as in Listing 23-15, to add the RenewalMatcher class, inside the namespace of the Generics2 class. The class has one method, which is called checkIfRenewalDateOrPremiumIncrease().

     
  } // End of PolicyMatcherGeneric  class
  // Declare the class
  class RenewalMatcher
  {
    /*
    A method which is generic because of the <T>
    The method has parameters of type T.
    The method can be used when passing floats, strings etc.
    It is generic.
    */
    public string checkIfRenewalDateOrPremiumIncrease<T>(T
      itemOne, T itemTwo)
    {
      /*
      The is operator checks if the result of an expression
      is compatible with a given type
      */
      if (itemOne is string)
      {
        DateTime renewalDate = Convert.ToDateTime(itemOne);
        if (renewalDate.Month == DateTime.Now.Month)
        {
          return ($"The customers {itemTwo} " +
            $"policy is due for renewal this month ");
        }
        else
        {
          return ($"The customers {itemTwo} policy is not " +
            $"due for renewal until month {renewalDate.Month}");
        }
      }
      else
      {
  double monthlyPremium = Convert.ToDouble(itemOne.ToString());
  double premiumIncrease = Convert.ToDouble(itemTwo.ToString());
  double newMonthlyPremium = monthlyPremium
          + (monthlyPremium * premiumIncrease / 100);
 return ($"The new monthly premium is {newMonthlyPremium:0.00}");
      }
    }// End of checkIfRenewalDateOrPremiumIncrease() method
  } // End of RenewalMatcher class
} // End of Chapter23 namespace
Listing 23-15

RenewalMatcher class added

Now we will make three calls, from the Main() method , to the method in our new class:

  • The first call passes a string followed by a string with the month being 05, but we should change the month in this string to match the current month on our computer.

  • The second call passes a string followed by a string with the month being 06, but we should change the month in this string so it does not match the current month on our computer.

  • The third call passes a double followed by an int.

  1. 55.

    Amend the Main() method to instantiate the new class, as in Listing 23-16.

     
      /*
      Call the new method passing it the two policy values
      with the correct data type
      */
      Console.WriteLine(myPolicyMatcher.checkIfTheSame(“PL123456”, 123456, 9.99));
      // Instantiate the RenewalMatcher class
      RenewalMatcher myRenewalMatcher = new RenewalMatcher();
    } // End of Main() method
Listing 23-16

Instantiate the RenewalMatcher class added

  1. 56.

    Amend the Main() method to call the new class method three times, as in Listing 23-17.

     
Console.WriteLine(myPolicyMatcher.checkIfTheSame("PL123456", 123456, 9.99));
// Instantiate the RenewalMatcher class
RenewalMatcher myRenewalMatcher = new RenewalMatcher();
// Call the checkIfRenewalDateOrPremiumIncrease three times
Console.WriteLine(myRenewalMatcher.checkIfRenewalDateOrPremiumIncrease("01/02/2021", "Life Insurance"));
      Console.WriteLine(myRenewalMatcher.checkIfRenewalDateOrPremiumIncrease("01/06/2021", "Home Insurance"));
      Console.WriteLine(myRenewalMatcher.checkIfRenewalDateOrPremiumIncrease(9.99, 10));
    } // End of Main() method
Listing 23-17

Call the new method three times

  1. 57.

    Click the File menu.

     
  2. 58.

    Choose Save All.

     
  3. 59.

    Click the Debug menu.

     
  4. 60.

    Choose Start Without Debugging.

     
The console window will appear, as in Figure 23-6, and display the details returned from the three method calls.

A series of text on the console window indicates the information that was obtained from the three different method calls.

Figure 23-6

Method with two generic parameters executed

So we have just used a generic method within a normal class, but we should look at the code of this method and see that it could and should be divided into two overloaded methods because what we are passing is a string followed by a string or double followed by an int. This method should not be doing different things depending on the types. At the start of the chapter, we said this, “Thankfully, when we wish to have similar logic for similar types, this is where generics come to the rescue.”

So let us create overloaded methods to split out the business logic in the current method.
  1. 61.

    Amend the RenewalMatcher class so it now contains the two overloaded methods, as in Listing 23-18.

     
  } // End of PolicyMatcherGeneric class
  class RenewalMatcher
    {
      /*
      Method overloading
      */
      public string checkIfRenewalDateOrPremiumIncrease<T>(string itemOne, T itemTwo)
      {
        /*
        The is operator checks if the result of an expression
        is compatible with a given type
        */
        DateTime renewalDate = Convert.ToDateTime(itemOne);
        if (renewalDate.Month == DateTime.Now.Month)
        {
          return ($"The customers {itemTwo} policy is due for renewal this month");
        }
        else
        {
          return ($"The customers {itemTwo} policy is not due for renewal until month {renewalDate.Month}");
        }
      }// End of checkIfRenewalDateOrPremiumIncrease() method
      /*
      Method overloading
      */
      public string checkIfRenewalDateOrPremiumIncrease<T>(double itemOne, T itemTwo)
      {
        double monthlyPremium = Convert.ToDouble(itemOne.ToString());
        double premiumIncrease = Convert.ToDouble(itemTwo.ToString());
        double newMonthlyPremium = monthlyPremium + (monthlyPremium * premiumIncrease / 100);
        return ($"The new monthly premium is {newMonthlyPremium:0.00}");
      }// End of checkIfRenewalDateOrPremiumIncrease() method
    } // End of RenewalMatcher class
}  // End of Chapter 23 namespace
Listing 23-18

Overloaded methods to separate the business logic

  1. 62.

    Click the File menu.

     
  2. 63.

    Choose Save All.

     
  3. 64.

    Click the Debug menu.

     
  4. 65.

    Choose Start Without Debugging.

     
The console window will appear, as in Figure 23-7, and display the same details returned from the three method calls, but we have used overloaded methods.

A series of text on the console window indicates that the identical information from all three method calls, despite overloaded methods being used.

Figure 23-7

Overloaded methods used

Chapter Summary

So, finishing this short chapter on generics, we should have seen much of what we have covered in the previous chapters on methods and classes and objects. We have seen a class that is generic and a method that is generic, and the methods we looked at could have fully generic parameters or a mixture of generic parameters and non-generic parameters. We have seen that generics give us great flexibility when we program an application, so instead of having different methods that accept different types, we can make one method that accepts different types through the use of generic types.

Wow, seriously, what an achievement. This really is elaborate programming with advanced C# features and concepts. We should be immensely proud of the learning to date. In finishing this chapter, we have increased our knowledge further. We are getting incredibly close to our target, which once seemed so far away.

An illustration of concentric circles with two differently colored regions above a text that reads, Our target is getting closer.

..................Content has been hidden....................

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