73. where constraints
Where constraints in C# impose rules on generic type parameters to ensure that only compatible types are used in generic classes or methods. They enhance type safety by restricting which types can be substituted for a given type parameter.
The where keyword is used to define constraints, such as:
- where T : class → Restricts T to reference types.
- where T : struct → Restricts T to value types.
- where T : new() → Requires T to have a parameterless constructor.
- where T : SomeBaseClass → Restricts T to inherit from SomeBaseClass.
- where T : IComparable<T> → Ensures T implements a specific interface.
Example:
public class Repository<T> where T : class, new()
{
public T CreateInstance() => new T();
}
Here, T must be a reference type and have a parameterless constructor.
Where constraints prevent invalid assignments, making generic classes more predictable. For example, a sorting algorithm might require T to implement IComparable<T> to ensure it can be compared with other instances.
Without constraints, developers might accidentally use incompatible types, leading to compilation errors or unexpected runtime behavior. Using constraints ensures that the code behaves as expected and is easier to maintain.
74. covariance
Covariance in C# allows a more derived type to be assigned to a generic type parameter when used as an output. This is particularly useful in generic interfaces and delegates. Covariant generic parameters are marked with the out keyword.
Covariance is useful when dealing with read-only collections, enabling more flexibility when working with inheritance.
Example:
IEnumerable<string> strings = new List<string> { "Hello", "World" };
IEnumerable<object> objects = strings; // Covariance works
Here, IEnumerable<string> is assignable to IEnumerable<object> because of covariance, allowing broader type compatibility.
Covariance is commonly seen in:
- IEnumerable<out T> – Enables iteration over elements of a derived type.
- Func<out T> – Enables returning a more specific type.
Another example:
Func<string> getString = () => "Hello";
Func<object> getObject = getString; // Works due to covariance
This works because Func<string> returns string, which can be safely assigned to Func<object>.
Covariance helps when designing APIs that return values while allowing for subtype flexibility, making generic code more adaptable.
75. contravariance
Contravariance in C# allows a generic type parameter to accept a less derived type when used as an input parameter. This is useful in cases where a method accepts arguments of a generic type. Contravariant type parameters are marked with the in keyword.
Contravariance allows flexibility when passing parameters to generic delegates and interfaces.
Example:
Action<object> action = obj => Console.WriteLine(obj);
Action<string> stringAction = action; // Contravariant assignment
Here, Action<object> can be assigned to Action<string> because object is a broader type than string, making it safe for use.
Contravariance is commonly used in:
- Action<in T> – Allows using a base type as a method parameter.
- IComparer<in T> – Enables sorting of elements based on a base type.
Another example:
IComparer<object> objectComparer = Comparer<object>.Default;
IComparer<string> stringComparer = objectComparer; // Works due to contravariance
Here, IComparer<object> can be assigned to IComparer<string> since it can compare any object, including string.
Contravariance is useful in delegate-based programming, event handling, and interfaces where input flexibility is needed. It allows for code reuse while maintaining type safety, making it an essential feature in generic programming.
76. T type
The T type is the most commonly used placeholder for a generic type parameter in C#. It represents an unspecified type in generic classes, interfaces, methods, and delegates. The letter T stands for "Type," but other names like U, V, or descriptive names like TKey and TValue can also be used.
Using T makes code more reusable and type-safe. A generic class can work with any data type without duplicating code for each possible type:
public class Box<T>
{
public T Value { get; set; }
public Box(T value) { Value = value; }
}
Here, T can be an int, string, or any custom type.
C# collections like List<T> and Dictionary<TKey, TValue> use generics extensively:
List<int> numbers = new List<int> { 1, 2, 3 };
Dictionary<string, int> wordCount = new Dictionary<string, int>();
Generics improve performance by avoiding boxing and unboxing of value types. They also reduce runtime errors since type mismatches are caught at compile time.
Though T is the conventional name, choosing a meaningful name (like TItem for a collection element) improves code readability.
77. default keyword
The default keyword in C# returns the default value of a type. It is useful in generics, nullable types, and scenarios where a variable needs to be initialized safely.
For value types (e.g., int, double, bool), default returns zero, 0.0, and false, respectively. For reference types (e.g., string, object), it returns null.
Example:
int number = default(int); // 0
bool flag = default(bool); // false
string text = default(string); // null
In generics, default(T) ensures safe initialization when the actual type is unknown:
public class Container<T>
{
private T value = default(T);
}
This prevents errors when T is a struct that lacks a default constructor.
Starting from C# 7.1, the default literal can infer the type:
int num = default;
bool isSet = default;
This makes code more concise while maintaining clarity. The default keyword is particularly useful in scenarios where safe, zero-initialized values are required, such as in nullable types (Nullable<T>) or when handling optional parameters.
78. extension method
An extension method is a static method that adds new functionality to existing types without modifying their source code. It allows developers to extend built-in or third-party classes as if they were part of the original definition.
To define an extension method, it must:
- Be inside a static class.
- Be a static method.
- Have the this keyword before the first parameter (the type being extended).
Example:
public static class StringExtensions
{
public static bool IsCapitalized(this string str)
{
return !string.IsNullOrEmpty(str) && char.IsUpper(str[0]);
}
}
Usage:
string word = "Hello";
bool result = word.IsCapitalized(); // True
The method appears as if it were a built-in method of string.
Common use cases include:
- Adding utility methods to built-in types.
- Enhancing LINQ capabilities (IEnumerable<T> extensions).
- Improving code readability.
Example with IEnumerable<T>:
public static class ListExtensions
{
public static T FirstOrDefaultCustom<T>(this IEnumerable<T> source)
{
return source.Any() ? source.First() : default;
}
}
Extension methods provide a powerful way to write cleaner, more modular, and reusable code without modifying the original class definitions.
79. operator overloading
Operator overloading allows developers to define custom behavior for built-in operators (+, -, ==, *, etc.) in user-defined types. This makes custom classes work more intuitively with standard operators.
To overload an operator, the operator keyword is used within the class. Example:
public class Vector
{
public int X { get; }
public int Y { get; }
public Vector(int x, int y) { X = x; Y = y; }
public static Vector operator +(Vector a, Vector b)
{
return new Vector(a.X + b.X, a.Y + b.Y);
}
public override string ToString() => $"({X}, {Y})";
}
Usage:
Vector v1 = new Vector(2, 3);
Vector v2 = new Vector(4, 5);
Vector result = v1 + v2; // Calls overloaded + operator
Console.WriteLine(result); // Output: (6, 8)
Operator overloading is particularly useful in mathematical, geometric, or custom data structures where arithmetic operations make sense.
Common operators that can be overloaded include:
- Arithmetic (+, -, *, /, %)
- Comparison (==, !=, <, >)
- Bitwise (&, |, ^, <<, >>)
Best practices include ensuring that overloaded operators follow intuitive mathematical rules and overloading complementary operators together (e.g., if == is overloaded, overload != as well).