Trái với những gì người khác đang nói, quá tải theo kiểu trả về là có thể và được thực hiện bởi một số ngôn ngữ hiện đại. Sự phản đối thông thường là trong mã như
int func();
string func();
int main() { func(); }
bạn không thể biết cái nào func()
đang được gọi. Điều này có thể được giải quyết theo một số cách:
- Có một phương pháp dự đoán để xác định hàm nào được gọi trong tình huống như vậy.
- Bất cứ khi nào một tình huống như vậy xảy ra, đó là một lỗi thời gian biên dịch. Tuy nhiên, có một cú pháp cho phép lập trình viên định hướng, ví dụ
int main() { (string)func(); }
.
- Đừng có tác dụng phụ. Nếu bạn không có tác dụng phụ và bạn không bao giờ sử dụng giá trị trả về của hàm, thì trình biên dịch có thể tránh việc gọi hàm ở vị trí đầu tiên.
Hai trong số các ngôn ngữ tôi thường xuyên ( ab ) sử dụng quá tải theo kiểu trả về: Perl và Haskell . Hãy để tôi mô tả những gì họ làm.
Trong Perl , có một sự phân biệt cơ bản giữa bối cảnh vô hướng và danh sách (và các bối cảnh khác, nhưng chúng ta sẽ giả vờ có hai). Mỗi hàm tích hợp trong Perl có thể làm những việc khác nhau tùy thuộc vào ngữ cảnh mà nó được gọi. Ví dụ, join
toán tử buộc liệt kê bối cảnh (trên vật được nối) trong khi scalar
toán tử buộc bối cảnh vô hướng, vì vậy hãy so sánh:
print join " ", localtime(); # printed "58 11 2 14 0 109 3 13 0" for me right now
print scalar localtime(); # printed "Wed Jan 14 02:12:44 2009" for me right now.
Mỗi toán tử trong Perl làm một cái gì đó trong bối cảnh vô hướng và một cái gì đó trong bối cảnh danh sách, và chúng có thể khác nhau, như được minh họa. (Điều này không chỉ dành cho các toán tử ngẫu nhiên như localtime
. Nếu bạn sử dụng một mảng @a
trong ngữ cảnh danh sách, nó sẽ trả về mảng, trong khi ở ngữ cảnh vô hướng, nó sẽ trả về số lượng phần tử. Ví dụ, print @a
in ra các phần tử, trong khi print 0+@a
in kích thước. ) Hơn nữa, mọi toán tử có thể buộc một bối cảnh, ví dụ như bổ sung các +
lực vô hướng. Mỗi mục trong man perlfunc
tài liệu này. Ví dụ, đây là một phần của mục nhập cho glob EXPR
:
Trong ngữ cảnh danh sách, trả về một danh sách (có thể trống) các mở rộng tên tệp trên giá trị EXPR
như shell Unix tiêu chuẩn /bin/csh
sẽ làm. Trong bối cảnh vô hướng, global lặp đi lặp lại thông qua các mở rộng tên tệp như vậy, trả về undef khi danh sách đã hết.
Bây giờ, những gì liên quan giữa danh sách và bối cảnh vô hướng? Vâng, man perlfunc
nói
Hãy nhớ quy tắc quan trọng sau: Không có quy tắc nào liên quan đến hành vi của một biểu thức trong ngữ cảnh danh sách với hành vi của nó trong ngữ cảnh vô hướng hoặc ngược lại. Nó có thể làm hai việc hoàn toàn khác nhau. Mỗi toán tử và hàm quyết định loại giá trị nào sẽ phù hợp nhất để trả về trong bối cảnh vô hướng. Một số toán tử trả về độ dài của danh sách sẽ được trả về trong ngữ cảnh danh sách. Một số toán tử trả về giá trị đầu tiên trong danh sách. Một số toán tử trả về giá trị cuối cùng trong danh sách. Một số nhà khai thác trả lại một số lượng các hoạt động thành công. Nói chung, họ làm những gì bạn muốn, trừ khi bạn muốn sự nhất quán.
Vì vậy, đó không phải là vấn đề đơn giản khi có một chức năng duy nhất và sau đó bạn thực hiện chuyển đổi đơn giản vào cuối. Trong thực tế, tôi đã chọn localtime
ví dụ cho lý do đó.
Nó không chỉ là các phần mềm tích hợp có hành vi này. Bất kỳ người dùng nào cũng có thể định nghĩa một hàm như vậy bằng cách sử dụng wantarray
, cho phép bạn phân biệt giữa danh sách, vô hướng và bối cảnh trống. Vì vậy, ví dụ, bạn có thể quyết định không làm gì nếu bạn bị gọi trong bối cảnh trống.
Bây giờ, bạn có thể phàn nàn rằng đây không phải là quá tải thực sự bởi giá trị trả về vì bạn chỉ có một chức năng, được cho biết bối cảnh mà nó được gọi và sau đó hành động theo thông tin đó. Tuy nhiên, điều này rõ ràng tương đương (và tương tự như cách Perl không cho phép quá tải thông thường theo nghĩa đen, nhưng một hàm chỉ có thể kiểm tra các đối số của nó). Hơn nữa, nó giải quyết tốt tình huống mơ hồ được đề cập ở phần đầu của phản ứng này. Perl không phàn nàn rằng nó không biết nên gọi phương thức nào; nó chỉ gọi nó Tất cả những gì nó phải làm là tìm ra bối cảnh mà hàm được gọi trong đó, điều này luôn luôn có thể:
sub func {
if( not defined wantarray ) {
print "void\n";
} elsif( wantarray ) {
print "list\n";
} else {
print "scalar\n";
}
}
func(); # prints "void"
() = func(); # prints "list"
0+func(); # prints "scalar"
(Lưu ý: Đôi khi tôi có thể nói toán tử Perl khi tôi muốn nói đến chức năng. Điều này không quan trọng đối với cuộc thảo luận này.)
Haskell có cách tiếp cận khác, cụ thể là không có tác dụng phụ. Nó cũng có một hệ thống loại mạnh và vì vậy bạn có thể viết mã như sau:
main = do n <- readLn
print (sqrt n) -- note that this is aligned below the n, if you care to run this
Mã này đọc một số dấu phẩy động từ đầu vào tiêu chuẩn và in căn bậc hai của nó. Nhưng điều gì đáng ngạc nhiên về điều này? Vâng, loại readLn
là readLn :: Read a => IO a
. Điều này có nghĩa là đối với bất kỳ loại nào có thể Read
(chính thức, mọi loại là một thể hiện của Read
lớp loại), readLn
đều có thể đọc nó. Làm thế nào mà Haskell biết rằng tôi muốn đọc một số dấu phẩy động? Vâng, kiểu sqrt
là sqrt :: Floating a => a -> a
, trong đó chủ yếu có nghĩa rằng sqrt
chỉ có thể chấp nhận số dấu phảy động đầu vào, và do đó Haskell suy ra những gì tôi muốn.
Điều gì xảy ra khi Haskell không thể suy ra những gì tôi muốn? Vâng, có một vài khả năng. Nếu tôi hoàn toàn không sử dụng giá trị trả về, Haskell chỉ đơn giản là không gọi hàm ở vị trí đầu tiên. Tuy nhiên, nếu tôi làm sử dụng giá trị trả về, sau đó Haskell sẽ phàn nàn rằng nó không thể suy ra các loại:
main = do n <- readLn
print n
-- this program results in a compile-time error "Unresolved top-level overloading"
Tôi có thể giải quyết sự mơ hồ bằng cách chỉ định loại tôi muốn:
main = do n <- readLn
print (n::Int)
-- this compiles (and does what I want)
Dù sao, toàn bộ cuộc thảo luận này có nghĩa là quá tải bởi giá trị trả về là có thể và được thực hiện, điều này trả lời một phần câu hỏi của bạn.
Phần khác của câu hỏi của bạn là tại sao nhiều ngôn ngữ không làm điều đó. Tôi sẽ để người khác trả lời. Tuy nhiên, một vài ý kiến: lý do chính có lẽ là cơ hội cho sự nhầm lẫn ở đây thực sự lớn hơn so với việc quá tải theo loại đối số. Bạn cũng có thể xem các lý do từ các ngôn ngữ riêng lẻ:
Ada : "Có vẻ như quy tắc giải quyết quá tải đơn giản nhất là sử dụng mọi thứ - tất cả thông tin từ bối cảnh càng rộng càng tốt - để giải quyết tham chiếu quá tải. Quy tắc này có thể đơn giản, nhưng không hữu ích. để quét các đoạn văn bản lớn tùy ý và tạo ra các suy luận phức tạp tùy ý (chẳng hạn như (g) ở trên). Chúng tôi tin rằng một quy tắc tốt hơn là một quy tắc làm rõ ràng nhiệm vụ mà người đọc hoặc trình biên dịch phải thực hiện và điều đó thực hiện nhiệm vụ này càng tự nhiên cho người đọc càng tốt. "
C ++ (tiểu mục 7.4.1of "Ngôn ngữ lập trình C ++" của Bjarne Stroustrup): "Các kiểu trả về không được xem xét trong độ phân giải quá tải. Lý do là để giữ độ phân giải cho một toán tử riêng lẻ hoặc hàm gọi độc lập với ngữ cảnh.
float sqrt(float);
double sqrt(double);
void f(double da, float fla)
{
float fl = sqrt(da); // call sqrt(double)
double d = sqrt(da); // call sqrt(double)
fl = sqrt(fla); // call sqrt(float)
d = sqrt(fla); // call sqrt(float)
}
Nếu loại trả về đã được tính đến, sẽ không còn có thể xem xét một cuộc gọi sqrt()
riêng lẻ và xác định chức năng nào được gọi. "(Lưu ý, để so sánh, trong Haskell không có chuyển đổi ngầm định .)
Java ( Đặc tả ngôn ngữ Java 9.4.1 ): "Một trong các phương thức được kế thừa phải là kiểu trả về thay thế cho mọi phương thức được kế thừa khác, nếu không sẽ xảy ra lỗi thời gian biên dịch." (Vâng, tôi biết điều này không đưa ra một lý do hợp lý. Tôi chắc chắn lý do được đưa ra bởi Gosling trong "Ngôn ngữ lập trình Java". Có lẽ ai đó có một bản sao? Tôi cá là "nguyên tắc ít bất ngờ nhất" về bản chất. ) Tuy nhiên, thực tế thú vị về Java: JVM cho phép quá tải theo giá trị trả về! Điều này được sử dụng, ví dụ, trong Scala và cũng có thể được truy cập trực tiếp thông qua Java bằng cách chơi xung quanh với các phần bên trong.
Tái bút Như một lưu ý cuối cùng, thực sự có thể quá tải bằng giá trị trả về trong C ++ bằng một mẹo. Nhân chứng:
struct func {
operator string() { return "1";}
operator int() { return 2; }
};
int main( ) {
int x = func(); // calls int version
string y = func(); // calls string version
double d = func(); // calls int version
cout << func() << endl; // calls int version
func(); // calls neither
}