Tại sao hầu hết các ngôn ngữ lập trình chỉ hỗ trợ trả về một giá trị duy nhất từ ​​một hàm? [đóng cửa]


118

Có một lý do tại sao các hàm trong hầu hết các ngôn ngữ lập trình (?) Được thiết kế để hỗ trợ bất kỳ số lượng tham số đầu vào nào nhưng chỉ có một giá trị trả về?

Trong hầu hết các ngôn ngữ, có thể "khắc phục" giới hạn đó, ví dụ: bằng cách sử dụng các tham số ngoài, trả về con trỏ hoặc bằng cách xác định / trả về các cấu trúc / lớp. Nhưng có vẻ lạ khi các ngôn ngữ lập trình không được thiết kế để hỗ trợ nhiều giá trị trả về theo cách "tự nhiên" hơn.

Có một lời giải thích cho điều này?


40
vì bạn có thể trả về một mảng ...
nathan hayfield

6
Vậy tại sao không chỉ cho phép một đối số? Tôi nghi ngờ nó gắn liền với lời nói. Lấy một số ý tưởng, đưa chúng vào một điều mà bạn trở lại với bất kỳ ai may mắn / không may mắn được lắng nghe. Sự trở lại gần giống như một ý kiến. "đây" là những gì bạn làm với "những cái này."
Erik Reppen

30
Tôi tin rằng cách tiếp cận của python khá đơn giản và thanh lịch: khi bạn phải trả về nhiều giá trị, chỉ cần trả về một tuple: def f(): return (1,2,3)và sau đó bạn có thể sử dụng tuple-unpacking để "tách" tuple : a,b,c = f() #a=1,b=2,c=3. Không cần tạo một mảng và trích xuất các phần tử theo cách thủ công, không cần xác định bất kỳ lớp mới nào.
Bakuriu

7
Tôi có thể thông báo cho bạn rằng Matlab có số lượng giá trị trả về thay đổi. Số lượng đối số đầu ra được xác định bởi chữ ký gọi (ví dụ [a, b] = f()so với [a, b, c] = f()) và thu được bên trong fbởi nargout. Tôi không phải là một fan hâm mộ lớn của Matlab nhưng đôi khi điều này thực sự có ích.
gerrit

5
Tôi nghĩ rằng nếu hầu hết các ngôn ngữ lập trình được thiết kế theo cách đó là gây tranh cãi. Trong lịch sử ngôn ngữ lập trình, có một số ngôn ngữ rất phổ biến được tạo ra theo cách đó (như Pascal, C, C ++, Java, VB cổ điển), nhưng ngày nay cũng có rất nhiều ngôn ngữ khác nhận được ngày càng nhiều người hâm mộ cho phép nhiều người quay trở lại các giá trị.
Doc Brown

Câu trả lời:


58

Một số ngôn ngữ, như Python , hỗ trợ nhiều giá trị trả về nguyên bản, trong khi một số ngôn ngữ như C # hỗ trợ chúng thông qua các thư viện cơ sở của chúng.

Nhưng nói chung, ngay cả trong các ngôn ngữ hỗ trợ chúng, nhiều giá trị trả về không được sử dụng thường xuyên vì chúng cẩu thả:

  • Các hàm trả về nhiều giá trị rất khó để đặt tên rõ ràng .
  • Rất dễ nhầm thứ tự của các giá trị trả về

    (password, username) = GetUsernameAndPassword()  
    

    (Vì lý do tương tự, nhiều người tránh có quá nhiều tham số cho một hàm; một số người thậm chí còn nói rằng một hàm không bao giờ nên có hai tham số cùng loại!)

  • Các ngôn ngữ OOP đã có một sự thay thế tốt hơn cho nhiều giá trị trả về: các lớp.
    Chúng được gõ mạnh hơn, chúng giữ các giá trị trả về được nhóm thành một đơn vị logic và chúng giữ tên của (thuộc tính) của các giá trị trả về nhất quán trong tất cả các sử dụng.

Một nơi họ đang khá thuận tiện là trong ngôn ngữ (như Python), nơi nhiều giá trị lợi nhuận từ một chức năng có thể được sử dụng như nhiều thông số đầu vào khác. Nhưng, các trường hợp sử dụng trong đó đây là một thiết kế tốt hơn so với sử dụng một lớp là khá mỏng.


50
Thật khó để nói rằng trả lại một tuple là trả lại nhiều thứ. Nó trở lại một tuple . Mã bạn đã viết chỉ cần giải nén nó một cách sạch sẽ bằng cách sử dụng một số đường cú pháp.

10
@Lego: Tôi không thấy sự khác biệt - một tuple theo định nghĩa nhiều giá trị. Điều gì bạn sẽ xem xét "nhiều giá trị trả về", nếu không phải điều này?
BlueRaja - Daniel Pflughoeft

19
Đó là một sự khác biệt thực sự mơ hồ, nhưng hãy xem xét một Tuple trống rỗng (). Đó là một điều hay không những điều? Cá nhân, tôi sẽ nói một điều. Tôi có thể chỉ định x = ()tốt, giống như tôi có thể chỉ định x = randomTuple(). Năm thứ hai nếu tuple trở rỗng hay không, tôi vẫn có thể gán một trong những trở tuple đến x.

19
... Tôi chưa bao giờ tuyên bố rằng các bộ dữ liệu không thể được sử dụng cho những thứ khác. Nhưng lập luận rằng "Python không hỗ trợ nhiều giá trị trả về, nó hỗ trợ các bộ dữ liệu" chỉ đơn giản là cực kỳ vô nghĩa. Đây vẫn là một câu trả lời đúng.
BlueRaja - Daniel Pflughoeft

14
Cả tuples và class đều không phải là "nhiều giá trị".
Andres F.

54

Bởi vì các hàm là các cấu trúc toán học thực hiện tính toán và trả về kết quả. Thật vậy, phần lớn "dưới vỏ bọc" của không ít ngôn ngữ lập trình chỉ tập trung vào một đầu vào và một đầu ra, với nhiều đầu vào chỉ là một lớp bọc mỏng xung quanh đầu vào - và khi một đầu ra giá trị duy nhất không hoạt động, sử dụng một đầu ra cấu trúc gắn kết (hoặc tuple, hoặc Maybe) là đầu ra (mặc dù giá trị trả về "đơn" đó bao gồm nhiều giá trị).

Điều này đã không thay đổi bởi vì các lập trình viên đã tìm thấy outcác tham số là các cấu trúc vụng về, chỉ hữu ích trong một tập hợp các kịch bản giới hạn. Giống như nhiều thứ khác, sự hỗ trợ không có ở đó bởi vì nhu cầu / nhu cầu không có ở đó.


5
@FrustratedWithFormsDesigner - điều này xuất hiện một chút trong một câu hỏi gần đây . Tôi có thể đếm bằng một tay số lần tôi muốn nhiều đầu ra trong 20 năm.
Telastyn

61
Các hàm trong Toán học và các hàm trong hầu hết các ngôn ngữ lập trình là hai con thú rất khác nhau.
tdammers

17
@tdammers trong những ngày đầu, họ rất giống nhau về suy nghĩ. Fortran, pascal và những nơi tương tự chịu ảnh hưởng của toán học nhiều hơn kiến ​​trúc điện toán.

9
@tdammers - Làm sao vậy? Ý tôi là đối với hầu hết các ngôn ngữ, nó kết thúc với tính toán lambda cuối cùng - một đầu vào, một đầu ra, không có tác dụng phụ. Tất cả mọi thứ khác là một mô phỏng / hack trên đó. Các chức năng lập trình có thể không thuần túy theo nghĩa nhiều đầu vào có thể mang lại cùng một đầu ra nhưng tinh thần là có.
Telastyn

16
@SteveEvers: thật không may là cái tên "hàm" đã chiếm lĩnh trong lập trình mệnh lệnh, thay vì "thủ tục" hay "thủ tục" thích hợp hơn. Trong lập trình hàm, một hàm giống với các hàm Toán học chặt chẽ hơn nhiều.
tdammers

35

Trong toán học, hàm "được xác định rõ" là một hàm chỉ có 1 đầu ra cho một đầu vào nhất định (như một lưu ý phụ, bạn chỉ có thể có các hàm đầu vào duy nhất và vẫn nhận được nhiều đầu vào bằng cách sử dụng currying ).

Đối với các hàm đa giá trị (ví dụ: căn bậc hai của một số nguyên dương chẳng hạn), đủ để trả về một bộ sưu tập hoặc chuỗi các giá trị.

Đối với các loại hàm mà bạn đang nói đến (ví dụ: các hàm trả về nhiều giá trị, các loại khác nhau ) Tôi thấy nó hơi khác so với bạn có vẻ: Tôi thấy sự cần thiết / sử dụng các tham số như một cách giải quyết để thiết kế tốt hơn hoặc cấu trúc dữ liệu hữu ích hơn. Ví dụ, tôi thích nếu *.TryParse(...)các phương thức trả về một Maybe<T>đơn nguyên thay vì sử dụng tham số ngoài. Hãy nghĩ về mã này trong F #:

let s = "1"
match tryParse s with
| Some(i) -> // do whatever with i
| None -> // failed to parse

Trình biên dịch / IDE / hỗ trợ phân tích là rất tốt cho các cấu trúc này. Điều này sẽ giải quyết phần lớn "nhu cầu" cho các thông số. Thành thật mà nói, tôi không thể nghĩ ra bất kỳ phương pháp nào khác ngoài việc đây sẽ không phải là giải pháp.

Đối với các kịch bản khác - những tình huống tôi không thể nhớ - một Tuple đơn giản đủ.


1
Ngoài ra, tôi thực sự muốn có thể viết bằng C #: var (value, success) = ParseInt("foo");loại thời gian biên dịch được kiểm tra vì (int, bool) ParseInt(string s) { }đã được khai báo. Tôi biết điều này có thể được thực hiện với thuốc generic, nhưng nó vẫn sẽ làm cho một bổ sung ngôn ngữ tốt đẹp.
Mặt nhăn nhó tuyệt vọng

10
@GrimaceofDespair những gì bạn thực sự muốn là phá hủy cú pháp, không phải nhiều giá trị trả về.
Domenic

2
@warren: Vâng. Xem ở đây và lưu ý rằng một giải pháp như vậy sẽ không liên tục: en.wikipedia.org/wiki/Well-def định
Steven Evers

4
Khái niệm toán học về tính xác định tốt không liên quan gì đến số lượng đầu ra mà hàm trả về. Nó có nghĩa là các đầu ra sẽ luôn giống nhau nếu đầu vào giống nhau. Nói một cách chính xác, các hàm toán học trả về một giá trị, nhưng giá trị đó thường là một tuple. Đối với một nhà toán học, về cơ bản không có sự khác biệt giữa điều này và trả về nhiều giá trị. Lập luận rằng các hàm lập trình chỉ nên trả về một giá trị vì đó là những hàm toán học không hấp dẫn lắm.
Michael Siler

1
@MichaelSiler (Tôi đồng ý với nhận xét của bạn) Nhưng xin lưu ý rằng đối số có thể đảo ngược: "đối số mà các hàm chương trình có thể trả về nhiều giá trị vì chúng có thể trả về một giá trị tuple duy nhất không hấp dẫn lắm" :)
Andres F.

23

Ngoài những gì đã được nói khi bạn nhìn vào các mô hình được sử dụng trong lắp ráp khi một hàm trả về, nó để lại một con trỏ tới đối tượng trả về trong một thanh ghi cụ thể. Nếu họ sử dụng biến / nhiều thanh ghi, hàm gọi sẽ không biết nơi nhận được giá trị trả về nếu hàm đó nằm trong thư viện. Vì vậy, điều đó sẽ làm cho việc liên kết đến các thư viện trở nên khó khăn và thay vì thiết lập một số lượng con trỏ có thể trả lại tùy ý mà họ đã đi với một. Các ngôn ngữ cấp cao hơn không hoàn toàn có lý do.


Ah! Điểm rất thú vị của chi tiết. +1!
Steven Evers

Đây phải là câu trả lời được chấp nhận. Thông thường mọi người nghĩ về máy mục tiêu khi họ xây dựng trình biên dịch. Một sự tương tự khác là tại sao chúng ta có int, float, char / string, v.v. bởi vì đó là những gì được hỗ trợ bởi máy mục tiêu. Ngay cả khi mục tiêu không phải là kim loại trần (ví dụ jvm), bạn vẫn muốn có được hiệu suất tốt bằng cách không mô phỏng quá nhiều.
imel96

26
... Bạn có thể dễ dàng xác định quy ước gọi để trả về nhiều giá trị từ một hàm, gần giống như cách bạn có thể xác định quy ước gọi để truyền nhiều giá trị cho một hàm. Đây không phải là một câu trả lời. -1
BlueRaja - Daniel Pflughoeft

2
Sẽ rất thú vị nếu biết các công cụ thực thi dựa trên ngăn xếp (JVM, CLR) đã từng xem xét / cho phép nhiều giá trị trả về hay chưa .. nó sẽ khá dễ dàng; người gọi chỉ cần bật đúng số giá trị, giống như nó đẩy đúng số lượng đối số!
Lorenzo Dematté

1
@David không, cdecl cho phép (về mặt lý thuyết) không giới hạn số lượng tham số (đó là lý do tại sao các hàm varargs có thể) . Mặc dù một số trình biên dịch C có thể giới hạn bạn tới vài chục hoặc hàng trăm đối số cho mỗi hàm, điều mà tôi nghĩ vẫn còn hơn cả hợp lý -_-
BlueRaja - Danny Pflughoeft

18

Rất nhiều trường hợp sử dụng mà trước đây bạn đã sử dụng nhiều giá trị trả về đơn giản là không cần thiết nữa với các tính năng ngôn ngữ hiện đại. Bạn muốn trả lại một mã lỗi? Ném một ngoại lệ hoặc trả lại một Either<T, Throwable>. Bạn muốn trả về một kết quả tùy chọn? Trả lại một Option<T>. Bạn muốn trả về một trong nhiều loại? Trả lại một Either<T1, T2>hoặc một liên minh được gắn thẻ.

Và ngay cả trong trường hợp bạn thực sự cần để trả lại nhiều giá trị, ngôn ngữ hiện đại thường hỗ trợ các bộ hoặc một số loại cấu trúc dữ liệu (danh sách, mảng, từ điển) hoặc các đối tượng cũng như một số hình thức destructuring ràng buộc hoặc phù hợp với mô hình, mà làm cho bao bì lên nhiều giá trị của bạn thành một giá trị duy nhất và sau đó phá hủy nó thành nhiều giá trị tầm thường.

Dưới đây là một vài ví dụ về các ngôn ngữ không hỗ trợ trả về nhiều giá trị. Tôi thực sự không thấy việc thêm hỗ trợ cho nhiều giá trị trả về sẽ làm cho chúng rõ ràng hơn đáng kể để bù đắp chi phí của một tính năng ngôn ngữ mới.

Hồng ngọc

def foo; return 1, 2, 3 end

one, two, three = foo

one
# => 1

three
# => 3

Con trăn

def foo(): return 1, 2, 3

one, two, three = foo()

one
# >>> 1

three
# >>> 3

Scala

def foo = (1, 2, 3)

val (one, two, three) = foo
// => one:   Int = 1
// => two:   Int = 2
// => three: Int = 3

Haskell

let foo = (1, 2, 3)

let (one, two, three) = foo

one
-- > 1

three
-- > 3

Perl6

sub foo { 1, 2, 3 }

my ($one, $two, $three) = foo

$one
# > 1

$three
# > 3

1
Tôi nghĩ một khía cạnh là, trong một số ngôn ngữ (như Matlab), một hàm có thể linh hoạt trong số lượng giá trị mà nó trả về; thấy bình luận của tôi ở trên . Có nhiều khía cạnh trong Matlab tôi không thích, nhưng đây là một trong số ít tính năng (có lẽ là duy nhất) tôi bỏ lỡ khi chuyển từ Matlab sang ví dụ Python.
gerrit

1
Nhưng những ngôn ngữ động như Python hay Ruby thì sao? Giả sử tôi viết một cái gì đó giống như sorthàm Matlab : sorted = sort(array)chỉ trả về mảng đã sắp xếp, trong khi [sorted, indices] = sort(array)trả về cả hai. Cách duy nhất tôi có thể nghĩ trong Python là truyền một lá cờ sortdọc theo dòng sort(array, nout=2)hoặc sort(array, indices=True).
gerrit

2
@MikeCellini Tôi không nghĩ vậy. Một hàm có thể cho biết có bao nhiêu đối số đầu ra mà hàm được gọi ( [a, b, c] = func(some, thing)) và hành động tương ứng. Điều này rất hữu ích, ví dụ, nếu tính toán đối số đầu ra đầu tiên là rẻ nhưng tính toán đối số thứ hai thì tốn kém. Tôi không quen thuộc với bất kỳ ngôn ngữ nào khác, nơi tương đương với Matlabs nargoutcó sẵn trong thời gian chạy.
gerrit

1
@gerrit giải pháp chính xác trong Python là viết cái này : sorted, _ = sort(array).
Miles Rout

1
@MilesRout: Và sorthàm có thể cho biết rằng nó không cần tính toán các chỉ số? Thật tuyệt, tôi không biết điều đó.
Jörg W Mittag

12

Lý do thực sự khiến một giá trị trả về duy nhất rất phổ biến là các biểu thức được sử dụng trong rất nhiều ngôn ngữ. Trong bất kỳ ngôn ngữ nào mà bạn có thể có một biểu thức như x + 1bạn đã nghĩ về các giá trị trả về đơn vì bạn đánh giá một biểu thức trong đầu bằng cách chia nó thành từng phần và quyết định giá trị của từng phần. Bạn nhìn vào xvà quyết định rằng giá trị của nó là 3 (ví dụ) và bạn nhìn vào 1 và sau đó bạn nhìn vàox + 1và đặt tất cả lại với nhau để quyết định rằng giá trị của tổng thể là 4. Mỗi phần cú pháp của biểu thức có một giá trị, không phải bất kỳ số lượng giá trị nào khác; đó là ngữ nghĩa tự nhiên của các biểu thức mà mọi người đều mong đợi. Ngay cả khi một hàm trả về một cặp giá trị, nó vẫn thực sự trả về một giá trị thực hiện công việc của hai giá trị, bởi vì ý tưởng về hàm trả về hai giá trị mà bằng cách nào đó không được gói vào một bộ sưu tập là quá kỳ lạ.

Mọi người không muốn đối phó với các ngữ nghĩa thay thế sẽ được yêu cầu để các hàm trả về nhiều hơn một giá trị. Ví dụ: trong ngôn ngữ dựa trên ngăn xếp như Forth, bạn có thể có bất kỳ số lượng giá trị trả về nào vì mỗi hàm chỉ cần sửa đổi đỉnh của ngăn xếp, bật đầu vào và đẩy đầu ra theo ý muốn. Đó là lý do tại sao Forth không có loại biểu thức mà các ngôn ngữ bình thường có.

Perl là một ngôn ngữ khác đôi khi có thể hoạt động giống như các hàm đang trả về nhiều giá trị, mặc dù nó thường chỉ được coi là trả về một danh sách. Cách liệt kê "nội suy" trong Perl cung cấp cho chúng tôi các danh sách (1, foo(), 3)có thể có 3 yếu tố như hầu hết mọi người không biết Perl mong đợi, nhưng có thể dễ dàng chỉ có 2 yếu tố, 4 yếu tố hoặc số lượng yếu tố lớn hơn tùy thuộc vào foo(). Danh sách trong Perl được làm phẳng để danh sách cú pháp không luôn có ngữ nghĩa của danh sách; nó có thể chỉ là một phần của một danh sách lớn hơn.

Một cách khác để có các hàm trả về nhiều giá trị sẽ là có một ngữ nghĩa biểu thức thay thế trong đó bất kỳ biểu thức nào cũng có thể có nhiều giá trị và mỗi giá trị đại diện cho một khả năng. Lấy x + 1lại, nhưng lần này hãy tưởng tượng rằng xcó hai giá trị {3, 4}, thì các giá trị của x + 1sẽ là {4, 5} và các giá trị của x + xsẽ là {6, 8} hoặc có thể {6, 7, 8} , tùy thuộc vào việc một đánh giá có được phép sử dụng nhiều giá trị cho hay không x. Một ngôn ngữ như thế có thể được triển khai bằng cách sử dụng quay lui giống như sử dụng Prolog để đưa ra nhiều câu trả lời cho một truy vấn.

Nói tóm lại, một lời gọi hàm là một đơn vị cú pháp duy nhất và một đơn vị cú pháp duy nhất có một giá trị duy nhất trong ngữ nghĩa biểu thức mà tất cả chúng ta đều biết và yêu thích. Bất kỳ ngữ nghĩa nào khác sẽ buộc bạn vào những cách làm việc kỳ lạ, như Perl, Prolog hoặc Forth.


9

Như được đề xuất trong câu trả lời này , đó là vấn đề hỗ trợ phần cứng, mặc dù truyền thống trong thiết kế ngôn ngữ cũng đóng một vai trò.

Khi một hàm trả về, nó để lại một con trỏ tới đối tượng trả về trong một thanh ghi cụ thể

Trong ba ngôn ngữ đầu tiên, Fortran, Lisp và COBOL, ngôn ngữ đầu tiên sử dụng một giá trị trả về duy nhất vì nó được mô hình hóa trên toán học. Cái thứ hai trả về một số lượng tham số tùy ý giống như cách nó nhận được: dưới dạng một danh sách (cũng có thể lập luận rằng nó chỉ được truyền và trả về một tham số duy nhất: địa chỉ của danh sách). Giá trị thứ ba trả về 0 hoặc 1 giá trị.

Những ngôn ngữ đầu tiên này ảnh hưởng rất nhiều đến thiết kế của các ngôn ngữ đi theo chúng, mặc dù ngôn ngữ duy nhất trả về nhiều giá trị, Lisp, không bao giờ thu hút được nhiều sự phổ biến.

Khi C đến, trong khi bị ảnh hưởng bởi các ngôn ngữ trước nó, nó đã tập trung rất nhiều vào việc sử dụng hiệu quả tài nguyên phần cứng, giữ mối liên hệ chặt chẽ giữa những gì ngôn ngữ C đã làm và mã máy thực hiện nó. Một số tính năng lâu đời nhất của nó, chẳng hạn như các biến "tự động" và "đăng ký", là kết quả của triết lý thiết kế đó.

Cũng phải chỉ ra rằng ngôn ngữ lắp ráp đã phổ biến rộng rãi cho đến những năm 80, khi cuối cùng nó bắt đầu bị loại bỏ khỏi sự phát triển chính thống. Những người viết trình biên dịch và tạo ngôn ngữ đã quen thuộc với lắp ráp, và, phần lớn, giữ cho những gì hoạt động tốt nhất ở đó.

Hầu hết các ngôn ngữ tách khỏi quy tắc này không bao giờ phổ biến, và do đó, không bao giờ đóng vai trò mạnh mẽ ảnh hưởng đến quyết định của các nhà thiết kế ngôn ngữ (tất nhiên, những người được truyền cảm hứng từ những gì họ biết).

Vì vậy, hãy đi kiểm tra ngôn ngữ lắp ráp. Trước tiên, hãy nhìn vào 6502 , một bộ vi xử lý năm 1975 được sử dụng nổi tiếng bởi các máy vi tính Apple II và VIC-20. Nó rất yếu so với những gì được sử dụng trong máy tính lớn và máy tính mini thời bấy giờ, mặc dù mạnh mẽ so với các máy tính đầu tiên của 20, 30 năm trước, vào buổi bình minh của ngôn ngữ lập trình.

Nếu bạn nhìn vào mô tả kỹ thuật, nó có 5 thanh ghi cộng với một vài cờ một bit. Thanh ghi "đầy đủ" duy nhất là Bộ đếm chương trình (PC) - thanh ghi đó trỏ đến lệnh tiếp theo sẽ được thực thi. Các thanh ghi khác trong đó bộ tích lũy (A), hai thanh ghi "chỉ mục" (X và Y) và con trỏ ngăn xếp (SP).

Gọi một chương trình con sẽ đặt PC vào bộ nhớ được chỉ ra bởi SP, và sau đó làm giảm SP. Trở về từ một chương trình con làm việc ngược lại. Người ta có thể đẩy và kéo các giá trị khác trên ngăn xếp, nhưng rất khó để tham chiếu bộ nhớ liên quan đến SP, vì vậy việc viết các chương trình con được đăng ký lại rất khó khăn. Điều này chúng ta coi là đương nhiên, gọi một chương trình con bất cứ lúc nào chúng ta cảm thấy, không quá phổ biến trên kiến ​​trúc này. Thông thường, một "ngăn xếp" riêng biệt sẽ được tạo để các tham số và địa chỉ trả về chương trình con sẽ được giữ riêng.

Nếu bạn nhìn vào bộ xử lý đã truyền cảm hứng cho 6502, 6800 , nó có một thanh ghi bổ sung, Thanh ghi chỉ mục (IX), rộng như SP, có thể nhận giá trị từ SP.

Trên máy, việc gọi một chương trình con được đăng ký lại bao gồm đẩy các tham số trên ngăn xếp, đẩy PC, thay đổi PC sang địa chỉ mới và sau đó chương trình con sẽ đẩy các biến cục bộ của nó lên ngăn xếp . Vì số lượng biến và tham số cục bộ đã biết, việc giải quyết chúng có thể được thực hiện liên quan đến ngăn xếp. Ví dụ, một hàm nhận hai tham số và có hai biến cục bộ sẽ trông như thế này:

SP + 8: param 2
SP + 6: param 1
SP + 4: return address
SP + 2: local 2
SP + 0: local 1

Nó có thể được gọi bất kỳ số lần vì tất cả không gian tạm thời nằm trên ngăn xếp.

Các 8080 , được sử dụng trên TRS-80 và một loạt các CP / M vi tính dựa trên có thể làm điều gì đó tương tự như 6800, bằng cách đẩy SP trên stack và sau đó popping nó trên đăng ký gián tiếp của nó, HL.

Đây là một cách rất phổ biến để thực hiện mọi thứ và nó thậm chí còn hỗ trợ nhiều hơn cho các bộ xử lý hiện đại hơn, với Con trỏ cơ sở giúp loại bỏ tất cả các biến cục bộ trước khi trở lại dễ dàng.

Vấn đề là, làm thế nào để bạn trả lại bất cứ điều gì ? Các bộ đăng ký bộ xử lý không có nhiều từ rất sớm và người ta thường cần sử dụng một số trong số chúng thậm chí để tìm ra bộ nhớ nào cần xử lý. Việc trả lại mọi thứ trên ngăn xếp sẽ rất phức tạp: bạn phải bật mọi thứ, lưu PC, đẩy các tham số trả về (sẽ được lưu trữ trong khi đó?), Sau đó đẩy PC lại và quay lại.

Vì vậy, những gì thường được thực hiện là đặt trước một đăng ký cho giá trị trả lại. Mã gọi biết giá trị trả về sẽ nằm trong một thanh ghi cụ thể, sẽ phải được giữ lại cho đến khi có thể được lưu hoặc sử dụng.

Hãy xem xét một ngôn ngữ cho phép nhiều giá trị trả về: Forth. Những gì Forth làm là giữ một ngăn xếp trả lại (RP) và ngăn xếp dữ liệu (SP) riêng biệt, sao cho tất cả một chức năng phải làm là bật tất cả các tham số của nó và để lại các giá trị trả về trên ngăn xếp. Vì ngăn xếp trả lại là riêng biệt, nó không cản trở.

Là một người đã học ngôn ngữ lắp ráp và Forth trong sáu tháng đầu tiên trải nghiệm với máy tính, nhiều giá trị trả về trông hoàn toàn bình thường đối với tôi. Các toán tử như Forth's /mod, trả về phép chia số nguyên phần còn lại, có vẻ hiển nhiên. Mặt khác, tôi có thể dễ dàng thấy một người có kinh nghiệm ban đầu là tâm trí C thấy khái niệm đó kỳ lạ như thế nào: nó đi ngược lại những kỳ vọng đã ăn sâu của họ về "chức năng" là gì.

Đối với môn toán ... tốt, tôi đã lập trình máy tính theo cách trước khi tôi có được các chức năng trong các lớp toán. Có một phần của toàn bộ CS và các ngôn ngữ lập trình mà bị ảnh hưởng bởi toán học, nhưng, sau đó một lần nữa, có cả một bộ phận mà không phải là.

Vì vậy, chúng ta có sự hợp nhất của các yếu tố trong đó toán học ảnh hưởng đến thiết kế ngôn ngữ ban đầu, trong đó các ràng buộc về phần cứng chỉ ra những gì dễ dàng thực hiện và các ngôn ngữ phổ biến ảnh hưởng đến cách thức phát triển phần cứng (máy Lisp và bộ xử lý máy Forth trong quá trình này).


@gnat Việc sử dụng "để thông báo" như trong "để cung cấp chất lượng thiết yếu" là có chủ ý.
Daniel C. Sobral

cảm thấy thoải mái để quay trở lại nếu bạn cảm thấy mạnh mẽ về điều này; mỗi ảnh hưởng đọc của tôi phù hợp hơn một chút ở đây: "ảnh hưởng ... theo một cách quan trọng"
gnat

1
+1 Như đã chỉ ra, số lượng CPU đăng ký thưa thớt so với bộ CPU hiện đại (cũng được sử dụng trong nhiều ABI, ví dụ x64 abi) có thể là một thay đổi trò chơi và lý do thuyết phục trước đây chỉ trả về 1 giá trị ngày nay có thể chỉ là một lý do lịch sử.
BitTickler

Tôi không tin rằng các CPU micro 8 bit đầu tiên có nhiều ảnh hưởng đến thiết kế ngôn ngữ và những thứ được cho là cần thiết trong các quy ước gọi C hoặc Fortran trên mọi kiến ​​trúc. Fortran giả định rằng bạn có thể vượt qua các đối số mảng (về cơ bản là con trỏ). Vì vậy, bạn đã có vấn đề triển khai lớn đối với Fortran bình thường trên các máy như 6502, do thiếu chế độ địa chỉ cho con trỏ + chỉ mục, như đã thảo luận trong câu trả lời của bạn và tại sao trình biên dịch C đến Z80 tạo mã kém? trên máy tính retro.SE.
Peter Cordes

Fortran, giống như C, cũng cho rằng bạn có thể vượt qua một số lượng đối số tùy ý và có quyền truy cập ngẫu nhiên vào chúng và một lượng người địa phương tùy ý, phải không? Như bạn vừa giải thích, bạn không thể thực hiện điều đó một cách dễ dàng trên 6502 vì việc đánh địa chỉ liên quan đến ngăn xếp không phải là một điều, trừ khi bạn từ bỏ reentrancy. Bạn có thể đưa chúng vào bộ nhớ tĩnh. Nếu bạn có thể vượt qua một danh sách arg tùy ý, bạn có thể thêm các tham số ẩn bổ sung cho các giá trị trả về (ví dụ: ngoài giá trị đầu tiên) không phù hợp với các thanh ghi.
Peter Cordes

7

Các ngôn ngữ chức năng mà tôi biết có thể trả về nhiều giá trị một cách dễ dàng thông qua việc sử dụng các bộ dữ liệu (trong các ngôn ngữ được nhập động, bạn thậm chí có thể sử dụng danh sách). Tuples cũng được hỗ trợ trong các ngôn ngữ khác:

f :: Int -> (Int, Int)
f x = (x - 1, x + 1)

// Even C++ have tuples - see Boost.Graph for use
std::pair<int, int> f(int x) {
  return std::make_pair(x - 1, x + 1);
}

Trong ví dụ trên, flà một hàm trả về 2 ints.

Tương tự, ML, Haskell, F #, v.v., cũng có thể trả về cấu trúc dữ liệu (con trỏ ở mức quá thấp đối với hầu hết các ngôn ngữ). Tôi chưa nghe nói về một ngôn ngữ GP hiện đại với một hạn chế như vậy:

data MyValue = MyValue Int Int

g :: Int -> MyValue
g x = MyValue (x - 1, x + 1)

Cuối cùng, outcác tham số có thể được mô phỏng ngay cả trong các ngôn ngữ chức năng bằng cách IORef. Có một số lý do tại sao không có hỗ trợ riêng cho các biến ngoài trong hầu hết các ngôn ngữ:

  • Ngữ nghĩa không rõ ràng : Hàm sau có in 0 hay 1 không? Tôi biết các ngôn ngữ sẽ in 0 và những ngôn ngữ sẽ in 1. Có những lợi ích cho cả hai ngôn ngữ này (cả về hiệu suất, cũng như phù hợp với mô hình tinh thần của lập trình viên):

    int x;
    
    int f(out int y) {
      x = 0;
      y = 1;
      printf("%d\n", x);
    }
    f(out x);
    
  • Hiệu ứng không cục bộ : Như trong ví dụ trên, bạn có thể thấy rằng bạn có thể có một chuỗi dài và chức năng trong cùng ảnh hưởng đến trạng thái toàn cầu. Nói chung, nó làm cho khó khăn hơn để suy luận về các yêu cầu của chức năng là gì, và nếu thay đổi là hợp pháp. Vì hầu hết các mô hình hiện đại đều cố gắng bản địa hóa các hiệu ứng (đóng gói trong OOP) hoặc loại bỏ các tác dụng phụ (lập trình chức năng), nó mâu thuẫn với các mô hình đó.

  • Là dự phòng : Nếu bạn có bộ dữ liệu, bạn có 99% chức năng của các outtham số và 100% sử dụng thành ngữ. Nếu bạn thêm con trỏ vào hỗn hợp, bạn chiếm 1% còn lại.

Tôi gặp khó khăn khi đặt tên một ngôn ngữ không thể trả về nhiều giá trị bằng cách sử dụng một tuple, lớp hoặc outtham số (và trong hầu hết các trường hợp, 2 hoặc nhiều hơn các phương thức đó được cho phép).


+1 Để đề cập đến cách các ngôn ngữ chức năng xử lý việc này theo cách thanh lịch & không đau đớn.
Andres F.

1
Về mặt kỹ thuật, bạn vẫn trả về một giá trị duy nhất: D (Chỉ là giá trị đơn lẻ này không đáng kể để phân tách thành nhiều giá trị).
Thomas Eding

1
Tôi sẽ nói rằng một tham số với ngữ nghĩa "ngoài" thực sự sẽ hoạt động như một trình biên dịch tạm thời được sao chép đến đích khi một phương thức thoát bình thường; một biến có ngữ nghĩa "inout" sẽ hoạt động như một trình biên dịch tạm thời được tải từ biến truyền vào khi nhập và ghi lại khi thoát; một với ngữ nghĩa "ref" sẽ hoạt động như một bí danh. Các tham số được gọi là "out" của C # thực sự là các tham số "ref" và hoạt động như vậy.
supercat

1
Bộ "giải pháp" cũng không miễn phí. Nó chặn các cơ hội tối ưu hóa. Nếu tồn tại ABI cho phép trả về N giá trị trong các thanh ghi CPU, trình biên dịch thực sự có thể tối ưu hóa, thay vì tạo một cá thể tuple và xây dựng nó.
BitTickler

1
@BitTickler không có gì ngăn cản trả lại n trường cấu trúc đầu tiên được thông qua bởi các thanh ghi nếu bạn kiểm soát ABI.
Maciej Piechotka

6

Tôi nghĩ rằng đó là vì biểu thức , chẳng hạn như (a + b[i]) * c.

Biểu thức bao gồm các giá trị "số ít". Do đó, một hàm trả về một giá trị số ít có thể được sử dụng trực tiếp trong một biểu thức, thay cho bất kỳ trong bốn biến được hiển thị ở trên. Hàm đa đầu ra ít nhất là hơi vụng về trong một biểu thức.

Cá nhân tôi cảm thấy rằng đây là những điều đó là đặc biệt về một giá trị trả về số ít. Bạn có thể giải quyết vấn đề này bằng cách thêm cú pháp để chỉ định giá trị nào trong số nhiều giá trị trả về mà bạn muốn sử dụng trong một biểu thức, nhưng nó chắc chắn sẽ vụng về hơn so với ký hiệu toán học cũ, ngắn gọn và quen thuộc với mọi người.


4

Nó làm phức tạp cú pháp một chút, nhưng không có lý do chính đáng nào ở cấp độ thực thi không cho phép nó. Trái với một số phản hồi khác, trả về nhiều giá trị, nếu có, dẫn đến mã rõ ràng và hiệu quả hơn. Tôi không thể đếm được mức độ thường xuyên tôi muốn tôi có thể trả lại X Y, hoặc boolean "thành công" giá trị hữu ích.


3
Bạn có thể cung cấp một ví dụ về nơi nhiều lợi nhuận cung cấp mã rõ ràng hơn và / hoặc hiệu quả hơn?
Steven Evers

3
Ví dụ, trong lập trình COM C ++, nhiều hàm có một [out]tham số, nhưng hầu như tất cả đều trả về một HRESULT(mã lỗi). Sẽ là khá thực tế khi chỉ cần có một cặp ở đó. Trong các ngôn ngữ có hỗ trợ tốt cho các bộ dữ liệu, chẳng hạn như Python, điều này được sử dụng trong rất nhiều mã tôi đã thấy.
Felix Dombek

Trong một số ngôn ngữ, bạn sẽ trả về một vectơ có tọa độ X và Y và trả về bất kỳ giá trị hữu ích nào sẽ được tính là "thành công" với các ngoại lệ, có thể mang giá trị hữu ích đó, được sử dụng cho thất bại.
doppelgreener

3
Rất nhiều thời gian bạn kết thúc việc mã hóa thông tin thành giá trị trả về theo những cách không rõ ràng - tức là; giá trị âm là mã lỗi, giá trị dương là kết quả. Yuk. Truy cập vào bảng băm, sẽ luôn lộn xộn để cho biết nếu mục đó được tìm thấy và cũng trả lại mục đó.
ddyer

@SteveEvers Hàm Matlab sortthường sắp xếp một mảng : sorted_array = sort(array). Đôi khi tôi cũng cần các chỉ số tương ứng : [sorted_array, indices] = sort(array). Đôi khi tôi chỉ muốn các chỉ số: [~, indices]= sort (mảng) . The function sort` thực sự có thể cho biết có bao nhiêu đối số đầu ra là cần thiết, vì vậy nếu cần thêm công việc cho 2 đầu ra so với 1, nó chỉ có thể tính toán các đầu ra đó nếu cần.
gerrit

2

Trong hầu hết các ngôn ngữ có chức năng được hỗ trợ, bạn có thể sử dụng lệnh gọi bất cứ nơi nào có thể sử dụng biến loại đó: -

x = n + sqrt(y);

Nếu hàm trả về nhiều hơn một giá trị thì nó sẽ không hoạt động. Các ngôn ngữ được gõ động như python sẽ cho phép bạn thực hiện điều này, nhưng, trong hầu hết các trường hợp, nó sẽ gây ra lỗi thời gian chạy trừ khi nó có thể giải quyết một cái gì đó hợp lý để làm với một tuple ở giữa một phương trình.


5
Chỉ không sử dụng các chức năng không phù hợp. Điều này không khác với "vấn đề" được đặt ra bởi các hàm không trả về giá trị hoặc trả về giá trị không phải là số.
ddyer

3
Trong các ngôn ngữ tôi đã sử dụng cung cấp nhiều giá trị trả về (ví dụ SciLab), giá trị trả về đầu tiên là đặc quyền và sẽ được sử dụng trong trường hợp chỉ cần một giá trị. Vì vậy, không có vấn đề thực sự ở đó.
Photon

Và ngay cả khi chúng không hoạt động, như với bộ giải nén của Python, bạn có thể chọn cái nào bạn muốn:foo()[0]
Izkata

Chính xác, nếu một hàm trả về 2 giá trị, thì kiểu trả về của nó là 2 giá trị, không phải là một giá trị. Ngôn ngữ lập trình không nên đọc suy nghĩ của bạn.
Đánh dấu E. Haase

1

Tôi chỉ muốn xây dựng dựa trên câu trả lời của Harvey. Ban đầu tôi đã tìm thấy câu hỏi này trên một trang web công nghệ tin tức (arstechnica) và tìm thấy một lời giải thích tuyệt vời mà tôi cảm thấy thực sự trả lời cốt lõi của câu hỏi này và thiếu từ tất cả các câu trả lời khác (trừ Harvey):

Nguồn gốc của một lần trả về từ các chức năng nằm trong mã máy. Ở cấp mã máy, một hàm có thể trả về một giá trị trong thanh ghi A (bộ tích lũy). Bất kỳ giá trị trả về khác sẽ được trên ngăn xếp.

Một ngôn ngữ hỗ trợ hai giá trị trả về sẽ biên dịch nó dưới dạng mã máy trả về một và đặt thứ hai vào ngăn xếp. Nói cách khác, giá trị trả về thứ hai sẽ kết thúc dưới dạng tham số out.

Nó giống như hỏi tại sao gán là một biến tại một thời điểm. Bạn có thể có một ngôn ngữ cho phép a, b = 1, 2 chẳng hạn. Nhưng nó sẽ kết thúc ở cấp mã máy là a = 1 theo sau là b = 2.

Có một số lý do trong việc có các cấu trúc ngôn ngữ lập trình mang một số ngữ nghĩa với những gì sẽ thực sự xảy ra khi mã được biên dịch và chạy.


Nếu các ngôn ngữ cấp thấp như C hỗ trợ nhiều giá trị trả về làm tính năng hạng nhất, các quy ước gọi C sẽ bao gồm nhiều thanh ghi giá trị trả về, giống như cách sử dụng tối đa 6 thanh ghi để truyền hàm số nguyên / con trỏ trong x86- 64 Hệ thống V ABI. (Trong thực tế x86-64 SysV không trả về các cấu trúc lên đến 16 byte được đóng gói vào cặp thanh ghi RDX: RAX. Điều này tốt nếu cấu trúc vừa được tải và sẽ được lưu trữ, nhưng chi phí giải nén thêm so với việc có các thành viên riêng biệt trong các chế độ riêng biệt nếu chúng hẹp hơn 64 bit.)
Peter Cordes

Quy ước rõ ràng sẽ là RAX, sau đó là regs chuyền. (RDI, RSI, RDX, RCX, R8, R9). Hoặc trong quy ước Windows x64, RCX, RDX, R8, R9. Nhưng vì C không thực sự có nhiều giá trị trả về, nên C ABI / quy ước gọi chỉ xác định nhiều thanh ghi trả về cho số nguyên rộng và một số cấu trúc. Xem Tiêu chuẩn cuộc gọi thủ tục cho Kiến trúc ARM®: 2 giá trị trả về riêng biệt nhưng có liên quan để biết ví dụ về việc sử dụng int int rộng để có được trình biên dịch để thực hiện asm hiệu quả để nhận 2 giá trị trả về trên ARM.
Peter Cordes

-1

Nó bắt đầu với môn toán. FORTRAN, được đặt tên cho "Công thức dịch" là trình biên dịch đầu tiên. FORTRAN đã và đang hướng đến vật lý / toán học / kỹ thuật.

COBOL, gần như cũ, không có giá trị trả lại rõ ràng; Nó hầu như không có chương trình con. Kể từ đó, nó chủ yếu là quán tính.

Ví dụ, Go có nhiều giá trị trả về và kết quả sẽ sạch hơn và ít mơ hồ hơn so với sử dụng tham số "out". Sau một chút sử dụng, nó rất tự nhiên và hiệu quả. Tôi khuyên bạn nên xem xét nhiều giá trị trả về cho tất cả các ngôn ngữ mới. Có lẽ đối với các ngôn ngữ cũ, quá.


4
điều này không trả lời câu hỏi được hỏi
gnat

@gnat như đối với tôi nó trả lời. OTOH người ta đã cần một số nền tảng để hiểu nó và người đó có thể sẽ không đặt câu hỏi ...
Netch

@Netch một người hầu như không cần nhiều nền tảng để hình dung rằng các câu như "FORTRAN ... là trình biên dịch đầu tiên" là một mớ hỗn độn. Nó thậm chí không sai. Cũng giống như phần còn lại của "câu trả lời" này
gnat

link nói rằng đã có những nỗ lực trước đó đối với các trình biên dịch, nhưng "Nhóm FORTRAN do John Backus tại IBM thường được ghi nhận là đã giới thiệu trình biên dịch hoàn chỉnh đầu tiên vào năm 1957". Câu hỏi được hỏi là tại sao chỉ có một? Như tôi đã nói. Nó chủ yếu là toán học và quán tính. Định nghĩa toán học của thuật ngữ "Hàm" yêu cầu chính xác một giá trị kết quả. Vì vậy, nó là một hình thức quen thuộc.
RickyS

-2

Có lẽ nó liên quan nhiều hơn đến di sản của cách gọi chức năng được thực hiện trong hướng dẫn máy của bộ xử lý và thực tế là tất cả các ngôn ngữ lập trình đều xuất phát từ mã máy: ví dụ: C -> Hội -> Máy.

Cách bộ xử lý thực hiện các cuộc gọi chức năng

Các chương trình đầu tiên được viết bằng mã máy và sau đó lắp ráp. Các bộ xử lý hỗ trợ các hàm gọi bằng cách đẩy một bản sao của tất cả các thanh ghi hiện tại vào ngăn xếp. Trở về từ hàm sẽ bật tập hợp các thanh ghi đã lưu từ ngăn xếp. Thông thường, một thanh ghi bị bỏ hoang để cho phép hàm trả về trả về một giá trị.

Bây giờ, về lý do tại sao các bộ xử lý được thiết kế theo cách này ... nó có thể là một câu hỏi về các hạn chế tài nguyên.

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.