Roslyn không biên dịch được mã


95

Sau khi tôi đã di chuyển dự án của mình từ VS2013 sang VS2015, dự án không còn được xây dựng nữa. Lỗi biên dịch xảy ra trong câu lệnh LINQ sau:

static void Main(string[] args)
{
    decimal a, b;
    IEnumerable<dynamic> array = new string[] { "10", "20", "30" };
    var result = (from v in array
                  where decimal.TryParse(v, out a) && decimal.TryParse("15", out b) && a <= b // Error here
                  orderby decimal.Parse(v)
                  select v).ToArray();
}

Trình biên dịch trả về một lỗi:

Lỗi CS0165 Sử dụng biến cục bộ chưa được gán 'b'

Nguyên nhân nào gây ra vấn đề này? Có thể sửa nó thông qua cài đặt trình biên dịch không?


11
@BinaryWorrier: Tại sao? Nó chỉ sử dụng bsau khi gán nó qua một outtham số.
Jon Skeet

1
Tài liệu VS 2015 cho biết "Mặc dù các biến được truyền dưới dạng đối số không cần phải được khởi tạo trước khi được truyền, nhưng phương thức được gọi là bắt buộc phải gán một giá trị trước khi phương thức trả về." vì vậy điều này trông giống như một lỗi vâng, nó được đảm bảo được khởi tạo bởi tryParse đó.
Rup

3
Bất kể lỗi là gì, mã này minh họa mọi thứ xấu về các outđối số. Điều đó có TryParsetrả về một giá trị nullable (hoặc tương đương) không.
Konrad Rudolph

1
@KonradRudolph where (a = decimal.TryParse(v)).HasValue && (b = decimal.TryParse(v)).HasValue && a <= btrông đẹp hơn nhiều
Rawling

2
Chỉ cần lưu ý, bạn có thể đơn giản hóa điều này thành decimal a, b; var q = decimal.TryParse((dynamic)"10", out a) && decimal.TryParse("15", out b) && a <= b;. Tôi đã mở một lỗi Roslyn về việc này.
Rawling

Câu trả lời:


112

Điều gì gây ra vấn đề này?

Đối với tôi, có vẻ như một lỗi trình biên dịch. Ít nhất, nó đã làm. Mặc dù các biểu thức decimal.TryParse(v, out a)decimal.TryParse(v, out b)biểu thức được đánh giá động, tôi mong đợi trình biên dịch vẫn hiểu rằng vào thời điểm nó đạt đến a <= b, cả hai abchắc chắn được gán. Ngay cả với những điều kỳ lạ mà bạn có thể gặp phải khi nhập động, tôi mong rằng sẽ chỉ đánh giá a <= bsau khi đánh giá cả hai TryParsecuộc gọi.

Tuy nhiên, nó chỉ ra rằng thông qua toán tử và chuyển đổi phức tạp, hoàn toàn khả thi để có một biểu thức A && B && Cđánh giá ACnhưng không B- nếu bạn đủ tinh ranh. Xem báo cáo lỗi Roslyn để biết ví dụ tài tình của Neal Gafter.

Thực hiện công việc đó dynamicthậm chí còn khó hơn - ngữ nghĩa liên quan khi toán hạng động khó mô tả hơn, bởi vì để thực hiện giải quyết quá tải, bạn cần đánh giá các toán hạng để tìm ra loại nào có liên quan, điều này có thể phản trực quan. Tuy nhiên, một lần nữa Neal đã đưa ra một ví dụ cho thấy rằng lỗi trình biên dịch là bắt buộc ... đây không phải là một lỗi, đó là một bản sửa lỗi . Neal đã nhận được rất nhiều kudo để chứng minh điều đó.

Có thể sửa nó thông qua cài đặt trình biên dịch không?

Không, nhưng có những lựa chọn thay thế tránh được lỗi.

Thứ nhất, bạn có thể ngăn nó hoạt động - nếu bạn biết rằng bạn sẽ chỉ sử dụng chuỗi, thì bạn có thể sử dụng IEnumerable<string> hoặc cung cấp cho biến phạm vi vmột loại string(tức là from string v in array). Đó sẽ là lựa chọn ưu tiên của tôi.

Nếu bạn thực sự cần giữ cho nó động, chỉ cần cung cấp bmột giá trị để bắt đầu bằng:

decimal a, b = 0m;

Điều này sẽ không gây hại gì - chúng tôi biết rằng thực sự đánh giá động của bạn sẽ không gây ra bất kỳ điều gì điên rồ, vì vậy cuối cùng bạn sẽ vẫn chỉ định một giá trị btrước khi sử dụng nó, khiến giá trị ban đầu không liên quan.

Ngoài ra, có vẻ như việc thêm dấu ngoặc đơn cũng hoạt động:

where decimal.TryParse(v, out a) && (decimal.TryParse("15", out b) && a <= b)

Điều đó thay đổi thời điểm kích hoạt các phần giải quyết quá tải khác nhau và tình cờ làm cho trình biên dịch hài lòng.

Vẫn còn một vấn đề - các quy tắc của đặc tả về việc gán xác định với &&toán tử cần được làm rõ để nêu rõ rằng chúng chỉ áp dụng khi &&toán tử đang được sử dụng trong triển khai "thông thường" với hai booltoán hạng. Tôi sẽ cố gắng đảm bảo điều này được khắc phục cho tiêu chuẩn ECMA tiếp theo.


Vâng! Áp dụng IEnumerable<string>hoặc thêm dấu ngoặc đã làm việc cho tôi. Bây giờ trình biên dịch xây dựng mà không có lỗi.
ramil89

1
sử dụng decimal a, b = 0m;có thể loại bỏ lỗi, nhưng sau đó a <= bsẽ luôn sử dụng 0m, vì giá trị ra vẫn chưa được tính toán.
Paw Baltzersen

12
@PawBaltzersen: Điều gì khiến bạn nghĩ như vậy? Nó sẽ luôn được gán trước khi so sánh - chỉ là trình biên dịch không thể chứng minh điều đó, vì một số lý do (về cơ bản là một lỗi).
Jon Skeet

1
Có một phương pháp phân tích cú pháp mà không có tác dụng phụ. decimal? TryParseDecimal(string txt)có thể là một giải pháp quá
Zahir

1
Tôi tự hỏi nếu đó là khởi tạo lười biếng; nó nghĩ rằng "nếu điều đầu tiên là đúng thì tôi không cần đánh giá điều thứ hai, nghĩa là bcó thể không được chỉ định"; Tôi biết đó là lý do không hợp lệ nhưng nó giải thích tại sao các dấu ngoặc đơn sửa chữa nó ...
durron597


16

Vì tôi đã học rất chăm chỉ trong báo cáo lỗi, tôi sẽ cố gắng giải thích điều này bản thân mình.


Hãy tưởng tượng Tlà một số kiểu do người dùng xác định với một kiểu truyền ngầm boolthay thế giữa falsetrue, bắt đầu bằng false. Theo như trình biên dịch biết, dynamicđối số đầu tiên đối với đối số đầu tiên &&có thể đánh giá theo kiểu đó, vì vậy nó phải bi quan.

Sau đó, nếu nó cho phép mã biên dịch, điều này có thể xảy ra:

  • Khi chất kết dính động đánh giá kết quả đầu tiên &&, nó thực hiện như sau:
    • Đánh giá đối số đầu tiên
    • Đó là một T- ẩn nó đến bool.
    • Ồ, đó là false, vì vậy chúng ta không cần đánh giá đối số thứ hai.
    • Đặt kết quả của &&đánh giá là đối số đầu tiên. (Không, không phải false, vì một số lý do.)
  • Khi chất kết dính động đánh giá thứ hai &&, nó thực hiện như sau:
    • Đánh giá lập luận đầu tiên.
    • Đó là một T- ẩn nó đến bool.
    • Ồ, vậy true, hãy đánh giá lập luận thứ hai.
    • ... Ồ, bkhông được chỉ định.

Tóm lại, trong thuật ngữ cụ thể, có các quy tắc "gán xác định" đặc biệt cho phép chúng ta không chỉ nói liệu một biến được "chắc chắn chỉ định" hay "không chắc chắn được gán", mà còn nếu nó "chắc chắn được gán sau falsecâu lệnh" hoặc "chắc chắn được gán sau truecâu lệnh ”.

Chúng tồn tại để khi xử lý &&||(và !???:) trình biên dịch có thể kiểm tra xem các biến có thể được gán trong các nhánh cụ thể của một biểu thức boolean phức tạp hay không.

Tuy nhiên, những điều này chỉ hoạt động trong khi kiểu của biểu thức vẫn là boolean . Khi một phần của biểu thức là dynamic(hoặc kiểu tĩnh không boolean), chúng ta không còn có thể nói một cách đáng tin cậy rằng biểu thức đó là truehoặc false- lần tiếp theo chúng ta ép kiểu boolđể quyết định nhánh nào sẽ sử dụng, nó có thể đã thay đổi ý định.


Cập nhật: điều này hiện đã được giải quyếtghi lại :

Các quy tắc gán xác định được thực hiện bởi các trình biên dịch trước đó cho các biểu thức động cho phép một số trường hợp mã có thể dẫn đến việc đọc các biến mà không chắc chắn được gán. Xem https://github.com/dotnet/roslyn/issues/4509 để biết một báo cáo về điều này.

...

Vì khả năng này, trình biên dịch không được phép biên dịch chương trình này nếu val không có giá trị ban đầu. Các phiên bản trước của trình biên dịch (trước VS2015) cho phép chương trình này biên dịch ngay cả khi val không có giá trị ban đầu. Roslyn hiện chẩn đoán nỗ lực này để đọc một biến có thể chưa được khởi tạo.


1
Sử dụng VS2013 trên máy khác của tôi, tôi đã thực sự quản lý để đọc bộ nhớ chưa được gán bằng cách sử dụng này. Nó không thú vị lắm :(
Rawling.

Bạn có thể đọc các biến chưa được khởi tạo với đại biểu đơn giản. Tạo một đại biểu outđến một phương thức có ref. Nó sẽ vui vẻ làm điều đó và nó sẽ thực hiện các biến được gán mà không thay đổi giá trị.
IllidanS4 muốn Monica trở lại

Vì tò mò, tôi đã thử nghiệm đoạn mã đó bằng C # v4. Tuy nhiên, vì tò mò - làm thế nào trình biên dịch quyết định sử dụng toán tử false/ truetrái ngược với toán tử ép kiểu ngầm định? Tại địa phương, nó sẽ gọi implicit operator boolvào số đầu tiên, sau đó gọi các toán hạng thứ hai, cuộc gọi operator falsetrên các toán hạng đầu tiên, tiếp theo là implicit operator booltrên các toán hạng đầu tiên một lần nữa . Điều này không có ý nghĩa gì đối với tôi, toán hạng đầu tiên về cơ bản sẽ chuyển thành boolean một lần, phải không?
Rob

@Rob Đây có phải là trường hợp dynamicchuỗi &&không? Về cơ bản, tôi đã thấy nó đi (1) đánh giá đối số đầu tiên (2) sử dụng kết hợp ngầm để xem liệu tôi có thể đoản mạch hay không (3) Tôi không thể, vì vậy hãy loại bỏ đối số thứ hai (4) bây giờ tôi biết cả hai loại, tôi có thể thấy tốt nhất &&&toán tử cuộc gọi do người dùng xác định (5) falsetrên đối số đầu tiên để xem liệu tôi có thể đoản mạch hay không (6) Tôi có thể (bởi vì falseimplicit boolkhông đồng ý), vì vậy kết quả là đối số đầu tiên ... và sau đó là đối số tiếp theo &&, (7) sử dụng diễn viên ngầm để xem liệu tôi có thể đoản mạch (một lần nữa).
Rawling

@ IllidanS4 Nghe có vẻ thú vị nhưng tôi chưa tìm ra cách làm. Bạn có thể cho tôi một đoạn mã?
Rawling

15

Đây không phải là một lỗi. Xem https://github.com/dotnet/roslyn/issues/4509#issuecomment-130872713 để biết ví dụ về cách một biểu thức động của biểu mẫu này có thể để lại một biến không được gán như vậy.


1
Vì câu trả lời của tôi được chấp nhận và ủng hộ cao, tôi đã chỉnh sửa nó để chỉ ra giải pháp. Cảm ơn tất cả công việc của bạn về vấn đề này - bao gồm việc giải thích sai lầm của tôi với tôi :)
Jon Skeet
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 cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.