C# in Depth 02 Generics

C#

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

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

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

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

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.

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.

f()=2

A unary function takes 1 argument.

f(x)=2x

A binary function takes 2 arguments.

f(x,y)=2xy

A ternary function takes 3 arguments.

f(x,y,z)=2xyz

An n-ary function takes n arguments.

f(x1,x2,...,xn)=2i=1nxi

 

📌 Arity of Generic Types and Methods

 

📌 Bad practice of Generics

Compile-time error; Can’t overload solely by type parameter name.

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:

And the first line of code uses type inference.

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

✔️ but succeed for

 

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:

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✔️:

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

< > 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️⃣

  • The name of this method is Create.
  • Then the type parameters been used in this Generic Method are inside the < > which are T1 and T2.
  • The return value is Tuple<T1, T2>.
  • The input argument is (T1 item1, T2 item2).

Example 2️⃣

  • The name of this method is CopyAtMost.
  • Then the type parameters been used in this Generic Method are inside the < > which is T.
  • 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.

The result will be:

 

📌 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.

The result will be:

 

📌5 broad cases for typeof()

1️⃣ No generics involved.

2️⃣ Generics involved but no type parameters

3️⃣ Generics involved with type parameters

4️⃣ Just a type parameter

5️⃣ Generics involved but no type arguments specified

📌Intuition on Open Generic Type, Generic Type & Closed Constructed Type

Open Generic Type:

Generic Type:

Closed Constructed Type:

 

📌 Then what is the output of typeof()?

⭐️ It will return what is the Type exactly when compiled. Taking following code as an example:

The output is:

⭐️ 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:

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.

Since the static field is independent, the result is trivial.

🔍 Few things to focus:

  1. the GenericCounter<string> value is independent of GenericCounter<int>.
  2. 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:

The output would be different! The only difference is that the constructor runs behind twice while it doesn't output a message.

 

Previous
Previous

C# 24 Study Notes Concurrency by Asynchronous

Next
Next

Clean Coder # 01. Professionalism