C# – Kĩ thuật Reflection trong .Net

Đối với nhiều người, reflection là một thuật ngữ lạ và ít khi được nhắc tới. Nhưng thực tế, thuật ngữ này đã xuất hiện và được áp dụng vào khá nhiều ngôn ngữ bậc cao phổ biến như C#, Java, Perl, PHP,…Vậy reflection là gì, và nó có công dụng gì trong việc lập trình hiện nay?

Để định nghĩa, trước tiên hãy thử hình dung một trường hợp là làm sao để thay đổi giá trị của 1 biến khi người dùng nhập tên biến vào lúc chương trình đang thực thi, hoặc làm sao để tạo một instance của form nếu chỉ sử dụng tên form?

Nếu chưa từng nghe qua về các khái niệm như reflection, assembly, disassembly có thể bạn cho rằng đây là một điều không thể. Tất nhiên trong .Net điều này là có thể nhờ chức năng reflection.

Đây là ví dụ dùng reflection để hiển thị thông tin các assembly, namespace, type,… trong C#. Bạn hoàn toàn có thể tự viết được chương trình dạng này sau khi nắm vững hết những vấn đề mà tôi trình bày bên dưới.

Download Demo (14KB)

Y2 ReflectorDemo1.0

I.  Định nghĩa

Reflection được hiểu là một chức năng trong .Net cho phép đọc thông tin từ các siêu dữ liệu (metadata) của assembly để tạo ra một đối tượng (có kiểu là Type) bao gói các thông tin đó lại. Với reflection, bạn có thể trích xuất để gọi và tạo ra các phương thức, truy cập và thay đổi các thuộc tính của đối tượng một cách linh động trong quá trình runtime.

II.  Lớp System.Type

Lớp System.Type là một abstract class đại diện cho các kiểu dữ liệu (kiểu lớp, interface, mảng, giá trị,…), đây là lớp chính để thực hiện các cơ chế Reflection đại diện cho các kiểu dữ liệu trong .Net. Bằng cách sử dụng lớp này, bạn có thể lấy về tất cả thông tin về các kiểu dữ liệu, phương thức, thuộc tính, sự kiện,… Ngoài ra ta còn có thể tạo ra các instance của kiểu dữ liệu và thực thi các phương thức của chúng, kĩ thuật này còn được gọi với thuật ngữ Late Binding.

Khi sử dụng lớp này bạn hãy thêm namespace System.Reflection vào vì các thành viên của System.Type hầu hết đều liên quan tới các lớp trong namespace này.

1. Phương thức GetType() và toán tử typeof:

Bạn có thể dùng phương thức GetType() của lớp Object để trả về đối tượng kiểu Type mô tả kiểu dữ liệu của đối tượng. Có được đối tượng Type này rồi, ta sẽ lấy thông tin của kiểu dữ liệu qua các thuộc tính của lớp Type.

Sau đây là một ví dụ đơn giản minh họa cách in ra tên kiểu dữ liệu của các biến qua thuộc tính FullName của lớp Type:

static void Main(string[] args)
{
    var number = 100;
    var text = "abc";

    Console.WriteLine(number.GetType().FullName);
    Console.WriteLine(text.GetType().FullName);

    Console.Read();
}

Phương thức GetType() chỉ có thể lấy được thông tin từ các biến đối tượng. Trong trường hợp muốn lấy thông tin của lớp thông qua tên lớp, bạn phải sử dụng phương thức tĩnh Type.GetType(), tuy nhiên tham số truyền vào cần phải ghi đầy đủ cả namespace.

static void Main()
{
    // Không được ghi tham số là string, int hoặc Int32
    Type mType1 = Type.GetType("System.Int32");
    Type mType2 = Type.GetType("System.String");

    Console.WriteLine(mType1.FullName);
    Console.WriteLine(mType2.FullName);

    Console.Read();
}

Sử dụng toán tử typeof, bạn có thể lấy về đối tượng kiểu System.Type của bất kì kiểu nào với cú pháp typeof(type).

static void Main()
{

    Console.WriteLine(typeof(Int32).FullName);
    Console.WriteLine(typeof(String).FullName);

    Console.Read();
}

Cả ba ví dụ trên đều cho ra cùng kết quả khi được thực thi:

System.Int32
System.String

2. Lấy các thông tin từ Type

Lớp Type cung cấp đầy đủ các phương thức cho phép lấy các thông tin của kiểu dữ liệu. Các phương thức này có dạng GetXXX(), mỗi phương thức trả về một hay một mảng đối tượng lưu trữ thông tin của mỗi thành viên trong kiểu dữ  liệu (bạn có thể nhận biết điều này thông qua cách đặt tên của các phương thức ở dạng số nhiều hay số ít). Các kiểu trả về của các phương thức này đều có hậu tố là Info: ConstructorInfo, EventInfo, FieldInfo, InterfaceInfo, MemberInfo, MethodInfo, PropertyInfo.

Ví dụ để lấy tất cả thành viên public của một lớp ta dùng phương thức GetMembers() như minh họa dưới đây:

class Program
{
    class MyClass
    {
        // Chỉ có mục đích minh họa
        public string Name { get; set; }
        public static int theValue;
        public void SayHello() {}
    }
    static void Main()
    {
        Type mType = typeof(MyClass);

        MemberInfo[] members = mType.GetMembers();

        Array.ForEach(members,mem =>
            Console.WriteLine(mem.MemberType.ToString().PadRight(12) + ": " + mem)
        );

        Console.Read();
    }
}

Khi chạy đoạn mã này bạn sẽ nhận được kết quả như sau:

GetMembersResult

3. Ví dụ về MethodInfo: thực thi một phương thức

Ta sẽ trở lại với phần mở đầu của bài viết mà tôi nói về trường hợp thực thi một phương thức dựa vào tên mà người dùng truyền vào. Hãy xem các phương thức mà lớp Type cung cấp, có một phương thức tên là GetMethod(string methodName). Giá trị trả về của phương thức này là một đối tượng kiểu System.Reflection.MethodInfo chứa các thông tin về phương thức. Và đây là một ví dụ đơn giản minh họa cách dùng phương thức này:

class Program
{
    class MyClass
    {
        public void SayHello()
        {
            Console.WriteLine("Hello");
        }
    }

    static void Main()
    {
        MyClass myClass = new MyClass();
        Type myType = myClass.GetType();

        // Lấy về phương thức SayHello
        MethodInfo myMethodInfo = myType.GetMethod("SayHello");

        // Thực thi phương thức SayHello của myClass với tham số là null
        myMethodInfo.Invoke(myClass, null);

        Console.Read();
    }
}

Kết quả xuất ra là:

Hello

Bởi vì phương thức SayHello() trên không yêu cầu tham số nên ta sẽ truyền null vào phương thức Invoke() của đối tượng MethodInfo. Đối với các phương thức yêu cầu tham số, ta phải tạo một mảng object[] để truyền giá trị vào. Hãy xem ví dụ sau:

class Program
{
    class MyClass
    {
        public void SayHello(string name)
        {
            Console.WriteLine("Hello, {0}", name);
        }
    }

    static void Main()
    {
        MyClass mClass = new MyClass();
        Type mType = mClass.GetType();

        // Lấy về phương thức SayHello
        MethodInfo mMethodInfo = mType.GetMethod("SayHello");

        object[] mParams = new object[] { " Hanoi Aptech" };
        // Thực thi phương thức SayHello với tham số là mParam
        mMethodInfo.Invoke(mClass, mParams);

        Console.Read();
    }
}

Kết quả xuất ra:

Hello, Hanoi Aptech

Trong trường hợp có nhiều overload của phương thức SayHello(), nếu bạn sử dụng phương thức GetMethod() trên thì sẽ nhận một exception với thông báo “Ambiguous match found” trong lúc runtime. Nguyên nhân là do chương trình không thể biết được phải lấy phương thức SayHello() nào. Lúc đó bạn phải dùng một overload khác của GetMethod() để xác định các tham số sẽ truyền vào phương thức SayHello(), và đây là ví dụ:

class Program
{
    class MyClass
    {
        public void SayHello()
        {
            Console.WriteLine("Hello");
        }
        public void SayHello(string name)
        {
            Console.WriteLine("Hello, {0}", name);
        }
    }
    static void Main()
    {
        MyClass myClass = new MyClass();
        Type myType = myClass.GetType();

        // Lấy về phương thức SayHello có 1 tham số kiểu string
        MethodInfo myMethodInfo = myType.GetMethod("SayHello", new Type[] { typeof(string) });

        // Thực thi phương thức SayHello của myClass với tham số
        myMethodInfo.Invoke(myClass, new object[] { "Hanoi Aptech" });

        Console.Read();
    }

}

Kết quả xuất ra:

Hello, Hanoi Aptech

Thay vì dùng phương thức GetMethod(), bạn có thể dùng trực tiếp phương thức instance của lớp Type là InvokeMember() với tham số thứ hai là BindingFlags. InvokeMethod.:

public object InvokeMember(
 string name,
 BindingFlags invokeAttr,
 Binder binder,
 object target,
 object[] args
);

Ví dụ trên có thể viết lại như sau:

class Program
{
    class MyClass
    {
        public void SayHello()
        {
            Console.WriteLine("Hello");
        }
    }

    static void Main()
    {
        MyClass mClass = new MyClass();
        Type mType = mClass.GetType();

        mType.InvokeMember("SayHello", BindingFlags.InvokeMethod, null, mClass, null);
        Console.Read();
    }
}

4. Ví dụ về ConstructorInfo: Tạo instance của đối tượng

Đây là vấn đề khá thực tế trong .Net, trước đây cũng có nhiều bạn thắc mắc về cách viết một phương thức tạo ra một instance của Form với tham số truyền vào là tên của Form đó ở dạng chuỗi. Vấn đề này không khó nếu như bạn đã biết cách dùng Reflection.

  • Sử dụng

Khám phá các thành viên của lớp Type, bạn có thể đoán ra là có thể sử dụng lớp ConstructorInfo để làm điều này.

Phương thức GetConstructor(Type[] types) của lớp Type yêu cầu một mảng kiểu Type theo đúng kiểu, trình tự và số lượng tham số mà constructor cần lấy về yêu cầu. Trong trường hợp không có tham số, lớp Type cung cấp sẵn một mảng Type rỗng là Type.EmptyTypes. Hãy xem hai ví dụ sau:

–     Constructor không có tham số (dùng default constructor):

namespace ConsoleApplication1
{

    class Program
    {
        class MyClass
        {
            public void SayHello()
            {
                Console.WriteLine("Hello");
            }
        }
        static void Main()
        {
            Type mType = Type.GetType("ConsoleApplication1.Program+MyClass");

            ConstructorInfo conInfo = mType.GetConstructor(Type.EmptyTypes);

            object obj = conInfo.Invoke(null);

            mType.InvokeMember("SayHello", BindingFlags.InvokeMethod, null, obj, null);

            Console.ReadLine();
        }
    }
}

– Constructor có tham số:

namespace ConsoleApplication1
{

    class Program
    {
        class MyClass
        {
            string _name;
            public MyClass(string name)
            {
                _name = name;
            }
            public void SayHello()
            {
                Console.WriteLine("Hello, "+_name);
            }
        }
        static void Main()
        {
            Type mType = Type.GetType("ConsoleApplication1.Program+MyClass");

            ConstructorInfo conInfo = mType.GetConstructor(new Type[] { typeof(string) });

            object obj = conInfo.Invoke(new object[] { "Yin Yang" });

            mType.InvokeMember("SayHello", BindingFlags.InvokeMethod, null, obj, null);

            Console.ReadLine();
        }
    }
}

Một chút thay đổi và bạn có thể dễ dàng hiểu được. Tuy nhiên có thể bạn thắc mắc về chuỗi tham số mà tôi sử dụng trong phương thức Type.GetType(). Nếu để ý bạn có thể thấy MyClass là một lớp nằm bên trong lớp Program (inner class), và để truy xuất đến lớp con ta phải dùng dấu ‘+’ để ko bị nhầm lẫn coi lớp Program là tên một namespace.
III. Lớp System.Activator

Bạn cảm thấy cách tạo instance như trên khá rắc rối, khó nhớ, và thực tế là có một cách khác để làm điều này, mặc dù về căn bản nó vẫn là một. Như đã nói trong phần trước, bởi vì yêu cầu tạo một instance từ tên kiểu khá phổ biến, .Net cung cấp sẵn cho ta lớp System.Activator để làm được điều này. Một giải pháp “nhỏ gọn” và giúp lập trình viên không phải dính tới những lí thuyết rườm rà mà ít khi được ứng dụng trong công việc.

Lớp System.Activator này sử dụng một phần của System.Type để tạo ra những phương thức tĩnh cho phép người dùng sử dụng bất cứ lúc nào, đơn giản và tiện lợi.

Hãy xem những gì mà System.Activator thể hiện và so sánh với System.Type, bạn có thể nhận thấy sự tương đồng của 2 lớp riêng biệt này. Tôi sử dụng hai lớp MyClass1 và MyClass2 để minh họa cho việc tạo đối tượng với constructor không và có tham số.

namespace ConsoleApplication1
{

    class Program
    {
        class MyClass1
        {
            public void SayHello()
            {
                Console.WriteLine("Hello");
            }
        }
        class MyClass2
        {
            string _name;

            public MyClass2(string name)
            {
                _name = name;
            }
            public void SayHello()
            {
                Console.WriteLine("Hello, "+_name);
            }
        }
        static void Main(string[] args)
        {
            Type mType1 = Type.GetType("ConsoleApplication1.Program+MyClass1");
            Type mType2 = Type.GetType("ConsoleApplication1.Program+MyClass2");

            object obj1 = Activator.CreateInstance(mType1);
            object obj2 = Activator.CreateInstance(mType2,"Yin Yang");

            mType1.InvokeMember("SayHello", BindingFlags.InvokeMethod, null, obj1, null);
            mType2.InvokeMember("SayHello", BindingFlags.InvokeMethod, null, obj2, null);

            Console.Read();
        }
    }
}

IV. Lớp System.Reflection.Assembly

Trong bài viết này tôi cũng có vài lần nhắc đến từ assembly, đây là một khái niệm cơ bản trong .Net. Cũng xin nhắc lại một chút về khái niệm này nếu như bạn đã bỏ lỡ nó khi bắt đầu tìm hiểu về .Net.

– Định nghĩa:.Net Assembly có thể được hiểu là kết quả của quá trình biên dịch từ mã nguồn sang tập tin nhị phân dựa trên .Net framework. Là thành phần cơ bản nhất .Net framework, assemly là thành phần không thể thiếu trong bất kì ứng dụng .Net nào và được thể hiện dưới hai dạng tập tin là EXE (process assembly) và DLL (library assembly). Assembly có thể được lưu trữ dưới dạng single-file hoặc multi-file, tùy theo kiểu dự án mà bạn làm việc.

Lớp System.Reflection.Assembly có thể được coi là một kiểu mô phỏng chi tiết về các assembly. Lớp Assembly chứa đầy đủ các thông tin cho phép chúng ta truy xuất thông tin và thực thi các phương thức lấy được từ các assembly. Có thể hiểu một cách tương tự: Lớp Type đại diện cho các kiểu dữ liệu, lớp Assembly đại diện cho các assembly.

Lớp Assembly cung cấp các phương thức tĩnh để nạp một assembly thông qua AssemblyName, đường dẫn tập tin hoặc từ tiến trình đang chạy. Bạn cũng có thể lấy Assembly dễ dàng từ property của lớp Type, ví dụ typeof(int).Assembly.

Từ Assemly lấy được, bạn có thể dùng phương thức GetTypes() lấy về các kiểu dữ liệu và thực hiện các kĩ thuật giống như trong phần về lớp System.Type tôi đã nói tới.

Sau đây là một ví dụ đơn giản sử dụng phương thức Assembly.GetExecutingAssembly() để lấy về assembly hiện tại và in ra màn hình các kiểu dữ liệu của nó:

class Program
{
    class MyClass { }

    static void Main()
    {

        Assembly ass = Assembly.GetExecutingAssembly();

        Type[] mTypes = ass.GetTypes();

        Array.ForEach(mTypes,type => Console.WriteLine(type.Name));

        Console.Read();
    }
}

Kết quả xuất ra:

Program
MyClass

V. Phần kết

Vậy là bạn đã được giới thiệu sơ lược về kĩ thuật Reflection cùng cách sử dụng nó trong lập trình. Khả năng của Reflection chưa dừng lại ở đây mà còn liên quan đến những kĩ thuật khác như Attribute, Reflection Emit,… đặc biệt bạn có thể ứng dụng trong việc tạo một IDE để lập trình .Net đơn giản.

Đây chỉ là ví dụ minh họa nên không chương trình Y2 RefectorDemo ở phần trên còn thiếu sót, tôi sẽ cung cấp source code đầy đủ trong phần sau khi giới thiệu về kĩ thuật Reflection Emit trong .Net.