Trình biên dịch .NET C# (.NET 4.0) biên dịch câu lệnh fixed
theo cách khá đặc biệt.Tại sao MSFT C# biên dịch một mảng "cố định thành phân rã" và "địa chỉ của phần tử đầu tiên" khác nhau?
Đây là một chương trình ngắn nhưng đầy đủ để cho bạn thấy những gì tôi đang nói đến.
using System;
public static class FixedExample {
public static void Main() {
byte [] nonempty = new byte[1] {42};
byte [] empty = new byte[0];
Good(nonempty);
Bad(nonempty);
try {
Good(empty);
} catch (Exception e){
Console.WriteLine(e.ToString());
/* continue with next example */
}
Console.WriteLine();
try {
Bad(empty);
} catch (Exception e){
Console.WriteLine(e.ToString());
/* continue with next example */
}
}
public static void Good(byte[] buffer) {
unsafe {
fixed (byte * p = &buffer[0]) {
Console.WriteLine(*p);
}
}
}
public static void Bad(byte[] buffer) {
unsafe {
fixed (byte * p = buffer) {
Console.WriteLine(*p);
}
}
}
}
Biên dịch bằng "csc.exe FixedExample.cs/unsafe/o +" nếu bạn muốn theo dõi.
Đây là IL tạo ra cho phương pháp Good
:
Tốt()
.maxstack 2
.locals init (uint8& pinned V_0)
IL_0000: ldarg.0
IL_0001: ldc.i4.0
IL_0002: ldelema [mscorlib]System.Byte
IL_0007: stloc.0
IL_0008: ldloc.0
IL_0009: conv.i
IL_000a: ldind.u1
IL_000b: call void [mscorlib]System.Console::WriteLine(int32)
IL_0010: ldc.i4.0
IL_0011: conv.u
IL_0012: stloc.0
IL_0013: ret
Đây là IL tạo ra cho phương pháp Bad
:
Bad()
.locals init (uint8& pinned V_0, uint8[] V_1)
IL_0000: ldarg.0
IL_0001: dup
IL_0002: stloc.1
IL_0003: brfalse.s IL_000a
IL_0005: ldloc.1
IL_0006: ldlen
IL_0007: conv.i4
IL_0008: brtrue.s IL_000f
IL_000a: ldc.i4.0
IL_000b: conv.u
IL_000c: stloc.0
IL_000d: br.s IL_0017
IL_000f: ldloc.1
IL_0010: ldc.i4.0
IL_0011: ldelema [mscorlib]System.Byte
IL_0016: stloc.0
IL_0017: ldloc.0
IL_0018: conv.i
IL_0019: ldind.u1
IL_001a: call void [mscorlib]System.Console::WriteLine(int32)
IL_001f: ldc.i4.0
IL_0020: conv.u
IL_0021: stloc.0
IL_0022: ret
Đây là những gì Good
làm:
- Lấy địa chỉ của bộ đệm [0].
- Tham khảo địa chỉ đó.
- Gọi WriteLine với giá trị không tham chiếu đó.
Đây là những gì 'Bad` làm:
- Nếu đệm là null, GOTO 3.
- Nếu buffer.Length = 0, GOTO 5.
- Store giá trị 0 trong địa phương! khe 0,
- GOTO 6.
- Lấy địa chỉ của bộ đệm [0].
- Tùy chọn địa chỉ đó (trong vùng địa phương 0, có thể là 0 hoặc bộ đệm ngay bây giờ).
- Gọi WriteLine với giá trị không tham chiếu đó.
Khi buffer
không vừa trống và không trống, hai chức năng này cũng thực hiện tương tự. Lưu ý rằng Bad
chỉ cần nhảy qua một vài vòng trước khi thực hiện cuộc gọi hàm WriteLine
.
Khi buffer
là null, Good
ném một NullReferenceException
trong cố định con trỏ declarator (byte * p = &buffer[0]
). Có lẽ đây là hành vi mong muốn để sửa một mảng được quản lý, bởi vì nói chung bất kỳ thao tác nào bên trong câu lệnh cố định cố định sẽ phụ thuộc vào tính hợp lệ của đối tượng đang được sửa. Nếu không thì tại sao mã đó nằm bên trong khối fixed
? Khi Good
được thông qua một tham chiếu null, nó không thành công ngay khi bắt đầu khối fixed
, cung cấp một dấu vết ngăn xếp có liên quan và thông tin. Các nhà phát triển sẽ thấy điều này và nhận ra rằng ông phải xác nhận buffer
trước khi sử dụng nó, hoặc có lẽ logic của mình không chính xác được giao null
đến buffer
.Dù bằng cách nào, việc nhập rõ ràng khối fixed
với một mảng được quản lý null
là không mong muốn.
Bad
xử lý trường hợp này khác nhau, thậm chí không mong muốn. Bạn có thể thấy rằng Bad
không thực sự ném ngoại lệ cho đến khi p
bị hủy đăng ký. Nó làm như vậy theo cách vòng tròn gán null vào cùng một vị trí cục bộ giữ p
, sau đó ném ngoại lệ khi câu lệnh chặn fixed
dereference p
.
Xử lý null
cách này có lợi thế là giữ mô hình đối tượng trong C# nhất quán. Tức là, bên trong khối fixed
, p
vẫn được coi là ngữ nghĩa như là một loại "con trỏ tới một mảng được quản lý" sẽ không, khi null, gây ra các vấn đề cho đến (hoặc trừ khi) nó bị bỏ qua. Nhất quán là tất cả tốt và tốt, nhưng vấn đề là p không phải là một con trỏ đến một mảng được quản lý. Nó là một con trỏ đến phần tử đầu tiên của buffer
, và bất kỳ ai đã viết mã này (Bad
) sẽ hiểu ý nghĩa ngữ nghĩa của nó như vậy. Bạn không thể nhận được kích thước của buffer
từ p
và bạn không thể gọi số p.ToString()
, vậy tại sao lại coi nó như thể đó là một đối tượng? Trong trường hợp buffer
là null, rõ ràng là lỗi mã hóa và tôi tin rằng nó sẽ hữu ích hơn rất nhiều nếu Bad
sẽ ném ngoại lệ tại khai báo con trỏ cố định, thay vì bên trong phương thức.
Vì vậy, có vẻ như là Good
xử lý null
tốt hơn Bad
. Điều gì về bộ đệm trống?
Khi buffer
có độ dài 0, Good
ném IndexOutOfRangeException
tại trình khai báo con trỏ cố định. Điều đó có vẻ như một cách hoàn toàn hợp lý để xử lý truy cập mảng giới hạn. Xét cho cùng, mã số &buffer[0]
phải được xử lý giống như &(buffer[0])
, rõ ràng là nên ném IndexOutOfRangeException
.
Bad
xử lý trường hợp này khác và một lần nữa không mong muốn. Cũng giống như trường hợp nếu buffer
là null
, khi buffer.Length == 0
, Bad
không ném ngoại lệ cho đến khi p
bị hủy đăng ký và tại thời điểm đó, nó ném NullReferenceException, không phải IndexOutOfRangeException! Nếu p
không bao giờ bị hủy đăng ký, thì mã đó thậm chí không ném ngoại lệ. Một lần nữa, có vẻ như ý tưởng ở đây là đưa ra p
ý nghĩa ngữ nghĩa của "con trỏ tới một mảng được quản lý". Nhưng một lần nữa, tôi không nghĩ rằng bất cứ ai viết mã này sẽ nghĩ về p
theo cách đó. Mã sẽ hữu ích hơn nhiều nếu nó đã ném IndexOutOfRangeException
vào khai báo con trỏ cố định, do đó thông báo cho nhà phát triển rằng mảng được truyền vào là trống và không phải là null
.
Có vẻ như fixed(byte * p = buffer)
phải được biên dịch thành cùng một mã như là fixed (byte * p = &buffer[0])
. Cũng lưu ý rằng mặc dù buffer
có thể có bất kỳ biểu thức tùy ý nào, kiểu của nó (byte[]
) được biết tại thời gian biên dịch và do đó mã trong Good
sẽ hoạt động cho bất kỳ biểu thức tùy ý nào.
Sửa
Trong thực tế, nhận thấy rằng việc thực hiện Bad
thực hiện kiểm tra lỗi trên buffer[0]
hai lần. Nó thực hiện một cách rõ ràng ở đầu phương thức, và sau đó thực hiện nó một cách rõ ràng tại lệnh ldelema
.
Vì vậy, chúng ta thấy rằng Good
và Bad
ngữ nghĩa khác nhau. Bad
dài hơn, có thể chậm hơn, và chắc chắn không cung cấp cho chúng tôi ngoại lệ mong muốn khi chúng tôi có lỗi trong mã của chúng tôi, và thậm chí không thành công nhiều hơn so với trong một số trường hợp.
Đối với những người tò mò, phần 18.6 của spec (C# 4.0) nói rằng hành vi là "Thực hiện định nghĩa" trong cả hai trường hợp thất bại:
Một cố định con trỏ-initializer có thể là một trong những như sau:
• Mã thông báo “&” tiếp theo là biến tham chiếu (§5.3.3) đến biến di chuyển (§18.3) của loại không được quản lý T, với điều kiện loại T * được chuyển đổi hoàn toàn thành con trỏ loại được đưa ra trong tuyên bố cố định. Trong trường hợp này, trình khởi tạo tính toán địa chỉ của biến đã cho và biến được đảm bảo duy trì ở một địa chỉ cố định trong khoảng thời gian của câu lệnh cố định.
• Biểu thức của một kiểu mảng với các phần tử của kiểu không được quản lý T, với điều kiện kiểu T * được chuyển đổi hoàn toàn thành kiểu con trỏ được đưa ra trong câu lệnh cố định. Trong trường hợp này, bộ khởi tạo tính toán địa chỉ của phần tử đầu tiên trong mảng và toàn bộ mảng được đảm bảo duy trì ở một địa chỉ cố định trong khoảng thời gian của câu lệnh cố định. Hành vi của câu lệnh cố định được xác định thực hiện nếu biểu thức mảng là null hoặc nếu mảng có phần tử không.
... trường hợp khác ...
điểm ngoái, MSDN documentation gợi ý rằng hai là "tương đương":
// Hai bài tập sau đây là tương đương ...
cố định (double * p = arr) {/ ... /}
cố định (double * p = & arr [0]) {/ ... /}
Nếu hai có nghĩa vụ phải là "tương đương", thì tại sao sử dụng ngữ nghĩa xử lý lỗi khác nhau đối với các tuyên bố trước đây ?
Nó cũng xuất hiện rằng nỗ lực bổ sung được đưa vào viết đường dẫn mã được tạo trong Bad
. Mã được biên dịch trong Good
hoạt động tốt cho tất cả các trường hợp lỗi và giống như mã trong Bad
trong trường hợp không phải là lỗi. Tại sao triển khai các đường dẫn mã mới thay vì chỉ sử dụng mã đơn giản được tạo cho Good
?
Tại sao nó được triển khai theo cách này?
@pst tôi loại bỏ 'SoWhat' và giải thích của nó cho ngắn gọn. Đã sửa. –
Bạn đã kiểm tra những gì C# spec nói về 'arr [0]'? Tôi khá chắc chắn 'arr [0]' ném một ngoại lệ khi 'arr' là null hoặc rỗng trong bất kỳ ngữ cảnh nào, bất kể câu lệnh' fixed' nào bao quanh nó. – hvd
@hvd Có đó là lý do tại sao 'cố định (byte * p = buffer)' chỉ nên được coi là 'cố định (byte * p = & buffer [0])'. Sau này sẽ chăm sóc kiểm tra lỗi sạch hơn. Tôi sẽ thêm điều này vào câu hỏi. –