C# in Depth 02 Generics
2.1 Generics
📌 Where Generics
is heavily used?
- Collections
- Delegates, particularly in LINQ
- Asynchronous code
- Nullable value types
2.1.1 Days before Generics
📄 Task -> suppose you have the following task:
You need to create a collection of strings in one method (GenerateNames
) and print those strings out in another method (PrintNames
).
📍 data: string
📍 Function1 : GenerateNames
. which generate names in such order and length
📍 Function2: `PrintNames
, which print the names from previous step
📌 Do it via Array
xstatic string[] GenerateNames()
{
string[] names = new string[5];
names[0] = "James";
names[1] = "Sam";
names[2] = "Carol";
names[3] = "Kathy";
names[4] = "Peter";
return names;
}
static void PrintNames(string[] names)
{
foreach(string name in names)
{
Console.WriteLine(name);
}
}
Pros: It will be compiled fast since the length is all set.
Cons: You need to allocate the array with certain length. You can't change the length after initialization.
📌 Do it via ArrayList
xxxxxxxxxx
static ArrayList GenerateNames()
{
ArrayList names = new ArrayList();
names.Add("Tom");
names.Add("Peter");
names.Add("Kathy");
return names;
}
static void PrintNames(ArrayList names)
{
foreach(string name in names) //here is an implicit cast from object to string
{
Console.WriteLine(name);
}
}
Pros: Now the array is dynamic array which you don't have to specify its length.
Cons: What stored in ArrayList
is object
which means you can store not just string
! It leads a serious problem that if the elements inside the ArrayList
are not string
and it will cause InvalidCastException
.
📌Do it via StringCollection
xxxxxxxxxx
static StringCollection GenerateNames()
{
StringCollection names = new StringCollection();
names.Add("Tom");
names.Add("Bill");
names.Add("Julia");
return names;
}
static void PrintNames(StringCollection names)
{
foreach(string name in names)
{
Console.WriteLine(name);
}
}
Pros: Now it is 1️⃣ type-safe 2️⃣dynamic array.
Cons: But it still hard to implement the features since we have to define IntegerCollection
, FloatCollection
, and etc. But what if we want to add Split()
function to every collection? We have to do it for all the collection! That's too bad!
2.1.2 Using Generics
📌The definition of parameters and arguments
xxxxxxxxxx
public void Method(string name, int value);
Method("Laura", 28);
parameters: declared in inputs, like name
and value
arguments: the actual variable and value puts into the function, like "Laura"
is the argument for name
parameter, 28
is the argument for the value
parameter.
📌type parameter and type argument
Generics play an dual concept as well.
xxxxxxxxxx
public class List<T>
{
}
List<string> names = new List<string>();
type parameter, T
type argument, string
📌What is [arity][https://en.wikipedia.org/wiki/Arity]?
Arity is the number of arguments or operands taken by a function or operation in logic, mathematics, and computer science.
Example:
A nullary function takes no arguments.
A unary function takes 1 argument.
A binary function takes 2 arguments.
A ternary function takes 3 arguments.
An
📌 Arity of Generic Types and Methods
xxxxxxxxxx
public void Method(){}; //Nongeneric method (generic arity 0)
public void Method<T>(){}; //Method with generic arity 1
public void Method<T1, T2>(){}; //Method with generic arity 2
📌 Bad practice of Generics
❌
xxxxxxxxxx
public void Method<TFirst>(){}
public void Method<TSecond>(){}
Compile-time error; Can’t overload solely by type parameter name.
❌
xxxxxxxxxx
public void Method<T, T>() {}
Compile-time error; duplicate type parameter T
📌What can't be Generics?
The following members can't be Generics:
- Fields
- Properties
- Indexers
- Constructors
- Events
- Finalizers
2.1.3 Type Inference
In Chinese, it is "类型推断". Simply means the compiler will get to know what type is this variable.
📌 How is type inference?
It is a powerful technique!! Suppose you have the following function:
xxxxxxxxxx
public static List<T> CopyAtMost<T>(List<T> input, int maxElements)
{
List<T> output = new List<T>();
int actualCount = Math.Min(input.Count, maxElements);
for (int i = 0; i < actualCount; i++)
{
output.Add(input[i]);
}
return output;
}
And the first line of code uses type inference.
xxxxxxxxxx
List<int> numbers = new List<int>();
List<int> chunk1 = CopyAtMost(numbers, 3); //Use type inference
List<int> chunk2 = CopyAtMost<int>(numbers, 3); //Explicitly specify the type
See? C# allow type inference to be used where otherwise the type arguments would have to be explicitly specified when creating.
📌What should be aware of?
Pay attention to null
! The null
literal doesn’t have a type, so type inference will:
❌ fail for
xxxxxxxxxx
Tuple.Create(null, 50);
✔️ but succeed for
xxxxxxxxxx
Tuple.Create((string) null, 50);
2.1.4 Type Constraints
From above section, they are all Generic types but without any constraints. For example, you can infer any type in List<T>
. But what if you have sort of rules to limit the kinds of types?
📝 Task:
Write a method that formats a list of items and ensures the items follow some sort of rules. For example, to format them in a particular culture instead of the default culture of the thread. The IFormattable
interface provides a suitable ToString(string, IFormatProvider)
method.
✏️False Solution❌:
xxxxxxxxxx
static void PrintItems<T>(List<IFormattable> items)
What's wrong with this? It limits the types of arguments. For example, you can't put List<decimal>
as arguments although decimal
implements the interface IFormattable
. The List<IFormattable>
can't cast to List<decimal>
.
✏️Correct Solution✔️:
xxxxxxxxxx
static void PrintItems<T>(List<T> items) where T : IFormattable
What is good about this? It doesn’t just change which types can be passed to the method. But it changes what is the "access" you can do with a value of type T
within the method.
📌What type constraints can do?
The above example demonstrates type constraints of interface
. So what else can type constraints do?
1️⃣ Reference type constraint. The type argument must be a reference type. where T : classes/interfaces/delegates
2️⃣ Value type constraint. The type argument must be a non-nullable value type. where T : struct/enum
3️⃣ Constructor constraint. The type argument must have a parameterless constructor. where T : new()
4️⃣ Conversion constraint. (I have no idea what it is...) where T : SomeType
📌A complicate example of Generics
xxxxxxxxxx
TResult Method<TArg, TResult>(TArg input)
where TArg : IComparable<TArg>
where TResult : class, new()
< >
what you see inside angle brackets behind method name are the type parameters been used in this Generic Method.
TResult
is the Generic Type for return
value
TArg
is the Generic Type for the input of this function
where TArg : IComparable<TArg>
means the Generic Type TArg
must implement IComparable<TArg>
where TResult : class, new()
means the Generic Type TResult
must be a reference type with a parameterless constructor.
With above example, do you feel more comfortable with the follow code?
Example 1️⃣
xxxxxxxxxx
public static Tuple<T1, T2> Create<T1, T2>(T1 item1, T2 item2)
- The name of this method is
Create
. - Then the type parameters been used in this Generic Method are inside the
< >
which areT1
andT2
. - The return value is
Tuple<T1, T2>
. - The input argument is
(T1 item1, T2 item2)
.
Example 2️⃣
xxxxxxxxxx
public static List<T> CopyAtMost<T>(List<T> input, int maxElements)
- The name of this method is
CopyAtMost
. - Then the type parameters been used in this Generic Method are inside the
< >
which isT
. - The return value is
List<T>
. - The input argument is
(List<T> input, int maxElements)
.
2.1.5. The default and typeof operator
📌Review typeof()
The typeof
operator obtains the System.Type
instance for a type.
The argument to the typeof
operator must be 1️⃣the name of a type or 2️⃣a type parameter.
The return value is the type.
xxxxxxxxxx
//suppose you have a method print its type
public static void PrintType<T>()
{
Console.WriteLine(typeof(T));
}
//use this method
static void dowork()
{
PrintType<int>();
PrintType<IEnumerable>();
PrintType<Dictionary<string, double>>();
}
The result will be:
xxxxxxxxxx
System.Int32
System.Collections.IEnumerable
System.Collections.Generic.Dictionary`2[System.String,System.Double]
📌 Review of default()
The argument of default
must be the name of a type or a type parameter.
The return value is the default value of that type.
xxxxxxxxxx
//suppose you have a method display its default value
public static void DisplayDefaultValue<T>()
{
var val = default(T);
Console.WriteLine($"Default value of {typeof(T)} is {(val == null? "null" : val.ToString())}.");
}
//use this method
static void dowork()
{
DisplayDefaultValue<int?>();
DisplayDefaultValue<System.Numerics.Complex>();
DisplayDefaultValue<System.Collections.Generic.List<int>>();
}
The result will be:
xxxxxxxxxx
Default value of System.Nullable`1[System.Int32] is null.
Default value of System.Numerics.Complex is (0, 0).
Default value of System.Collections.Generic.List`1[System.Int32] is null.
📌5 broad cases for typeof()
1️⃣ No generics involved.
xxxxxxxxxx
typeof(string)
2️⃣ Generics involved but no type parameters
xxxxxxxxxx
typeof(List<int>)
3️⃣ Generics involved with type parameters
xxxxxxxxxx
typeof(List<TItem>)
4️⃣ Just a type parameter
xxxxxxxxxx
typeof(T)
5️⃣ Generics involved but no type arguments specified
xxxxxxxxxx
typeof(List<>)
📌Intuition on Open Generic Type, Generic Type & Closed Constructed Type
Open Generic Type:
xxxxxxxxxx
typeof(Dictionary<, >)
Generic Type:
xxxxxxxxxx
typeof(Dictionary<TKey, TValue>)
Closed Constructed Type:
xxxxxxxxxx
typeof(Dictionary<string, int>)
📌 Then what is the output of typeof()
?
⭐️ It will return what is the Type exactly when compiled. Taking following code as an example:
xxxxxxxxxx
//create a method printing closed constructed type
public static void PrintClosedConstructedType()
{
Console.WriteLine(typeof(int));
Console.WriteLine(typeof(List<int>));
}
//create a method printing closed constructed type eventually but with generic method
public static void PrintGenericType<T>()
{
Console.WriteLine("typeof(T) = {0}", typeof(T));
Console.WriteLine("typeof(List<T>) = {0}", typeof(List<T>));
}
//create a method printing open generic type
public static void PrintOpenGenericType()
{
Console.WriteLine(typeof(List<>));
Console.WriteLine(typeof(Dictionary<,>));
}
static void dowork()
{
Console.WriteLine("\nPrint Closed Constructed Type...");
PrintClosedConstructedType();
Console.WriteLine("\nPrint Generic Type...");
PrintGenericType<int>();
Console.WriteLine("\nPrint Open Generic Type...");
PrintOpenGenericType();
}
The output is:
xxxxxxxxxx
Print Closed Constructed Type...
System.Int32
System.Collections.Generic.List`1[System.Int32]
Print Generic Type...
typeof(T) = System.Int32
typeof(List<T>) = System.Collections.Generic.List`1[System.Int32]
Print Open Generic Type...
System.Collections.Generic.List`1[T]
System.Collections.Generic.Dictionary`2[TKey,TValue]
⭐️ That said, whenever code is executing within a generic type or method, the type parameter always refers to a closed, constructed type.
🔍Let's take a look at the format:
xxxxxxxxxx
System.Collections.Generic.List`1[System.Int32]
The List`1
indicates that this is a generic type called List
with generic arity 1 (one type parameter), and the type arguments are shown in square brackets afterward.
2.1.6 Generic type initialization
Just remember: ⭐️ Each closed, constructed type is initialized separately and has its own independent set of static fields.
xxxxxxxxxx
class GenericCounter<T>
{
private static int value;
static GenericCounter()
{
Console.WriteLine("Initializing counter for {0}", typeof(T));
}
public static void Increment()
{
value++;
}
public static void Display()
{
Console.WriteLine("Counter for {0} : {1}", typeof(T), value);
}
}
//..
static void dowork()
{
GenericCounter<int>.Increment(); //Trigger the static constructor!
GenericCounter<int>.Increment();
GenericCounter<int>.Display();
GenericCounter<string>.Display(); //Trigger the static constructor!
GenericCounter<string>.Increment();
GenericCounter<string>.Display();
}
Since the static
field is independent, the result is trivial.
xxxxxxxxxx
Initializing counter for System.Int32
Counter for System.Int32 : 2
Initializing counter for System.String
Counter for System.String : 0
Counter for System.String : 1
🔍 Few things to focus:
- the
GenericCounter<string>
value is independent ofGenericCounter<int>
. - the
static
constructor is run every initialization(twice): once for each closed, constructed type.
🔄 But if I switch the static
constructor to a normal constructor:
xxxxxxxxxx
GenericCounter()
{
Console.WriteLine("Initializing counter for {0}", typeof(T));
}
//..
static void dowork()
{
GenericCounter<int>.Increment();
GenericCounter<int>.Increment();
GenericCounter<int>.Display();
GenericCounter<string>.Display();
GenericCounter<string>.Increment();
GenericCounter<string>.Display();
}
The output would be different! The only difference is that the constructor runs behind twice while it doesn't output a message.
xxxxxxxxxx
Counter for System.Int32 : 2
Counter for System.String : 0
Counter for System.String : 1