私の主要言語は静的型付け(Java)です。 Javaでは、すべてのメソッドから単一の型を返す必要があります。たとえば、String
を条件付きで返すメソッドや、Integer
を条件付きで返すメソッドは使用できません。しかし、たとえばJavaScriptでは、これは非常に可能です。
静的に型付けされた言語では、これが悪い考えである理由がわかります。すべてのメソッドがObject
(すべてのクラスが継承する共通の親)を返した場合、あなたとコンパイラは何を扱っているのかわかりません。実行時にすべての間違いを発見する必要があります。
しかし、動的に型付けされた言語では、コンパイラさえないかもしれません。動的に型付けされた言語では、なぜ複数の型を返す関数が悪い考えなのかは明らかではありません。静的言語の私のバックグラウンドでは、そのような関数を書くことを避けていますが、コードが見えない方法でコードをよりクリーンにする可能性がある機能について、私は細心の注意を払っていることを恐れています。
編集:私は自分の例を削除します(より良い例を考えることができるまで)。私が作ろうとしていないことについて回答するように人々を動かしていると思います。
他の回答とは異なり、異なるタイプを返すことが許容される場合があります。
例1
_sum(2, 3) → int
sum(2.1, 3.7) → float
_
一部の静的型付き言語では、これにはオーバーロードが含まれるため、いくつかのメソッドがあり、それぞれが事前定義された固定型を返すと考えることができます。動的言語では、これは同じ関数であり、次のように実装されます。
_var sum = function (a, b) {
return a + b;
};
_
同じ関数、異なるタイプの戻り値。
例2
OpenID/OAuthコンポーネントからの応答を想像してください。一部のOpenID/OAuthプロバイダーには、個人の年齢など、より多くの情報が含まれている場合があります。
_var user = authProvider.findCurrent();
// user is now:
// {
// provider: 'Facebook',
// name: {
// firstName: 'Hello',
// secondName: 'World',
// },
// email: '[email protected]',
// age: 27
// }
_
他の人は、それが電子メールアドレスまたは仮名であろうと、最低限を持っているでしょう。
_var user = authProvider.findCurrent();
// user is now:
// {
// provider: 'Google',
// email: '[email protected]'
// }
_
繰り返しますが、同じ機能、異なる結果。
ここで、異なるタイプを返すことの利点は、タイプやインターフェースは気にせず、実際にオブジェクトに含まれているコンテキストでは特に重要です。たとえば、ウェブサイトに成熟した言語が含まれているとしましょう。次に、findCurrent()
は次のように使用できます:
_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();
}
_
これをコードにリファクタリングすると、すべてのプロバイダが独自の関数を持ち、明確に定義された固定型を返すため、コードベースが低下し、コードが重複するだけでなく、メリットもありません。次のような恐怖を犯すことになるかもしれません:
_var age;
if (['Facebook', 'Yahoo', 'Blogger', 'LiveJournal'].contains(user.provider)) {
age = user.age;
}
_
一般的に、静的に型付けされた言語の道徳的同等物が悪い考えであるのと同じ理由でそれは悪い考えです:どの具体的なタイプが返されるかわからないので、結果から何ができるかわかりません(任意の値で実行できるいくつかのこと)。静的型システムでは、戻り値の型などのコンパイラチェック済み注釈がありますが、動的言語でも同じ知識が存在します。これは非公式であり、ソースコードではなく頭脳とドキュメントに保存されます。
ただし、多くの場合、型が返される韻と理由があり、その効果は静的型システムのオーバーロードまたはパラメトリック多態性に似ています。つまり、結果の型isは予測可能ですが、表現するのはそれほど簡単ではありません。
ただし、特定の関数が正しく設計されていない他の理由があることに注意してください。たとえば、無効な入力でfalseを返すsum
関数は、主に戻り値が役に立たず、エラーが発生しやすい(0 <- >誤解)。
動的言語では、別のタイプを返すかどうかを尋ねるべきではありませんが、-異なるAPIを持つオブジェクトを返す必要があります。ほとんどの動的言語は実際には型を気にしませんが、 ダックタイピング のさまざまなバージョンを使用します。
たとえば、この方法は理にかなっています:
def load_file(file):
if something:
return ['a ', 'list', 'of', 'strings']
return open(file, 'r')
ファイルと文字列のリストの両方が(Pythonでは)文字列を返す反復可能であるためです。非常に異なるタイプ、同じAPI(リスト上のファイルメソッドを呼び出そうとしない限り、これは別の話です)。
list
またはTuple
を条件付きで返すことができます(Tuple
はPythonの不変リストです)。
正式に行うこと:
def do_something():
if ...:
return None
return something_else
または:
function do_something(){
if (...) return null;
return sth;
}
python None
とJavascript null
はどちらもそれ自体が型なので、異なる型を返します。
これらすべての使用例は、静的言語で対応するものを持ち、関数は適切なインターフェースを返すだけです。
異なるAPIを返すのが良い考えかどうかに関しては、ほとんどの場合IMOは意味をなしません。頭に浮かぶのは、 @ MainMaが言ったことに近いもの です。APIがさまざまな詳細情報を提供できる場合、可能な場合はより詳細な情報を返すことは理にかなっています。
あなたの質問で私は少し泣きたくなります。あなたが提供した使用例ではありませんが、誰かが無意識のうちにこのアプローチを過度に採用するためです。これは、途方もなく保守不可能なコードから少し離れています。
エラー条件のユースケースは一種の意味があり、静的に型付けされた言語のnullパターン(すべてがパターンである必要があります)は同じタイプのことを行います。関数呼び出しがobject
を返すか、またはnull
を返します。
しかし、「これを使用してファクトリパターンを作成します」と言って、foo
またはbar
またはbaz
のいずれかを返すには、短いステップです関数の気分。これをデバッグすることは、呼び出し元がfoo
を期待しているのにbar
が与えられたときに悪夢になります。
だからあなたは心を閉じられているとは思わない。言語の機能をどのように使用するかについて、あなたは適切に注意を払っています。
開示:私の背景は静的に型付けされた言語であり、私は一般に、保守が必要な大規模で多様なチームに取り組んできましたコードはかなり高かった。したがって、私の見方もおそらく歪んでいます。
Java=でジェネリックを使用すると、静的型の安全性を維持しながら、別の型を返すことができます。関数呼び出しのジェネリック型パラメーターで返す型を指定するだけです。
もちろん、JavaScriptで同様のアプローチを使用できるかどうかは、未解決の問題です。 JavaScriptは動的に型付けされた言語であるため、object
を返すのは当然の選択のようです。
静的型付け言語での作業に慣れているときに動的戻りシナリオが機能する場所を知りたい場合は、C#のdynamic
キーワードを検討することを検討してください。 Rob Coneryは、dynamic
キーワードを使用して、400行のコードで オブジェクトリレーショナルマッパーを記述する を正常に実行できました。
もちろん、すべてのdynamic
が実際に行うのは、object
変数をいくつかのランタイムタイプセーフでラップすることだけです。
実際、静的に型付けされた言語であっても、異なる型を返すことは決して珍しいことではありません。そのため、たとえば、ユニオンタイプがあります。
実際、Java=のメソッドは、ほとんど常に4つのタイプの1つを返します:ある種のオブジェクト、null
、または例外、またはまったく返されません。
多くの言語では、エラー条件は、結果タイプまたはエラータイプのいずれかを返すサブルーチンとしてモデル化されます。たとえば、Scalaの場合:
def transferMoney(amount: Decimal): Either[String, Decimal]
もちろんこれは愚かな例です。戻り値の型は、「文字列または10進数を返す」ことを意味します。慣例により、左側のタイプはエラータイプ(この場合はエラーメッセージの文字列)で、右側のタイプは結果タイプです。
これは、例外も制御フロー構造であることを除いて、例外と似ています。実際、それらはGOTO
と表現力において同等です。
SOLID=原則についてはまだ回答がありません。特に、リスコフの代替原則に従う必要があります。期待されるタイプ以外のタイプを受け取るクラスは、何もせずに、取得したもので機能します。返されるタイプをテストします。
したがって、追加のプロパティをオブジェクトにスローしたり、返された関数を、元の関数が意図していたことをまだ達成するある種のデコレータでラップしたりする場合、関数を呼び出すコードがこれに依存しない限り、問題はありません。任意のコードパスでの動作。
文字列や整数を返す代わりに、スプリンクラーシステムや猫を返すのがよい例です。これは、呼び出し側のコードがfunctionInQuestion.hiss()を呼び出すだけの場合は問題ありません。実際には、呼び出し元のコードが想定している暗黙のインターフェイスがあり、動的に型付けされた言語によって、インターフェイスを明示的にする必要はありません。
悲しいことに、あなたの同僚はおそらくそうするでしょう。そのため、おそらくドキュメントで同じ作業を行う必要があります。ただし、インターフェースを定義するときのように、一般的に受け入れられている簡潔な機械分析可能な方法はありません。それらを持っている言語で。
条件付きで異なる型を返すのは悪い考えだと思います。これが頻繁に発生する方法の1つは、関数が1つ以上の値を返すことができるかどうかです。値を1つだけ返す必要がある場合は、値を配列にパックするのではなく返すだけで、呼び出し側の関数で値をアンパックする必要がないようにするのが妥当と思われます。ただし、これ(および他のほとんどの場合)では、発信者に両方のタイプを区別して処理する義務があります。この関数は、常に同じ型を返すかどうかを判断するのが簡単になります。
「悪い習慣」は、言語が静的に型付けされているかどうかに関係なく存在します。静的言語は、これらの慣習からあなたを遠ざけるためにより多くのことを行います。静的言語はより正式な言語であるため、静的言語で「悪い慣行」について不平を言うユーザーが増えるかもしれません。ただし、根本的な問題は動的言語にあり、正当化されるかどうかを判断できます。
ここにあなたが提案するものの好ましくない部分があります。返されるタイプがわからない場合、戻り値をすぐに使用できません。それについて何かを「発見」しなければならない。
total = sum_of_array([20, 30, 'q', 50])
if (type_of(total) == Boolean) {
display_error(...)
} else {
record_number(total)
}
多くの場合、このようなコードの切り替えは単に悪い習慣です。コードが読みにくくなります。この例では、例外ケースのスローとキャッチが一般的である理由がわかります。別の言い方をすると:関数がその機能を実行できない場合、関数は正常に戻りません。私があなたの関数を呼び出す場合、私はこれをしたいです:
total = sum_of_array([20, 30, 'q', 50])
display_number(total)
最初の行が正常に戻るので、total
には実際には配列の合計が含まれていると想定しています。正常に戻らない場合は、プログラムの別のページにジャンプします。
エラーの伝播だけではない別の例を使用してみましょう。たぶんsum_of_arrayは賢く、「それが私のロッカーの組み合わせだ!」のように、人が読める文字列を返す場合があります。配列が[11,7,19]の場合に限ります。良い例を考えるのに苦労しています。とにかく、同じ問題が当てはまります。戻り値で何かを行う前に、戻り値を検査する必要があります。
total = sum_of_array([20, 30, 40, 50])
if (type_of(total) == String) {
write_message(total)
} else {
record_number(total)
}
関数が整数または浮動小数点数を返すと便利だと主張するかもしれません。例:
sum_of_array(20, 30, 40) -> int
sum_of_array(23.45, 45.67, 67.789044) -> float
しかし、あなたが関係している限り、それらの結果は異なるタイプではありません。あなたはそれらを両方とも数値として扱うつもりです、そしてそれはあなたが気にするすべてです。したがって、sum_of_arrayは数値型を返します。これがポリモーフィズムについてです。
したがって、関数が複数の型を返す可能性がある場合、違反している可能性があるいくつかのプラクティスがあります。それらを知ることは、特定の関数がとにかく複数の型を返す必要があるかどうかを判断するのに役立ちます。
私が自分でさまざまなタイプを送信しているのを見る1つの場所は、無効な入力、または「例外的な」条件がそれほど例外的ではない「貧乏人例外」のためのものです。たとえば、 PHPユーティリティ関数 の私のリポジトリから、次の例を要約します。
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;
}
関数は名目上はブール値を返しますが、無効な入力ではNULLを返します。 PHP 5.3、すべての内部PHP関数も同様にこのように動作します。 PHP関数は公称入力でFALSEまたはINTを返します。以下を参照してください:
strpos('Hello', 'e'); // Returns INT(1)
strpos('Hello', 'q'); // Returns BOOL(FALSE)
これは悪い考えではないと思います!この最も一般的な意見とは対照的に、すでにRobert Harveyによって指摘されているように、Javaのような静的に型付けされた言語は、あなたが求めているような状況に正確にGenericsを導入しました。実際にはJavaコンパイル時に(可能な限り)タイプセーフを維持しようとし、ジェネリックスがコードの重複を回避することがありますが、なぜですか?異なるメソッドを処理/返す同じメソッドまたは同じクラスを記述できるため。このアイデアを示す非常に簡単な例を作成します。
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);
動的型付き言語では、コンパイル時に型保証がないため、多くの型で機能する同じコードを自由に記述できます。静的型付け言語でもこの問題を解決するためにジェネリックが導入されたので、動的言語で異なる型を返す関数を書くことは悪い考えではないことは明らかです。
ソフトウェアの開発は基本的に、複雑さを管理するための芸術であり、技術です。余裕のあるポイントでシステムを絞り込み、他のポイントではオプションを制限しようとします。関数のインターフェイスはコントラクトであり、コードの一部を操作するために必要な知識を制限することにより、コードの複雑さを管理するのに役立ちます。さまざまな型を返すことで、関数のインターフェイスに、返すさまざまな型のすべてのインターフェイスを追加し、どのインターフェイスが返されるかについて明らかでない規則を追加することで、大幅に拡張します。
ダイナミックな土地では、それはすべてアヒルのタイピングです。公開/公開されている最も責任のあることは、潜在的に異なる型を、それらに同じインターフェースを与えるラッパーでラップすることです。
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
ジェネリックラッパーを使用せずにさまざまな型をまとめて抽出することは時々意味があるかもしれませんが、JSを作成してから7年間、正直に言うと、非常に頻繁に実行することが合理的または便利であるとは思いませんでした。ほとんどの場合、私は、物事が統合されているオブジェクトの内部など、閉じた環境のコンテキストで私がやろうとしていることです。しかし、それは、例が思い浮かぶほど頻繁に行ったことではありません。
ほとんどの場合、型についてはまったく考えないでください。動的言語で必要な場合、型を扱います。もういや。それが肝心なのです。すべての引数の型をチェックしないでください。これは、同じメソッドが不明確な方法で一貫性のない結果をもたらす可能性がある環境でのみ実行したくなるものです(したがって、そのようなことは絶対に行わないでください)。しかし、それは重要なタイプではなく、あなたが私に与えたものがどのように機能するかです。
関数が何をするかはcontextに依存するため、Perlはこれをよく使用します。たとえば、関数は、リストコンテキストで使用される場合は配列を返し、スカラー値が期待される場所で使用される場合は配列の長さを返します。 「Perlコンテキスト」の最初のヒットであるチュートリアル から:
my @now = localtime();
次に、@ nowは配列変数(@の意味)であり、(40、51、20、9、0、109、5、8、0)のような配列が含まれます。
代わりに、結果がスカラーでなければならない方法で関数を呼び出す場合($変数はスカラーです):
my $now = localtime();
次に、まったく異なる処理を行います。$ nowは、「Fri Jan 9 20:51:40 2009」のようになります。
REST APIを実装する場合も考えられます。ここで、返されるものの形式は、クライアントの要求に依存します。たとえば、HTML、JSON、XMLなどです。技術的にはこれらすべてがバイトのストリームでも、考え方は似ています。