Tại sao không quá tải với các loại trả lại được phép? (ít nhất là trong các ngôn ngữ thường được sử dụng)


9

Tôi không biết về tất cả các ngôn ngữ lập trình, nhưng rõ ràng rằng thường thì khả năng quá tải một phương thức có tính đến kiểu trả về của nó (giả sử các đối số của nó là cùng một số và loại) không được hỗ trợ.

Ý tôi là như thế này:

 int method1 (int num)
 {

 }
 long method1 (int num)
 {

 }

Nó không phải là một vấn đề lớn đối với lập trình nhưng trong một số trường hợp, tôi sẽ hoan nghênh nó.

Rõ ràng sẽ không có cách nào để các ngôn ngữ đó hỗ trợ mà không có cách phân biệt phương thức nào đang được gọi, nhưng cú pháp cho phương thức đó có thể đơn giản như một cái gì đó như [int] method1 (num) hoặc [long] method1 (num) bằng cách đó, trình biên dịch sẽ biết cái nào sẽ được gọi.

Tôi không biết làm thế nào trình biên dịch hoạt động nhưng điều đó dường như không khó thực hiện, vì vậy tôi tự hỏi tại sao điều đó thường không được thực hiện.

Đó là những lý do tại sao một cái gì đó như thế không được hỗ trợ?


Có thể câu hỏi của bạn sẽ tốt hơn với một ví dụ trong đó chuyển đổi ngầm giữa hai loại trả về không tồn tại - ví dụ: các lớp FooBar.

Chà, tại sao một tính năng như vậy sẽ hữu ích?
James Youngman

3
@JamesYoungman: Ví dụ để phân tích các chuỗi thành các loại khác nhau, bạn có thể có một phương thức int read (String s), float read (String s), v.v. Mỗi biến thể quá tải của phương thức thực hiện phân tích cú pháp cho loại thích hợp.
Giorgio

Đây chỉ là một vấn đề với các ngôn ngữ gõ tĩnh. Có nhiều kiểu trả về là một thói quen khá phổ biến trong các ngôn ngữ được gõ động như Javascript hoặc Python.
Gort Robot

1
@StevenBurnap, ừm, không. Với ví dụ JavaScript, bạn hoàn toàn không thể thực hiện chức năng nạp chồng. Vì vậy, đây thực sự chỉ là một vấn đề với các ngôn ngữ hỗ trợ quá tải tên hàm.
David Arno

Câu trả lời:


19

Nó phức tạp kiểm tra loại.

Khi bạn chỉ cho phép quá tải dựa trên các loại đối số và chỉ cho phép khấu trừ các loại biến từ trình khởi tạo của chúng, tất cả thông tin loại của bạn sẽ chảy theo một hướng: lên cây cú pháp.

var x = f();

given      f   : () -> int  [upward]
given      ()  : ()         [upward]
therefore  f() : int        [upward]
therefore  x   : int        [upward]

Khi bạn cho phép thông tin loại di chuyển theo cả hai hướng, chẳng hạn như suy ra loại biến từ cách sử dụng của nó, bạn cần một bộ giải ràng buộc (như Thuật toán W, cho các hệ thống loại Hindley Milner) để xác định loại.

var x = parse("123");
print_int(x);

given      parse        : string -> T  [upward]
given      "123"        : string       [upward]
therefore  parse("123") : ∃T           [upward]
therefore  x            : ∃T           [upward]
given      print_int    : int -> ()    [upward]
therefore  print_int(x) : ()           [upward]
therefore  int -> ()    = ∃T -> ()     [downward]
therefore  ∃T           = int          [downward]
therefore  x            : int          [downward]

Ở đây, chúng ta cần để lại kiểu xnhư một biến kiểu chưa được giải quyết ∃T, trong đó tất cả những gì chúng ta biết về nó là nó có thể phân tích được. Chỉ sau này, khi xđược sử dụng ở một loại cụ thể, chúng ta mới có đủ thông tin để giải quyết ràng buộc và xác định rằng ∃T = int, nó truyền thông tin loại xuống cây cú pháp từ biểu thức cuộc gọi vào x.

Nếu chúng tôi không xác định được loại x, thì mã này cũng sẽ bị quá tải (vì vậy người gọi sẽ xác định loại) hoặc chúng tôi sẽ phải báo cáo lỗi về sự không rõ ràng.

Từ điều này, một nhà thiết kế ngôn ngữ có thể kết luận:

  • Nó thêm phức tạp để thực hiện.

  • Nó làm cho việc đánh máy chậm hơn trong các trường hợp bệnh lý, theo cấp số nhân.

  • Thật khó để tạo ra các thông báo lỗi tốt.

  • Nó quá khác so với hiện trạng.

  • Tôi không cảm thấy muốn thực hiện nó.


1
Ngoài ra: Sẽ rất khó để hiểu rằng trong một số trường hợp, trình biên dịch của bạn sẽ đưa ra các lựa chọn mà lập trình viên hoàn toàn không mong đợi, dẫn đến khó tìm thấy lỗi.
gnasher729

1
@ gnasher729: Tôi không đồng ý. Tôi thường xuyên sử dụng Haskell, có tính năng này và tôi chưa bao giờ bị cắn bởi sự lựa chọn quá tải của nó (viz. Ví dụ kiểu chữ). Nếu một cái gì đó mơ hồ, nó chỉ buộc tôi thêm một chú thích loại. Và tôi vẫn chọn thực hiện suy luận đầy đủ trong ngôn ngữ của mình vì nó cực kỳ hữu ích. Câu trả lời này là tôi chơi người ủng hộ của quỷ.
Jon Purdy

4

Bởi vì nó mơ hồ. Sử dụng C # làm ví dụ:

var foo = method(42);

Những gì quá tải chúng ta nên sử dụng?

Ok, có lẽ đó là một chút không công bằng. Trình biên dịch không thể tìm ra loại sẽ sử dụng nếu chúng tôi không nói với nó bằng ngôn ngữ giả định của bạn. Vì vậy, việc gõ ngầm là không thể trong ngôn ngữ của bạn và có các phương thức ẩn danh và Linq cùng với nó ...

Cái này thì sao? (Chữ ký được xác định lại một chút để minh họa điểm.)

short method(int num) { ... }
int method(int num) { ... }

....

long foo = method(42);

Chúng ta nên sử dụng intquá tải, hoặc shortquá tải? Chúng tôi chỉ đơn giản là không biết, chúng tôi sẽ phải chỉ định nó với [int] method1(num)cú pháp của bạn . Đó là một chút đau đớn để phân tích và viết thành thật.

long foo = [int] method(42);

Vấn đề là, đó là cú pháp tương tự đáng ngạc nhiên với một phương thức chung trong C #.

long foo = method<int>(42);

(C ++ và Java có các tính năng tương tự.)

Nói tóm lại, các nhà thiết kế ngôn ngữ đã chọn giải quyết vấn đề theo một cách khác để đơn giản hóa việc phân tích cú pháp và cho phép các tính năng ngôn ngữ mạnh mẽ hơn nhiều.

Bạn nói rằng bạn không biết nhiều về trình biên dịch. Tôi rất khuyên bạn nên tìm hiểu về ngữ pháp và trình phân tích cú pháp. Khi bạn hiểu ngữ pháp miễn phí ngữ cảnh là gì, bạn sẽ có ý tưởng tốt hơn nhiều về lý do tại sao sự mơ hồ là một điều xấu.


Điểm hay về thuốc generic, mặc dù bạn có vẻ như đang bị xáo trộn shortint.
Robert Harvey

Vâng @RobertHarvey bạn đúng. Tôi đã đấu tranh cho một ví dụ để minh họa điểm. Nó sẽ hoạt động tốt hơn nếu methodtrả về một số ngắn hoặc int và loại được xác định là dài.
RubberDuck

Điều đó có vẻ tốt hơn một chút.
RubberDuck

Tôi không mua các đối số của bạn rằng bạn không thể có suy luận kiểu, chương trình con ẩn danh hoặc hiểu đơn âm trong một ngôn ngữ với tình trạng quá tải kiểu trả về. Haskell làm điều đó, và Haskell có tất cả ba. Nó cũng có đa hình tham số. Quan điểm của bạn về long/ int/ shortlà về sự phức tạp của việc phân nhóm và / hoặc chuyển đổi ngầm định hơn là về quá tải kiểu trả về. Rốt cuộc, số lượng chữ bị quá tải đối với kiểu trả về của chúng trong C ++, Java, C♯ và nhiều loại khác, và điều đó dường như không gây ra vấn đề gì. Bạn chỉ có thể tạo một quy tắc: ví dụ: chọn loại cụ thể / chung nhất.
Jörg W Mittag

@ JörgWMittag quan điểm của tôi không phải là nó sẽ làm cho nó không thể, chỉ là nó làm cho mọi thứ trở nên phức tạp không cần thiết.
RubberDuck

0

Tất cả các tính năng ngôn ngữ thêm phức tạp, vì vậy chúng phải cung cấp đủ lợi ích để biện minh cho các vấn đề không thể tránh khỏi, các trường hợp góc và sự nhầm lẫn của người dùng mà mọi tính năng tạo ra. Đối với hầu hết các ngôn ngữ, ngôn ngữ này đơn giản là không cung cấp đủ lợi ích để biện minh cho nó.

Trong hầu hết các ngôn ngữ, bạn sẽ mong muốn biểu thức method1(2)có một loại xác định và giá trị trả về ít nhiều có thể dự đoán được. Nhưng nếu bạn cho phép quá tải các giá trị trả về, điều đó có nghĩa là không thể nói biểu thức đó có nghĩa gì mà không xem xét bối cảnh xung quanh nó. Xem xét những gì xảy ra khi bạn có một unsigned long long foo()phương thức mà việc thực hiện kết thúc bằng return method1(2)? Điều đó có nên gọi là longquá tải quay trở lại hoặc intquá tải quay trở lại hay chỉ đơn giản là đưa ra một lỗi biên dịch?

Ngoài ra, nếu bạn phải trợ giúp trình biên dịch bằng cách chú thích kiểu trả về, bạn không chỉ phát minh ra nhiều cú pháp hơn (làm tăng tất cả các chi phí đã nói ở trên về việc cho phép tính năng tồn tại), mà bạn còn thực hiện tương tự như việc tạo hai phương pháp được đặt tên khác nhau trong một ngôn ngữ "bình thường". Là [long] method1(2)trực quan hơn long_method1(2)?


Mặt khác, một số ngôn ngữ chức năng như Haskell với hệ thống loại tĩnh rất mạnh cho phép loại hành vi này, bởi vì suy luận kiểu của chúng đủ mạnh để bạn hiếm khi cần chú thích kiểu trả về trong các ngôn ngữ đó. Nhưng điều đó chỉ có thể bởi vì những ngôn ngữ đó thực sự đảm bảo an toàn loại, hơn bất kỳ ngôn ngữ truyền thống nào, cùng với việc yêu cầu tất cả các chức năng phải thuần túy và minh bạch tham chiếu. Đây không phải là điều có thể khả thi trong hầu hết các ngôn ngữ OOP.


2
"cùng với việc yêu cầu tất cả các chức năng phải thuần túy và minh bạch tham chiếu": Làm thế nào để điều này làm cho quá tải kiểu trả về dễ dàng hơn?
Giorgio

@Giorgio Không - Rust không thực thi chức năng puritiy và nó vẫn có thể thực hiện quá tải kiểu trả về (mặc dù quá tải của cô ấy trong Rust rất khác so với các ngôn ngữ khác (bạn chỉ có thể quá tải bằng cách sử dụng các mẫu))
Idan Arye

Phần [dài] và [int] là để có cách gọi một cách rõ ràng phương thức, trong hầu hết các trường hợp, cách gọi nó có thể được suy ra trực tiếp từ loại biến mà việc thực thi phương thức được gán.
dùng2638180

0

có sẵn trong Swift và hoạt động tốt ở đó. Rõ ràng bạn không thể có một loại mơ hồ ở cả hai bên vì vậy nó phải được biết ở bên trái.

Tôi đã sử dụng điều này trong một API mã hóa / giải mã đơn giản .

public protocol HierDecoder {
  func dech() throws -> String
  func dech() throws -> Int
  func dech() throws -> Bool

Điều đó có nghĩa là các cuộc gọi nơi các loại tham số được biết đến, chẳng hạn như initmột đối tượng, hoạt động rất đơn giản:

    private static let typeCode = "ds"
    static func registerFactory() {
        HierCodableFactories.Register(key:typeCode) {
            (from) -> HierCodable in
            return try tgDrawStyle(strokeColor:from.dech(), fillColor:from.dechOpt(), lineWidth:from.dech(), glowWidth: from.dech())
        }
    }
    func typeKey() -> String { return tgDrawStyle.typeCode }
    func encode(to:HierEncoder) {
        to.ench(strokeColor)
        to.enchOpt(fillColor)
        to.ench(lineWidth)
        to.ench(glowWidth)
    }

Nếu bạn đang chú ý, bạn sẽ nhận thấy dechOptcuộc gọi ở trên. Tôi đã phát hiện ra một cách khó khăn là quá tải cùng tên hàm mà bộ phân biệt trả về một tùy chọn quá dễ bị lỗi vì bối cảnh gọi có thể đưa ra một kỳ vọng nó là một tùy chọn.


-5
int main() {
    auto var1 = method1(1);
}

Trong trường hợp đó, trình biên dịch có thể a) từ chối cuộc gọi vì nó không rõ ràng b) chọn đầu tiên / cuối cùng c) để lại kiểu var1và tiến hành suy luận kiểu và ngay khi một số biểu thức khác xác định loại var1sử dụng để chọn thực hiện đúng. Ở dòng dưới cùng, cho thấy một trường hợp trong đó suy luận kiểu là không tầm thường hiếm khi chứng minh một điểm khác với suy luận kiểu đó nói chung là không tầm thường.
back2dos

1
Không phải là một lập luận mạnh mẽ. Rust, ví dụ, sử dụng nhiều suy luận kiểu, và trong một số trường hợp, đặc biệt là với thuốc generic, không thể nói bạn cần loại nào. Trong những trường hợp như vậy, bạn chỉ cần đưa ra loại rõ ràng thay vì dựa vào suy luận kiểu.
8bittree

1
Uh ... điều đó không thể hiện một loại trả lại quá tải. method1phải được khai báo để trả về một loại cụ thể.
Gort Robot
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.