C#. Generics. Basic concepts. Generic classes and structures





Generics. Basic concepts. Generic classes and structures


Contents


Search other resources:

1. The concept of generics. Disadvantages of non-generalized aggregated data structures (collections). The need to apply generics

In the first versions of C# .NET, when creating programs, data was described by strictly defined types, such as int, float, double, and the like. On the basis of these types, it was possible to create the basic elements of the program – arrays, structures, classes, and the like.

The need to create the same elements for different types gave rise to the redundancy of the code. So, if the program needed to declare three arrays of data of types int, float and char, then each array had to be declared separately. Also, it was necessary to develop codes for processing these arrays separately. If each array must be processed in the same way (for example, finding an element), then the code for processing each array was repeated. Also, it was necessary to develop codes for processing these arrays separately. If each array must be processed in the same way (for example, finding an element), then the code for processing each array was repeated. The only difference was in the description of the data type of the array (int, float, char, etc.). As a result, the need arose to create a mechanism that would allow each array to be processed in a single unified way, regardless of its underlying data type.

The second reason was to improve type safety when using non-generics collections. In this kind of collections (CollectionBase, ArrayList, HashTable, SortedList, and others), data is stored as an object type, which is basic for all types. When extracting data from a collection, you must explicitly specify the type conversion (otherwise the compiler will generate an error at the compilation stage). If you specify the wrong type during explicit type conversion, an error will occur at runtime. If you specify the wrong type during explicit type conversion, an error will occur at runtime. To avoid this, you need to add additional blocks of checks using the try-catch, is, as statements. Such checks will add extra code to the program, which will complicate it.

Considering the above disadvantages, starting with the .NET 2.0 version, support for so-called generics was introduced into the C# language.

Generalizations is a C# language tool that allows you to create program code containing a single (typed) solution to a problem for various types, with its subsequent application for any specific type (int, float, char, etc.).

The term generic is associated with the concept of a parameterized type. With the help of parameterized types, you can create some elements of the language (classes, structures, etc.) in such a way that the processed data in them is represented by a type parameter.

 

2. Benefits of using generics

Using generics provides the following benefits:

  • simplification of the program code. Using generics allows you to implement the algorithm for any type of element. There is no need to create similar variants of the algorithm for different types (int, float, string, etc.);
  • ensuring typical safety. Generic elements (classes, methods, collections, etc.) contain only objects of a certain type, specified when they are declared;
  • in generalizations, the need for an explicit type conversion when converting an object or other type of processed data is eliminated;
  • productivity increase. When using generics, structured types are passed by value. This does not perform boxing and unboxing, which slow down program execution.

 

3. To what elements of the language can generalizations be applied?

Generalizations can be applied to:

  • classes;
  • structures;
  • interfaces;
  • methods;
  • delegates.

Separately, the application of generics to collections is highlighted. A number of generic collections have been created on the basis of this mechanism: Collection<T>, List<T>, Dictionary<TKey, TValue>, SortedList<TKey, TValue>, Stack<T>, Queue<T>, LinkedList<T>.

If a language element (class, structure, interface, method, delegate) operates with a parameterized data type (generalization), then this element is called generalized (generalized class, generalized method, etc.).

 

4. Syntax for declaring a generic class for several types T1, T2, …, TN

In C#, it is possible to declare generic classes that work with some generic types. These types serve as parameters that can be passed to the class.

The general form of declaring a generic class is as follows:

class ClassName<T1, T2, ..., TN>
{
  // Implementation of class methods and fields
  // that work with generic types T1, T2, ..., TN
  // ...
}

here

  • ClassName – generic class name;
  • T1, T2, …, TN – the names of the generic types used to work in the ClassName class. Type names can be anything (for example, Type or TT). Type names are enclosed in brackets <>.

For example, a simplified view of a generic class that receives Type1, Type2 as parameters is:

class ClassName<Type1, Type2>
{
  // ...
}

After declaring a generic class, you can create a reference to that class. In the simplest case, the general form of creating a reference to such a class is

ClassName<t1, t2, ..., tN> obj = new ClassName<t1, t2, ..., tN>(arguments);

here

  • t1, t2, tN – placeholder type names that act as arguments. These can be value types int, float, char, etc., built-in .NET types, or user-defined types defined in a program;
  • ClassName<t1, t2, …, tN>(arguments) – a call to a class constructor that receives arguments;
  • obj – the name of the instance of the generic class.

 

5. Syntax for declaring a generic class and creating a reference to it for one type T

If in a generic class only one type T acts as a type parameter, then the general form can be simpler

class ClassName<T>
{
  // ...
}

After that, you can declare a reference to the generic class ClassName using the following form

ClassName<type> obj = new ClassName<type>(arguments);

here

  • type – a placeholder type that is set as an argument;
  • ClassName<type>(arguments) – calling a class constructor with an enumeration of arguments;
  • obj – the name of the reference being declared.

 

6. Generalized structures. Syntax for declaring a generic structure for type T

Structures, like classes, can use the generic type T as a parameter. The general form of declaring a generic structure for several types is as follows

struct StructName <T1, T2, ..., TN>
{
  // ...
}

here

  • StructName – the name of structure;
  • T1, T2, TN – the names of the parameterized types used by the structure.

The creation of an instance of a structure named StructName has the following general form:

StructName<t1, t2, ..., tN> val = new StructName<t1, t2, ..., tN>(arguments);

here

  • t1, t2, …, tN – placeholder types on the basis of which a specific instance named val is formed;
  • arguments – structure constructor arguments.

 

7. Examples of generic classes that take the type T as a parameter

Example 1. Implement a Point class that defines a point on a coordinate plane. In the class, implement:

  • internal fields x, y;
  • constructor with 2 parameters;
  • properties to access internal class fields;
  • a method that outputs the values of the internal fields of the class.

 

using System;

namespace ConsoleApplication3
{
  // A class that describes a point..
  // The class receives the type T as a parameter.
  class Point<T>
  {
    // Internal fields
    T x, y;

    // Constructor
    public Point(T _x, T _y)
    {
      x = _x; y = _y;
    }

    // Properties
    public T X
    {
      get { return x; }
      set { x = value; }
    }

    public T Y
    {
      get { return y; }
      set { y = value; }
    }

    // Method that displays data on the screen
    public void Print(string message)
    {
      Console.Write(message + " => ");
      Console.WriteLine("x = {0}, y = {1}", x, y);
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // Demonstration of using the generic Point<T> class
      // 1. Create an instance with binding to int type
      Point<int> objInt = new Point<int>(23, 15);
      objInt.Print("objInt");

      // 2. Create an instance with binding to the double type
      Point<double> objDouble = new Point<double>(7.88, -3.22);
      objDouble.Print("objDouble = ");

      Console.ReadKey();
    }
  }
}

In the above code, in the Main() function, an instance of the Point class is created for type int

Point<int> objInt = new Point<int>(23, 15);

Accordingly, an instance of the Point class is created for the double type.

Point<double> objDouble = new Point<double>(7.88, -3.22);

The types int and double that are set for the Point<T> class are called closed constructed types.

Example 2. Declare a generic class that describes the Trapezoid. The class must be generic and operate on some type T (the class receives the type T as a parameter). Implement the following elements in the class:

  • internal fields a, b, h, which determine the lengths of the sides and the height of the trapezoid, respectively;
  • constructors;
  • properties A, B, H for access to internal fields;
  • the Area() method, which returns the area of the trapezoid.

The program code of the class is as follows:

using System;

namespace ConsoleApp19
{
  // Generic class that implements trapezoid
  class Trapezoid<T>
  {
    // Internal fields of the class
    private T a, b, h;

    // Constructor
    public Trapezoid(T _a, T _b, T _h)
    {
      a = _a;
      b = _b;
      h = _h;
    }

    // Properties to access the class fields
    public T A
    {
      get { return a; }
      set { a = value; }
    }

    public T B
    {
      get { return b; }
      set { b = value; }
    }

    public T H
    {
      get { return h; }
      set { h = value; }
    }

    // Method that returns the area of a trapezoid
    public double Area()
    {
      double a = Convert.ToDouble(this.a);
      double b = Convert.ToDouble(this.b);
      double h = Convert.ToDouble(this.h);
      return (a + b) * h / 2;
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // Demonstrate how the generic Trapezoid<T> class works
      // 1. Create reference with placeholder type of short
      Trapezoid<short> tr1 = new Trapezoid<short>(3, 5, 2);

      // 2. Calculate the area tr1 and display it
      Console.WriteLine("Area of tr1 = {0:f3}", tr1.Area());

      // 3. Create tr2 reference with float placeholder
      Trapezoid<float> tr2 = new Trapezoid<float>(1.5f, 2.8f, 1.1f);

      // 4. Calculate the area of tr2
      double area = tr2.Area();
      Console.WriteLine("Area of tr2 = {0:f3}", tr2.Area());

      Console.ReadKey();
    }
  }
}

The result of the program

Area of tr1 = 8.000
Area of tr2 = 2.365

 

8. Examples of declaring and using generic structures

Example 1. A Line<T> structure is declared that implements a line segment on the coordinate plane. The segment is specified by the coordinates of the points (x1; y1), (x2; y2).

The main() function demonstrates the creation of an instance of a structure with parameters of types double and long.

using System;

namespace ConsoleApp19
{
  // A generalized structure that takes as a parameter the type T.
  // The structure defines a line with the coordinates of the points.
  struct Line<T>
  {
    // Internal fields - coordinates of points
    public T x1, y1, x2, y2;

    public Line(T x1, T y1, T x2, T y2)
    {
      this.x1 = x1; this.y1 = y1;
      this.x2 = x2; this.y2 = y2;
    }

    public void Print(string message)
    {
      Console.Write(message + "   ");
      Console.WriteLine("x1 = {0}, y1 = {1}, x2 = {2}, y2 = {3}",
          x1, y1, x2, y2);
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // Declare an instance of the Line<T> structure with a double parameter
      Line<double> line1 = new Line<double>(2.5, 3.8, -1.4, 8.2);
      line1.Print("line1: ");

      // Declare an instance of the Line<T> structure with a long parameter
      Line<long> line2 = new Line<long>(2323L, -2332L, 5000L, 3000L);
      line2.Print("line2: ");

      Console.ReadKey();
    }
  }
}

The result of the program:

line1:   x1 = 2.5, y1 = 3.8, x2 = -1.4, y2 = 8.2
line2:   x1 = 2323, y1 = -2332, x2 = 5000, y2 = 3000

Example 2.

Implement a ComplexNumber<T> structure that describes a complex number. The structure receives as a parameter the type T. In the body of the structure, implement the following elements:

  • internal fields of the generalized type T, which determine the real component (re) and the imaginary component (im) of the complex number;
  • a constructor with two parameters of the generic type T;
  • properties for reading field values of the ComplexNumber<T> structure;
  • overloaded operators + (plus) and (minus), which implement respectively addition and subtraction of complex numbers of type ComplexNumber<T>;
  • the Print() method for displaying information about a complex number.

Implement type validation in the ComplexNumber<T> structure: only numeric types can be used.

The text of the solution of the task is as follows

using System;

namespace ConsoleApp19
{
  // A generalized structure describing a complex number.
  // The structure receives as a parameter the type T.
  struct ComplexNumber<T>
  {
    // Internal fields
    private T re, im;

    // Constructor
    public ComplexNumber(T _re, T _im)
    {
      // Checking in the constructor if the type T is numeric
      if ((_re is String)||(_re is Char)||(_re is Boolean)||
          (_im is String)||(_im is Char)||(_im is Boolean))
      {
        // If the type is not numeric, throw an exception
        throw new Exception();
      }

      re = _re; im = _im;
    }

    // Properties for reading complex number fields
    public T Re
    {
      get { return re; }
    }

    public T Im
    {
      get { return im; }
    }

    // Method that displays information about the current complex number
    public void Print(string message)
    {
      Console.Write(message + " ");
      Console.WriteLine("re = {0}, im = {1}", re, im);
    }

    // The overloaded addition operator +.
    // Everything is converted to double type.
    // The operator returns the concrete type ComplexNumber <double>.
    public static ComplexNumber<double> operator + (ComplexNumber<T> c1,
        ComplexNumber<T> c2)
    {
      // 1. Convert any numeric type T to double type
      double re = Convert.ToDouble(c1.re) + Convert.ToDouble(c2.re);
      double im = Convert.ToDouble(c1.im) + Convert.ToDouble(c2.im);

      // 2. Create a new instance (reference) with binding to the double type
      ComplexNumber<double> res = new ComplexNumber<double>(re, im);

      // 3. Return the result
      return res;
    }

    // Operator subtracting complex numbers.
    // Everything is converted to float type.
    // The operator returns the concrete type ComplexNumber<float>.
    public static ComplexNumber<float> operator - (ComplexNumber<T> c1,
      ComplexNumber<T> c2)
    {
      // Convert any numeric type to float
      float re = Convert.ToSingle(c1.re) - Convert.ToSingle(c2.re);
      float im = Convert.ToSingle(c1.im) - Convert.ToSingle(c2.im);

      // Declare a reference to an instance with a binding to the float type
      ComplexNumber<float> res = new ComplexNumber<float>(re, im);

      // Return the result
      return res;
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // 1. Declare 2 complex numbers for type double
      //    2.8 + 0.5*j
      ComplexNumber<double> cm1 = new ComplexNumber<double>(2.8, 0.5);

      // 1.7 - 2.2*j
      ComplexNumber<double> cm2 = new ComplexNumber<double>(1.7, -2.2);

      // Add complex numbers.
      // Use the overloaded operator +
      ComplexNumber<double> cm3 = cm1 + cm2;
      cm3.Print("cm3: ");

      // ------------------------------------
      // 2. For int type
      ComplexNumber<int> cm4 = new ComplexNumber<int>(2, 8);
      ComplexNumber<int> cm5 = new ComplexNumber<int>(4, -3);

      // substract numbers - get the corresponding float instance
      ComplexNumber<float> cm6 = cm4 - cm5;
      cm6.Print("cm6: ");

      Console.ReadKey();
    }
  }
}

The result of the program

cm3: re = 4.5, im = -1.7
cm6: re = -2, im = 11

In the class, the operator functions operator+() and operator-() return the result for the double filler type. This is logical, since the double type is the widest numeric type that can correctly accept the values of other “narrow” types such as int, float, short, long and the like.

The type correctness check (definition of a non-numeric type) is carried out immediately in the constructor of the ComplexNumber<T> class. Complex numbers are data of numeric types (float, double, int, short, long, etc.). If in the main() function we try to add the code of the ComplexNumber<string> class that operates on strings, for example

ComplexNumber<string> cmS = new ComplexNumber<string>("a2", "aa3");
ComplexNumber<string> cmS2 = new ComplexNumber<string>("5", "-4");
ComplexNumber<double> cmS3 = cmS + cmS2;

then the compiler will throw an exception when the constructor is called.

 

9. Restrictions on generics

The following restrictions are imposed on generics in programs:

  • properties cannot be generic but can be used in a generic class (structure);
  • indexers cannot be generic but can be used in a generic class (structure);
  • overloaded operators cannot be generic. However, the use of a type parameter T is allowed here;
  • events (event) cannot be generic but can be used in generic classes (structures);
  • the extern modifier cannot be applied to a generic class;
  • pointer types cannot be used in arguments of the types;
  • if a static field (static) is used in a generic class (structure), then a unique copy of this field is created in an object of each specific type (int, double, etc.). That is, there is no single static field for all objects of various types that are constructed;
  • arithmetic operations (+, , * and others) and comparison operations cannot be applied to the generic type T. This is due to the fact that when creating an instance with a placeholder type (instantiation), a data type that does not support these operations can be used instead of a type parameter.

 

10. The concept of a closed-designed and open-designed type.

If a class (structure, etc.) is declared with some type T by pattern

class ClassName<T>
{
  ...
}

then this class is of the open-designed type. In fact, this class is an abstraction.

If, based on the declared class ClassName<T>, an instance of a class with some placeholder type is declared, then this class type is a closed-designed type. For example, for the filler of type double, the following type will be formed

ClassName<double>

which is closed-designed.

If the type argument is a type parameter, then that type is considered as opened type. Any other type that is not a public type is considered as closed type.

If all type arguments are given for a generic type, then that type is considered a constructed type. If all type arguments are private types, then that type is considered a closed-designed type. If some of the type arguments are public types (T), then this type is considered open-designed.

 

11. An example of a generic class that receives two types as parameters T1, T2

The DoubleTypes<T1, T2> class is declared, which receives as parameters two types with the names T1, T2. The following elements are declared in the class:

  • internal fields var1, var2, respectively, of types T1, T2;
  • constructor;
  • the Print() method, which outputs the values of the fields var1, var2.

The text of the console program is as follows

using System;

namespace ConsoleApp19
{
  // A class that receives two parameters of type T1, T2
  class DoubleTypes<T1, T2>
  {
    // Internal fields of class
    T1 var1;
    T2 var2;

    // Constructor
    public DoubleTypes(T1 v1, T2 v2)
    {
      var1 = v1;
      var2 = v2;
    }

    // Method that displays information about internal fields
    public void Print()
    {
      Console.WriteLine("var1 = {0}", var1);
      Console.WriteLine("var2 = {0}", var2);
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // 1. Creating a reference for placeholder types int, string
      DoubleTypes<int, string> dt = new DoubleTypes<int, string>(23, "abc");
      dt.Print();

      // 2. Create reference for char, bool
      DoubleTypes<char, bool> dt2 = new DoubleTypes<char, bool>('z', true);
      dt2.Print();

      // 3. Create reference float, short
      DoubleTypes<float, short> dt3 = new DoubleTypes<float, short>(25.5F, 11);
      dt3.Print();

      // 4. Create reference for double type
      DoubleTypes<double, double> dt4 = new DoubleTypes<double, double>(23.3, 1.8);
      dt4.Print();

      Console.ReadKey();
    }
  }
}

The result of the program

var1 = 23
var2 = abc
var1 = z
var2 = True
var1 = 25.5
var2 = 11
var1 = 23.3
var2 = 1.8

 

12. Setting default values for variables of a generic type. The default keyword. Example

Since, when declaring a generic class, the type parameter is not known in advance, it is impossible to define a default value for variables of a generic type. In this case, the keyword default(T) is used. After using default, the value of the reference type is assigned null, and the value of the structure type is 0.

For example. If in the generic class MyGenericClass<T> two generic variables with the names a, b are declared, then a default value can be assigned to these variables in the class constructor according to the following pattern.

// Generic class
class MyGenericClass<T>
{
  // Generic variables
  T a, b;

  // Constructor
  public MyGenericClass()
  {
    // Assigning a default value to the variables a, b
    a = default(T);
    b = default(T);
  }
}

 


Related topics