A detailed explanation of C - generics

Keywords: Programming

This article focuses on generics in C, which play an important role in C, especially when building project framework.

1, What is generics

Generics is a new syntax introduced by C × 2.0, not a syntax sugar, but a function provided by framework upgrade in 2.0.

When programming programs, we often encounter modules with very similar functions, but they handle different data. But we have no way. We can only write multiple methods to handle different data types. At this time, the question arises. Is there a way to use the same method to handle the incoming parameters of different types? Generics are designed to solve this problem.

2, Why use generics

Let's take a look at the following example:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric
{
    public class CommonMethod
    {
        /// <summary>
        /// Print a int value
        /// 
        /// Because the parameter type is written dead when the method is declared
        /// Married man Eleven San
        /// </summary>
        /// <param name="iParameter"></param>
        public static void ShowInt(int iParameter)
        {
            Console.WriteLine("This is {0},parameter={1},type={2}",
                typeof(CommonMethod).Name, iParameter.GetType().Name, iParameter);
        }

        /// <summary>
        /// Print a string value
        /// </summary>
        /// <param name="sParameter"></param>
        public static void ShowString(string sParameter)
        {
            Console.WriteLine("This is {0},parameter={1},type={2}",
                typeof(CommonMethod).Name, sParameter.GetType().Name, sParameter);
        }

        /// <summary>
        /// Print a DateTime value
        /// </summary>
        /// <param name="oParameter"></param>
        public static void ShowDateTime(DateTime dtParameter)
        {
            Console.WriteLine("This is {0},parameter={1},type={2}",
                typeof(CommonMethod).Name, dtParameter.GetType().Name, dtParameter);
        }
    }
}
View Code

Results:

From the above results, we can see that the functions of these three methods are the same except for the different parameters passed in. In version 1.0, there was no concept of generics, so what should I do. I believe many people will think about the inheritance of one of the three features of OOP. We know that in C ා language, object is the base class of all types. Optimize the above code as follows:

public static void ShowObject(object oParameter)
{
      Console.WriteLine("This is {0},parameter={1},type={2}",
         typeof(CommonMethod), oParameter.GetType().Name, oParameter);
}

Results:

From the above results, we can see that the use of object type meets our requirements and solves the code reusability. Some people may ask why the object type is defined. Why can I pass in int, string and other types? There are two reasons:

1. The object type is the parent of all types.

2. By inheritance, a child class has all the properties and behaviors of the parent class. Where a parent class appears, it can be replaced by a child class.

But the above object type method will bring another problem: boxing and unpacking, which will damage the performance of the program.

Microsoft launched generics at the time of C × 2.0, which can solve the above problems well.

3, Generic type parameters

In a generic type or method definition, a type parameter is a placeholder for a specific type specified by the client when it instantiates a variable of a generic type. Generic classes (GenericList < T >) cannot be used as is because they are not real types; they are more like blueprints of types. To use GenericList < T >, the client code must declare and instantiate the construction type by specifying the type parameter in angle brackets. The type parameter for this particular class can be any type recognized by the compiler. You can create any number of construction type instances, each with a different type parameter.

The code in the above example can be modified as follows:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 
 7 namespace MyGeneric
 8 {
 9     public class GenericMethod
10     {
11         /// <summary>
12         /// generic method
13         /// </summary>
14         /// <typeparam name="T"></typeparam>
15         /// <param name="tParameter"></param>
16         public static void Show<T>(T tParameter)
17         {
18             Console.WriteLine("This is {0},parameter={1},type={2}",
19                 typeof(GenericMethod), tParameter.GetType().Name, tParameter.ToString());
20         }
21     }
22 }

Call:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric
{
    class Program
    {
        static void Main(string[] args)
        {

            int iValue = 123;
            string sValue = "456";
            DateTime dtValue = DateTime.Now;

            Console.WriteLine("***********CommonMethod***************");
            CommonMethod.ShowInt(iValue);
            CommonMethod.ShowString(sValue);
            CommonMethod.ShowDateTime(dtValue);
            Console.WriteLine("***********Object***************");
            CommonMethod.ShowObject(iValue);
            CommonMethod.ShowObject(sValue);
            CommonMethod.ShowObject(dtValue);
            Console.WriteLine("***********Generic***************");
            GenericMethod.Show<int>(iValue);
            GenericMethod.Show<string>(sValue);
            GenericMethod.Show<DateTime>(dtValue);
            Console.ReadKey();
        }
    }
}
View Code

Display results:

Why can generics solve the above problems?

Generics are declared in delay: that is, the specific parameter type is not specified at the time of definition, and the parameter type is specified at the time of call. The idea of delay is very popular in program architecture design. For example: distributed cache queue, delayed loading of EF, etc.

How does generics work?

The console program will eventually be compiled into an exe program. When the exe is clicked, it will be compiled by JIT (instant compiler) and finally generate binary code, which can be executed by the computer. After generics are added to the syntax, the compiler of VS has been upgraded. After upgrading, when encountering generics during compilation, special processing will be done: generate placeholders. After JIT compilation again, the placeholders generated by the above compilation will be replaced with specific data types. Here is an example:

1 Console.WriteLine(typeof(List<>));
2 Console.WriteLine(typeof(Dictionary<,>));

Results:

As you can see from the screenshot above, generics generate placeholders after compilation.

Note: placeholders can only be entered in English input mode. You only need to press the key of wavy line (the key on the left of number 1) once, instead of pressing Shift key.

1. Generic performance issues

Please take a look at an example to compare the performance of ordinary methods, methods of Object parameter type, and generic methods.

Add a Monitor class to let the three methods perform the same operation and compare the duration:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 
 7 namespace MyGeneric
 8 {
 9     public class Monitor
10     {
11         public static void Show()
12         {
13             Console.WriteLine("****************Monitor******************");
14             {
15                 int iValue = 12345;
16                 long commonSecond = 0;
17                 long objectSecond = 0;
18                 long genericSecond = 0;
19 
20                 {
21                     Stopwatch watch = new Stopwatch();
22                     watch.Start();
23                     for (int i = 0; i < 100000000; i++)
24                     {
25                         ShowInt(iValue);
26                     }
27                     watch.Stop();
28                     commonSecond = watch.ElapsedMilliseconds;
29                 }
30                 {
31                     Stopwatch watch = new Stopwatch();
32                     watch.Start();
33                     for (int i = 0; i < 100000000; i++)
34                     {
35                         ShowObject(iValue);
36                     }
37                     watch.Stop();
38                     objectSecond = watch.ElapsedMilliseconds;
39                 }
40                 {
41                     Stopwatch watch = new Stopwatch();
42                     watch.Start();
43                     for (int i = 0; i < 100000000; i++)
44                     {
45                         Show<int>(iValue);
46                     }
47                     watch.Stop();
48                     genericSecond = watch.ElapsedMilliseconds;
49                 }
50                 Console.WriteLine("commonSecond={0},objectSecond={1},genericSecond={2}"
51                     , commonSecond, objectSecond, genericSecond);
52             }
53         }
54 
55         #region PrivateMethod
56         private static void ShowInt(int iParameter)
57         {
58             //do nothing
59         }
60         private static void ShowObject(object oParameter)
61         {
62             //do nothing
63         }
64         private static void Show<T>(T tParameter)
65         {
66             //do nothing
67         }
68         #endregion
69 
70     }
71 }

Main() method call:

1 Monitor.Show();

Results:

It can be seen from the results that generic methods have the highest performance, followed by common methods, and object methods have the lowest performance.

4, Generic class

In addition to methods being generic, classes can also be generic, for example:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 
 7 namespace MyGeneric
 8 {
 9     /// <summary>
10     /// Generic class
11     /// </summary>
12     /// <typeparam name="T"></typeparam>
13     public class GenericClass<T>
14     {
15         public T _T;
16     }
17 }

Call in Main() method:

1 // T yes int type
2 GenericClass<int> genericInt = new GenericClass<int>();
3 genericInt._T = 123;
4 // T yes string type
5 GenericClass<string> genericString = new GenericClass<string>();
6 genericString._T = "123";

In addition to generic classes, there can also be generic interfaces, such as:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 
 7 namespace MyGeneric
 8 {
 9     /// <summary>
10     /// generic interface
11     /// </summary>
12     public interface IGenericInterface<T>
13     {
14         //Return value of generic type
15         T GetT(T t);
16     }
17 }

You can also have generic delegates:

1 public delegate void SayHi<T>(T t);//generic delegate

Note:

1. Generics can be declared without specifying specific types, but they must specify specific types when used, for example:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 
 7 namespace MyGeneric
 8 {
 9     /// <summary>
10     /// You must specify a specific type when using generics,
11     /// The specific types here are int
12     /// </summary>
13     public class CommonClass :GenericClass<int>
14     {
15     }
16 }

If the fruit class is also generic, you can inherit without specifying specific types, for example:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 
 7 namespace MyGeneric
 8 {
 9     /// <summary>
10     /// You must specify a specific type when using generics,
11     /// The specific types here are int
12     /// </summary>
13     public class CommonClass :GenericClass<int>
14     {
15     }
16 
17     /// <summary>
18     /// Subclasses are also generic. You can inherit without specifying specific types
19     /// </summary>
20     /// <typeparam name="T"></typeparam>
21     public class CommonClassChild<T>:GenericClass<T>
22     {
23 
24     }
25 }

2. This is also the case when a class implements a generic interface, for example:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 
 7 namespace MyGeneric
 8 {
 9     /// <summary>
10     /// Specific type must be specified
11     /// </summary>
12     public class Common : IGenericInterface<string>
13     {
14         public string GetT(string t)
15         {
16             throw new NotImplementedException();
17         }
18     }
19 
20     /// <summary>
21     /// You may not know the specific type, but the subclass must also be generic
22     /// </summary>
23     /// <typeparam name="T"></typeparam>
24     public class CommonChild<T> : IGenericInterface<T>
25     {
26         public T GetT(T t)
27         {
28             throw new NotImplementedException();
29         }
30     }
31 }

5. Generic constraints

Let's take a look at the following example:

Define a People class with properties and methods:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 
 7 namespace MyGeneric
 8 {
 9     public interface ISports
10     {
11         void Pingpang();
12     }
13 
14     public interface IWork
15     {
16         void Work();
17     }
18 
19 
20     public class People
21     {
22         public int Id { get; set; }
23         public string Name { get; set; }
24 
25         public void Hi()
26         {
27             Console.WriteLine("Hi");
28         }
29     }
30 
31     public class Chinese : People, ISports, IWork
32     {
33         public void Tradition()
34         {
35             Console.WriteLine("Benevolence, righteousness, propriety, wisdom and trust, gentleness, courtesy and thrift");
36         }
37         public void SayHi()
38         {
39             Console.WriteLine("Have you eaten yet?");
40         }
41 
42         public void Pingpang()
43         {
44             Console.WriteLine("Play table tennis...");
45         }
46 
47         public void Work()
48         {
49             throw new NotImplementedException();
50         }
51     }
52 
53     public class Hubei : Chinese
54     {
55         public Hubei(int version)
56         { }
57 
58         public string Changjiang { get; set; }
59         public void Majiang()
60         {
61             Console.WriteLine("Playing mahjong..");
62         }
63     }
64 
65 
66     public class Japanese : ISports
67     {
68         public int Id { get; set; }
69         public string Name { get; set; }
70         public void Hi()
71         {
72             Console.WriteLine("Hi");
73         }
74         public void Pingpang()
75         {
76             Console.WriteLine("Play table tennis...");
77         }
78     }
79 }

Instantiate in the Main() method:

 1 People people = new People()
 2 {
 3         Id = 123,
 4         Name = "Go your own way"
 5 };
 6 Chinese chinese = new Chinese()
 7 {
 8         Id = 234,
 9         Name = "A sunny day"
10 };
11 Hubei hubei = new Hubei(123)
12 {
13         Id = 345,
14         Name = "Fleeting time"
15 };
16 Japanese japanese = new Japanese()
17 {
18         Id = 7654,
19         Name = "werwer"
20 };

At this time, there is a requirement: you need to print out the values of the Id and Name properties, and modify the ShowObject() method as follows:

However, it is reported that there are no Id and Name properties in the object class. Some people may say that forced type conversion is OK

1 public static void ShowObject(object oParameter)
2 {
3          Console.WriteLine("This is {0},parameter={1},type={2}",
4          typeof(CommonMethod), oParameter.GetType().Name, oParameter);
5 
6          Console.WriteLine($"{((People)oParameter).Id}_{((People)oParameter).Name}");
7 }

After this modification, the code will not report an error. At this time, we call in the Main() method:

1 CommonMethod.ShowObject(people);
2 CommonMethod.ShowObject(chinese);
3 CommonMethod.ShowObject(hubei);
4 CommonMethod.ShowObject(japanese);

Results:

It can be seen that the program reported an error, because Japanese did not inherit from People, and the type conversion here failed. This can cause type insecurity. So how to solve the problem of type insecurity? That is to use generic constraints.

The so-called generic constraint is actually the constraint type T. So t must follow certain rules. For example, t must inherit from a class, or t must implement an interface, and so on. So how to assign constraints to generics? In fact, it's also very simple. You only need where keyword and constraint conditions.

There are five generic constraints.

constraint s description
T: structure Type parameter must be of value type
T: Class Type parameters must be reference types; this also applies to any class, interface, delegate, or array type.
T: new() Type parameters must have a public constructor with no parameters. When used with other constraints, the new() constraint must be specified last.
T: < base class name > Type parameters must be or derived from the specified base class.
T: < interface name > The type parameter must be the specified interface or implement the specified interface. You can specify multiple interface constraints. Constraint interfaces can also be generic.

1. Base class constraint

The method constraint T type printed above must be of type People.

 1 /// <summary>
 2 /// Base class constraints: Constraints T Must be People Type or People Subclasses
 3 /// </summary>
 4 /// <typeparam name="T"></typeparam>
 5 /// <param name="tParameter"></param>
 6 public static void Show<T>(T tParameter) where T : People
 7 {
 8       Console.WriteLine($"{tParameter.Id}_{tParameter.Name}");
 9       tParameter.Hi();
10 }

Note:

When the base class is constrained, the base class cannot be a sealed class, that is, it cannot be a sealed class. The sealed class indicates that the class cannot be inherited, so it is meaningless to use it as a constraint here, because the sealed class has no subclass.

2. Interface constraints

/// <summary>
/// Interface constraint
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="t"></param>
/// <returns></returns>
public static T Get<T>(T t) where T : ISports
{
      t.Pingpang();
      return t;
}

3. Reference type constraint class

The reference type constraint guarantees that T must be of reference type.

 1 /// <summary>
 2 /// Reference type constraint
 3 /// </summary>
 4 /// <typeparam name="T"></typeparam>
 5 /// <param name="t"></param>
 6 /// <returns></returns>
 7 public static T Get<T>(T t) where T : class
 8 {
 9       return t;
10 }

4. Value type constraint struct

The value type constraint guarantees that T must be of value type.

 1 /// <summary>
 2 /// Value type type constraint
 3 /// </summary>
 4 /// <typeparam name="T"></typeparam>
 5 /// <param name="t"></param>
 6 /// <returns></returns>
 7 public static T Get<T>(T t) where T : struct
 8 {
 9       return t;
10 }

5. Parameterless constructor constraint new()

 1 /// <summary>
 2 /// new()constraint
 3 /// </summary>
 4 /// <typeparam name="T"></typeparam>
 5 /// <param name="t"></param>
 6 /// <returns></returns>
 7 public static T Get<T>(T t) where T : new()
 8 {
 9      return t;
10 }

Generic constraints can also constrain multiple at the same time, for example:

1 public static void Show<T>(T tParameter)
2             where T : People, ISports, IWork, new()
3 {
4       Console.WriteLine($"{tParameter.Id}_{tParameter.Name}");
5       tParameter.Hi();
6       tParameter.Pingpang();
7       tParameter.Work();
8 }

Note: when there are multiple generic constraints, the new() constraint must be at the end.

6, Covariance and inversion of generics

Covariant and contravariant are generated in. NET 4.0. They can only be placed in front of the interface or delegate generic parameters. out covariant covariant is used to modify the return value. In: contravariant is used to modify the incoming parameters.

Here's an example:

Define an Animal class:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 
 7 namespace MyGeneric
 8 {
 9     public class Animal
10     {
11         public int Id { get; set; }
12     }
13 }

Then define a Cat class to inherit from the Animal class:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 
 7 namespace MyGeneric
 8 {
 9     public class Cat :Animal
10     {
11         public string Name { get; set; }
12     }
13 }

The Main() method can be called as follows:

 1 // Direct declaration Animal class
 2 Animal animal = new Animal();
 3 // Direct declaration Cat class
 4 Cat cat = new Cat();
 5 // Declare that the subclass object points to the parent class
 6 Animal animal2 = new Cat();
 7 // statement Animal Class set
 8 List<Animal> listAnimal = new List<Animal>();
 9 // statement Cat Class set
10 List<Cat> listCat = new List<Cat>();

So the question is: is the following code correct?

1 List<Animal> list = new List<Cat>();

Some people may think it's right: because a Cat belongs to Animal, then a group of Cat should also belong to Animal. But it's actually a mistake to declare this: there is no parent-child relationship between list < Cat > and list < Animal >.

Then we can use covariance and inversion.

1 // covariant
2 IEnumerable<Animal> List1 = new List<Animal>();
3 IEnumerable<Animal> List2 = new List<Cat>();

F12 view definition:

As you can see, there is an out keyword before T in the generic interface, and T can only be a return value type, not a parameter type, which is covariant. After covariance is used, the base class is declared on the left, and the base class or subclass of the base class can be declared on the right.

Covariance can be used not only for interfaces, but also for delegates:

1 Func<Animal> func = new Func<Cat>(() => null);

In addition to using the. NET framework to define the concept, we can also customize covariance, for example:

 1 /// <summary>
 2 /// out Covariance can only be a return result
 3 /// </summary>
 4 /// <typeparam name="T"></typeparam>
 5 public interface ICustomerListOut<out T>
 6 {
 7      T Get();
 8 }
 9 
10 public class CustomerListOut<T> : ICustomerListOut<T>
11 {
12      public T Get()
13      {
14          return default(T);
15      }
16 }

Use custom covariance:

1 // Use custom covariance
2 ICustomerListOut<Animal> customerList1 = new CustomerListOut<Animal>();
3 ICustomerListOut<Animal> customerList2 = new CustomerListOut<Cat>();

Let's see the inverter.

There is an In keyword before the T of the generic interface, and t can only be a method parameter, not a return value type, which is the inversion. See the following custom inverter:

 1 /// <summary>
 2 /// Inversion can only be method parameter
 3 /// </summary>
 4 /// <typeparam name="T"></typeparam>
 5 public interface ICustomerListIn<in T>
 6 {
 7      void Show(T t);
 8 }
 9 
10 public class CustomerListIn<T> : ICustomerListIn<T>
11 {
12      public void Show(T t)
13      {
14      }
15 }

Use custom inverter:

1 // Use custom inverter
2 ICustomerListIn<Cat> customerListCat1 = new CustomerListIn<Cat>();
3 ICustomerListIn<Cat> customerListCat2 = new CustomerListIn<Animal>();

Covariance and inversion can also be used at the same time. See the following example:

 1 /// <summary>
 2 /// inT Invert
 3 /// outT covariant
 4 /// </summary>
 5 /// <typeparam name="inT"></typeparam>
 6 /// <typeparam name="outT"></typeparam>
 7 public interface IMyList<in inT, out outT>
 8 {
 9      void Show(inT t);
10      outT Get();
11      outT Do(inT t);
12 }
13 
14 public class MyList<T1, T2> : IMyList<T1, T2>
15 {
16 
17      public void Show(T1 t)
18      {
19           Console.WriteLine(t.GetType().Name);
20      }
21 
22      public T2 Get()
23      {
24           Console.WriteLine(typeof(T2).Name);
25           return default(T2);
26       }
27 
28       public T2 Do(T1 t)
29       {
30            Console.WriteLine(t.GetType().Name);
31            Console.WriteLine(typeof(T2).Name);
32            return default(T2);
33        }
34  }

Use:

1 IMyList<Cat, Animal> myList1 = new MyList<Cat, Animal>();
2 IMyList<Cat, Animal> myList2 = new MyList<Cat, Cat>();//covariant
3 IMyList<Cat, Animal> myList3 = new MyList<Animal, Animal>();//Invert
4 IMyList<Cat, Animal> myList4 = new MyList<Animal, Cat>();//Invert+covariant

7. Generic caching

As we learned before, no matter how many times you instantiate a static type in a class, there will only be one in memory. A static constructor is only executed once. In a generic class, the T type is different. Each different T type will produce a different copy, so it will produce different static properties and different static constructors. See the following example:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 
 7 namespace MyGeneric
 8 {
 9     public class GenericCache<T>
10     {
11         static GenericCache()
12         {
13             Console.WriteLine("This is GenericCache static constructor ");
14             _TypeTime = string.Format("{0}_{1}", typeof(T).FullName, DateTime.Now.ToString("yyyyMMddHHmmss.fff"));
15         }
16 
17         private static string _TypeTime = "";
18 
19         public static string GetCache()
20         {
21             return _TypeTime;
22         }
23     }
24 }

Then create a new test class to test the execution order of GenericCache class:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading;
 6 using System.Threading.Tasks;
 7 
 8 namespace MyGeneric
 9 {
10     public class GenericCacheTest
11     {
12         public static void Show()
13         {
14             for (int i = 0; i < 5; i++)
15             {
16                 Console.WriteLine(GenericCache<int>.GetCache());
17                 Thread.Sleep(10);
18                 Console.WriteLine(GenericCache<long>.GetCache());
19                 Thread.Sleep(10);
20                 Console.WriteLine(GenericCache<DateTime>.GetCache());
21                 Thread.Sleep(10);
22                 Console.WriteLine(GenericCache<string>.GetCache());
23                 Thread.Sleep(10);
24                 Console.WriteLine(GenericCache<GenericCacheTest>.GetCache());
25                 Thread.Sleep(10);
26             }
27         }
28     }
29 }

The Main() method calls:

1 GenericCacheTest.Show();

Results:

As you can see from the screenshot above, generics will create a copy of each type, so the static constructor will execute five times. And the value of each static property is the same. With this feature of generics, caching can be implemented.

Note: you can cache only once for different types. Generic caching is more efficient than dictionary caching. Generic cache cannot be released actively

Posted by aksival on Sat, 11 Apr 2020 01:05:52 -0700