Những chủ đề tiến bộ trong C#
Các ép kiểu do người dùng định
nghĩa – Phần 2
É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ực-thà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.