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