Generic class in C#
What are C# generics?
Generics in C# are a powerful feature that enables you to create flexible and reusable code. They allow you to customize methods, classes, structures, or interfaces to work with various data types by using placeholders known as type parameters for one or more of the types they store or manipulate.
Consider the following code, which demonstrates a generic class:
public class GenericClass<T>
{
public T Value { get; set; }
}
var genericString = new GenericClass<string>();
var genericInt = new GenericClass<int>();
genericString.Value = "Generic";
genericInt.Value = 0;
Generics are essential in modern C# development as they enable you to create type-safe classes. They help reduce redundancy in code by allowing you to write logic that seamlessly works with different data types. Embracing generics can significantly enhance your development productivity and code clarity.
Generic Classes and Methods
Generic classes, properties, or methods can be used with various data types while maintaining the same logic. You can define them using syntax like <T>
, <T, R>
, <T, U, V>
, and so on, with no limit to the number of type parameters you can use. Here are some code examples:
Generic Classes:
public class ClassGeneric<T> { }
public class ClassGeneric<TKey, Tvalue> { }
var generic1 = new ClassGeneric<string>();
var generic2 = new ClassGeneric<string, bool>();
Not only classes but also interfaces can be generics:
Generic Interfaces:
public class Interface<T> { }
public class Simple : Interface<string> { }
var generic1 = new Simple();
You can also apply generics at the class level:
Generics at Class Level:
public interface Interface<T> { }
public class Simple<T> : Interface<T> { }
var generic1 = new Simple<string>();
var generic2 = new Simple<char>();
In the first example, Simple
is defined as an implementation of Interface<string>
, meaning it will always implement this specific generic interface for strings. In the second example, we've defined a more flexible approach where we specify the generic type at the class level.
Additionally, you can define struct generics:
Generics at Struct Level:
public struct SimpleStruct<T> { }
var simple = new SimpleStruct<string>();
Generic methods follow the same principles as generic classes, except they can be applied to both the return type and parameter types of a method. Here's an example illustrating generics at the method level:
public class SimpleGeneric
{
public T GenericMethod<T>(T t) => t;
}
Constraints
Constraints are vital in generics as they define type restrictions for the compiler. If generics have no constraints, any type can be used with them. However, when generics have constraints, attempting to use a different type will result in a compiler error. Constraints are specified using the where
keyword. Here are several common constraints:
where T : struct
public class Unit<T> where T: struct
{
public T Value { get; set; }
}
var unit1 = new Unit<int>();
var unit2 = new Unit<object>(); // causes error, must be non-nullable value
where T: unmanaged
public class Unit<T> where T: unmanaged
{
public unsafe void Run(T t)
{
T* array = stackalloc T[24];
}
public T Go(T t) => t;
}
var unit1 = new Unit<int>();
var unit2 = new Unit<object>(); // causes error, must be non-nullable value
Both examples above work similarly; the difference is that we apply where T: unmanaged
when working with low-level libraries and frameworks.
where T: class
public class Unit<T> where T: class
{
public T Value { get; set; }
}
var unit1 = new Unit<object>();
var unit2 = new Unit<int>(); // causes error, must be reference type
where T : new()
The type argument must have a public parameterless constructor. When used together with other constraints, the new()
constraint must be specified last.
public class Unit<T> where T: new()
{
public T Value { get; set; }
}
public class UnitClass { }
public struct UnitStruct { }
var unit1 = new Unit<UnitClass>();
var unit2 = new Unit<UnitStruct>();
var unit3 = new Unit<string>(); // causes error, must be a non-abstract type with a public parameterless
where T: notnull
The type argument must be a non-nullable type, either a non-nullable reference type or a non-nullable value type.
public class Unit<T> where T : notnull
{
public T Value { get; set; }
}
var unit1 = new Unit<int?>();
var unit2 = new Unit<object>();
For more details about constraints, you can visit this site.
Variant & Contravariant Generic Interfaces
If a generic interface has covariant or contravariant generic type parameters, it's called variant. Covariance allows interface methods to have more derived return types than defined by the generic type parameters. Contravariance allows interface methods to have argument types that are less derived than specified by the generic parameters. For this purpose, we use "out" and "in." Here are some examples to illustrate the differences:
interface ICovariant<out T>
{
T Execute();
//bool Validate(T t); Error Invalid variance
}
interface IContravariant<in T>
{
void Execute(T t);
//T Execute(); Error Invalid variance
}
interface IVariance<in T, out R>
{
R Execute();
bool IsSatisfied(T t);
}
To clarify, when a generic interface has out T
in its type parameter, it allows to use T
as a return type, but not as input arguments. Conversely, when a generic interface has in T
in its type parameter, it permits using T
as an input argument but not as a return type.
One example of variance can be found in Action
and Func
implementations, both of which are variant generic delegates:
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
Conclusion
Generics play a crucial role in C# development. They are used in various scenarios, such as creating reusable classes, working with type-safe collections, enhancing performance by avoiding boxing and unboxing operations, and implementing data structures and algorithms that can work with different data types. By using generics effectively, you can write more versatile and type-safe code while.