C# Advanced (Series #4)

7 min read

c#
  • C# – Anonymous Types
  • C# – Dynamic Types
  • C# – Ternary Operator ?:
  • C# Generics & Generic Constraints
  • C# – Delegates
  • C# – Func Delegate
  • C# – Action Delegate
  • C# – Anonymous Method
  • C# – Events
  • C# – Extension Method
  • C# – HttpClient

C# Generics

Generic có nghĩa là hình thức chung, không cụ thể. Trong C#, generic có nghĩa là không dành riêng cho một kiểu dữ liệu cụ thể.

C# cho phép bạn định nghĩa các lớp chung, giao diện, lớp trừu tượng, trường, phương thức, phương thức tĩnh, thuộc tính, sự kiện, đại biểu và toán tử bằng cách sử dụng type parameter và không có kiểu dữ liệu cụ thể. Một type parameter là một placeholder cho một loại cụ thể được chỉ định khi tạo một phiên bản của generic type.

Một generic type được khai báo bằng cách chỉ định một type parameter trong dấu ngoặc nhọn sau tên loại, ví dụ: TypeName<T> trong đó T is a type parameter.

Generic Class

Generic classes được xác định bằng cách sử dụng type parameter trong dấu ngoặc nhọn sau tên lớp. Sau đây định nghĩa một lớp chung.

// Example: Define Generic Class
class DataStore<T>
{
    public T Data { get; set; }
}

Ở trên, DataStore là một generic class. T được gọi là type parameter, có thể được sử dụng làm kiểu trường, thuộc tính, tham số phương thức, kiểu trả về và delegates trong lớp DataStore. Ví dụ: Data là thuộc tính chung vì chúng tôi đã sử dụng type parameter T làm loại của nó thay vì loại dữ liệu cụ thể.

NOTE: Không bắt buộc phải sử dụng T làm type parameter. Bạn có thể đặt bất kỳ tên nào cho type parameter. Nói chung, T được sử dụng khi chỉ có một type parameter. Bạn nên sử dụng tên type parameter dễ đọc hơn theo yêu cầu như TSession, TKey, TValue, v.v. Tìm hiểu thêm về Nguyên tắc đặt tên type parameter.

// Example: Generic Class with Multiple Type Parameters
class KeyValuePair<TKey, TValue>
{
    public TKey Key { get; set; }
    public TValue Value { get; set; }
}

Instantiating Generic Class

Bạn có thể tạo một thể hiện của các generic classes bằng cách chỉ định một actual type trong dấu ngoặc nhọn. Phần sau đây tạo một thể hiện của lớp chung DataStore

DataStore<string> store = new DataStore<string>();

Ở trên, chúng tôi đã chỉ định string type trong dấu ngoặc nhọn khi tạo một instance. Vì vậy, T sẽ được thay thế bằng loại chuỗi bất cứ khi nào T được sử dụng trong toàn bộ class tại thời điểm biên dịch. Do đó, loại của thuộc tính Data sẽ là string.

The following figure illustrates how generics works.

Bạn có thể gán giá trị chuỗi cho thuộc tính Data. Cố gắng gán các giá trị khác với chuỗi sẽ dẫn đến lỗi thời gian biên dịch.

DataStore<string> store = new DataStore<string>();
store.Data = "Hello World!";
//obj.Data = 123; //compile-time error

Bạn có thể chỉ định các kiểu dữ liệu khác nhau cho các đối tượng khác nhau, như hiển thị bên dưới.

// Example: Generic class
DataStore<string> strStore = new DataStore<string>();
strStore.Data = "Hello World!";
//strStore.Data = 123; // compile-time error
DataStore<int> intStore = new DataStore<int>();
intStore.Data = 100;
//intStore.Data = "Hello World!"; // compile-time error
KeyValuePair<int, string> kvp1 = new KeyValuePair<int, string>();
kvp1.Key = 100;
kvp1.Value = "Hundred";
KeyValuePair<string, string> kvp2 = new KeyValuePair<string, string>();
kvp2.Key = "IT";
kvp2.Value = "Information Technology";

Generic Class Characteristics

  • Một generic class làm tăng khả năng sử dụng lại. Càng nhiều type parameters có nghĩa là nó sẽ có thể tái sử dụng nhiều hơn. Tuy nhiên, việc khái quát hóa quá nhiều khiến mã khó hiểu và khó bảo trì.
  • Một generic class có thể là base class cho các generic hoặc non-generic hoặc abstract classes khác.
  • Một generic class có thể được bắt nguồn từ other generic or non-generic interfaces, classes, or abstract classes.

Generic Fields

Một generic class có thể bao gồm các generic fields. Tuy nhiên, nó không thể được khởi tạo.

// Example: Generic Methods
class DataStore<T>
{
    private T[] _data = new T[10];
    
    public void AddOrUpdate(int index, T item)
    {
        if(index >= 0 && index < 10)
            _data[index] = item;
    }
    public T GetData(int index)
    {
        if(index >= 0 && index < 10)
            return _data[index];
        else 
            return default(T);
    }
}

Ở trên, các phương thức AddorUpdate() và GetData() là các generic methods. Kiểu dữ liệu thực tế của tham số item sẽ được chỉ định tại thời điểm khởi tạo lớp DataStore<T>, như hiển thị bên dưới.

// Example: Generic Methods
DataStore<string> cities = new DataStore<string>();
cities.AddOrUpdate(0, "Mumbai");
cities.AddOrUpdate(1, "Chicago");
cities.AddOrUpdate(2, "London");
DataStore<int> empIds = new DataStore<int>();
empIds.AddOrUpdate(0, 50);
empIds.AddOrUpdate(1, 65);
empIds.AddOrUpdate(2, 89);

The generic parameter type can be used with multiple parameters with or without non-generic parameters and return type.

Sau đây là nạp chồng generic method hợp lệ.

// Example: Generic Method Overloading
public void AddOrUpdate(int index, T data) { }
public void AddOrUpdate(T data1, T data2) { }
public void AddOrUpdate<U>(T data1, U data2) { }
public void AddOrUpdate(T data) { }

A non-generic class có thể bao gồm các generic methods bằng cách chỉ định type parameter trong dấu ngoặc nhọn với tên phương thức, như được hiển thị bên dưới.

// Example: Generic Method in Non-generic Class
class Printer
{
    public void Print<T>(T data)
    {
        Console.WriteLine(data);
    }
}
Printer printer = new Printer();
printer.Print<int>(100);
printer.Print(200); // type infer from the specified value
printer.Print<string>("Hello");
printer.Print("World!"); // type infer from the specified value

Advantages of Generics

  • Generics increase the reusability of the code. You don’t need to write code to handle different data types.
  • Generics are type-safe. You get compile-time errors if you try to use a different data type than the one specified in the definition.
  • Generic has a performance advantage because it removes the possibilities of boxing and unboxing.

C# Generic Constraints

C# allows you to use constraints to restrict client code to specify certain types while instantiating generic types. It will give a compile-time error if you try to instantiate a generic type using a type that is not allowed by the specified constraints.

You can specify one or more constraints on the generic type using the where clause after the generic type name.

GenericTypeName<T> where T  : contraint1, constraint2

The following example demonstrates a generic class with a constraint to reference types when instantiating the generic class.

// Example: Declare Generic Constraints
class DataStore<T> where T : class
{
    public T Data { get; set; }
}

Above, we applied the class constraint, which means only a reference type can be passed as an argument while creating the DataStore class object. So, you can pass reference types such as class, interface, delegate, or array type. Passing value types will give a compile-time error, so we cannot pass primitive data types or struct types.

DataStore<string> store = new DataStore<string>(); // valid
DataStore<MyClass> store = new DataStore<MyClass>(); // valid
DataStore<IMyInterface> store = new DataStore<IMyInterface>(); // valid
DataStore<IEnumerable> store = new DataStore<IMyInterface>(); // valid
DataStore<ArrayList> store = new DataStore<ArrayList>(); // valid
//DataStore<int> store = new DataStore<int>(); // compile-time error 

The following table lists the types of generic constraints.

ConstraintDescription
classThe type argument must be any class, interface, delegate, or array type.
class?The type argument must be a nullable or non-nullable class, interface, delegate, or array type.
structThe type argument must be non-nullable value types such as primitive data types int, char, bool, float, etc.
new()The type argument must be a reference type which has a public parameterless constructor. It cannot be combined with struct and unmanaged constraints.
notnullAvailable C# 8.0 onwards. The type argument can be non-nullable reference types or value types. If not, then the compiler generates a warning instead of an error.
unmanagedThe type argument must be non-nullable unmanged types.
< base class name>The type argument must be or derive from the specified base class. The Object, Array, ValueType classes are disallowed as a base class constraint. The Enum, Delegate, MulticastDelegate are disallowed as base class constraint before C# 7.3.
< base class name>?The type argument must be or derive from the specified nullable or non-nullable base class
< interface name>The type argument must be or implement the specified interface.
< interface name>?The type argument must be or implement the specified interface. It may be a nullable reference type, a non-nullable reference type, or a value type
where T: UThe type argument supplied for T must be or derive from the argument supplied for U.

where T : struct

The following example demonstrates the struct constraint that restricts type argument to be non-nullable value type only.

// Example: struct Constraints
class DataStore<T> where T : struct
{
    public T Data { get; set; }
}
DataStore<int> store = new DataStore<int>(); // valid
DataStore<char> store = new DataStore<char>(); // valid
DataStore<MyStruct> store = new DataStore<MyStruct>(); // valid
//DataStore<string> store = new DataStore<string>(); // compile-time error 
//DataStore<IMyInterface> store = new DataStore<IMyInterface>(); // compile-time error 
//DataStore<ArrayList> store = new DataStore<ArrayList>(); // compile-time error 

where T : new()

The following example demonstrates the struct constraint that restricts type argument to be non-nullable value type only.

// Example: new() Constraint
class DataStore<T> where T : class, new()
{
    public T Data { get; set; }
}
DataStore<MyClass> store = new DataStore<MyClass>(); // valid
DataStore<ArrayList> store = new DataStore<ArrayList>(); // valid
//DataStore<string> store = new DataStore<string>(); // compile-time error 
//DataStore<int> store = new DataStore<int>(); // compile-time error 
//DataStore<IMyInterface> store = new DataStore<IMyInterface>(); // compile-time error 

where T : baseclass

// Example: BaseClass Constraint
class DataStore<T> where T : IEnumerable
{
    public T Data { get; set; }
}
DataStore<ArrayList> store = new DataStore<ArrayList>(); // valid
DataStore<List> store = new DataStore<List>(); // valid
//DataStore<string> store = new DataStore<string>(); // compile-time error 
//DataStore<int> store = new DataStore<int>(); // compile-time error 
//DataStore<IMyInterface> store = new DataStore<IMyInterface>(); // compile-time error 
//DataStore<MyClass> store = new DataStore<MyClass>(); // compile-time error 
Avatar photo

Leave a Reply

Your email address will not be published. Required fields are marked *