Extension Methods – thêm phương thức mới cho class mà không cần sửa đổi class gốc

Extension Methods – thêm phương thức mới cho class mà không cần sửa đổi class gốc

Khi thiết kế, lập trình, sẽ có tình huống ta muốn thêm phương thức mới vào một lớp đã có sẵn. Với cách thông thường, ta sẽ mở source code của class đó ra và thêm phương thức đó vào. Nhưng như vậy buộc ta phải dịch lại toàn bộ project có liên quan mà việc này thì nhiều khi không thể làm được như là khi ta sử dụng class của các bên thứ ba chẳng hạn.
Nếu rơi vào tình huống trên, ta thường có 3 giải pháp khác để giải quyết vấn đề:

  • Thừa kế lớp đó và thêm phương thức vào lớp dẫn xuất
  • Tạo ra một lớp Helper và tạo phương thức tĩnh
  • Tạo một lớp trong đó tạo một thể hiện của lớp cần thêm phương thức và tạo phương thức mới cần thêm (Sử dụng Aggregation thay vì Inheritance)

Thừa kế

Muốn thêm phương thức X vào lớp A, đơn giản tạo lớp B thừa kế A và thêm phương thức X vào B:

class B : A {
	public void X() { /* thân hàm X */ }
}

Tạo Helper class

Nếu muốn áp dụng cách thừa kế mà lại gặp phải lớp A không cho thừa kế (sealed class) thì khó rồi. Ta làm một lớp B và tạo ra một phương thức tĩnh cho phép truyền thể hiện của A vào để xử lý.
Ví dụ một lớp không thể thừa kế được là String và ta muốn thêm phương thức để đếm số lượng từ trong chuỗi:

class StringHelper {
	public static int WordCount(string s) { 
		return str.Split(new char[] {' ', '.', '?', ',', '\r', '\n'}, StringSplitOptions.RemoveEmptyEntries).Length;
	}
}

Khi chạy dùng thì chỉ việc gọi:

Console.Write("WordCount = " + StringHelper.WordCount("Hello the world."));

Với cách dùng phương thức static, bạn có thể gặp phải tình huống như sau:

HelperClass.Operation2(HelperClass.Operation1(x, arg1), arg2)

Dùng được nhưng nhìn không đẹp lắm :(

Dùng Aggregation

Ngoài ra cũng có thể tạo phương thức X trong lớp B, trong B giữ tham chiếu đến thể hiện của lớp A

sealed class A {
	public int P {set, get;}
	public int Q {set, get;}
	public int Sum() { return P + Q; }
}
class B {
	private A _a;
	public B(A a) { _a = a; }
	public int Product() { return a.P * b.Q; }
}

Cách dùng:

var a = new A { P = 2, Q = 3 };
Console.WriteLine("Sum = " + a.Sum());
var b = new B(a);
Console.WriteLine("Product = " + b.Product());

Giải pháp trong .Net

Cả bốn phương pháp trên đều có nhược điểm của nó, từ .NET 3.0 trở đi cung cấp cho ta một cách thức thêm phương thức vào các lớp có sẵn tương đối hay và tiện dụng.
Tôi sẽ lấy ví dụ trường hợp muốn thêm phương thức WordCount cho lớp String ở trên, tôi sẽ tạo một static class dạng như StringHelper ở trên nhưng việc gọi hàm sẽ dùng thể hiện (instance) chứ không dùng static class:

public static class StringExtension
{
	public static int WordCount(this String s)
	{
		return s.Split(new char[] {' ', '.', '?', ',', '\r', '\n'}, StringSplitOptions.RemoveEmptyEntries).Length;
	}
}

class Program
{
	static void Main(string[] args)
	{
		var s = "Hello the world";
		Console.Write("WordCount =" + s.WordCount());
	}
}

Điều khác biệt so với HelperClass là class được khai báo là static và tham số s đươc khai báo với thêm từ khóa this, khi đó phương thức WordCount được sử dụng với thể hiện s của lớp String: s.WordCount()

Nếu các bạn nào đã dùng LINQ và để ý thì khi ta using System.Linq thì có thể dùng rất nhiều chức năng do LINQ cung cấp như OrderBy, Aggregate, Distinct… Về thực chất thì đó cũng là Extension Methods.

Quay trở lại với cách gọi hàm nếu dùng Helper class thì sẽ là:

HelperClass.Operation2(HelperClass.Operation1(x, arg1), arg2)

Với Extension Methods sẽ là:

x.Operation1(arg1).Operation2(arg2)

nhìn gọn và đẹp hơn nhiều :)

Xung đột tên

Việc định nghĩa các phương thức mở rộng như ở trên có thể dẫn đến việc xung đột tên giữa phương thức mở rộng và phương thức được xây dựng sẵn trong lớp. Khi đó trình biên dịch và IDE sẽ có cảnh báo (không phải lỗi) và khi chạy thì sẽ ưu tiên thực thi phương thức được xây dựng sẵn trong lớp.

class Alerter {
	public string GetName() { return "SUCO"; }
}

static class AlerterExtension {
	public static string GetName(this Alerter a) { return "suco"; }
}

class Program {
	static void Main(string[] args) {
		var a = new Alerter();
		Console.Write(a.GetName());
	}
}

Kết quả nhận được sẽ là: SUCO

Giới hạn và nhược điểm

  • Extension method chỉ áp dụng trên method, đúng như tên gọi của nó. Nó không áp dụng cho: Property, Indexer, Operator và Constructor.
  • Không áp dụng được cho các thể hiện dạng Value type như struct, enum…
  • Khi đã using một namespace là bạn sẽ import toàn bộ các phương thức mở rộng được định nghĩa trong namespace đó. Không có cách nào (tôi chưa biết) để chỉ import riêng rẽ những phương thức mong muốn
  • Không chạy được trong biểu thức lambda
  • Không tạo được override (do bản chất là hàm static)
  • Có thể gây nhầm lẫn trong trường hợp bị xung đột tên
Khi trích dẫn bài viết từ tek.eten.vn, xin vui lòng ghi rõ nguồn. Chúng tôi sẽ rất cảm ơn bạn!