Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (2.59 MB, 226 trang )
tác thành viên của một trong những lớp thích hợp. việc ép kiểu phải được đánh dấu là implicit hoặc explicit để chỉ định cách mà
bạn muốn sử dụng với nó. cũng giống như việc ép kiểu cơ bản : nếu bạn biết việc ép kiểu là an toàn ,dù là bất cứ giá trị nào đựợc
giữ bởi biến nguồn, thì bạn định nghĩa nó như là implicit.ngược lại nếu bạn biết việc ép kiểu có thể đi đến sự liều lĩnh - mất dữ
liệu hay một biệt lệ sẽ bị tung ra - bạn nên định nghĩa ép kiểu như là explicit.
Bạn nên định nghĩa bất kỳ kiểu ép kiểu mà bạn viết là tường minh nếu có bất kì giá trị dữ liệu nguồn nào mà việc ép kiểu có khả
năng thất bại, hoặc nếu có sự mạo hiểm do một biệt lệ được tung ra.
Cú pháp của việc định nghĩa ép kiểu cũng giống như việc overload thao tác . không phải ngẫu nhiên mà ta nói thế , bởi vì theo
cách mà ép kiểu được xem như là thao tác là tác động của nó là chuyển từ kiểu dữ liệu nguồn sang kiểu dữ liệu đích. để minh hoạ
cho cú pháp này, cú pháp sau được lấy từ ví dụ mà sẽ được giới thiệu sau đây trong phần này:
public static implicit operator float (Currency value)
{
// xử lí
}
Đoạn mã này là một phần của cấu trúc - currency - được dùng để lưu trữ tiền.ép kiểu được định nghĩa ở đây cho phép chúng ta
chuyển đổi 1 cách ẩn dụ giá trị của 1 kiểu tiền tệ sang 1 số thực ( float). chú ý rằng nếu việc chuyển được khai báo như là
implicit, thì trình biên dịch cho phép nó sử dụng cả implicit và explicit. nếu nó được khai báo như là explicit , thì trình biên dịch
chỉ cho phép nó sử dụng như là explicit.
Trong khai báo này việc ép kiểu được khai báo là static. giống như các thao tác được overload , C# đòi hỏi việc ép kiểu là static.
điều này có nghĩa là mỗi ép kiểu cũng lấy một thông số , mà là kiểu dữ liệu trong nguồn
Thực hành ép kiểu dữ liệu do người sử dụng định nghĩa.
Trong phần này, chúng ta sẽ xem xét việc ép kiểu implicit và explicit của kiểu dữ liệu này trong ví dụ Simplecurrency. trong ví
dụ này chúng ta định nghĩa 1 cấu trúc struct, currency, mà giữ tiền USA. thông thường, C# cung cấp kiểu thập phân ( decimal)
cho mục đích này, nhưng bạn vẫn có thể viết riêng 1 cấu trúc struct hay một lớp để trình bày giá trị tiền nếu bạn muốn biểu diễn
quy trình tài chính phức tạp và do đó muốn có một phương thức cụ thể để thực thi như là một lớp.
cấu trúc của ép kiểu là giống nhau cho struct hay lớp . trong ví dụ này là struct, nhưng nó cũng làm việc tốt nếu bạn khai báo
currency như là một lớp.
khởi đầu , định nghĩa cấu trúc currency như sau:
struct Currency
{
public uint Dollars;
public ushort Cents;
public Currency(uint dollars, ushort cents)
{
this.Dollars = dollars;
this.Cents = cents;
}
Việc dùng kiểu dữ liệu không dấu cho trường Dollar và cent bảo đảm rằng một thể hiện của currency chỉ giữ 1 số dương.chúng ta
giới hạn nó bằng cách này để có thể minh hoạ một số điểm về tường minh sau này.để giữ cho lớp đơn giản,ta chọn các trường là
public, nhưng nói chung bạn sẽ phải định nghĩa chúng private, và định nghĩa những thuộc tính đáp ứng cho dollar và cent
Chúng ta hãy bắt đầu bằng cách giả sử như là bạn muốn chuyển giá trị từ currency sang float, mà phần nguyên của kiểu float sẽ
trình bày dollar:
Currency balance = new Currency(10,50);
float f = balance; // ta muốn f được đặt là 10.5
Để cho phép làm điều này , cần định nghĩa 1 ép kiểu. từ đây ta thêm vào trong cấu trúc currency:
public static implicit operator float (Currency value)
{
return value.Dollars + (value.Cents/100.0f);
}
Ép kiểu này là implicit, Đây là sự chọn lựa dễ nhận thấy , bởi vì , nó nên rõ ràng từ định nghĩa trong currency, bất kì giá trị nào
lưu trữ trong currency cũng có thể lưu trong kiểu float.
Nếu chuyển ngược thì sao? từ một số float sang currency .trong trường hợp này việc chuyển đổi có thể không làm việc ,nếu float
lưu trữ số âm,còn currency thì không , và số này sẽ lưu trữ phần làm tròn vào trong trường dollar của currency.nếu float chứa
đựng một giá trị không thích hợp việc chuyển nó sẽ gây ra một kết quả không dự đoán truớc. do đó việc chuyển đổi này nên được
khai báo là explicit. sau đây là đoạn mã thử đầu tiên , tuy nhiên nó không gửi kết quả hoàn toàn đúng:
public static explicit operator Currency (float value)
{
uint dollars = (uint)value;
ushort cents = (ushort)((value-dollars)*100);
return new Currency(dollars, cents);
}
Đoạn mã sau sẽ dịch đúng :
float amount = 45.63f;
Currency amount2 = (Currency)amount;
Tuy nhiên đoạn mã sau sẽ báo lỗi bởi vì nó sử dụng một ép kiểu tường mình một cách không rõ ràng :
float amount = 45.63f;
Currency amount2 = amount; // sai
Sau đây là phương thức main() mà khởi tạo một struct Currency, và thực hiện một vài việc chuyển đổi. vào đầu đoạn mã, chúng
ta viết giá trị của biến balance theo 2 cách ( để minh họa 1 số điều cho phần sau)
static void Main()
{
try
{
Currency balance = new Currency(50,35);
Console.WriteLine(balance);
Console.WriteLine("balance is " + balance);
Console.WriteLine("balance is (using ToString()) " +
balance.ToString());
float balance2= balance;
Console.WriteLine("After converting to float, = " + balance2);
balance = (Currency) balance2;
Console.WriteLine("After converting back to Currency, = " + balance);
Console.WriteLine("Now attempt to convert out of range value of " +
"-$100.00 to a Currency:");
checked
{
balance = (Currency) (-50.5);
Console.WriteLine("Result is " + balance.ToString());
}
}
}
catch(Exception e)
{
Console.WriteLine("Exception occurred: " + e.Message);
}
Chú ý rằng ta đặt toàn bộ đoạn mã trong khối try để bắt bất cứ biệt lệ nào xảy ra trong quá trình ép kiểu. Sau khi chạy ta có kết
quả sau :
SimpleCurrency
50.35
Balance is $50.35
Balance is (using ToString()) $50.35
After converting to float, = 50.35
After converting back to Currency, = $50.34
Now attempt to convert out of range value of -$100.00 to a Currency:
Result is $4294967246.60486
Kết quả cho thấy đoạn mã không làm việc như mong đợi. ở phần đầu việc chuyển cho kết quả sai là 50.34 thay vì 50.35. trong
phần hai, không có biệt lệ nào được sinh ra khi ta cố chuyển một giá trị nằm ngoài vùng.
Lỗi dầu tiên là do làm tròn.. nếu ép kiểu từ float sang uint , máy tính sẽ cắt bỏ số hơn là làm tròn nó.máy tính lưu trữ số dạng nhị
phân hơn là thập phân.và phần dư 0.35 không thể được trình bày một cách chính xác như là phần dư dạng nhị phân.do đó máy
tính lưu trữ một số nhỏ hơn 0.35, mà có thể trình bày chính xác trong dạng nhị phân.nhân cho 100 và lấy phần dư nhỏ hơn 35 cắt
thành 34 cent.rõ ràng trong hoàn cảnh này, lỗi cắt bỏ là nghiêm trọng.để tránh chúng thì chắc rằng một sự làm tròn thông minh
phải được thi hành trong việc chuyển đổi số.thật may mắn Microsoft đã viết một lớp để làm điều đó : System.Convert .
System.Convert chứa đựng một số lượng lớn những phương thức static biểu diễn việc chuyển đổi số, và Phương thức mà chúng
ta muốn là System.convert.Uint6().
Bây giờ chúng ta sẽ kiểm tra xem tại sao biệt lệ tràn đã không xuất hiện . vấn đề ở đây là : nơi mà việc tràn xuất hiện không thực
sự nằm trong hàm main()- nó ở bên trong mã của thao tác ép kiểu. mà được gọi từ phương thức main() . và chúng ta đã không
kiểm tra đoạn mã đó.
Giải pháp ở đây là cho phép kiểm tra ngay trong hàm ép kiểu
public static explicit operator Currency (float value)
{
checked
{
uint dollars = (uint)value;
ushort cents = Convert.ToUInt16((value-dollars)*100);
return new Currency(dollars, cents);
}
}
Chú ý rằng ta sử dụng convert.uint16() để tính phần xu thay cho đoạn mã trên.ta không cần dùng cách này để tính phần dollar vì
việc cắt bỏ trong giá trị float đã đưa ra kết quả ta cần
Ép kiểu giữa những lớp
Ví dụ trên cho ta thấy việc ép kiểu giữa 2 kiểu dữ liệu đã được định nghĩa trước.tuy nhiên ta cũng có thể ép kiểu giữa 2 cấu trúc
hoặc lớp mà ta định nghĩa. có 2 hạn chế cần quan tâm :
- Ta không thể định nghĩa một ép kiểu nếu một trong những lớp được dẫn xuất từ 1 lớp khác.
- Ép kiểu phải được định nghĩa bên trong việc định kiểu dữ liệu nguồn hay đích.
Để minh hoạ những yêu cầu này , giả sử rằng ta có biểu đồ lớp sau:
Nói cách khác,lớp c và d được dẫn xuất gián tiếp từ lớp a.trong trường hợp này,chỉ có những ép kiểu riêng giữa a,b,c,d mà hợp
pháp sẽ được chuyển là những lớp c và d bởi vì những những lớp này không được dẫn xuất từ mỗi lớp khác.mã có thể như sau :
public static explicit operator D(C value)
{
// and so on
}
public static explicit operator C(D value)
{
// and so on
}
Cho mỗi kiểu ép kiểu này , ta có quyền chọn nơi mà ta đặt định nghĩa- bên trong lớp định nghĩa C hoặc bên trong lớp định nghĩa
D, nhưng không nằm ở bất cứ chổ nào khác.C# đòi hỏi bạn đặt định nghĩa của 1 ép kiểu bên trong lớp ( hoặc cấu trúc)nguồn hoặc
bên trong lớp ( hoặc cấu trúc) đích
Mỗi lần bạn định nghĩa một ép kiểu bên trong 1 lớp , bạn không thể định nghĩa giống như vậy bên trong những lớp khác.rõ ràng,
chỉ nên có 1 hàm ép kiểu cho mỗi chuyển đổi. nếu không trình biên dịch không biết sử dụng cái nào.
Ép kiểu giữa lớp dẫn xuất và lớp cơ sở
Để xem làm thế nào việc ép kiểu này làm, ta xem xét 2 lớp Mybase và Myderived , trong đó Mydrived được dẫn xuất trực tiếp
hoặc gián tiếp từ lớp cơ sở
đầu tiên từ lớp Myderived đến Mybase ; luôn luôn ( giả sử hàm dựng có giá trị)có thể viết :
MyDerived derivedObject = new MyDerived();
MyBase baseCopy = derivedObject;
Trong trường hợp này,chúng ta ép kiểu không tường minh từ myderived đến mybase. điều này làm việc bởi vì luật là bất kì tham
chiếu đến 1 kiểu mybase được cho phép để chuyển thành đối tượng của lớp mybase hoặc đến đối tượng bất kì được dẫn xuất từ
lớp mybase.trong ngôn ngữ lập trình hướng đối tượng, thể hiện của lớp dẫn xuất là thể hiện của một lớp cơ sở cộng thêm với một
thứ gì đó thêm. tất cả chức năng và thuộc tính được định nghĩa trong lớp cơ sở cũng được định nghĩa trong lớp dẫn xuất .
Bây giờ ta có thể viết
MyBase derivedObject = new MyDerived();
MyBase baseObject = new MyBase();
MyDerived derivedCopy1 = (MyDerived) derivedObject; // OK
MyDerived derivedCopy2 = (MyDerived) baseObject;
Tất cả các câu lệnh trên là hợp lệ trong C# và minh họa việc ép kiểu từ lớp cơ sở sang lớp dẫn xuất. tuy nhiên câu lệnh cuối sẽ
tung ra biệt lệ khi thực thi.
Chú ý rằng những lệnh ép kiểu mà trình biên dịch cung cấp , mà chuyển giữa lớp cơ sở và lớp dẫn xuất thì không thực sự chuyển
bất cứ dữ liệu nào trên các đối tượng.tất cả chúng làm là thiết lập một sự tham chiếu mới để quy cho một đối tượng nếu nó hợp lệ
cho việc chuyển đổi .những lệnh ép kiểu này thì rất khác trong tự nhiên từ những cái mà ta thường xuyên tự định nghĩa.ví dụ,
trong ví dụ Simplecurrency chúng ta định nghĩa việc ép kiểu là chuyển giữa 1 kiểu tiền tệ sang kiểu số thực. trong ép kiểu thựcthành-currency, chúng ta thực sự tạo một cấu trúc currency mới và khởi tạo nó với giá trị được yêu cầu .những lệnh ép kiểu tiền
định nghĩa giữa những lớp cơ s ở và lớp dẫn xuất không làm điều này.nếu ta thực sự chuyển 1 thể hiện Mybase thành một đối
tượng Myderived thực với giá trị dựa trên nội dung của thể hiện Mybase, ta sẽ không thể sử dụng cú pháp ép kiểu để làm điều
này.tuỳ chọn hợp lí nhất là thường xuyên định nghĩa 1 hàm dựng của lớp dẫn xuất mà lấy thể hiện của lớp cơ sở như là 1 thông
số và có hàm dựng này biểu diễn việc khởi tạo chính xác:
class DerivedClass : BaseClass
{
public DerivedClass(BaseClass rhs)
{
// khởi tạo đối tượng từ thể hiện Base
}
// etc.
Ép kiểu boxing và unboxing
Ví dụ với cấu trúc currency:
Currency balance = new Currency(40,0);
object baseCopy = balance;
Khi ép kiểu không tường minh trên được thực hiện nội dung của balance được sao chép vào heap trong đối tượng box và đối
tượng basecopy tham khảo đến đối tượng này.khi chúng ta định nghĩa cấu trúc currency , .net framework cung cấp không tường
minh một lớp ( ẩn) khác , một lớp currency boxed, mà chứa đựng tất cả các trường như là cấu trúc currency nhưng là kiểu tham
chiếu lưu trong heap.điều này xảy ra bất cứ khi nào chúng ta định nghĩa một kiểu dữ liệu- dù đó là struct hay kiểu liệt kê
( enum) ,và kiểu tham khảo boxed tồn tại đáp ứng đến tất cả kiểu dữ liệu nguyên thuỷ int,double,uint, và ...ta không thể truy nhập
vào các lớp này nhưng chúng sẽ làm việc bất cứ khi nào có việc ép kiểu thành đối tượng khi chúng ta ép kiểu currency thành đối
tượng một thể hiện currency boxed tạo ra và khởi tạo với tất cả các giá trị từ cấu trúc currency. trong ví dụ trên basecopy sẽ tham
khảo đến lớp currency boxed.
Ép kiểu còn được biết đến như là unboxing,dùng cho việc ép kiểu giữa những lớp kiểu tham chiếu cơ sở và kiểu tham chiếu dẫn
xuất.đó là ép kiểu tường minh, bởi vì 1 biệt lệ sẽ được tung ra nếu đối tượng được ép kiểu không ép đúng.
object derivedObject = new Currency(40,0);
object baseObject = new object();
Currency derivedCopy1 = (Currency)derivedObject; // OK
Currency derivedCopy2 = (Currency)baseObject; // Exception thrown
Khi sử dụng boxing và unboxing điều quan trọng để hiểu là cả hai tiến trình này thực sự sao chép dữ liệu vào một đối tượng
boxed hay unboxed. chính vì lí do đó, thao tác trên đối tượng hộp sẽ không tácđộng đến nội dung của kiểu dữ liệu nguyên thuỷ.
Multiple casting
Ví dụ với cấu trúc currency , giả sử trình biên dịch chạm trán với các dòng mã sau:
Currency balance = new Currency(10,50);
long amount = (long)balance;
double amountD = balance;
Đầu tiên chúng ta khởi tạo 1 thể hiện currency ,sau đó ép nó thành kiểu long.vấn đề là ta chưa định nghĩa ép kiểu cho việc này.
tuy nhiên đoạn mã nay vẫn biên dịch thành công bởi vì trình biên dịch nhận ra rằng ta đã định nghĩa ép kiểu không tường minh
để chuyển currency thành float.và nó biết cách chuyển tường minh từ float sang long. ví lí do đó, nó sẽ biên dịch đầu tiên là
chuyển balance sang float rồi từ float sang long.tương tự cho kiểu double tuy nhiên do chuyển tử float sang double không tường
minh , do đó chúng ta có thể viết lại tường minh :
Currency balance = new Currency(10,50);
long amount = (long)(float)balance;
double amountD = (double)(float)balance;
Đoạn mã sau gây ra lỗi :
Currency balance = new Currency(10,50);
long amount = balance;
Do việc chuyển từ float sang long cần tường minh.
Nếu ta không cẩn thận khi nào ta sẽ định nghĩa ép kiểu, thì trình biên dịch có thể sẽ dẫn đến một kết quả không mong đợi..Ví dụ,
giả sử một ai khác trong nhóm đang viết cấu trúc Currency,mà sẽ hửu ích nếu có khả năng chuyển 1 số uint chứa tổng số Cent
thành 1 kiểu Currency ( Cent chứ không phải Dollar bởi vì nó sẽ không làm mất đi phần thập phân của Dollar ) .vì thế việc ép
kiểu này có thể được viết như sau :
public static implicit operator Currency (uint value)
{
return new Currency(value/100u, (ushort)(value%100));
} // Don't do this!
Lưu ý chữ u sau số 100 đảm bảo rằng value/100u sẽ đuợc phiên dịch thành 1 số uint. nếu ta viết value/100 thì trình biên dịch sẽ
phiên dịch sẽ phiên dịch nó là số int chứ không phải uint.
Lý do ta không nên viết mã kiểu này là vì : tất cả ta làm trong đó là chuyển 1 uint chứa 350 thành 1 kiểu Currency và ngược trở
lại. Ta sẽ có gì sau khi thi hành mã này :
uint bal = 350;
Currency balance = bal;
uint bal2 = (uint)balance;
Câu trả lời không phải là 350 mà là 3. Ta chuyển 350 thành 1 Currency 1 cách không tường minh, trả về kết quả
Balance.Dollars=3 , Balance.Cents=50. Sau đó trình biên dịch tính toán hướng tốt nhất để chuyển trở lại.Balance được chuyển
không tường minh thành 1 kiểu float ( giá trị 3.5) và số này được chuyển tường minh thành 1 số uint với giá trị 3
Vấn đề là có 1 sự xung đột giữa cách các ép kiểu của ta dịch các số nguyên integer.các ép kiểu giữa Currency và float dịch 1 số
nguyên giá trị 1 thành 1 doolar, nhưng cách ép kiểu uint-to-Currency cuối nhất sẽ dịch giá trị này là 1 cent.Nếu ta muốn lớp của
ta dễ dàng để dùng thì ta nên chắc rằng tất cả các ép kiểu của ta cư xử hợp với nhau,theo hướng cho ra cùng 1 kết quả. Trong
trường hợp này ,giải pháp là viết lại hàm ép kiểu uint-to-Balance để nó phiên dịch 1 số nguyên integer giá trị 1 thành 1 dollar.
public static implicit operator Currency (uint value)
{
return new Currency(value, 0);
}
1 cách kiểm tra tốt là xét xem 1 chuyển đổi có cho ra cùng kết quả hay không.Lớp Currency đưa ra 1 ví dụ tốt cho kiểm tra này :
Currency balance = new Currency(50, 35);
ulong bal = (ulong) balance;
Hiện tại chỉ có 1 cách mà trình biên dịch có thể thực hiện việc chuyển đổi này: bằng cách chuyển Currency thành 1 float 1 cách
không tường minh.việc chuyển float thành ulong đòi hỏi 1 sự tường minh.
Giả sử ta thêm 1 cách ép kiểu khác,để chuyển 1 cách không tường minh từ Currency thành uint.Ta sẽ làm điều này bằng việc cập
nhật lại cấu trúc Currency bằng cách thêm vào các ép kiểu thành và từ kiểu uint. Ta xem đây là ví dụ SimpleCurrency2:
public static implicit operator Currency (uint value)
{
return new Currency(value, 0);
}
public static implicit operator uint (Currency value)
{
return value.Dollars;
}
Bây giờ trình biên dịch có 1 cách khác để chuyển từ Currency thành ulong: là chuyển từ Currency thành uint 1 cách tường minh
sau đó thành ulong 1 cách không tường minh.
để kiểm tra ví dụ SimpleCurrency2, ta sẽ thêm đoạn mã này vào phần kiểm tra trong SimpleCurrency:
try
{
Currency balance = new Currency(50,35);
Console.WriteLine(balance);
Console.WriteLine("balance is " + balance);
Console.WriteLine("balance is (using ToString()) " + balance.ToString());
uint balance3 = (uint) balance;
Console.WriteLine("Converting to uint gives " + balance3);
Chạy ví dụ ta có kết quả
SimpleCurrency2
50
balance is $50.35
balance is (using ToString()) $50.35
Converting to uint gives 50
After converting to float, = 50.35
After converting back to Currency, = $50.34
Now attempt to convert out of range value of -$100.00 to a Currency:
Exception occurred: Arithmetic operation resulted in an overflow.
Kết quả chỉ ra việc chuyển đổi thành uint thành công,ta đã mất phần cent của Currency trong việc chuyển này. Ép kểu 1 số float
âm thành Currency đã gây ra 1 biệt lệ.
Tuy nhiên kết quả cũng giải thích 1 vấn đề nữa mà ta cần nhận thức khi làm việc với ép kiểu.dòng đầu tiên của kết quả không
trình bày balance đúng,trình bày 50 thay vì $50.35.
Console.WriteLine(balance);
Console.WriteLine("balance is " + balance);
Console.WriteLine("balance is (using ToString()) " + balance.ToString());
Chỉ có 2 dòng cuối trình bày đúng Currency thành chuỗi.Vấn để ở đây là khi ta kết hợp ép kiểu với phương thức overload,ta lấy 1
nguồn khác không dự đoán trước được.Ta sẽ xem kĩ hơn vấn đề này sau.
Câu lệnh thứ 3 Console.WriteLine() gọi tường minh phương thức Currency.ToString() đảm bảo Currency được trình bày là
chuỗi.Cái thứ 2 không làm như vậy. tuy nhiên ,chuỗi " balance is" được truyền đến Console.WriteLine làm rõ hơn rằng thông số
được phiên dịch như chuỗi. Chính vì vậy Currency,ToString() sẽ được gọi không tường minh.
Phương thức Console.WriteLine đầu tiên đơn giản truyền1 cấu trúc Currency đến Console.WriteLine.Console.WriteLine có
nhiều hàm overload,nhưng không có cái nào trong chúng lấy cấu trúc Currency .Vì vậy trình biên dịch sẽ bắt đầu tìm xem nó có
thể ép Currency thành kiểu nào để làm cho nó phù hợp với overload của Console.WriteLine. khi nó xảy ra , một trong những
overload Console.WriteLine() được thiết kế để trình bày uint 1 cách nhanh chóng và hiệu quả, và nó lấy 1 uint như là 1 thông số,
và ta vừa cung cấp 1 ép kiểu chuyển Currency thành uint không tường minh.Kết quả là nó có thể trình bày.
Quả thực Console.WriteLine có overload khác lấy 1 double làm thông số và trình bày giá trị của double.nếu ta xem kĩ kết quả từ
ví dụ SimpleCurrency đầu ta sẽ sẽ thấy dòng kết quả đầu tiên sẽ trình bày Currency như là 1 số double.trong ví dụ đó không có
việc ép kiểu trực tiếp từ Currency thàng uint ,vì vậy trình biên dịch sẽ lấy Currency-to-float-to-double để làm.
Code for Download:
SimpleCurrency
SimpleCurrency2
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 toà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ố nguyên bằng thuật toá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
Các chỉ thị tiền xử lí trong C#
Bên cạnh các từ khoá thường dùng, C# có 1 số lệnh tiền xử lí .những lệnh này không bao giờ được biên dịch thành bất kì dòng
lệnh nào trong mã thực thi. thay vào đó nó có ảnh hưởng đến các khía cạnh của quy trình biên dịch. ví dụ , ta có thể dùng chỉ dẫn
tiền xử lí để ngăn trình biên dịch biên dịch một phần đoạn mã nào đó .
Chỉ thị tiền xử lí được phân biệt bằng cách bắt đầu với dấu # .
#define và #undef
#define được dùng ví dụ như :
#define DEBUG
Cho trình biên dịch biết biểu tượng với tên được đặt ( DEBUG) tồn tại. nó hơi giống như khai báo biến nhưng nó không có giá trị
- mà chỉ tồn tại.
Trái ngược với #define là #undef : bỏ định nghĩa biểu tượng : #undef DEBUG
Ta cần đặt các chỉ thị #define và #undef vào đầu tập tin nguồn trước bất kì khai báo đối tượng được biên dịch.
lưu ý : các chỉ thị tiền xử lí không có dấu ' ; ' ở cuối câu lệnh thường thì các lệnh chỉ trên 1 dòng . nếu nó thấy 1 chỉ thị tiền xử lí,
nó xem lệnh kết tiếp sẽ nằm ở dòng kế tiếp.
#if,#elif,#else,#endif
các chỉ thị này thông báo cho trình biên dịch biết liệu có biên dịch đoạn mã hay không, ví dụ :
int DoSomeWork(double x)
{
// do something
#if DEBUG
Console.WriteLine("x is " + x);
#endif
}
Dòng lệnh Console.Writeline chỉ được thực hiện khi DEBUG được định nghĩa.( bằng chỉ thị #define). khi thấy #if nó kiểm tra
xem biểu tượng có tồn tại hay không nếu có thì biên dịch đoạn mã bên trong ngược lại bỏ qua đoạn mã bên trong giữa #if và
#endif. ta dùng cách này khi muốn vá lỗi, mà lỗi liên quan đến đoạn mã trong #if ,khi muốn đoạn mã này chạy ta định nghĩa