Mastering C# Generics
Generics in C# have become an integral part of the language since their introduction, providing numerous benefits even if their workings are not fully understood by all developers. In his video, "How To Create Generics in C#, Including New Features," Tim Corey explains why generics are important, how to create them, and demonstrates their practical applications.
This article provides a comprehensive guide to C# generics, offering valuable insights from Tim Corey’s video on the topic. It covers the fundamentals of generics, including type safety, performance benefits, and practical applications. The article also explores creating generic methods, classes, interfaces, and applying constraints to generics. Additionally, it highlights real-world use cases and the importance of using generics to write efficient, type-safe code.
Introduction
C# generics offer a powerful way to create flexible, reusable, and type-safe code by allowing developers to define classes, methods, interfaces, and collections that work with any data type. By using a generic class or generic method, developers can define a generic type parameter (e.g., T) that can represent any data type. This eliminates the need for code duplication and enhances code reuse, while ensuring type safety at compile time. Generic collection classes like List
Tim at (0:00) introduces the topic by emphasizing the widespread use of generics in C#. He aims to explain why generics are essential and demonstrate how to create and use them effectively.
Creating the Project
Tim begins by creating a new console application named "GenericsDemoApp" to focus solely on demonstrating generics without any UI distractions. He sets up the project using .NET 8 and Visual Studio 2022.
Basics of Generics
Tim at (2:22) starts with an example using the List
List<int> numbers = new List<int> { 1, 2, 3 };
List<string> strings = new List<string> { "Tim", "Corey", "Sue" };
List<int> numbers = new List<int> { 1, 2, 3 };
List<string> strings = new List<string> { "Tim", "Corey", "Sue" };
The List
Type Safety and Efficiency
Tim highlights the importance of type safety provided by generics. The compiler checks the types at design time, preventing type mismatches and ensuring safe code execution. Generics also avoid the need for boxing and unboxing, leading to more efficient code.
Inefficiency of Non-Generic Collections
To demonstrate the inefficiency of using non-generic collections, Tim creates a List and shows how it can hold different types of objects.
List<object> objects = new List<object> { "Tim", 4, 3.6m };
List<object> objects = new List<object> { "Tim", 4, 3.6m };
He explains that using non-generic collections can lead to type mismatches and inefficiencies due to boxing and unboxing.
Performance Comparison
Tim at (6:15) runs a performance comparison between using a List<object>
and a List<int>
to add one million items. He uses a Stopwatch to measure the elapsed time for each operation.
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i <= 1_000_000; i++)
{
objects.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"List of object elapsed time: {stopwatch.ElapsedMilliseconds} ms");
stopwatch.Restart();
for (int i = 1; i <= 1_000_000; i++)
{
numbers.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"List of int elapsed time: {stopwatch.ElapsedMilliseconds} ms");
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i <= 1_000_000; i++)
{
objects.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"List of object elapsed time: {stopwatch.ElapsedMilliseconds} ms");
stopwatch.Restart();
for (int i = 1; i <= 1_000_000; i++)
{
numbers.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"List of int elapsed time: {stopwatch.ElapsedMilliseconds} ms");
The results show that adding integers to a List<object>
due to the absence of boxing and unboxing.
Creating a Type Checker Method
Tim at (10:14) demonstrates how to create a generic method named TypeChecker. This method checks the type of a given value and prints it out, illustrating the flexibility and power of generics.
public static void TypeChecker<T>(T value)
{
Console.WriteLine($"Type: {typeof(T)}, Value: {value}");
}
public static void TypeChecker<T>(T value)
{
Console.WriteLine($"Type: {typeof(T)}, Value: {value}");
}
The TypeChecker method uses the typeof operator to determine the type of the generic parameter T and prints both the type and the value.
Using the Type Checker Method
Tim provides examples of calling the TypeChecker method with different types of arguments.
TypeChecker(1); // Type: System.Int32, Value: 1
TypeChecker("Tim"); // Type: System.String, Value: Tim
TypeChecker(1.1); // Type: System.Double, Value: 1.1
TypeChecker(1); // Type: System.Int32, Value: 1
TypeChecker("Tim"); // Type: System.String, Value: Tim
TypeChecker(1.1); // Type: System.Double, Value: 1.1
By passing various types to the TypeChecker method, Tim shows how generics can handle different data types seamlessly.
Creating a Generic Class: Better List
Tim at (16:25) moves on to creating a generic class named BetterList. This class encapsulates a list of a specified type and provides additional functionality.
public class BetterList<T>
{
private List<T> data = new List<T>();
public void AddToList(T value)
{
data.Add(value);
Console.WriteLine($"{value} has been added to the list");
}
}
public class BetterList<T>
{
private List<T> data = new List<T>();
public void AddToList(T value)
{
data.Add(value);
Console.WriteLine($"{value} has been added to the list");
}
}
The BetterList class includes a private List
Using the Better List Class
Tim provides examples of using the BetterList class with different types.
BetterList<int> betterNumbers = new BetterList<int>();
betterNumbers.AddToList(5);
BetterList<PersonRecord> people = new BetterList<PersonRecord>();
people.AddToList(new PersonRecord("Tim", "Corey"));
BetterList<int> betterNumbers = new BetterList<int>();
betterNumbers.AddToList(5);
BetterList<PersonRecord> people = new BetterList<PersonRecord>();
people.AddToList(new PersonRecord("Tim", "Corey"));
In these examples, BetterList
Creating a Generic Interface
Tim at (21:48) introduces the idea of a generic interface named IImportance. This interface defines a method to determine which of two values is more important.
public interface IImportance<T>
{
T MostImportant(T a, T b);
}
public interface IImportance<T>
{
T MostImportant(T a, T b);
}
Implementing the Generic Interface
Tim shows how to implement this interface for different types. He starts with an integer implementation.
public class EvaluateImportance : IImportance<int>
{
public int MostImportant(int a, int b)
{
return a > b ? a : b;
}
}
public class EvaluateImportance : IImportance<int>
{
public int MostImportant(int a, int b)
{
return a > b ? a : b;
}
}
Next, he implements the interface for strings, using the length of the strings to determine importance.
public class EvaluateStringImportance : IImportance<string>
{
public string MostImportant(string a, string b)
{
return a.Length > b.Length ? a : b;
}
}
public class EvaluateStringImportance : IImportance<string>
{
public string MostImportant(string a, string b)
{
return a.Length > b.Length ? a : b;
}
}
These implementations demonstrate how the same interface can be applied to different types with specific logic for each type.
Applying Constraints to Generics
Tim at (25:21) explains how to apply constraints to generics, ensuring they meet certain conditions. For instance, a generic type can be constrained to have an empty constructor or implement a particular interface.
public class SampleClass<T> where T : new()
{
// Class implementation
}
public class SampleClassWithInterface<T> where T : IImportance<T>
{
// Class implementation
}
public class SampleClass<T> where T : new()
{
// Class implementation
}
public class SampleClassWithInterface<T> where T : IImportance<T>
{
// Class implementation
}
These constraints help to ensure that the generic type meets the necessary criteria, preventing runtime errors and enhancing type safety.
Microsoft’s Implementation of IEnumrable
Tim discusses how Microsoft uses the IEnumerable interface to constrain numeric operations. This allows for arithmetic operations like addition and subtraction on generic types.
public class MathOperations<T> where T : INumber<T>
{
public T Add(T x, T y)
{
return x + y;
}
}
public class MathOperations<T> where T : INumber<T>
{
public T Add(T x, T y)
{
return x + y;
}
}
By constraining the generic type T to INumber
Using Generics with Different Numeric Types
Tim at (33:55) expands on the MathOperations class to demonstrate how generics can be used with different numeric types, such as doubles and decimals.
Tim shows how to create instances of MathOperations for integers and doubles:
MathOperations<int> intMath = new MathOperations<int>();
Console.WriteLine(intMath.Add(1, 4)); // Outputs: 5
MathOperations<double> doubleMath = new MathOperations<double>();
Console.WriteLine(doubleMath.Add(1.5, 4.3)); // Outputs: 5.8
MathOperations<int> intMath = new MathOperations<int>();
Console.WriteLine(intMath.Add(1, 4)); // Outputs: 5
MathOperations<double> doubleMath = new MathOperations<double>();
Console.WriteLine(doubleMath.Add(1.5, 4.3)); // Outputs: 5.8
This demonstrates the flexibility of generics, allowing different numeric types to be handled seamlessly within the same class.
Handling Different Numeric Types
Tim emphasizes the importance of type safety by showing that you cannot mix different numeric types. For example, trying to add a double to an integer will result in a compile-time error.
// This will result in a compile-time error
// Console.WriteLine(intMath.Add(1.5, 4));
// This will result in a compile-time error
// Console.WriteLine(intMath.Add(1.5, 4));
Avoiding Type Conversion Overhead
Tim explains the benefits of using generics to avoid the overhead associated with type conversion. For example, converting integers to doubles for mathematical operations and then back to integers can be costly. Using generics allows direct operations on the native types, preserving performance and precision.
Generics in Practice
Tim advises caution when using generics, recommending that developers use them appropriately and avoid overuse. He highlights the benefits of generics, such as type safety, reduced boxing and unboxing, compile-time checking, and enhanced code readability.
He also points out that generics are prevalent in collections like List
Conclusion
Tim Corey’s detailed exploration of advanced generics in C# provides valuable insights into their practical applications and benefits. If you're looking to deepen your understanding of generics and see real-world examples in action, be sure to watch Tim Corey's detailed video on C# Generics. His clear explanations and hands-on demonstrations will help you fully grasp the concepts and apply them effectively in your own projects.