Tại sao các câu lệnh gán trả về một giá trị?


126

Điều này được cho phép:

int a, b, c;
a = b = c = 16;

string s = null;
while ((s = "Hello") != null) ;

Theo hiểu biết của tôi, việc gán s = ”Hello”;chỉ nên “Hello”được gán cho s, nhưng thao tác không nên trả về bất kỳ giá trị nào. Nếu đó là sự thật, thì ((s = "Hello") != null)sẽ tạo ra một lỗi, vì nullsẽ được so sánh với không có gì.

Lý do đằng sau cho phép các câu lệnh gán trả về một giá trị là gì?


44
Để tham khảo, hành vi này được xác định trong thông số C # : "Kết quả của biểu thức gán đơn giản là giá trị được gán cho toán hạng bên trái. Kết quả có cùng loại với toán hạng bên trái và luôn được phân loại là giá trị."
Michael Petrotta

1
@Skurmedel Tôi không phải là người downvoter, tôi đồng ý với bạn. Nhưng có lẽ bởi vì nó được cho là một câu hỏi tu từ?
jsmith

Trong gán pascal / delphi: = trả về không có gì. tôi ghét nó.
Andrey

20
Tiêu chuẩn cho các ngôn ngữ trong nhánh C. Bài tập là biểu thức.
Hans Passant

"gán ... chỉ nên khiến cho Hello Hello được gán cho s, nhưng thao tác không nên trả về bất kỳ giá trị nào" - Tại sao? "Lý do đằng sau cho phép các câu lệnh gán trả về một giá trị là gì?" - Bởi vì nó hữu ích, như ví dụ của riêng bạn chứng minh. (Chà, ví dụ thứ hai của bạn là ngớ ngẩn, nhưng while ((s = GetWord()) != null) Process(s);không phải).
Jim Balter

Câu trả lời:


160

Theo hiểu biết của tôi, gán s = "Xin chào"; chỉ nên khiến "Xin chào" được gán cho s, nhưng thao tác không nên trả về bất kỳ giá trị nào.

Hiểu biết của bạn là 100% không chính xác. Bạn có thể giải thích tại sao bạn tin điều sai lầm này?

Lý do đằng sau cho phép các câu lệnh gán trả về một giá trị là gì?

Trước hết, các câu lệnh gán không tạo ra một giá trị. Biểu thức chuyển nhượng tạo ra một giá trị. Một biểu thức chuyển nhượng là một tuyên bố pháp lý; chỉ có một số ít các biểu thức là các câu lệnh pháp lý trong C #: đang chờ một biểu thức, xây dựng thể hiện, tăng, giảm, gọi và các biểu thức gán có thể được sử dụng khi dự kiến ​​một câu lệnh.

Chỉ có một loại biểu thức trong C # không tạo ra một loại giá trị nào đó, đó là một lời gọi của một cái gì đó được gõ là trả về khoảng trống. (Hoặc, tương tự, chờ đợi một nhiệm vụ không có giá trị kết quả liên quan.) Mỗi ​​loại biểu thức khác tạo ra một giá trị hoặc biến hoặc tham chiếu hoặc truy cập thuộc tính hoặc truy cập sự kiện, v.v.

Lưu ý rằng tất cả các biểu thức hợp pháp như các tuyên bố đều hữu ích cho các tác dụng phụ của chúng . Đó là cái nhìn sâu sắc quan trọng ở đây, và tôi nghĩ có lẽ nguyên nhân của trực giác của bạn rằng các bài tập nên là các phát biểu chứ không phải các biểu thức. Lý tưởng nhất là chúng ta có chính xác một tác dụng phụ cho mỗi câu lệnh và không có tác dụng phụ nào trong một biểu thức. Nó một chút kỳ quặc rằng mã phụ ảnh hưởng có thể được sử dụng trong một bối cảnh biểu hiện ở tất cả.

Lý do đằng sau cho phép tính năng này là vì (1) nó thường thuận tiện và (2) nó là thành ngữ trong các ngôn ngữ giống như C.

Người ta có thể lưu ý rằng câu hỏi đã được đặt ra: tại sao thành ngữ này trong các ngôn ngữ giống như C?

Thật không may, Dennis Ritchie không còn có thể hỏi nữa, nhưng tôi đoán là một bài tập hầu như luôn để lại giá trị vừa được gán trong một thanh ghi. C là một loại ngôn ngữ rất "gần với máy". Có vẻ hợp lý và phù hợp với thiết kế của C rằng có một tính năng ngôn ngữ có nghĩa là "tiếp tục sử dụng giá trị mà tôi vừa gán". Rất dễ dàng để viết một trình tạo mã cho tính năng này; bạn chỉ cần tiếp tục sử dụng thanh ghi lưu trữ giá trị được gán.


1
Không biết rằng bất cứ điều gì trả về giá trị (ngay cả cấu trúc thể hiện đều được coi là biểu thức)
user437291

9
@ user437291: Nếu xây dựng thể hiện không phải là một biểu thức, bạn không thể làm bất cứ điều gì với đối tượng được xây dựng - bạn không thể gán nó cho một cái gì đó, bạn không thể chuyển nó thành một phương thức và bạn không thể gọi bất kỳ phương thức nào trên đó Sẽ khá vô dụng, phải không?
Timwi

1
Thay đổi một biến thực sự là "chỉ" một tác dụng phụ của toán tử gán, mà, tạo ra một biểu thức, mang lại một giá trị như mục đích chính của nó.
mk12

2
"Sự hiểu biết của bạn là 100% không chính xác." - Mặc dù việc sử dụng tiếng Anh của OP không phải là tốt nhất, nhưng "sự hiểu biết" của anh ấy / cô ấy là về những gì nên xảy ra, biến nó thành một ý kiến, vì vậy nó không phải là thứ có thể "không chính xác 100%" . Một số nhà thiết kế ngôn ngữ đồng ý với "nên" của OP và thực hiện các bài tập không có giá trị hoặc nếu không thì cấm họ trở thành biểu hiện phụ. Tôi nghĩ đó là một phản ứng thái quá đối với =/ ==typo, điều này dễ dàng được giải quyết bằng cách không cho phép sử dụng giá trị =trừ khi nó được ngoặc đơn. ví dụ, if ((x = y))hoặc if ((x = y) == true)được cho phép nhưng if (x = y)không.
Jim Balter

"Không biết rằng bất cứ thứ gì trả về giá trị ... đều được coi là biểu thức" - Hmm ... mọi thứ trả về giá trị đều được coi là biểu thức - cả hai đều gần như đồng nghĩa.
Jim Balter

44

Bạn đã không cung cấp câu trả lời? Đó là để kích hoạt chính xác các loại cấu trúc bạn đã đề cập.

Một trường hợp phổ biến trong đó thuộc tính này của toán tử gán được sử dụng là đọc các dòng từ một tệp ...

string line;
while ((line = streamReader.ReadLine()) != null)
    // ...

1
+1 Đây phải là một trong những ứng dụng thiết thực nhất của công trình này
Mike Burton

11
Tôi thực sự thích nó cho các trình khởi tạo thuộc tính thực sự đơn giản, nhưng bạn phải cẩn thận và rút ra dòng "đơn giản" thấp để dễ đọc: return _myBackingField ?? (_myBackingField = CalculateBackingField());Ít rắc rối hơn nhiều so với kiểm tra null và gán.
Tom Mayfield

34

Việc sử dụng biểu thức gán yêu thích của tôi là dành cho các thuộc tính khởi tạo lười biếng.

private string _name;
public string Name
{
    get { return _name ?? (_name = ExpensiveNameGeneratorMethod()); }
}

5
Đó là lý do tại sao rất nhiều người đã đề xuất ??=cú pháp.
cấu hình

27

Đối với một, nó cho phép bạn xâu chuỗi các bài tập của mình, như trong ví dụ của bạn:

a = b = c = 16;

Mặt khác, nó cho phép bạn gán và kiểm tra kết quả trong một biểu thức:

while ((s = foo.getSomeString()) != null) { /* ... */ }

Cả hai đều có thể là lý do mơ hồ, nhưng chắc chắn có những người thích những công trình này.


5
+1 Chaining không phải là một tính năng quan trọng nhưng thật tuyệt khi thấy nó "chỉ hoạt động" khi có quá nhiều thứ không.
Mike Burton

+1 cho chuỗi cũng; Tôi luôn nghĩ rằng đó là một trường hợp đặc biệt được xử lý bởi ngôn ngữ chứ không phải là hiệu ứng phụ tự nhiên của "giá trị trả về biểu thức".
Caleb Bell

2
Nó cũng cho phép bạn sử dụng chuyển nhượng trong lợi nhuận:return (HttpContext.Current.Items["x"] = myvar);
Serge Shultz

14

Ngoài các lý do đã được đề cập (chuỗi phân công, thiết lập và kiểm tra trong vòng lặp, v.v.), để sử dụng đúngusing câu lệnh bạn cần tính năng này:

using (Font font3 = new Font("Arial", 10.0f))
{
    // Use font3.
}

MSDN không khuyến khích khai báo đối tượng dùng một lần bên ngoài câu lệnh đang sử dụng, vì sau đó nó sẽ vẫn ở trong phạm vi ngay cả sau khi nó đã được xử lý (xem bài viết MSDN tôi liên kết).


4
Tôi không nghĩ rằng đây thực sự là sự phân công chuỗi mỗi se.
kenny

1
@kenny: Uh ... Không, nhưng tôi cũng không bao giờ tuyên bố điều đó. Như tôi đã nói, ngoài các lý do đã được đề cập - bao gồm cả chuỗi chuyển nhượng - câu lệnh sử dụng sẽ khó sử dụng hơn rất nhiều, không phải vì thực tế là toán tử gán trả về kết quả của phép toán gán. Điều này thực sự không liên quan đến chuỗi phân công.
Martin Törnwall

1
Nhưng như Eric đã lưu ý ở trên, "câu lệnh gán không trả về giá trị". Đây thực sự chỉ là cú pháp ngôn ngữ của câu lệnh sử dụng, bằng chứng là người ta không thể sử dụng "biểu thức gán" trong câu lệnh sử dụng.
kenny

@kenny: Xin lỗi, thuật ngữ của tôi rõ ràng là sai. Tôi nhận ra rằng câu lệnh sử dụng có thể được thực hiện mà không có ngôn ngữ có hỗ trợ chung cho các biểu thức gán gán trả về một giá trị. Điều đó, trong mắt tôi, sẽ không nhất quán khủng khiếp.
Martin Törnwall

2
@kenny: Trên thực tế, người ta có thể sử dụng một câu lệnh định nghĩa và gán hoặc bất kỳ biểu thức nào trong using. Tất cả những quy phạm pháp luật: using (X x = new X()), using (x = new X()), using (x). Tuy nhiên, trong ví dụ này, nội dung của câu lệnh sử dụng là một cú pháp đặc biệt hoàn toàn không dựa vào phép gán trả về một giá trị - Font font3 = new Font("Arial", 10.0f)không phải là biểu thức và không hợp lệ ở bất kỳ nơi nào mong đợi biểu thức.
cấu hình

10

Tôi muốn giải thích về một điểm cụ thể mà Eric Lippert đã đưa ra trong câu trả lời của mình và đặt sự chú ý vào một dịp đặc biệt mà không ai có thể chạm vào được. Eric nói:

[...] một nhiệm vụ hầu như luôn để lại giá trị vừa được gán trong một thanh ghi.

Tôi muốn nói rằng bài tập sẽ luôn để lại giá trị mà chúng tôi đã cố gán cho toán hạng bên trái của chúng tôi. Không chỉ "hầu như luôn luôn". Nhưng tôi không biết vì tôi chưa tìm thấy vấn đề này trong tài liệu này. Về mặt lý thuyết, đây có thể là một quy trình được thực hiện rất hiệu quả để "bỏ lại phía sau" và không đánh giá lại toán hạng bên trái, nhưng nó có hiệu quả không?

'Hiệu quả' có cho tất cả các ví dụ cho đến nay được xây dựng trong các câu trả lời của chủ đề này. Nhưng hiệu quả trong trường hợp thuộc tính và bộ chỉ mục sử dụng bộ truy cập get- và set? Không có gì. Xem xét mã này:

class Test
{
    public bool MyProperty { get { return true; } set { ; } }
}

Ở đây chúng ta có một thuộc tính, thậm chí không phải là một trình bao bọc cho một biến riêng. Bất cứ khi nào được kêu gọi anh ta sẽ trở về đúng, bất cứ khi nào cố gắng thiết lập giá trị của mình, anh ta sẽ không làm gì cả. Vì vậy, bất cứ khi nào tài sản này được đánh giá, anh ta sẽ được trung thực. Hãy xem điều gì xảy ra:

Test test = new Test();

if ((test.MyProperty = false) == true)
    Console.WriteLine("Please print this text.");

else
    Console.WriteLine("Unexpected!!");

Đoán xem nó in cái gì? Nó in Unexpected!!. Hóa ra, bộ truy cập thiết lập thực sự được gọi, không có gì. Nhưng sau đó, người truy cập get không bao giờ được gọi cả. Việc chuyển nhượng chỉ đơn giản là để lại falsegiá trị mà chúng tôi đã cố gắng gán cho tài sản của chúng tôi. Và falsegiá trị này là những gì câu lệnh if đánh giá.

Tôi sẽ kết thúc với một ví dụ thực tế khiến tôi phải nghiên cứu vấn đề này. Tôi đã tạo một bộ chỉ mục là một trình bao bọc thuận tiện cho một bộ sưu tập ( List<string>) mà một lớp của tôi có một biến riêng.

Tham số được gửi tới bộ chỉ mục là một chuỗi, được coi là một giá trị trong bộ sưu tập của tôi. Trình truy cập get chỉ đơn giản trả về giá trị true hoặc false nếu giá trị đó tồn tại trong danh sách hay không. Do đó, bộ truy cập get là một cách khác để sử dụng List<T>.Containsphương thức.

Nếu bộ truy cập tập hợp chỉ mục được gọi với một chuỗi làm đối số và toán hạng bên phải là một bool true, anh ta sẽ thêm tham số đó vào danh sách. Nhưng nếu cùng một tham số được gửi đến người truy cập và toán hạng bên phải là một bool false, thay vào đó anh ta sẽ xóa phần tử khỏi danh sách. Do đó, bộ truy cập đã được sử dụng như là một thay thế thuận tiện cho cả hai List<T>.AddList<T>.Remove.

Tôi nghĩ rằng tôi đã có một "API" gọn gàng và gọn gàng trong danh sách với logic riêng của tôi được triển khai như một cổng. Với sự giúp đỡ của một người lập chỉ mục, tôi có thể làm nhiều việc với một vài tổ hợp phím. Chẳng hạn, làm thế nào tôi có thể thử thêm một giá trị vào danh sách của mình và xác minh rằng nó có trong đó không? Tôi nghĩ rằng đây là dòng mã duy nhất cần thiết:

if (myObject["stringValue"] = true)
    ; // Set operation succeeded..!

Nhưng như ví dụ trước đây của tôi đã chỉ ra, trình truy cập get được cho là để xem liệu giá trị thực sự có trong danh sách thậm chí không được gọi hay không. Các truegiá trị luôn được bỏ lại phía sau một cách hiệu quả phá hủy bất cứ logic tôi đã thực hiện trong get accessor tôi.


Câu trả lời rất thú vị. Và nó khiến tôi suy nghĩ tích cực về sự phát triển dựa trên thử nghiệm.
Elliot

1
Mặc dù điều này rất thú vị, nhưng đó là nhận xét về câu trả lời của Eric, không phải là câu trả lời cho câu hỏi của OP.
Jim Balter

Tôi sẽ không mong đợi một biểu thức gán đã thử trước tiên nhận được giá trị của thuộc tính đích. Nếu biến là biến đổi và các loại khớp nhau, tại sao giá trị cũ là gì? Chỉ cần đặt giá trị mới. Tôi không phải là một người hâm mộ của bài tập trong các bài kiểm tra IF. Là nó trả về đúng bởi vì bài tập đã thành công, hoặc đó là một giá trị nào đó, ví dụ như một số nguyên dương được ép buộc thành true hoặc bởi vì bạn đang gán một boolean? Bất kể việc thực hiện, logic là mơ hồ.
Davos

6

Nếu gán không trả về giá trị, dòng a = b = c = 16sẽ không hoạt động.

Ngoài ra đôi khi có thể viết những thứ như while ((s = readLine()) != null)có thể hữu ích.

Vì vậy, lý do đằng sau để cho phép gán trả về giá trị được gán, là để cho bạn làm những việc đó.


Không ai đề cập đến nhược điểm - Tôi đến đây tự hỏi tại sao các bài tập không thể trả về null, để bài tập chung so với lỗi so sánh 'if (Something = 1) {}' sẽ không biên dịch thay vì luôn luôn trả về đúng. Bây giờ tôi thấy rằng điều đó sẽ tạo ra ... các vấn đề khác!
Jon

1
@Jon lỗi đó có thể dễ dàng được ngăn chặn bằng cách không cho phép hoặc cảnh báo về việc =xảy ra trong một biểu thức không được ngoặc đơn (không tính các dấu ngoặc của chính câu lệnh if / while). gcc đưa ra những cảnh báo như vậy và về cơ bản đã loại bỏ loại lỗi này khỏi các chương trình C / C ++ được biên dịch cùng với nó. Thật xấu hổ khi các nhà văn biên dịch khác đã chú ý rất ít đến điều này và một số ý tưởng hay khác trong gcc.
Jim Balter

4

Tôi nghĩ bạn đang hiểu sai về cách trình phân tích cú pháp sẽ diễn giải cú pháp đó. Bài tập sẽ được đánh giá đầu tiên và sau đó kết quả sẽ được so sánh với NULL, tức là câu lệnh tương đương với:

s = "Hello"; //s now contains the value "Hello"
(s != null) //returns true.

Như những người khác đã chỉ ra, kết quả của một bài tập là giá trị được gán. Tôi thấy khó có thể tưởng tượng được lợi thế để có

((s = "Hello") != null)

s = "Hello";
s != null;

không tương đương ...


Mặc dù bạn có ý nghĩa thực tế rằng hai dòng của bạn tương đương với tuyên bố của bạn rằng việc chuyển nhượng không trả về giá trị là không đúng về mặt kỹ thuật. Tôi hiểu rằng kết quả của một bài tập là một giá trị (theo thông số C #) và theo nghĩa đó không khác gì so với toán tử cộng trong biểu thức như 1 + 2 + 3
Steve Ellinger

3

Tôi nghĩ lý do chính là sự giống nhau (có chủ ý) với C ++ và C. Làm cho toán tử xác nhận (và rất nhiều cấu trúc ngôn ngữ khác) hoạt động giống như các đối tác C ++ của họ chỉ tuân theo nguyên tắc ít gây bất ngờ nhất và bất kỳ lập trình viên nào đến từ một xoăn khác ngôn ngữ khung có thể sử dụng chúng mà không tốn nhiều suy nghĩ. Dễ dàng chọn các lập trình viên C ++ là một trong những mục tiêu thiết kế chính của C #.


2

Vì hai lý do bạn đưa vào bài đăng
1) để bạn có thể thực hiện a = b = c = 16
2) để bạn có thể kiểm tra nếu một bài tập thành công if ((s = openSomeHandle()) != null)


1

Việc 'a ++' hoặc 'printf ("foo")' có thể hữu ích như là một câu lệnh khép kín hoặc là một phần của biểu thức lớn hơn có nghĩa là C phải cho phép khả năng kết quả biểu thức có thể hoặc không đã sử dụng. Do đó, có một khái niệm chung là các biểu thức có thể hữu ích 'trả lại' một giá trị cũng có thể làm như vậy. Chuỗi chuyển nhượng có thể hơi "thú vị" trong C và thậm chí thú vị hơn trong C ++, nếu tất cả các biến trong câu hỏi không có cùng loại chính xác. Tập quán như vậy có lẽ tốt nhất nên tránh.


1

Một trường hợp sử dụng ví dụ tuyệt vời khác, tôi sử dụng điều này mọi lúc:

var x = _myVariable ?? (_myVariable = GetVariable());
//for example: when used inside a loop, "GetVariable" will be called only once

0

Một lợi thế nữa mà tôi không thấy được đưa ra trong các câu trả lời ở đây, đó là cú pháp chuyển nhượng dựa trên số học.

Bây giờ x = y = b = c = 2 + 3có nghĩa là một cái gì đó khác nhau về số học so với ngôn ngữ kiểu C; trong số học, đây là một khẳng định, chúng tôi tuyên bố rằng x bằng y, v.v. và trong ngôn ngữ kiểu C, đó là một lệnh làm cho x bằng y, v.v. sau khi nó được thực thi.

Điều này nói rằng, vẫn còn đủ mối quan hệ giữa số học và mã mà không có nghĩa là không cho phép những gì tự nhiên trong số học trừ khi có lý do chính đáng. (Một điều khác mà các ngôn ngữ kiểu C lấy từ việc sử dụng ký hiệu bằng là sử dụng == để so sánh bằng. Ở đây, mặc dù bởi vì hầu hết các quyền == trả về một giá trị loại chuỗi này là không thể.)


@JimBalter Tôi tự hỏi liệu trong năm năm nữa, bạn có thực sự có thể tranh luận hay không.
Jon Hanna

@JimBalter không, bình luận thứ hai của bạn thực sự là một đối số phản biện và là một đối số âm thanh. Suy nghĩ lại của tôi là bởi vì nhận xét đầu tiên của bạn là không. Tuy nhiên, khi học số học chúng ta học hỏi a = b = cphương tiện đó abclà những điều tương tự. Khi học ngôn ngữ kiểu C, chúng ta sẽ học ngôn ngữ đó saua = b = c , abgiống như vậy c. Chắc chắn có một sự khác biệt về ngữ nghĩa, như chính câu trả lời của tôi đã nói, nhưng khi còn nhỏ, lần đầu tiên tôi học lập trình bằng ngôn ngữ sử dụng =cho bài tập nhưng không cho phép a = b = cđiều này có vẻ không hợp lý với tôi, mặc dù vậy
Jon Hanna

Không thể tránh khỏi vì ngôn ngữ đó cũng được sử dụng =để so sánh bình đẳng, vì vậy trong đó a = b = csẽ có nghĩa là gì a = b == ctrong các ngôn ngữ kiểu C. Tôi thấy chuỗi được cho phép trong C trực quan hơn rất nhiều vì tôi có thể rút ra một sự tương tự với số học.
Jon Hanna

0

Tôi thích sử dụng giá trị trả về chuyển nhượng khi tôi cần cập nhật một loạt các công cụ và trả lại cho dù có bất kỳ thay đổi nào hay không:

bool hasChanged = false;

hasChanged |= thing.Property != (thing.Property = "Value");
hasChanged |= thing.AnotherProperty != (thing.AnotherProperty = 42);
hasChanged |= thing.OneMore != (thing.OneMore = "They get it");

return hasChanged;

Hãy cẩn thận. Bạn có thể nghĩ rằng bạn có thể rút ngắn nó về điều này:

return thing.Property != (thing.Property = "Value") ||
    thing.AnotherProperty != (thing.AnotherProperty = 42) ||
    thing.OneMore != (thing.OneMore = "They get it");

Nhưng điều này thực sự sẽ ngừng đánh giá hoặc các tuyên bố sau khi nó tìm thấy sự thật đầu tiên. Trong trường hợp này có nghĩa là nó dừng gán các giá trị tiếp theo một khi nó gán giá trị đầu tiên khác.

Xem https://dotnetfiddle.net/e05Rh8 để chơi xung quanh với điều này

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.