Các ép kiểu do người dùng định nghĩa
Trong các chương trước ta được học về cách chuyển đổi giá trị giữa những kiểu dữ liệu
cơ bản. chúng ta cũng đã học hai cách ép kiểu là :
- Không tường minh (Implicit)
- Tường minh (Explicit)
Vì c# cho phép ta định nghĩa những lớp và cấu trúc riêng,do đó ta cũng muốn có những
cách thức mà cho phép ta chuyển đổi giữa những loại dữ liệu của riêng ta. C# cho phép
làm đi
ều đó.cơ chế của nó là ta có thể định nghĩa một ép kiểu như là một thao 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;