Có phải là một ý tưởng tồi khi trả về các loại dữ liệu khác nhau từ một chức năng trong một ngôn ngữ được gõ động?


65

Ngôn ngữ chính của tôi được gõ tĩnh (Java). Trong Java, bạn phải trả về một loại duy nhất từ ​​mọi phương thức. Ví dụ: bạn không thể có một phương thức trả về một cách có Stringđiều kiện hoặc trả về một cách có điều kiện Integer. Nhưng trong JavaScript, ví dụ, điều này là rất có thể.

Trong một ngôn ngữ gõ tĩnh tôi hiểu tại sao đây là một ý tưởng tồi. Nếu mọi phương thức được trả về Object(cha mẹ chung mà tất cả các lớp kế thừa từ đó) thì bạn và trình biên dịch không biết bạn đang xử lý vấn đề gì. Bạn sẽ phải khám phá tất cả những sai lầm của bạn trong thời gian chạy.

Nhưng trong một ngôn ngữ được gõ động, thậm chí có thể không có trình biên dịch. Trong một ngôn ngữ được gõ động, tôi không rõ tại sao một hàm trả về nhiều loại là một ý tưởng tồi. Nền tảng về ngôn ngữ tĩnh của tôi khiến tôi tránh viết các hàm như vậy, nhưng tôi sợ rằng tôi đang suy nghĩ kỹ về một tính năng có thể làm cho mã của tôi sạch hơn theo cách mà tôi không thể nhìn thấy.


Chỉnh sửa : Tôi sẽ xóa ví dụ của mình (cho đến khi tôi có thể nghĩ ra một ví dụ tốt hơn). Tôi nghĩ rằng nó chỉ đạo mọi người trả lời một điểm mà tôi không cố gắng thực hiện.


Tại sao không ném một ngoại lệ trong trường hợp lỗi?
TrueWill

1
@TrueWill Tôi giải quyết điều đó trong câu tiếp theo.
Daniel Kaplan

Bạn có nhận thấy rằng đây là một thông lệ trong các ngôn ngữ năng động hơn? Nó phổ biến trong các ngôn ngữ chức năng , nhưng có các quy tắc được thực hiện rất rõ ràng. Và tôi biết rằng nó phổ biến, đặc biệt là trong JavaScript, cho phép các đối số là các loại khác nhau (vì về cơ bản đó là cách duy nhất để thực hiện nạp chồng hàm), nhưng tôi hiếm khi thấy điều này được áp dụng cho các giá trị trả về. Ví dụ duy nhất tôi có thể nghĩ đến là PowerShell với việc đóng / mở mảng hầu hết tự động ở mọi nơi và các ngôn ngữ kịch bản là một trường hợp đặc biệt.
Aaronaught

3
Không được đề cập, nhưng có nhiều ví dụ (ngay cả trong Java Generics) của các hàm lấy kiểu trả về làm tham số; ví dụ trong Common Lisp, chúng ta có (coerce var 'string)sản lượng a stringhoặc (concatenate 'string this that the-other-thing)tương tự. Tôi cũng đã viết những thứ như thế ThingLoader.getThingById (Class<extends FindableThing> klass, long id). Và, ở đó, tôi chỉ có thể trả lại một cái gì đó phân lớp những gì bạn yêu cầu: loader.getThingById (SubclassA.class, 14)có thể trả lại một SubclassBcái kéo dài SubclassA...
BRPocock

1
Các ngôn ngữ được gõ động giống như Chiếc thìa trong phim Ma trận . Đừng cố định nghĩa Spoon là một chuỗi hoặc một số . Điều đó là không thể. Thay vào đó ... chỉ cố gắng để hiểu sự thật. Rằng không có Muỗng .
Phản ứng

Câu trả lời:


42

Ngược lại với các câu trả lời khác, có những trường hợp trả về các loại khác nhau được chấp nhận.

ví dụ 1

sum(2, 3)  int
sum(2.1, 3.7)  float

Trong một số ngôn ngữ được nhập tĩnh, điều này liên quan đến quá tải, vì vậy chúng tôi có thể xem xét rằng có một số phương thức, mỗi phương thức trả về loại cố định, được xác định trước. Trong các ngôn ngữ động, đây có thể là chức năng tương tự, được triển khai như sau:

var sum = function (a, b) {
    return a + b;
};

Cùng một chức năng, các loại giá trị trả lại khác nhau.

Ví dụ 2

Hãy tưởng tượng bạn nhận được phản hồi từ thành phần OpenID / OAuth. Một số nhà cung cấp OpenID / OAuth có thể chứa nhiều thông tin hơn, chẳng hạn như tuổi của người đó.

var user = authProvider.findCurrent();
// user is now:
// {
//     provider: 'Facebook',
//     name: {
//         firstName: 'Hello',
//         secondName: 'World',
//     },
//     email: 'hello.world@example.com',
//     age: 27
// }

Những người khác sẽ có tối thiểu, nó sẽ là một địa chỉ email hoặc bút danh.

var user = authProvider.findCurrent();
// user is now:
// {
//     provider: 'Google',
//     email: 'hello.world@example.com'
// }

Một lần nữa, cùng chức năng, kết quả khác nhau.

Ở đây, lợi ích của việc trả về các loại khác nhau đặc biệt quan trọng trong bối cảnh bạn không quan tâm đến các loại và giao diện, nhưng đối tượng thực sự chứa gì. Ví dụ: hãy tưởng tượng một trang web chứa ngôn ngữ trưởng thành. Sau đó, findCurrent()có thể được sử dụng như thế này:

var user = authProvider.findCurrent();
if (user.age || 0 >= 16) {
    // The person can stand mature language.
    allowShowingContent();
} else if (user.age) {
    // OpenID/OAuth gave the age, but the person appears too young to see the content.
    showParentalAdvisoryRequestedMessage();
} else {
    // OpenID/OAuth won't tell the age of the person. Ask the user himself.
    askForAge();
}

Tái cấu trúc mã này thành mã trong đó mỗi nhà cung cấp sẽ có chức năng riêng sẽ trả về một loại cố định, được xác định rõ sẽ không chỉ làm giảm cơ sở mã và gây ra sự sao chép mã, mà còn không mang lại bất kỳ lợi ích nào. Người ta có thể sẽ làm những điều kinh khủng như:

var age;
if (['Facebook', 'Yahoo', 'Blogger', 'LiveJournal'].contains(user.provider)) {
    age = user.age;
}

5
"Trong các ngôn ngữ được nhập tĩnh, điều này liên quan đến quá tải" Tôi nghĩ bạn có nghĩa là "trong một số ngôn ngữ được nhập tĩnh" :) Các ngôn ngữ được nhập tĩnh không yêu cầu quá tải cho sumví dụ như ví dụ của bạn .
Andres F.

17
Ví dụ cụ thể của bạn, hãy xem xét Haskell : sum :: Num a => [a] -> a. Bạn có thể tổng hợp một danh sách bất cứ thứ gì là một con số. Không giống như javascript, nếu bạn cố gắng tổng hợp một cái gì đó không phải là một số, lỗi sẽ được bắt gặp tại thời điểm biên dịch.
Andres F.

3
@MainMa Trong Scala, Iterator[A]có phương thức def sum[B >: A](implicit num: Numeric[B]): B, một lần nữa cho phép tổng hợp bất kỳ loại số nào và được kiểm tra tại thời điểm biên dịch.
Petr Pudlák

3
@Bakuriu Tất nhiên bạn có thể viết một hàm như vậy, mặc dù trước tiên bạn phải quá tải +cho chuỗi (bằng cách triển khai Num, đó là một ý tưởng khủng khiếp về thiết kế nhưng hợp pháp) hoặc phát minh ra một toán tử / hàm khác bị quá tải bởi các số nguyên và chuỗi tương ứng. Haskell có tính đa hình đặc biệt thông qua các lớp loại. Một danh sách chứa cả số nguyên và chuỗi khó hơn nhiều (có thể là không thể nếu không có phần mở rộng ngôn ngữ) nhưng lại là một vấn đề khá khác.

2
Tôi không đặc biệt ấn tượng với ví dụ thứ hai của bạn. Hai đối tượng này là cùng một "loại" khái niệm, chỉ là trong một số trường hợp, một số trường nhất định không được xác định hoặc điền. Bạn có thể biểu thị điều đó tốt ngay cả trong một ngôn ngữ gõ tĩnh với nullcác giá trị.
Aaronaught

31

Nói chung, đó là một ý tưởng tồi vì những lý do tương tự, tương đương đạo đức trong ngôn ngữ gõ tĩnh là một ý tưởng tồi: Bạn không biết loại cụ thể nào được trả về, vì vậy bạn không biết bạn có thể làm gì với kết quả đó (ngoài vài điều có thể được thực hiện với bất kỳ giá trị nào). Trong một hệ thống kiểu tĩnh, bạn có các chú thích được kiểm tra bởi trình biên dịch về các kiểu trả về và như vậy, nhưng trong một ngôn ngữ động, kiến ​​thức tương tự vẫn tồn tại - nó chỉ là không chính thức và được lưu trữ trong não và tài liệu chứ không phải trong mã nguồn.

Tuy nhiên, trong nhiều trường hợp, có vần và lý do loại nào được trả về và hiệu ứng tương tự như quá tải hoặc đa hình tham số trong các hệ thống loại tĩnh. Nói cách khác, loại kết quả thể dự đoán được, nó không hoàn toàn đơn giản để diễn đạt.

Nhưng lưu ý rằng có thể có một số lý do khác khiến một chức năng cụ thể được thiết kế tồi: Ví dụ: sumchức năng trả về sai trên đầu vào không hợp lệ là ý tưởng tồi chủ yếu vì giá trị trả về đó là vô dụng và dễ bị lỗi (0 <-> nhầm lẫn).


40
Hai xu của tôi: Tôi ghét nó khi điều đó xảy ra. Tôi đã thấy các thư viện JS trả về null khi không có kết quả, một đối tượng trên một kết quả và một mảng trên hai hoặc nhiều kết quả. Vì vậy, thay vì chỉ lặp qua một mảng, bạn buộc phải xác định xem nó có rỗng không và nếu không, nếu đó là một mảng, và nếu có, hãy làm một việc, nếu không thì làm một việc khác. Đặc biệt là, theo cách hiểu thông thường, bất kỳ nhà phát triển lành mạnh nào cũng sẽ chỉ thêm Đối tượng vào Mảng và tái chế logic chương trình từ việc xử lý một mảng.
phyrfox

10
@Izkata: Tôi không nghĩ NaNlà "phản biện" chút nào. NaN thực sự là một đầu vào hợp lệ cho bất kỳ phép tính dấu phẩy động nào. Bạn có thể làm bất cứ điều gì với NaNđiều đó bạn có thể làm với một con số thực tế. Đúng, kết quả cuối cùng của phép tính có thể không hữu ích cho bạn, nhưng nó sẽ không dẫn đến các lỗi thời gian chạy lạ và bạn không cần phải kiểm tra nó mọi lúc - bạn chỉ có thể kiểm tra một lần vào cuối một loạt tính toán. NaN không phải là một loại khác , nó chỉ là một giá trị đặc biệt , giống như một đối tượng Null .
Aaronaught

5
@Aaronaught Mặt khác NaN, giống như đối tượng null, có xu hướng ẩn khi xảy ra lỗi, chỉ để chúng bật lên sau đó. Ví dụ, chương trình của bạn có khả năng nổ tung nếu NaNtrượt vào vòng lặp có điều kiện.
Izkata

2
@Izkata: NaN và null có những hành vi hoàn toàn khác nhau. Một tham chiếu null sẽ nổ tung ngay lập tức khi truy cập, một NaN tuyên truyền. Tại sao điều sau xảy ra là rõ ràng, nó cho phép bạn viết các biểu thức toán học mà không phải kiểm tra kết quả của mọi biểu thức phụ. Cá nhân tôi thấy null càng gây hại hơn, bởi vì NaN đại diện cho một khái niệm số thích hợp mà bạn không thể thoát khỏi mà không, về mặt toán học, tuyên truyền là hành vi đúng.
Phoshi

4
Tôi không biết tại sao điều này lại trở thành một cuộc tranh luận "null so với mọi thứ khác". Tôi chỉ đơn thuần chỉ ra rằng NaNgiá trị là (a) không thực sự là một kiểu trả về khác và (b) có ngữ nghĩa dấu phẩy động được xác định rõ, và do đó thực sự không đủ điều kiện tương tự như một giá trị trả về được gõ thay đổi. Thật ra nó không khác mấy so với số 0 trong số học số nguyên; nếu số 0 "vô tình" trượt vào các tính toán của bạn thì bạn thường có khả năng kết thúc bằng số 0 do lỗi hoặc chia cho số không. Các giá trị "vô cực" được xác định bởi dấu phẩy động của IEEE có phải là xấu không?
Aaronaught

26

Trong các ngôn ngữ động, bạn không nên hỏi liệu có trả về các loại khác nhau không nhưng các đối tượng có API khác nhau . Vì hầu hết các ngôn ngữ động không thực sự quan tâm đến các loại, nhưng sử dụng các phiên bản khác nhau của kiểu gõ vịt .

Khi trả lại các loại khác nhau có ý nghĩa

Ví dụ phương pháp này có ý nghĩa:

def load_file(file): 
    if something: 
       return ['a ', 'list', 'of', 'strings'] 
    return open(file, 'r')

Bởi vì cả hai tệp và một danh sách các chuỗi đều lặp lại (trong Python) mà trả về chuỗi. Các loại rất khác nhau, cùng một API (trừ khi ai đó cố gắng gọi các phương thức tệp trong danh sách, nhưng đây là câu chuyện khác nhau).

Bạn có thể quay trở lại một cách có điều kiện listhoặc tuple( tuplelà một danh sách bất biến trong python).

Chính thức thậm chí làm:

def do_something():
    if ...: 
        return None
    return something_else

hoặc là:

function do_something(){
   if (...) return null; 
   return sth;
}

trả về các loại khác nhau, vì cả python Nonevà Javascript nullđều là các loại riêng.

Tất cả các trường hợp sử dụng này sẽ có bản sao của chúng trong các ngôn ngữ tĩnh, hàm sẽ trả về một giao diện thích hợp.

Khi trả về các đối tượng với các API khác nhau một cách có điều kiện là một ý tưởng tốt

Về việc trả lại các API khác nhau có phải là một ý tưởng hay hay không, IMO trong hầu hết các trường hợp nó không có ý nghĩa. Chỉ có ví dụ hợp lý xuất hiện trong tâm trí là một cái gì đó gần với những gì @MainMa đã nói : khi API của bạn có thể cung cấp lượng chi tiết khác nhau, có thể có ý nghĩa để trả lại chi tiết hơn khi có sẵn.


4
Câu trả lời tốt. Đáng lưu ý rằng tương đương Java / gõ tĩnh là có một hàm trả về một giao diện chứ không phải là một loại cụ thể cụ thể, với giao diện đại diện cho API / trừu tượng hóa.
mikera

1
Tôi nghĩ rằng bạn đang nitpicking, trong Python một danh sách và một tuple có thể có "loại" khác nhau, nhưng chúng thuộc cùng một "loại vịt", tức là chúng hỗ trợ cùng một bộ hoạt động. Và những trường hợp như thế này chính xác là lý do tại sao gõ vịt đã được giới thiệu. Bạn cũng có thể thực hiện x = getInt () trong Java.
Kaerber

Quay trở lại các loại khác nhau, có nghĩa là các đối tượng quay trở lại với các API khác nhau. (Một số ngôn ngữ làm cho cách đối tượng được xây dựng thành một phần cơ bản của API, một số ngôn ngữ thì không; đó là vấn đề về cách thức hệ thống biểu cảm.) Trong ví dụ do_filevề danh sách / tệp của bạn, trả về một chuỗi lặp.
Gilles 'SO- ngừng trở nên xấu xa'

1
Trong ví dụ Python đó, bạn cũng có thể gọi iter()cả danh sách và tệp để đảm bảo kết quả chỉ có thể được sử dụng như một trình vòng lặp trong cả hai trường hợp.
RemcoGerlich

1
trở lại None or somethinglà một kẻ giết người để tối ưu hóa hiệu suất được thực hiện bởi các công cụ như PyPy, Numba, Pyston và những người khác. Đây là một trong những Python-ism làm cho python không quá nhanh.
Matt

9

Câu hỏi của bạn làm tôi muốn khóc một chút. Không phải cho việc sử dụng ví dụ bạn cung cấp, nhưng bởi vì ai đó sẽ vô tình đưa phương pháp này đi quá xa. Đó là một bước ngắn từ mã vô lý không thể nhầm lẫn.

Điều kiện lỗi sử dụng loại trường hợp có ý nghĩa và mẫu null (mọi thứ phải là một mẫu) trong các ngôn ngữ được nhập tĩnh thực hiện cùng loại. Cuộc gọi chức năng của bạn trả về một objecthoặc nó trả về null.

Nhưng đó là một bước ngắn để nói "Tôi sẽ sử dụng điều này để tạo ra một mô hình nhà máy " và trở về một trong hai foohoặc barhoặc baztùy thuộc vào tâm trạng của chức năng. Gỡ lỗi này sẽ trở thành một cơn ác mộng khi người gọi mong đợi foonhưng được đưa ra bar.

Vì vậy, tôi không nghĩ rằng bạn đang đóng cửa suy nghĩ. Bạn đang thận trọng trong cách sử dụng các tính năng của ngôn ngữ.

Tiết lộ: nền tảng của tôi là các ngôn ngữ được nhập tĩnh và tôi thường làm việc trong các nhóm lớn hơn, đa dạng hơn, nơi nhu cầu về mã duy trì khá cao. Vì vậy, quan điểm của tôi có lẽ cũng bị sai lệch.


6

Sử dụng Generics trong Java cho phép bạn trả về một loại khác, trong khi vẫn giữ được sự an toàn của loại tĩnh. Bạn chỉ cần chỉ định loại mà bạn muốn trả về trong tham số loại chung của lệnh gọi hàm.

Tất nhiên, liệu bạn có thể sử dụng một cách tiếp cận tương tự trong Javascript hay không, tất nhiên, là một câu hỏi mở. Vì Javascript là một ngôn ngữ được gõ động, trả về một objectdường như là sự lựa chọn rõ ràng.

Nếu bạn muốn biết kịch bản hoàn trả động có thể hoạt động ở đâu khi bạn quen làm việc với ngôn ngữ được nhập tĩnh, hãy xem xét dynamictừ khóa trong C #. Rob Conery đã có thể viết thành công một Mapper quan hệ đối tượng trong 400 dòng mã bằng dynamictừ khóa.

Tất nhiên, tất cả dynamicthực sự là bọc một objectbiến với một số loại an toàn thời gian chạy.


4

Đó là một ý tưởng tồi, tôi nghĩ, để trả lại các loại khác nhau có điều kiện. Một trong những cách điều này xuất hiện thường xuyên đối với tôi là nếu hàm có thể trả về một hoặc nhiều giá trị. Nếu chỉ cần trả về một giá trị, có vẻ hợp lý khi chỉ trả về giá trị, thay vì đóng gói nó trong Mảng, để tránh phải giải nén nó trong chức năng gọi. Tuy nhiên, điều này (và hầu hết các trường hợp khác của điều này) đặt ra một nghĩa vụ đối với người gọi để phân biệt và xử lý cả hai loại. Hàm sẽ dễ dàng hơn để lý do nếu nó luôn trả về cùng loại.


4

"Thực hành xấu" tồn tại bất kể ngôn ngữ của bạn được gõ tĩnh. Ngôn ngữ tĩnh thực hiện nhiều hơn để giúp bạn tránh xa các thực tiễn này và bạn có thể tìm thấy nhiều người dùng phàn nàn về "thực hành xấu" trong ngôn ngữ tĩnh vì đó là ngôn ngữ chính thức hơn. Tuy nhiên, các vấn đề tiềm ẩn nằm ở một ngôn ngữ động và bạn có thể xác định liệu chúng có hợp lý hay không.

Đây là phần phản đối của những gì bạn đề xuất. Nếu tôi không biết loại nào được trả về, thì tôi không thể sử dụng giá trị trả về ngay lập tức. Tôi phải "khám phá" một cái gì đó về nó.

total = sum_of_array([20, 30, 'q', 50])
if (type_of(total) == Boolean) {
  display_error(...)
} else {
  record_number(total)
}

Thông thường loại chuyển đổi trong mã này chỉ đơn giản là thực hành xấu. Nó làm cho mã khó đọc hơn. Trong ví dụ này, bạn thấy tại sao ném và bắt các trường hợp ngoại lệ là phổ biến. Nói cách khác: nếu chức năng của bạn không thể thực hiện được những gì nó nói, thì nó sẽ không trở lại thành công . Nếu tôi gọi chức năng của bạn, tôi muốn làm điều này:

total = sum_of_array([20, 30, 'q', 50])
display_number(total)

Bởi vì dòng đầu tiên trả về thành công, tôi giả sử rằng totalthực sự có chứa tổng của mảng. Nếu nó không trở lại thành công, chúng tôi sẽ chuyển sang một trang khác trong chương trình của tôi.

Chúng ta hãy sử dụng một ví dụ khác không chỉ là về lỗi lan truyền. Có thể sum_of_array cố gắng trở nên thông minh và trả về một chuỗi có thể đọc được của con người trong một số trường hợp, như "Đó là sự kết hợp khóa của tôi!" nếu và chỉ khi mảng là [11,7,19]. Tôi đang gặp khó khăn khi nghĩ về một ví dụ tốt. Dù sao, vấn đề tương tự áp dụng. Bạn phải kiểm tra giá trị trả về trước khi bạn có thể làm bất cứ điều gì với nó:

total = sum_of_array([20, 30, 40, 50])
if (type_of(total) == String) {
  write_message(total)
} else {
  record_number(total)
}

Bạn có thể lập luận rằng nó sẽ hữu ích cho hàm trả về số nguyên hoặc số float, ví dụ:

sum_of_array(20, 30, 40) -> int
sum_of_array(23.45, 45.67, 67.789044) -> float

Nhưng những kết quả đó không phải là các loại khác nhau, theo như bạn quan tâm. Bạn sẽ coi cả hai như những con số, và đó là tất cả những gì bạn quan tâm. Vì vậy sum_of_array trả về kiểu số. Đây là những gì đa hình là về.

Vì vậy, có một số thực tiễn bạn có thể vi phạm nếu chức năng của bạn có thể trả về nhiều loại. Biết chúng sẽ giúp bạn xác định xem hàm cụ thể của bạn có trả về nhiều loại không.


Xin lỗi, tôi đã dẫn bạn lạc lối với tấm gương tội nghiệp của tôi. Quên tôi đã đề cập đến nó ở nơi đầu tiên. Đoạn đầu tiên của bạn là điểm. Tôi cũng thích ví dụ thứ hai của bạn.
Daniel Kaplan

4

Trên thực tế, không có gì lạ khi trả về các loại khác nhau ngay cả trong một ngôn ngữ gõ tĩnh. Đó là lý do tại sao chúng ta có các loại công đoàn, ví dụ.

Trong thực tế, các phương thức trong Java hầu như luôn trả về một trong bốn loại: một số loại đối tượng nullhoặc một ngoại lệ hoặc chúng không bao giờ trả về.

Trong nhiều ngôn ngữ, các điều kiện lỗi được mô hình hóa như các chương trình con trả về loại kết quả hoặc loại lỗi. Ví dụ: trong Scala:

def transferMoney(amount: Decimal): Either[String, Decimal]

Đây là một ví dụ ngu ngốc, tất nhiên. Kiểu trả về có nghĩa là "trả về một chuỗi hoặc một số thập phân". Theo quy ước, loại bên trái là loại lỗi (trong trường hợp này, một chuỗi có thông báo lỗi) và loại bên phải là loại kết quả.

Điều này tương tự với các ngoại lệ, ngoại trừ thực tế là các ngoại lệ cũng là các cấu trúc luồng điều khiển. Trên thực tế, chúng tương đương với sức mạnh biểu cảm GOTO.


Phương thức trong Java trả về ngoại lệ nghe có vẻ lạ. " Trả lại một ngoại lệ " ... hmm
gnat

3
Một ngoại lệ là một kết quả có thể có của một phương thức trong Java. Trên thực tế, tôi đã quên một loại thứ tư: Unit(một phương thức không bắt buộc phải trả về). Đúng, bạn không thực sự sử dụng returntừ khóa, nhưng dù sao đó cũng là kết quả của phương pháp. Với các ngoại lệ được kiểm tra, nó thậm chí còn được đề cập rõ ràng trong chữ ký loại.
Jörg W Mittag

Tôi hiểu rồi. Quan điểm của bạn dường như có một số giá trị, nhưng cách nó được trình bày không làm cho nó trông hấp dẫn ... hoàn toàn ngược lại
gnat

4
Trong nhiều ngôn ngữ, các điều kiện lỗi được mô hình hóa như một chương trình con trả về loại kết quả hoặc loại lỗi. Các ngoại lệ là một ý tưởng tương tự, ngoại trừ chúng cũng là các cấu trúc dòng điều khiển ( GOTOthực sự có sức mạnh biểu cảm tương đương ).
Jörg W Mittag

4

Chưa có câu trả lời nào đề cập đến các nguyên tắc RẮN. Cụ thể, bạn nên tuân theo nguyên tắc thay thế Liskov, rằng bất kỳ lớp nào nhận được một loại khác với loại dự kiến ​​vẫn có thể hoạt động với bất cứ thứ gì họ nhận được, mà không cần làm gì để kiểm tra loại nào được trả về.

Vì vậy, nếu bạn ném một số thuộc tính bổ sung vào một đối tượng hoặc bọc một hàm được trả về bằng một loại trang trí nào đó vẫn hoàn thành chức năng ban đầu dự định thực hiện, thì tốt, miễn là không có mã nào gọi hàm của bạn dựa vào điều này hành vi, trong bất kỳ đường dẫn mã.

Thay vì trả về một chuỗi hoặc một số nguyên, một ví dụ tốt hơn có thể là nó trả về một hệ thống phun nước hoặc một con mèo. Điều này tốt nếu tất cả các mã cuộc gọi sẽ làm là gọi hàmInQuestion.hiss (). Thực tế, bạn có một giao diện ngầm định mà mã cuộc gọi đang mong đợi và một ngôn ngữ được gõ động sẽ không buộc bạn phải làm cho giao diện rõ ràng.

Đáng buồn thay, đồng nghiệp của bạn có thể sẽ làm như vậy, vì vậy bạn có thể phải thực hiện cùng một công việc trong tài liệu của mình, ngoại trừ không có cách nào được chấp nhận rộng rãi, ngắn gọn, có thể phân tích bằng máy - như khi bạn xác định giao diện trong một ngôn ngữ có chúng.


3

Một nơi mà tôi thấy mình gửi các loại khác nhau là cho đầu vào không hợp lệ hoặc "ngoại lệ của người nghèo" trong đó điều kiện "ngoại lệ" không phải là rất đặc biệt. Ví dụ, từ kho lưu trữ các hàm tiện ích PHP của tôi, ví dụ rút gọn này:

function ensure_fields($consideration)
{
        $args = func_get_args();
        foreach ( $args as $a ) {
                if ( !is_string($a) ) {
                        return NULL;
                }
                if ( !isset($consideration[$a]) || $consideration[$a]=='' ) {
                        return FALSE;
                }
        }

        return TRUE;
}

Hàm này thường trả về BOOLESE, tuy nhiên, nó trả về NULL trên đầu vào không hợp lệ. Lưu ý rằng kể từ PHP 5.3, tất cả các hàm PHP bên trong cũng hoạt động theo cách này . Ngoài ra, một số hàm PHP nội bộ trả về FALSE hoặc INT trên đầu vào danh nghĩa, xem:

strpos('Hello', 'e');  // Returns INT(1)
strpos('Hello', 'q');  // Returns BOOL(FALSE)

4
Và đây là lý do tại sao tôi ghét PHP. Vâng, một trong những lý do, dù sao.
Aaronaught

1
Một downvote bởi vì ai đó không thích ngôn ngữ mà tôi phát triển một cách chuyên nghiệp?!? Đợi cho đến khi họ phát hiện ra rằng tôi là người Do Thái! :)
dotancohen

3
Đừng luôn cho rằng những người bình luận và những người đánh giá thấp là những người giống nhau.
Aaronaught

1
Tôi thích có một ngoại lệ thay vì null, vì (a) nó không thành công , vì vậy nó có khả năng được sửa trong giai đoạn phát triển / thử nghiệm, (b) rất dễ xử lý, vì tôi có thể bắt gặp tất cả các ngoại lệ hiếm gặp / bất ngờ một lần cho toàn bộ ứng dụng của tôi (trong bộ điều khiển phía trước của tôi) và chỉ cần đăng nhập nó (tôi thường gửi email cho nhóm phát triển), vì vậy nó có thể được sửa sau đó. Và tôi thực sự ghét rằng thư viện PHP tiêu chuẩn chủ yếu sử dụng cách tiếp cận "return null" - nó thực sự làm cho mã PHP dễ bị lỗi hơn, trừ khi bạn kiểm tra mọi thứ với isset(), đó là quá nhiều gánh nặng.
scriptin

1
Ngoài ra, nếu bạn quay lại nulldo lỗi, xin vui lòng làm rõ điều đó trong docblock (ví dụ @return boolean|null), vì vậy nếu tôi gặp mã của bạn một ngày nào đó tôi sẽ không phải kiểm tra thân hàm / phương thức.
scriptin

3

Tôi không nghĩ rằng đây là một ý tưởng tồi! Trái ngược với ý kiến ​​phổ biến nhất này, và như đã được Robert Harvey chỉ ra, các ngôn ngữ được gõ tĩnh như Java đã giới thiệu Generics chính xác cho các tình huống như tình huống bạn đang hỏi. Trên thực tế Java cố gắng giữ an toàn (bất cứ nơi nào có thể) trong thời gian biên dịch và đôi khi Generics tránh sao chép mã, tại sao? Bởi vì bạn có thể viết cùng một phương thức hoặc cùng một lớp xử lý / trả về các loại khác nhau . Tôi sẽ chỉ đưa ra một ví dụ rất ngắn gọn để thể hiện ý tưởng này:

Java 1.4

public static Boolean getBoolean(String property){
    return (Boolean) properties.getProperty(property);
}
public static Integer getInt(String property){
    return (Integer) properties.getProperty(property);
}

Java 1.5+

public static <T> getValue(String property, Class<T> clazz) throws WhateverCheckedException{
    return clazz.getConstructor(String.class).newInstance(properties.getProperty(property));
}
//the call will be
Boolean b = getValue("useProxy",Boolean.class);
Integer b = getValue("proxyPort",Integer.class);

Trong một ngôn ngữ gõ động, vì bạn không có kiểu an toàn tại thời gian biên dịch, bạn hoàn toàn tự do viết cùng một mã hoạt động cho nhiều loại. Vì ngay cả trong một ngôn ngữ gõ tĩnh, Generics đã được giới thiệu để giải quyết vấn đề này, rõ ràng một gợi ý rằng viết một hàm trả về các loại khác nhau trong một ngôn ngữ động không phải là một ý tưởng tồi.


Một downvote khác mà không cần giải thích! Cảm ơn cộng đồng SE!
nhiệt

2
Có thiện cảm +1. Bạn nên thử mở câu trả lời của mình bằng một câu trả lời rõ ràng và trực tiếp hơn, và không bình luận về các câu trả lời khác nhau. (Tôi sẽ cho bạn thêm +1, vì tôi đồng ý với bạn, nhưng tôi chỉ có một để cho.)
DougM

3
Generics không tồn tại trong các ngôn ngữ gõ động (mà tôi biết). Không có lý do gì để họ làm thế. Hầu hết các tổng quát là một trường hợp đặc biệt, trong đó "thùng chứa" thú vị hơn những gì trong đó (đó là lý do tại sao một số ngôn ngữ, như Java, thực sự sử dụng kiểu xóa). Loại chung thực sự là một loại của chính nó. Các phương thức chung , không có kiểu chung thực tế, như trong ví dụ của bạn ở đây, hầu như luôn chỉ là cú pháp đường cho một ngữ nghĩa gán và truyền. Không phải là một ví dụ đặc biệt hấp dẫn về những gì câu hỏi thực sự hỏi về.
Aaronaught

1
Tôi cố gắng tổng hợp hơn một chút: "Vì ngay cả trong một ngôn ngữ gõ tĩnh, Generics đã được giới thiệu để giải quyết vấn đề này, rõ ràng gợi ý rằng viết một hàm trả về các loại khác nhau trong ngôn ngữ động không phải là ý tưởng tồi." Tốt hơn rồi? Kiểm tra câu trả lời của Robert Harvey hoặc nhận xét của BRPocock và bạn sẽ nhận ra rằng ví dụ về Generics có liên quan đến câu hỏi này
thermz

1
Đừng cảm thấy tồi tệ, @thermz. Downvote thường nói nhiều về người đọc hơn nhà văn.
Karl Bielefeldt

2

Phát triển phần mềm về cơ bản là một nghệ thuật và thủ công quản lý sự phức tạp. Bạn cố gắng thu hẹp hệ thống tại các điểm bạn có thể đủ khả năng và giới hạn các tùy chọn ở các điểm khác. Giao diện của hàm là hợp đồng giúp quản lý độ phức tạp của mã bằng cách giới hạn kiến ​​thức cần thiết để làm việc với bất kỳ đoạn mã nào. Bằng cách trả về các loại khác nhau, bạn mở rộng đáng kể giao diện của chức năng bằng cách thêm vào tất cả các giao diện của các loại khác nhau mà bạn trả về VÀ thêm các quy tắc không rõ ràng về giao diện nào được trả về.


1

Perl sử dụng điều này rất nhiều bởi vì những gì một chức năng làm phụ thuộc vào bối cảnh của nó . Chẳng hạn, một hàm có thể trả về một mảng nếu được sử dụng trong ngữ cảnh danh sách hoặc độ dài của mảng nếu được sử dụng ở nơi dự kiến ​​giá trị vô hướng. Từ hướng dẫn là lần truy cập đầu tiên cho "bối cảnh perl" , nếu bạn thực hiện:

my @now = localtime();

Sau đó @now là một biến mảng (đó là ý nghĩa của @) và sẽ chứa một mảng như (40, 51, 20, 9, 0, 109, 5, 8, 0).

Nếu thay vào đó, bạn gọi hàm theo cách mà kết quả của nó sẽ phải là vô hướng, với (biến $ là vô hướng):

my $now = localtime();

sau đó nó làm một cái gì đó hoàn toàn khác: $ bây giờ sẽ là một cái gì đó như "Thứ Sáu 9 tháng 1 20:51:40 2009".

Một ví dụ khác tôi có thể nghĩ đến là trong việc triển khai API REST, trong đó định dạng của những gì được trả về phụ thuộc vào những gì khách hàng muốn. Ví dụ: HTML hoặc JSON hoặc XML. Mặc dù về mặt kỹ thuật đó là tất cả các luồng byte, ý tưởng là tương tự nhau.


Bạn có thể muốn đề cập đến cách cụ thể thực hiện điều này trong perl - Wantarray ... và có thể là một liên kết đến một số cuộc thảo luận nếu nó tốt hay xấu - Sử dụng bừa bãi được coi là có hại tại PerlMonks

Tình cờ, tôi đang xử lý vấn đề này với API nghỉ ngơi Java mà tôi đang xây dựng. Tôi đã tạo điều kiện cho một tài nguyên trả về XML hoặc JSON. Loại mẫu số chung thấp nhất trong trường hợp đó là a String. Bây giờ mọi người đang yêu cầu tài liệu về loại trả lại. Các công cụ java có thể tự động tạo nó nếu tôi trả về một lớp, nhưng vì tôi đã chọn Stringnên tôi không thể tự động tạo tài liệu. Nhìn nhận lại / đạo đức của câu chuyện, tôi ước tôi trở lại một loại cho mỗi phương pháp.
Daniel Kaplan

Nghiêm khắc, điều này dẫn đến một hình thức quá tải. Nói cách khác, có nhiều chức năng được liên kết và tùy thuộc vào chi tiết của cuộc gọi, một hoặc phiên bản khác được chọn.
Ian

1

Ở vùng đất năng động, tất cả là về gõ vịt. Điều tiếp xúc / công khai có trách nhiệm nhất phải làm là bọc các loại có khả năng khác nhau trong một trình bao bọc cung cấp cho chúng cùng một giao diện.

function ThingyWrapper(thingy){ //a function constructor (class-like thingy)

    //thingy is effectively private and persistent for ThingyWrapper instances

    if(typeof thingy === 'array'){
        this.alertItems = function(){
            thingy.forEach(function(el){ alert(el); });
        }
    }
    else {
        this.alertItems = function(){
            for(var x in thingy){ alert(thingy[x]); }
        }
    }
}

function gimmeThingy(){
    var
        coinToss = Math.round( Math.random() ),//gives me 0 or 1
        arrayThingy = [1,2,3],
        objectThingy = { item1:1, item2:2, item3:3 }
    ;

    //0 dynamically evaluates to false in JS
    return new ThingyWrapper( coinToss ? arrayThingy : objectThingy );
}

gimmeThingy().alertItems(); //should be same every time except order of numbers - maybe

Thỉnh thoảng có thể hiểu được các loại khác nhau hoàn toàn không có trình bao bọc chung chung nhưng thành thật trong 7 năm viết JS, đó không phải là điều tôi thấy hợp lý hoặc thuận tiện để làm thường xuyên. Chủ yếu là những gì tôi sẽ làm trong bối cảnh môi trường kín như bên trong của một vật thể, nơi mọi thứ được kết hợp với nhau. Nhưng đó không phải là điều tôi đã làm thường xuyên đủ để bất kỳ ví dụ nào nhảy vào tâm trí.

Hầu hết, tôi sẽ khuyên bạn ngừng suy nghĩ về các loại càng nhiều. Bạn đối phó với các loại khi bạn cần bằng một ngôn ngữ động. Không còn nữa. Đó là toàn bộ vấn đề. Đừng kiểm tra kiểu của mọi đối số. Đó là điều mà tôi chỉ muốn làm trong một môi trường mà cùng một phương pháp có thể mang lại kết quả không nhất quán theo những cách không thể tin được (vì vậy chắc chắn đừng làm điều gì đó như vậy). Nhưng đó không phải là vấn đề quan trọng, đó là cách mà bạn cho tôi làm việc.

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.