Bài 7: Reflection


Reflection là một mô hình “chương trình như dữ liệu”, cho phép một phần của chương trình truy vấn và gọi các phần khác một cách “động”, như là các assembly, kiểu dữ liệu hoặc các thành phần của kiểu. Nó đặc biệt hữu dụng với các mô hình lập trình late-bound(1) và các công cụ.

Đoạn code sau sử dụng reflection để tìm và gọi các kiểu:

foreach (Type type in typeof(Program).Assembly.DefinedTypes)
{
    if (type.IsAssignableTo(typeof(IStory)) &&
        !type.IsInterface)
    {
        IStory? story = (IStory?)Activator.CreateInstance(type);
        if (story is not null)
        {
            var text = story.TellMeAStory();
            Console.WriteLine(text);
        }
    }
}

interface IStory
{
    string TellMeAStory();
}

class BedTimeStore : IStory
{
    public string TellMeAStory() => "Once upon a time, there was an orphan learning magic ...";
}

class HorrorStory : IStory
{
    public string TellMeAStory() => "On a dark and stormy night, I heard a strange voice in the cellar ...";
}

Đoạn code này tự động liệt kê tất cả các kiểu trong một assembly (2) đã implement một interface cụ thể, khởi tạo một instance của từng type và gọi một phương thức trên đối tượng thông qua interface đó. Đoạn code này cũng có thể được viết theo cách “tĩnh” (1)(xem giải thích về late-bound) vì nó chỉ truy vấn đến các kiểu trong một assembly mà nó đang tham chiếu, nhưng để làm như vậy, nó cần được truyền một tập hợp tất cả các instance để xử lý, có thể dưới dạng List<IStory>. Cách tiếp cận late-bound này sẽ có nhiều khả năng được sử dụng hơn nếu thuật toán này tải các assembly tùy ý từ một thư mục add-in. Reflection thường được sử dụng trong các tình huống như vậy, khi các assembly và type không được biết trước.

Reflection có lẽ là hệ thống động nhất được cung cấp trong .NET. Nó nhằm mục đích cho phép các nhà phát triển tạo trình tải mã binary và trình gọi phương thức của riêng họ, với ngữ nghĩa có thể khớp hoặc khác với các chính sách mã tĩnh (được xác định bởi runtime). Reflection hiển thị một mô hình đối tượng phong phú, dễ dàng áp dụng cho các trường hợp sử dụng hẹp nhưng yêu cầu hiểu biết sâu hơn về hệ thống kiểu .NET khi các tình huống trở nên phức tạp hơn.

Reflection cũng cho phép một chế độ riêng biệt trong đó mã bytecode IL được tạo có thể được biên dịch JIT (3) trong thời gian chạy, đôi khi được sử dụng để thay thế một thuật toán chung bằng một thuật toán chuyên biệt. Nó thường được sử dụng trong serializers (4) or object relational mapper (5) sau khi đã biết mô hình đối tượng và các chi tiết khác.

(1) late-bound: Thông thường khi viết một chương trình, giả sử ta gọi đến một phương thức thì phương thức được gọi đã phải được viết trước đó (trong cùng chương trình hoặc từ một thư viện có sẵn). Khi đó trình biên dịch đã biết chính xác phương thức nằm ở đâu trong bộ nhớ để gọi. Một lời gọi hàm chỉ đơn giản là di chuyển đến và tiếp tục thực thi tại vị trí khai báo phương thức đó. Ta gọi đây là early-bound hoặc static binding. Ngược với early-bound là late-bound, khi đó lời gọi sẽ thông qua một con trỏ hàm: vào thời điểm biên dịch, trình dịch chỉ biết là nó sẽ gọi một phương thức nằm đâu đó trong bộ nhớ, vị trí cụ thể sẽ được lấy từ một biến và giá trị cụ thể chỉ biết vào lúc chạy chương trình. Bạn có thể đọc thêm bài: GIẢI THÍCH CÁC KHÁI NIỆM TRONG OOP – TÍNH ĐA HÌNH để hiểu rõ hơn.

(2) Là một đơn vị triển khai code IL trong .NET. Thực chất nó chính là file .exe hoặc .dll, trong .NET Framework bạn có thể tạo là các assembly chứa trong nhiều DLL (nhưng trong .NET Core và .NET 5+ đã bỏ đi tính năng này).

(3) JIT, Just-In-Time: các máy ảo Java và .NET cho phép biên dịch các đoạn mã bytecode hoặc IL thành mã máy và thực thi trực tiếp các mã máy đó, điều này giúp cho hiệu suất tăng lên đáng kể. Một dạng khác của JIT nữa là AOT (Ahead-Of-Time), khi đó thay vì phải chờ đến khi thực thi mới bắt đầu dịch thì mã IL sẽ được dịch luôn ngay khi cài đặt chương trình, nhờ đó thời gian khởi động chương trình lần đầu sẽ nhanh hơn (cài đặt lâu hơn). Engine v8 của JavaScript cũng có hỗ trợ JIT trong một số phần (https://blog.chromium.org/2015/07/revving-up-javascript-performance-with.html). (Xem thêm bài https://daohainam.com/2023/04/17/bai-9-kha-nang-sinh-code)

(4) serializer: là các thư viện cho phép chúng ta chuyển các object trong bộ nhớ thành một dạng dữ liệu có thể lưu trữ được, và giúp chúng ta chuyển từ dạng dữ liệu đó về lại các object trong bộ nhớ. Một ví dụ là Json.NET, cho phép convert từ object sang JSON và ngược lại, hoặc trong .NET có các serializer cho phép chuyển đổi sang dạng XML, binary (https://learn.microsoft.com/en-us/dotnet/standard/serialization/). Bạn bắt buộc phải serialize các object nếu muốn lưu nó xuống file, truyền qua mạng, hoặc lưu trữ trong cơ sở dữ liệu.

(5) object relational mapper: Là các thư viện giúp bạn map các tập dữ liệu được trả về từ database vào các object dễ dàng hơn. Một ORM khá phổ biến là Dapper, bạn có thể truy cập vào trang github của nó để xem các ví dụ: https://github.com/DapperLib/Dapper.

One thought on “Bài 7: Reflection

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s