các tham số của kiểu cấu trúc không bắt buộc phải được chỉ định


9

Tôi đã nhận thấy một số hành vi kỳ quái trong mã của tôi khi vô tình nhận xét một dòng trong một chức năng trong khi xem xét mã. Rất khó để tái tạo nhưng tôi sẽ mô tả một ví dụ tương tự ở đây.

Tôi đã có lớp kiểm tra này:

public class Test
{
    public void GetOut(out EmailAddress email)
    {
        try
        {
            Foo(email);
        }
        catch
        {
        }
    }

    public void Foo(EmailAddress email)
    {
    }
}

không có sự phân công nào cho Email GetOutmà thông thường sẽ gây ra lỗi:

Tham số ngoài 'email' phải được chỉ định trước khi điều khiển rời khỏi phương thức hiện tại

Tuy nhiên, nếu EmailAddress nằm trong một cấu trúc trong một cụm riêng biệt thì không có lỗi nào được tạo và mọi thứ sẽ được biên dịch tốt.

public struct EmailAddress
{
    #region Constructors

    public EmailAddress(string email)
        : this(email, string.Empty)
    {
    }

    public EmailAddress(string email, string name)
    {
        this.Email = email;
        this.Name = name;
    }

    #endregion

    #region Properties

    public string Email { get; private set; }
    public string Name { get; private set; }

    #endregion
}

Tại sao trình biên dịch không thực thi Email đó phải được gán cho? Tại sao mã này biên dịch nếu cấu trúc được tạo trong một cụm riêng biệt, nhưng nó không biên dịch nếu cấu trúc được xác định trong lắp ráp hiện có?


2
Nếu bạn đang sử dụng một lớp, bạn phải 'mới' lên một thể hiện của đối tượng. Nó không cần thiết cho các cấu trúc. docs.microsoft.com/en-us/dotnet/csharp/programming-guide/... (tìm kiếm văn bản này trên trang đó cụ thể là: các lớp học Không giống như, cấu trúc có thể được khởi tạo mà không sử dụng operato mới)
— Dortimer

1
Ngay khi cấu trúc Dog của bạn có một biến, nó sẽ không biên dịch :)
— André Sanson

Trong ví dụ này, với struct Dog{}, tất cả đều tốt.
— Henk Holterman

2
@ johnny5 Sau đó đưa ra ví dụ.
— André Sanson

1
OK, điều này thật thú vị. Được sao chép với ứng dụng Bảng điều khiển Core 3 và lib lớp .St Chuẩn.
— Henk Holterman

Câu trả lời:


12

TLDR: Đây là một lỗi đã biết từ lâu. Lần đầu tiên tôi viết về nó vào năm 2010:

https://bloss.msdn.microsoft.com/ericlippert/2010/01/18/a-definite-assocate-anomaly/

Nó vô hại và bạn có thể bỏ qua nó một cách an toàn, và tự chúc mừng mình vì đã tìm thấy một lỗi khó hiểu.

Tại sao trình biên dịch không thực thi mà Emailphải được gán chắc chắn?

Ồ, nó làm, trong một thời trang. Nó chỉ có một ý tưởng sai về điều kiện ngụ ý rằng biến đó chắc chắn được gán, như chúng ta sẽ thấy.

Tại sao mã này biên dịch nếu cấu trúc được tạo trong một cụm riêng biệt, nhưng nó không biên dịch nếu cấu trúc được xác định trong lắp ráp hiện có?

Đó là mấu chốt của lỗi. Lỗi này là hậu quả của giao điểm của cách trình biên dịch C # thực hiện kiểm tra gán xác định trên các cấu trúc và cách trình biên dịch tải siêu dữ liệu từ các thư viện.

Xem xét điều này:

struct Foo 
{ 
  public int x; 
  public int y; 
}
// Yes, public fields are bad, but this is just 
// to illustrate the situation.
void M(out Foo f)
{

OK, tại thời điểm này chúng ta biết gì? flà một bí danh cho một biến loại Foo, do đó, bộ lưu trữ đã được phân bổ và chắc chắn ít nhất là ở trạng thái nó ra khỏi bộ cấp phát lưu trữ. Nếu có một giá trị được đặt trong biến bởi người gọi, giá trị đó là ở đó.

Chúng ta yêu cầu gì? Chúng tôi yêu cầu phải fđược chỉ định chắc chắn tại bất kỳ điểm nào mà kiểm soát rời khỏi Mbình thường. Vì vậy, bạn sẽ mong đợi một cái gì đó như:

void M(out Foo f)
{
  f = new Foo();
}

mà thiết lập f.xvà f.ycác giá trị mặc định của họ. Nhưng những gì về điều này?

void M(out Foo f)
{
  f = new Foo();
  f.x = 123;
  f.y = 456;
}

Điều đó cũng sẽ ổn thôi. Nhưng, và đây là kicker, tại sao chúng ta chỉ cần gán các giá trị mặc định để thổi bay chúng sau đó? Trình kiểm tra phân công xác định của C # sẽ kiểm tra xem mọi trường có được gán không! Đây là hợp pháp:

void M(out Foo f)
{
  f.x = 123;
  f.y = 456;
}

Và tại sao điều đó không hợp pháp? Đó là một loại giá trị. flà một biến và nó đã chứa một giá trị hợp lệ của loại Foo, vì vậy hãy đặt các trường và chúng ta đã hoàn thành, phải không?

Đúng. Vậy lỗi là gì?

Lỗi mà bạn đã phát hiện ra là: vì tiết kiệm chi phí, trình biên dịch C # không tải siêu dữ liệu cho các trường riêng của các cấu trúc trong các thư viện được tham chiếu . Siêu dữ liệu đó có thể rất lớn và nó sẽ làm chậm trình biên dịch để giành được rất ít chiến thắng để tải tất cả vào bộ nhớ mỗi lần.

Và bây giờ bạn sẽ có thể suy ra nguyên nhân của lỗi bạn đã tìm thấy. Khi trình biên dịch kiểm tra xem tham số out có được gán chắc chắn hay không, nó sẽ so sánh số lượng các trường đã biết với số trường được khởi tạo xác định và trong trường hợp của bạn, nó chỉ biết về các trường công khai bằng 0 vì siêu dữ liệu của trường riêng không được tải . Trình biên dịch kết luận "không có trường nào được yêu cầu, không có trường nào được khởi tạo, chúng tôi tốt."

Giống như tôi đã nói, lỗi này đã tồn tại hơn một thập kỷ và những người như bạn thỉnh thoảng khám phá lại và báo cáo nó. Nó vô hại, và nó khó có thể được sửa chữa bởi vì sửa nó là lợi ích gần như bằng không nhưng chi phí hiệu năng lớn.

Và tất nhiên, lỗi này không repro cho các trường cấu trúc riêng trong mã nguồn trong dự án của bạn, vì rõ ràng trình biên dịch đã có thông tin về các trường riêng trong tay.


@ johnny5: Bạn không nên nhận lỗi. Xem dotnetfiddle.net/ZEKiUk . Bạn có thể đăng một repro đơn giản?
— Eric Lippert

1
Cảm ơn vì câu đố đó là vì tôi đã xác định x và y là thuộc tính thay vì thành viên
— johnny 5

1
@ johnny5: Nếu bạn chỉ xác định thuộc tính kiểu C # 1.0 bình thường thì theo quan điểm của người kiểm tra phân công xác định, đó là một phương thức, không phải là một trường. Nếu bạn đã xác định thuộc tính tự động kiểu C # 3.0+, trình biên dịch sẽ biết rằng có một trường riêng hỗ trợ nó; các quy tắc cho sự phân công nhất định của điều đó đã được điều chỉnh trong nhiều năm và bây giờ tôi không nhớ lại các quy tắc chính xác.
— Eric Lippert

Nếu bạn sử dụng System.TimeSpanthay thế, lỗi sẽ xảy ra: error CS0269: Use of unassigned out parameter 'email'và error CS0177: The out parameter 'email' must be assigned to before control leaves the current method. Chỉ có một trường không tĩnh của TimeSpan, cụ thể là _ticks. Nó là internalđể lắp ráp mscorlib của nó. Là lắp ráp đặc biệt? Tương tự với System.DateTime, và lĩnh vực của nó làprivate
— Jeppe Stig Nielsen

@JeppeStigNielsen: Tôi không biết điều gì xảy ra với điều đó! Nếu bạn tìm ra nó, xin vui lòng cho tôi biết.
— Eric Lippert

1

Trong khi nó trông giống như một lỗi, nó có ý nghĩa gì đó.

'Lỗi thiếu' chỉ xuất hiện khi sử dụng thư viện lớp. Và một thư viện lớp có thể đã được viết bằng ngôn ngữ .net khác, ví dụ VB.Net. 'Theo dõi phân công xác định' là một tính năng của C #, không phải của khung.

Vì vậy, về tổng thể tôi không nghĩ đó là một lỗi nhưng tôi không biết về một tuyên bố có thẩm quyền cho việc này.


Nếu bạn đang nhập một cụm vào C #, mặc dù cấu trúc có thể nằm trong một cụm được viết bằng ngôn ngữ khác, Mã sử ​​dụng nó vẫn ở c # vì vậy không nên sử dụng theo dõi gán xác định.
— ngày 5

1
Không cần thiết. C # sẽ không cho phép bạn sử dụng biến đơn vị (cục bộ) nhưng đồng thời khung đảm bảo nó sẽ được đặt thành 0 ( default(T)). Vì vậy, không có vi phạm về an toàn bộ nhớ hoặc một cái gì đó tương tự.
— Henk Holterman

3
Tôi có thể đưa ra một tuyên bố có thẩm quyền như vậy. :) Đó là một lỗi đã biết từ lâu.
— Eric Lippert

1
@EricLippert Cảm ơn, tôi đã hy vọng bạn sẽ thấy điều này vào một lúc nào đó
— johnny 5
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookie và Chính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.