Delegate
Delegate có thể được xem như là kiểu đối tượng mới trong C#, mà có mơt số điểm quen
thuộc với lớp.chúng tồn tại trong tình huống mà ta muốn truyền phương thức xung quanh
những phương thức khác.để minh hoạ ta xem dòng mã sau:
int i = int.Parse("99");
Chúng ta quen với việc truyền dữ liệu đến một phương thức như là thơng số,vì vậy ý
tường truyền phương thức như là thơng số nghe có vẻ hơi lạ đối với chúng ta.tuy nhiên có
trường hợp mà ta có 1 phương thức mà làm 1 điều gì đó, nhiều hơn là xử lí dữ liệu,
phương thức đó có thể cần làm điều gì đó mà liên quan đến việc thực thi phương thức
khác.phức tạp hơn, bạn khơng biết vào lúc nào thì phương thức thứ hai sẽ được biên dịch.
thơng tin đó chỉ biết vào lúc chạy , và chính vì lí do đó mà phương thức 2 sẽ cần truyền
vào như là thơng số cho phương thức đầu tiên.điều này nghe có vẻ hơi khó hiểu,nhưng nó
sẽ được làm rõ hơn trong 1 vài ví dụ sau:
Luồng bắt đầu: C# có thể bảo máy tính bắt đầu một chuỗi thực thi mới song song với
việc thực thi đương thời.1 chuỗi liên tiếp này gọi là luồng,và việc bắt đầu này được làm
bằng cách dùng phương thức, Start() trên 1 thể hiện của lớp cơ
sở.System.Threading.Thread. ( chi tiết hơn về luồng ở chương 5).khi chương trình bắt
đầu chạy,nơi nó bắt đầu là main(). tương tự như vậy khi bạn muốn máy tính chạy một
chuỗi thực thi thì bạn phải báo cho máy tính biết bắt đầu chạy là ở đâu. bạn phải cung cấp
cho nó chi tiết của phương thức mà việc thực thi có thể bắt đầu.-nói cách khác , phương
thức Thread.Start() phải lấy 1 thông số mà định nghĩa phương thức được thi hành bởi
luồng.
Lớp thư viện chung . khi 1 nhiệm vụ chứa đựng nhiệm vụ con mà mã của các nhiệm vụ
con này được viết trong các thư viện chỉ có sử dụng thư viện mới biết nó làm gì.ví dụ ,
chúng ta muốn viết một lớp chứa một mảng đối tuợng và sắp nó tăng dần. 1 phần công
việc được lặp lại là lấy 2 đối tượng trong lớp so sánh với nhau để xem đối tượng nào
đứng truớc.nếu ta muốn lớp có khả năng sắp xếp bất kì đối tượng nào, khơng có cách nào
có thể làm được việc so sánh trên .mã client dùng mảng đối tượng của ta sẽ bảo cho ta
biết cách so sánh cụ thể đối tượng mà nó muốn sắp xếp.nói cách khác , mã client sẽ phải
truyền cho lớp của ta phương thức thích hợp mà có thể được gọi, để làm việc so sánh.
Nguyên tắc chung là: mã của ta sẽ cần thông báo cho thời gian chạy .NET biết phương
thức nào xử lí tình huống nào.
Vì thế chúng ta phải thiết lập những nguyên tắc mà đôi lúc , những phương thức cần lấy
chi tiết của phương thức khác như là thông số.kế tiếp chúng ta sẽ minh họa cách làm điều
đó.cách đơn giản nhất là truyền tên của phương thức như là thông số.giả sử chúng ta
muốn bắt đầu một luồng mới, và chúng ta có phương thức được gọi là entrypoint(), mà ta
muốn luồng bắt đầu chạy từ đó:
void EntryPoint()
{
// làm những gì luồng mới cần làm
}
Có thể chúng ta bắt đầu luồng mới với một đoạn mã :
Thread NewThread = new Thread();
Thread.Start(EntryPoint); // sai
Thật sự đây là cách đơn giản nhất. trong một vài ngôn ngữ dùng cách này như c và c++ (
trong c và c++ thông số entrypoint là con trỏ hàm)
Không may, cách thực thi trực tiếp này gây ra một số vấn đề về an tồn kiểu.nhớ rằng ta
đang lập trình hướng đơí tượng, phương thức hiếm khi nào tồn tại độc lập , mà thường là
phải kết hợp với phương thức khác trưóc khi được gọi.vì vậy .NET khơng cho làm điều
này.thay vào đó nếu ta muốn truyền phương thức ta phải gói chi tiết của phương thức
trong một loại đối tượng mới là 1 delegate. delegate đơn giản là một kiểu đối tượng đặc
biệt- đặc biệt ở chổ ,trong khi tất cả đối tượng chúng ta định nghĩa trước đây chứa đựng
dữ liệu , thì delegate chứa đựng chi tiết của phương thức.
Dùng delegate trong C#
Đầu tiên ta phải định nghĩa delegate mà ta muốn dùng ,nghĩa là bảo cho trình biên dịch
biết loại phương thức mà delegate sẽ trình bày.sau đó ta tạo ra các thể hiện của delegate.
Cú pháp
delegate void VoidOperation(uint x);
Ta chỉ định mỗi thể hiện của delegate có thể giữ một tham chiếu đến 1 phương thức mà
chứa một thông số uint và trả vầ kiểu void.
Ví dụ khác : nếu bạn muốn định nghĩa 1 delegate gọi là twolongsOp mà trình bày 1 hàm
có 2 thông số kiểu long và trả về kiểu double. ta có thể viết :
delegate double TwoLongsOp(long first, long second);
Hay 1 delegate trình bày phương thức khơng nhận thơng số và trả về kiểu string
delegate string GetAString();
Cú pháp cũng giống như phương thức , ngoại trừ việc khơng có phần thân của phương
thức,và bắt đầu với delegate.ta cũng có thể áp dụng các cách thức truy nhập thông thường
trên một định nghĩa delegate - public,private,protected ...
public delegate string GetAString();
Mỗi lần ta định nghĩa một delegate chúng ta có thể tạo ra một thể hiện của nó mà ta có
thể dùng đề lưu trữ các chi tiết của 1 phưong thức cụ thể.
Lưu ý : với lớp ta có 2 thuật ngữ riêng biệt : lớp để chỉ định nghĩa chung , đối tượng để
chỉ một thể hiện của 1 lớp, tuy nhiên đối với delegate ta chỉ có một thuật ngữ là '1
delegate' khi tạo ra một thể hiện của delegate ta cũng gọi nó là delegate. vì vậy cần xem
xét ngữ cảnh để phân biệt.
Đoạn mã sau minh hoạ cho 1 delegate:
private delegate string GetAString();
static void Main(string[] args)
{
int x = 40;
GetAString firstStringMethod = new GetAString(x.ToString);
Console.WriteLine("String is" + firstStringMethod());
// With firstStringMethod initialized to x.ToString(),
// the above statement is equivalent to saying
// Console.WriteLine("String is" + x.ToString());
}
Trong mã này , ta tạo ra delegate GetAString, và khởi tạo nó để nó tham khảo đến
phương thức ToString() của một biến nguyên x .chúng ta sẽ biên dịch lỗi nếu cố gắng
khởi tạo FirstStringMethod với bất kì phương thức nào có thơng số vào và kiểu trả về là
chuỗi.
1 đặc tính của delegate là an toàn- kiểu ( type-safe) để thấy rằng chúng phải đảm bảo dấu
ấn ( signature) của phương thức được gọi là đúng.tuy nhiên 1 điều thú vị là, chúng không
quan tâm kiểu của đối tượng phương thức là gì khi gọi hoặc thậm chí liệu rằng phương
thức đó là static hay là một phưong thức thể hiện.
Để thấy điều này ta mở rộng đoạn mã trên, dùng delegate FirstStringMethod để gọi các
phương thức khác trên những đối tượng khác - 1 phương thức thể hiện và 1 phương thức
tĩnh .ta cũng dùng lại cấu trúc currency, và cấu trúc currency đã có overload riêng của nó
cho phương thức ToString().để xem xét delegate với phương thức tĩnh ta thêm 1 phương
thức tĩnh với cùng dấu ấn như currency:
struct Currency
{
public static string GetCurrencyUnit()
{
return "Dollar";
}
Bây giờ ta sử dụng thể hiện GetAString như sau:
private delegate string GetAString();
static void Main(string[] args)
{
int x = 40;
GetAString firstStringMethod = new GetAString(x.ToString);
Console.WriteLine("String is " + firstStringMethod());
Currency balance = new Currency(34, 50);
firstStringMethod = new GetAString(balance.ToString);
Console.WriteLine("String is " + firstStringMethod());
firstStringMethod = new GetAString(Currency.GetCurrencyUnit);
Console.WriteLine("String is " + firstStringMethod());
Đoạn mã này chỉ cho ta biết làm thế nào để gọi 1 phương thức qua trung gian là
delegate,đăng kí lại delegate để tham chiếu đến một phương thức khác trên 1 thể hiện
khác của lớp.
Tuy nhiên ta vẫn chưa nắm rõ được quy trình truyền 1 delegate đến 1 phương thức khác,
cũng như chưa thấy được lợi ích của delegate qua ví dụ trên. như ta có thể gọi trực tiếp
ToString() từ int hay currency mà không cần delegate.ta cần những ví dụ phức tạp hơn để
hiểu rõ delegate. ta sẽ trình bày 2 ví dụ : ví dụ 1 đơn giản sử dụng delegate để gọi vào
thao tác khác..nó chỉ rõ làm thế nào để truyền delegate đến phương thức và cách sử dụng
mảng trong delegate . ví dụ 2 phức tạp hơn là lớp BubbleSorter, mà thực thi 1 phương
thức sắp xếp mảng đối tượng tăng dần. lớp này sẽ rất khó viết nếu khơng có delegate.
Ví dụ SimpleDelegate
Trong ví dụ này ta sẽ tạo lớp MathOperations mà có vài phương thức static để thực thi 2
thao tác trên kiểu double, sau đó ta dùng delegate để gọi những phương thức này.lớp như
sau:
class MathsOperations
{
public static double MultiplyByTwo(double value)
{
return value*2;
}
public static double Square(double value)
{
return value*value;
}
}
Sau đó ta gọi phương thức này như sau:
using System;
namespace Wrox.ProCSharp.AdvancedCSharp
{
delegate double DoubleOp(double x);
class MainEntryPoint
{
static void Main()
{
DoubleOp [] operations =
{
new DoubleOp(MathsOperations.MultiplyByTwo),
new DoubleOp(MathsOperations.Square)
};
for (int i=0 ; i
{
Console.WriteLine("Using operations[{0}]:", i);
ProcessAndDisplayNumber(operations[i], 2.0);
ProcessAndDisplayNumber(operations[i], 7.94);
ProcessAndDisplayNumber(operations[i], 1.414);
Console.WriteLine();
}
}
static void ProcessAndDisplayNumber(DoubleOp action, double value)
{
double result = action(value);
Console.WriteLine(
"Value is {0}, result of operation is {1}", value, result);
}
Trong đoạn mã này ta khởi tạo 1 mảng delegate doubleOp.mỗi phần tử của mảng được
khởi động để tham chiếu đến 1 thao tác khác được thực thi bởi lớp MathOperations.sau
đó , nó lặp xuyên suốt mảng,ứng dụng mỗi thao tác đến 3 kiểu giá trị khác nhau.điều này
minh họa cách sử dụng delegate- là có thể nhóm những phương thức lại với nhau thành
mảng để sử dụng, để ta có thể gọi một vài phương thức trong vịng lặp.
Chỗ quan trọng trong đoạn mã là chỗ ta truyền 1 delegate vào phương thức
ProcessAndDisplayNumber(), ví dụ:
ProcessAndDisplayNumber(operations[i], 2.0);
Ở đây ta truyền tên của delegate,nhưng khơng có thơng số nào.cho rằng operation[i] là 1
delegate :
operation[i] nghĩa là 'delegate',nói cách khác là phương thức đại diện cho delegate
operation[i](2.0) nghĩa là ' gọi thực sự phương thức này, truyền giá trị vào trong ngoặc'.
Phương thức ProcessAndDisplayNumber() được định nghĩa để lấy 1 delegate như là
thơng số đầu tiên của nó :
static void ProcessAndDisplayNumber(DoubleOp action, double value)
Sau đó khi ở trong phương thức này , ta gọi:
double result = action(value);
Thể hiện delegate action được gọi và kết quả trả về được lưu trữ trong result
chạy ví dụ ta có:
SimpleDelegate
Using operations[0]:
Value is 2, result of operation is 4
Value is 7.94, result of operation is 15.88
Value is 1.414, result of operation is 2.828
Using operations[1]:
Value is 2, result of operation is 4
Value is 7.94, result of operation is 63.0436
Value is 1.414, result of operation is 1.999396
Ví dụ BubleSorter
Sau đây ta sẽ xem 1 ví dụ cho thấy sự hữu ích của delegate. ta sẽ tạo lớp bublesorter. lớp
này thực thi 1 phương thức tĩnh,Sort(), lấy thông số đầu là 1 mảng đối tượng, và sắp xếp
lại chúng tăng dần.ví dụ để sắp xếp 1 mảng số ngun bằng thuật tốn Bubble sort :
///đây khơng phải là 1 phần của ví dụ
for (int i = 0; i < sortArray.Length; i++)
{
for (int j = i + 1; j < sortArray.Length; j++)
{
if (sortArray[j] < sortArray[i]) // problem with this test
{
int temp = sortArray[i]; // swap ith and jth entries
sortArray[i] = sortArray[j];
sortArray[j] = temp;
}
}
}
Thuật toán này tốt cho số nguyên, nhưng ta muốn phương thức sort() sắp xếp cho mọi đối
tượng,ta thấy vấn đề nằm ở dòng if(sortArray[j] < sortArray[i]) trong đoạn mã trên.bởi ta
muốn so sánh 2 đối tượng trên mảng mà cái nào là lớn hơn.chúng ta có thể sắp xếp kiểu
int, nhưng làm thế nào để sắp xếp những lớp chưa biết hoặc không xác định cho đến lúc
chạy.câu trả lời là mã client, mà biết về lớp muốn sắp xếp, phải truyền 1 delegate gói
trong một phương thức sẽ làm cơng việc so sánh
Định nghĩa delegate như sau:
delegate bool CompareOp(object lhs, object rhs);
Và xây dựng phương thức sort() là :
static public void Sort(object [] sortArray, CompareOp gtMethod)
Phần hướng dẫn cho phương thức này sẽ nói rõ rằng gtmethod phải tham chiếu đến 1
phương thức static có 2 đối số,và trả về true nếu giá trị của đối số thứ 2 là 'lớn hơn' (
nghĩa là năm sau trong mảng) đối số thứ nhất.
mặc dù ta có thể sử dụng delegate ở đây,nhưng cũng có thể giải quyết vấn đề bằng cách
sử dụng interface. .NET xây dựng 1 interface IComparer cho mục đích này. tuy nhiên , ta
sử dụng delegate vì loại vấn đề này thì thường có khuynh hướng dùng delegate.
Sau đây là lớp bublesorter :
class BubbleSorter
{
static public void Sort(object [] sortArray, CompareOp gtMethod)
{
for (int i=0 ; i
{
for (int j=i+1 ; j
{
if (gtMethod(sortArray[j], sortArray[i]))
{
object temp = sortArray[i];
sortArray[i] = sortArray[j];
sortArray[j] = temp;
}
}
}
}
}
Để dùng lớp này ta cần định nghĩa 1 số lớp khác mà có thể dùng thiết lập mảng cần sắp
xếp.ví dụ , cơng ty điện thoại có danh sách tên khách hàng, và muốn sắp danh sách theo
lương.mỗi nhân viên trình bày bởi thể hiện của một lớp , Employee:
class Employee
{
private string name;
private decimal salary;
public Employee(string name, decimal salary)
{
this.name = name;
this.salary = salary;
}
public override string ToString()
{
return string.Format(name + ", {0:C}", salary);
}
public static bool RhsIsGreater(object lhs, object rhs)
{
Employee empLhs = (Employee) lhs;
Employee empRhs = (Employee) rhs;
return (empRhs.salary > empLhs.salary) ? true : false;
}
}
Lưu ý để phù hợp với dấu ấn của delegate CompareOp, chúng ta phải định nghĩa
RhsIsGreater trong lớp này lấy 2 đối tượng để tham khảo,hơn là tham khảo employee như
là thơng số.điều này có nghĩa là ta phải ép kiểu những thông số vào trong tham khảo
employee để thực thi việc so sánh.
Bây giờ ta viết mã yêu cầu sắp xếp :
using System;
namespace Wrox.ProCSharp.AdvancedCSharp
{
delegate bool CompareOp(object lhs, object rhs);
class MainEntryPoint
{
static void Main()
{
Employee [] employees =
{
new Employee("Karli Watson", 20000),
new Employee("Bill Gates", 10000),
new Employee("Simon Robinson", 25000),
new Employee("Mortimer", (decimal)1000000.38),
new Employee("Arabel Jones", 23000),
new Employee("Avon from 'Blake's 7'", 50000)};
CompareOp employeeCompareOp = new
CompareOp(Employee.RhsIsGreater);
BubbleSorter.Sort(employees, employeeCompareOp);
for (int i=0 ; i
Console.WriteLine(employees[i].ToString());
}
}
Chạy mã này sẽ thấy employees được sắp xếp theo lương
BubbleSorter
Bill Gates, £10,000.00
Karli Watson, £20,000.00
Arabel Jones, £23,000.00
Simon Robinson, £25,000.00
Avon from 'Blake's 7', £50,000.00
Mortimer, £1,000,000.38
Multicast delegate
Đến lúc này mỗi delegate mà chúng ta sử dụng chỉ gói ghém trong 1 phương thức đơn
gọi.gọi delegate nào thì dùng phương thức đó.nếu ta muốn gọi nhiều hơn 1 phương thức,
ta cần tạo một lời gọi tường minh xuyên suốt delegate nhiều hơn một lần.tuy nhiên, 1
delegate có thể gói ghém nhiều hơn 1 phương thức. 1 delegate như vậy gọi là multicast
delegate. nếu 1 multicast delegate được gọi, nó sẽ gọi liên tiếp những phương thức theo
thứ tự.để làm điều này, delegate phải trả về là void.nếu ta dùng một delegate có kiểu trả
về là void , trình biên dịch sẽ coi như đây là một multicast delegate.xem ví dụ sau , dù cú
pháp giống như trước đây nhưng nó thực sự là một multicast delegate, operations,
delegate void DoubleOp(double value);
// delegate double DoubleOp(double value); // can't do this now
class MainEntryPoint
{
static void Main()
{
DoubleOp operations = new DoubleOp(MathOperations.MultiplyByTwo);
operations += new DoubleOp(MathOperations.Square);
Trong ví dụ trên muốn tham khảo đến 2 phương thức ta dùng mảng delegate. ở đây , đơn
giản ta chỉ thêm 2 thao tác này vào trong cùng một multicast delegate.multiccast delegate
nhận toán tử + và +=. nếu ta muốn , ta có thể mở rộng 2 dịng mã trên , có cùng cách tác
động :
DoubleOp operation1 = new DoubleOp(MathOperations.MultiplyByTwo);
DoubleOp operation2 = new DoubleOp(MathOperations.Square);
DoubleOp operations = operation1 + operation2;
multicast delegate cũng biết toán tử - và -= để bỏ đi phương thức được gọi từ delegate.
một muticast delegate là một lớp được dẫn xuất từ System.MulticastDelegate mà lại được
dẫn xuất từ System.Delegate. System.MulticastDelegate có thêm những thành phần để
cho phép nối những phương thức gọi cùng với nhau vào một danh sách.
Minh hoạ cho sử dụng multicast delegate ta sử dụng lại ví dụ simpleDelegate biến nó
thành một ví dụ mới MulticastDelegate. bởi vì ta cần delegate trả về kiểu void , ta phải
viết lại những phương thức trong lớp Mathoperations ,chúng sẽ trình bày kết quả thay vì
trả về :
class MathOperations
{
public static void MultiplyByTwo(double value)
{
double result = value*2;
Console.WriteLine(
"Multiplying by 2: {0} gives {1}", value, result);
}
public static void Square(double value)
{
double result = value*value;
Console.WriteLine("Squaring: {0} gives {1}", value, result);
}
}
Để dàn xếp sự thay đổi này , ta viết lại ProcessAndDisplayNumber:
static void ProcessAndDisplayNumber(DoubleOp action, double value)
{
Console.WriteLine("\nProcessAndDisplayNumber called with value = " +
value);
action(value);
}
Bây giờ thử multicast delegate ta vừa tạo :
static void Main()
{
DoubleOp operations = new DoubleOp(MathOperations.MultiplyByTwo);
operations += new DoubleOp(MathOperations.Square);
ProcessAndDisplayNumber(operations, 2.0);
ProcessAndDisplayNumber(operations, 7.94);
ProcessAndDisplayNumber(operations, 1.414);
Console.WriteLine();
}
Bây giờ mỗi lần ProcessAndDisplayNumber được gọi ,nó sẽ trình bày 1 thơng điệp để
báo rằng nó được gọi câu lệnh sau:action(value);
Sẽ làm cho mỗi phương thức gọi trong thể hiện delegate action được gọi liên tiếp nhau.
Kết quả :
MulticastDelegate
ProcessAndDisplayNumber called with value = 2
Multiplying by 2: 2 gives 4
Squaring: 2 gives 4
ProcessAndDisplayNumber called with value = 7.94
Multiplying by 2: 7.94 gives 15.88
Squaring: 7.94 gives 63.0436
ProcessAndDisplayNumber called with value = 1.414
Multiplying by 2: 1.414 gives 2.828
Squaring: 1.414 gives 1.999396
Nếu dùng multicast delegate , ta nên nhận thức đến thứ tự phương thức được nối với nhau
trong cùng một delegate sẽ được gọi là khơng xác định. do đó ta nên tránh viết mã mà
những phương thức được gọi liên hệ với nhau theo một thứ tự cụ thể.
Code for Download:
SimpleDelegate
BubleSorter
MultiCastDelegate