Một ứng dụng của dynamic trong C#

Với dynamic type được giới thiệu ở phiên bản 4.0, C# giờ đây đã hỗ trợ dynamic typing. Các bạn có thể xem một số ví dụ tại MSDN.

Cách dùng thì đơn giản rồi, điều quan trọng là khi nào thì dùng dynamic? Xem bài này nếu bạn quan tâm đến lí do chính thức mà Microsoft đưa dynamic vào C#. Ở đây chỉ nói về một ví dụ rất đơn giản mà các sách hay dùng.

Static typing giúp phát hiện các lỗi về method signature (ví dụ, trình dịch sẽ bắt lỗi khi bạn truyền thiếu tham số), type compability (ví dụ, bạn không thể gán object kiểu Book cho biến kiểu Car được). Hơn nữa, Intellisense (chức năng nhắc mã trong IDE) chỉ hoạt động được với các biến static (vì biến dynamic không có thông tin về kiểu). Như vậy có vẻ static typing là một lựa chọn quá an toàn, quá tiện lợi, và không có lí do gì để dùng dynamic trong C#.

Tuy nhiên, theo như một số sách và trang web, thì có những tình huống mà static typing cũng không giúp phát hiện lỗi cho chúng ta, chẳng hạn như khi parse XML:

string bookDef = "<Book><Title>The wedding singer</Title></Book>" 
XElement book = XElement.Parse(bookDef); 
Console.WriteLine(book.Descendants("Title").FirstOrDefault().Value);

bookDef có thể là string được tạo sẵn, cũng có thể do người dùng nhập vào, hay đọc từ tập tin, v.v. Dù thế nào đi nữa, trình dịch không thể (và không quan tâm) giá trị của nó khi dịch lệnh XElement book… Điều đó nghĩa là nếu bạn lỡ viết nhầm “Title” thành “Tittle” trong lệnh Console.WriteLine, trình dịch cũng không thể phát hiện được, và bạn sẽ nhận Exception vào lúc runtime.

string bookDef = "<Book><Title>The wedding singer</Title></Book>" 
dynamic book = DynamicXml.Parse(bookDef); 
Console.WriteLine(book.Title);

XML được parse lúc runtime thành một object, với property là các element. Object này được gán cho book, và chúng ta có thể lấy thông tin từ XML một cách rất… OOP. Điều này có thể thực hiện được với static typing sử dụng Reflection, nhưng dynamic giúp cho công việc trở nên đơn giản hơn rất nhiều. Và chúng ta cũng không mất gì cả, vì đằng nào thì static typing cũng không giúp bắt những lỗi như “Tittle” khi biên dịch.

Để xây dựng DynamicXml, tham khảo hướng dẫn của MSDN.

Chú ý là có những thứ trong XML khó có thể diễn tả trong C#, như namespace, quy tắc đặt tên, v.v, cho nên ví dụ này chỉ ở mức ứng dụng là làm ví dụ :D. HM nghĩ nếu xây dựng được một DynamicXml hỗ trợ đầy đủ XML, thì việc sử dụng nó cũng chả đơn giản hơn XElement là mấy (chưa nói đến chúng ta đã có LINQ2XML).

Dĩ nhiên là dynamic có những lí do hợp lí để tồn tại, nhưng đó là chuyện khác.

Covariance và contravariance trong C# Generics

Xét interface IList<T> với hai method là void Add(T item) và T Get(int index), và List<T> là class implement IList<T>. Ngoài ra ta có các kiểu cha con Mammal, Elephant.

Bây giờ ta có lệnh gán IList<Mammal> a = new List<Elephant>(). Vì a có kiểu khai báo là IList<Mammal>, nó có thể gọi method void Add(Mammal item). Tuy nhiên, List<Elephant>() lại chỉ hiện thực method void Add(Elephant item), nên ta không thể gọi a.Add(tiger). Do đó có hai phương án. Phương án thứ nhất là trình biên dịch chấp nhận lệnh gán trên, các tình huống gây lỗi sẽ được bắt lúc runtime. Phương án thứ hai là không cho biên dịch lệnh gán này. C# chọn phương án thứ hai. Lí do có lẽ là để phù hợp với quan điểm về static typing.

Còn method Mammal Get(int index)? Giả sử ta gọi Mammal b = a.Get(0). Bởi vì List<Elephant> hiện thực method Elephant Get(int index), và Elephant là con của Mammal, nên lệnh gọi này hoàn toàn hợp lệ cả lúc biên dịch lẫn runtime.

Như vậy, nếu chúng ta chỉ sử dụng type T (trong IList<T>) cho các giá trị output, thì sẽ không bao giờ xảy ra chuyện rắc rối như với a.Add(Elephant item). Chính vì lí do này, C# 4.0 giới thiệu thêm kí hiệu out cho phép ta chú thích với trình biên dịch rằng, type T chỉ dùng cho output. Nói cách khác, nếu IList<T> chỉ có method Get, ta có thể khai báo interface này là IList<out T>, và khi đó IList<Mammal> a = new List<Elephant>() là một phép gán hợp lệ.

Trong tình huống ngược lại, IList<Elephant> a = new List<Mammal>(), thì method Get mới lại là kẻ gây rắc rối. Vì method này trong IList<Elephant> trả về Elephant, còn hiện thực của List<Mammal> lại trả về Mammal, và tất nhiên không phải tất cả các Mammal đều là Elephant. Tương tự như trường hợp ban đầu, nếu ở đây, IList<T> không có Get, tức là T không được dùng cho giá trị output mà chỉ cho input, thì phép gán sẽ hoàn toàn hợp lệ. Để chú thích cho trình duyệt điều này, ta dùng từ khóa in, tương tự như out.

Còn hai thuật ngữ covariance và contravariance liên quan gì đến in và out? Dựa vào bài viết này, có thể liên hệ như sau: IList<out T> cho phép bảo toàn chiều “lớn hơn” giữa Mammal và Elephant, còn IList<in T> đảo chiều “lớn hơn” giữa Elephant và Mammal.

Nãy giờ bạn có thắc mắc tại sao lại là IList<…> a = new List<…> chứ không phải List<…> a = new List<…> không? Đó là vì CLR chỉ hỗ trợ tính năng này cho interface và delegate (theo "C# in depth”). Vậy tại sao CLR lại chỉ hỗ trợ như vậy. Câu trả lời dễ hiểu nhất mà HM từng thấy là ở đây, do một thành viên trong nhóm Visual C# đưa ra:

Sure, that’s a reasonable example but you haven’t shown anything that couldn’t also be done with interfaces. Just make interface ILookup<out T> and have Lookup<T> implement it. What compelling additional benefit over interface variance does your scenario for class variance add?

We don’t have to provide a justification for not implementing a feature. Not implementing a feature is free. Rather, we have to provide a justification for implementing a feature — features can cost millions of dollars to Microsoft, and impose an even larger burden upon our customers who must then spend time and money learning about the feature.

Đoạn comment thứ hai cho chúng ta thêm một câu hỏi: vậy tính năng vừa bàn ở trên trong C# 4.0 dùng để làm gì? Khi nào thì dùng? Cái này thì phải suy nghĩ đã :D.